Webデザインのための備忘録

webデザインをするために学んだことをアウトプットするためのブログです。

Edgeで動かない!フルページUIのコーディングでハマったことと解決方法

こんにちは。mamenoです。

今回は、fullpage.jsのような、1スクロールで1セクション移動するデザインをコーディングしたときに、Edgeではまって後悔したので備忘録として残しておこうと思います。

デモはこちら(コードペンが開きます)。

はじめにやったこと

fullpage.js 使わない で検索しました。 Fullpage.jsはどうやら商用利用の際はお金がかかるそうなので、それを使わない実装方法を検索。

そしたら、こんな記事がありました。

モダンなJSとCSSで作るライブラリ不要の全画面スクロール演出(2019年版)

デモもあったので確認したら、まさに求めていた動き! こちらを利用させてもらって中身のコーディングをすすめました。

しかし、この時点で私は考慮すべきことを忘れていました。

そう、chrome以外のブラウザでの確認です。

scroll-snap-typeがEdgeでスムーズにいかない

Edge + マウスでの動作確認の際、 動きがカクつく現象が発生しました。

確認してみると、そもそも参考にしたデモサイトでも同じような動きになっていました...

今後こういった広範囲に関係する実装をする時は、デモサイトも主要ブラウザで確認しようと強く思いました...

ということで、scroll-snap-typeがEdgeでカクつく現象をどうにかする方法はないかと調べました。

chatGPTにも聞いたら、以下の対応をしてみてはどうかと提案を受けましたのでやってみました。

1. スクロールパフォーマンスの最適化

スムーズスクロールの有効化: Edge では scroll-behavior: smooth; を使用して、スクロールの挙動をスムーズにすることができます。

html {
  scroll-behavior: smooth;
}

もともとやっておりました。なので効果なし。

2. スクロールスナップのプロパティ調整

scroll-snap-type の値を調整してみてください。mandatory から proximity に変更することで、ユーザーがページをスクロールした際の挙動が改善される場合があります。

.container {
  scroll-snap-type: y proximity;
}
  • mandatory...現在表示しているセクションか、次のセクション、どちらかを強制的にスナップさせる(前後のセクションが見切れることがない)
  • proximity...強制的にスナップさせず、スナップ位置に近ければスナップ位置に合わせる。各スナップ位置の中間地点でスクロールが止まっている場合はその位置でとまる(前後のセクションが見切れることがある)。

proximityはそもそもの要件には満たないこと(確実にどちらかのセクションでスナップさせたかった)、実際やってみても効果を感じられなかったのでこちらもなし。

3. ハードウェアアクセラレーションの利用

CSS の will-change プロパティを使用して、ブラウザにどのプロパティが変更される可能性があるかを事前に知らせ、ハードウェアアクセラレーションの利用を促します。スクロールパフォーマンスが向上することがあります。

.scroll-container {
  will-change: scroll-position;
}

こちらのプロパティは知りませんでした。

とりあえず適用してみましたが効果なし。

ただ、今調べていたらこんな記事が。

will-changeを使ってなめらかなアニメーションを作成しよう

Edgeはサポートしていないので代わりが必要??っぽかったです。

詳しくなくてすみません。気になる方は調べてみてください。

ということで、いまいち解決策が見つからなかったので別の手段で実装できないか考えることにしました。

考えを変えて、スライダーのライブラリを使ってみる

いろいろ考えているうちに、

要はこれって、フルスクリーンのスライドが縦に動いてるってことだよねと考え、だったらswiper.jsに代表されるスライダーのライブラリでいい感じにできるのではないかと思いました。

swiper.jsでのフルページスクロールの作り方

大体ググれば説明してくれているのがありますが、こちらにも一応書いておきます。

1. CDNの読み込み

実際の案件ではCDNではなくnpmなどでインストールするのがいいとは思いますが、簡単なデモなのでCDNで書いておきます。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

//CDN読み込み
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>

</head> 
<body>

</body>
</html>

2. オリジナルCSSの作成

style.cssを作成し、head内にリンクおき、以下を記述

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>


<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
//CSSよみこみ
<link rel="stylesheet" href="style.css">

<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>

</head> 
<body>

</body>
</html>

style.css

/* 全てのmargin・paddingを0に */
*,
*::after,
*::before {
  margin: 0;
  padding: 0;
}

section.fullpage-wrapper {
  position: relative;
  height: 100vh;
  overflow: hidden;
}
section.fullpage-wrapper .fullpageSwiper {
  height: 100%;
  background: #f1f1f1;
}
.swiper-slide {
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
-ms-touch-action: auto;
      touch-action: auto;
}

/* わかりやすいように色を変えているだけ */
.swiper-slide.slide1, .swiper-slide.slide3, .swiper-slide.slide5 {
  background: #acacac;
}

3. swiperの設定をオリジナルのJSファイルに書く

script.jsを作成し、head内にリンクをおき、以下を記述

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>


<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css"/>
<link rel="stylesheet" href="style.css">

<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
//JSよみこみ
<script src="script.js" defer></script>

</head> 
<body>

</body>
</html>

script.js

var swiper = new Swiper(".fullpageSwiper", {
  direction: "vertical",
  slidesPerView: 1,
  mousewheel: true,
  autoHeight: true,
  speed: 1000,
  keyboard: {
    enabled: true,
  },
  mousewheel: {
    releaseOnEdges: true,
    forceToAxis: true,
    sensitivity: 1,
  },
  pagination: {
    el: ".swiper-pagination",
    clickable: true,
  },

});

以上です。

デモはこちら(コードペンが開きます)。

フルページの実装の際に気をつけるべきだと思ったこと

最近流行りの?横スクロールもそうですが、縦幅が短い、横長のPCでの考慮をしないと、縦にコンテンツがはみ出してしまって上下みきれてしまう現象が発生してしまいます。

本来はこの下に抜粋文などがあるが、みられない状態

参考元:

www.fujiya-peko.co.jp

コンテナを100vhにして、overflow:hiddenに設定すると、このような現象になってしまいます。 こちらについても考慮する必要があるなと感じました。

ひとまず気をつけるべきは上下の余白設定。 px・rem・emなどの固定値ではなく、vh・%での指定や、@media screen and (max-height: ●●px)など、こまかい指定が必要だと思いました。

MySQLのselect構文〜基本的な構文まとめ

sqlの基本を学んでいる最中ですが、構文が多くてこんがらがってきたので簡単にまとめました。
ややこしいところは自分なりの和訳つき。

抽出

レコードの抽出(全フィールド)

select * from テーブル名

レコードの抽出(指定フィールド)

select 表示するフィールド名(,区切りで) from テーブル名

条件のある抽出

select *(または指定フィールド) from テーブル名
where 条件式

比較演算子

 select *(または指定フィールド) from テーブル名
 where 指定フィールド < 条件

比較演算子 … = , < , > , <= , >=など

論理演算子

 select *(または指定フィールド) from テーブル名
 where 指定フィールド and 条件

論理演算子(処理の優先順に) … not 、 and 、 or

その他の演算子

 --【範囲検索】
 where フィールド名 between 最小値 and 最大値
 
 --【いずれか検索】
 where フィールド名 in (値1, 値2, 値3)
 
 --【あいまい検索】
 where フィールド名 like パターン(「%」・・・0文字以上の文字列、「_」・・・任意の一文字)

レコードの並び替え

order by フィールド名 並び順(※)

※...desc(降順)/ asc(昇順・デフォルト省略可)


レコード数の制限

limit 開始位置(デフォルト0・省略可), レコード数(上限数)

集約関数

select 集約関数 from テーブル名

全レコード数を抽出

select count(*) from テーブル名

演算結果のグループ化

select 集約関数 from テーブル名
group by グループ化するフィールド名

文字数を数える

 -- char_length(文字数を知りたいフィールド名)
 
 ex)select char_length(content) as 文字数 from posts;
 -- 「posts」テーブルから、contentフィールドの文字数を「文字数」というフィールド名として抜き出してね。

文字を抜き出す

 -- substring(抜き出したいフィールド名, 何文字から, 何文字目まで)
 
 ex)select substring(content, 1, 10) as 抜粋文 from posts;
 --「posts」テーブルから、contentフィールドの1文字目から10文字目までを「抜粋文」というフィールド名として抜き出してね。

演算結果フィールドに別名をつける

 select 集約関数 as 別名 from テーブル名

集約関数の結果に条件を付ける

where は使えないので、 having を使用する。
使い方はwhereと同じ。

select 集約関数 ( as 別名 ) from テーブル名
group by フィールド名
having 条件式;

結合

和結合

select 表示するフィールド名 from テーブル名
union オプション(※、省略可能)
select 表示するフィールド名 from 結合するテーブル名

※ distinct(デフォルト)・・・重複は削除 、 all・・・重複も表示

内部結合

内部結合は、テーブル名と結合するテーブル名を「 join 」で繋げ、
両テーブルのどこのフィールドが一致しているのかを「 on 」で示す。

select 表示するテーブル名.フィールド名 from テーブル名 (innerjoin 結合するテーブル名
    on テーブル名.関連づけるフィールド名 = 結合するテーブル名.関連づけるフィールド名
    
ex)select -①
     posts.id,
     posts.title,
     posts.content,
     posts.user_id,
     users.name
    from posts join users -②
    on posts.user_id = users.id; -③
    
--  ①posts テーブルの id、title、content、user_idと、users テーブルのnameを表示してね。
--   ②posts テーブルに、usersテーブルをくっつけるんだよ。(内部結合)
--  ③【postsテーブルのuser_id】は、【usersテーブルのid】と一緒だよ。

外部結合

外部結合は、テーブル名と結合するテーブル名を「 left join 」で繋げ、
両テーブルのどこのフィールドが一致しているのかを「 on 」で示す。

select 表示するテーブル名.フィールド名 from 全レコード表示するテーブル名 leftouterjoin 結合するテーブル名
    on 全レコード表示するフィールド名.関連づけるフィールド名 = 結合するテーブル名.関連づけるフィールド名
    
ex)select -①
     new_posts.id,
     new_posts.title,
     new_posts.content,
     new_posts.created,
     users.name
    from new_posts left join users -②
    on new_posts.user_id = users.id; -③

-- ①new_posts テーブルの id、title、content、createdと、users テーブルのnameを表示してね。
-- ②new_posts テーブルに usersテーブルをくっつけるんだよ。(外部結合)
-- ③【new_postsテーブルのuser_id】は、 【usersテーブルのid】と一緒だよ。

相関名をつける

ex)select
      p.id,
      p.title,
      p.content,
      p.created,
      u.name
    from new_posts as p left join users as u -①
    on p.user_id = u.id
    order by p.created desc;    

① の「from テーブル名」の部分に、「as 別名」をつけることで、その別名を他の箇所でも使うことができる。

抽出条件を保存する

一連の処理は、「ビュー」として保存しておくことができる。

create view ビュー名(フィールド名1, フィールド名2, フィールド名3) as 抽出条件のselect文;

保存したビューは、手PB売ると同じ扱いができる。

-- 表示してみる
select 表示するフィールド名 from ビュー名;

ビューの削除

  drop view ビュー名;

phpお問い合わせフォーム コードの和訳

はじめに(基本情報)

index.htmlに記述されているお問い合わせフォームは以下の通り。

  • お名前(name属性 = your_name )
  • メールアドレス(name属性 = your_email )
  • お問い合わせ内容(name属性 = inquiry )

それぞれの内容が入っているかどうか(空文字でないか)と、
メールアドレスについては形式が正しいかもチェックし、
エラーがあった場合はそれについてのエラーメッセージを順不同リストで、
エラーがない場合は入力内容を表示するコードです。

コードは以下の通り。

  <h1>入力確認</h1>

  <?php
  // エラーメッセージを格納する変数を初期化
  $error = '';

  //お名前の入力チェック
  if ($_POST['your_name'] == '') {
    $error .= '<li>お名前を入力してください。</li>';
  }

  //メールアドレスの入力チェック
  if ($_POST['your_email'] == '') {
    $error .= '<li>メールアドレスを入力してください。</li>';
  }

  //メールアドレスの形式チェック
  if ($_POST['your_email'] != '' && !filter_var($_POST['your_email'], FILTER_VALIDATE_EMAIL)) {
    //空欄でかつ形式が正しくなければ
    $error .= '<li>メールアドレスを正しく入力してください。</li>';
  }

  //メッセージの入力チェック
  if ($_POST['inquiry'] == '') {
    $error .= '<li>お問い合わせ内容を入力してください。</li>';
  }

  if (!empty($error)) {
    echo '<ul>' . $error . '</ul>';
  } else {
  ?>

  <dl>
    <dt>お名前</dt>
    <dd>
      <?php echo htmlspecialchars($_POST['your_name'], ENT_QUOTES, 'UTF-8'); ?>
    </dd>
    <dt>メールアドレス</dt>
    <dd>
      <?php echo htmlspecialchars($_POST['your_email'], ENT_QUOTES, 'UTF-8'); ?>
    </dd>
    <dt>お問い合わせ内容</dt>
    <dd>
      <?php echo htmlspecialchars($_POST['inquiry'], ENT_QUOTES, 'UTF-8'); ?>
    </dd>
  </dl>

  <?php } ?>

このコードを和訳してみました。

和訳してみた

<h1>入力確認</h1>

<?php
$errorという変数(空文字列)を作る = 初期化


  //お名前の入力チェック
  もし [your_name] が空なら {
    $error に < li >お名前を入力してください。< /li >を追加代入
  }

//メールアドレスの入力チェック
  もし [your_email]が空なら {
    $error に < li >メールアドレスを入力してください。< /li >を追加代入
  }
//メールアドレスの形式チェック
  もし [your_email] が空ではない が メールアドレス形式でないなら {
    $error に < li >メールアドレスを正しく入力してください。< /li >を追加代入
  }

  //メッセージの入力チェック
  もし [inquiry] が空なら {
    $error に < li >お問い合わせ内容を入力してください。< /li >を追加代入
  }

/-----------------------
ここまででエラー項目があれば$errorに追加代入されているので、ここからどう画面に表示していくかを書く。
/-----------------------

  $error が空文字でなければ = 1つでもエラーがあれば {
    echo  < ul > . < li >で囲まれた各エラーメッセージ . < /ul >
  } $errorが空文字なら = エラーがなければ {
  ?>

<dl>
    <dt>お名前</dt>
    <dd>
      <?php //your_nameに入っている値を表示 ?>
    </dd>
    <dt>メールアドレス</dt>
    <dd>
      <?php  //your_emailに入っている値を表示 ?>
    </dd>
    <dt>お問い合わせ内容</dt>
    <dd>
      <?php  //inquiryに入っている値を表示  ?>
    </dd>
</dl>


<?php    
   } //最後の閉じタグ
?>

if if と if else は違う

タイトルの通り、if文についての個人的に勘違いしていたことです。

やりたかったこと

1~30までの数字を出し、3の倍数の時は「あほ」になり、5の倍数の時は「犬」になる。

最初に書いたコード

var number;

for (var i = 1; i <= 30; i++) {
  number = i;
  if (i % 3 == 0) {
    number = number + "あほ";
  } else if (i % 5 == 0) {
    number = number + "犬";
  }
  document.write(number + "<br>");
}

これで、実際に出たのはこちら。 1 2 3あほ 4 5犬 6あほ 7 8 9あほ 10犬 11 12あほ 13 14 15あほ 16 17 18あほ 19 20犬 21あほ 22 23 24あほ 25犬 26 27あほ 28 29 30あほ

一見できてるっぽいけど、3の倍数でもあり5の倍数でもある15と30に「あほ」としか表示されていません。 つまり、現時点では「3の倍数」であると判断されているということ。

var number;

for (var i = 1; i <= 30; i++) {
  number = i;
  if (i % 3 == 0) {
    number = number + "あほ"; //ここで終わっている
  } else if (i % 5 == 0) {
    number = number + "犬";
  }
  document.write(number + "<br>");
}

なぜこうなるのか?

if{...}else if{...}文は、複数ある{...}のうち、1つしか処理されない。 つまり、今回のコードでは、まず if(i % 3 == 0){...}を処理して、trueだったら 「当てはまるやんけ〜!終わり!」 で、その下の処理は全く見ない。

ではどうしたらいいのか

答えは非常に簡単だった。 else ififに変えるだけ。

var number;

for (var i = 1; i <= 30; i++) {
  number = i;
  if (i % 3 == 0) {
    number = number + "あほ";
  }
  if (i % 5 == 0) { //ここをifにする
    number = number + "犬";
  }
  document.write(number + "<br>");
}

こうすると、ifの中身はとりあえず全部処理しようとするので、 一つ目の処理が当てはまっても、 「当てはまったで〜!ほな、次の条件は...おっ、これも当てはまる!」 という感じで、 当てはまるもの全ての処理をする。

今回の式で言うと、

var number;

for (var i = 1; i <= 30; i++) {
  number = i;
  if (i % 3 == 0) {
    number = number + "あほ"; //当てはまったで〜 → 「あほ」を出力
  }
  if (i % 5 == 0) { 
    number = number + "犬"; //当てはまったで〜 → 「犬」を出力
  }
  document.write(number + "<br>");
}

//2つにあてはまったら「あほ」「犬」の順番で出力される。

と言うこと。

if文は全部else ifで処理したことしかなかったので、勉強になりました。

ハンバーガーメニューの作り方(JSで要素を作ろう編)

こんにちは。 webデザイン学習中、mamenoです。

前回、ハンバーガーメニューの作り方(疑似要素で頑張ろう編)という記事で、ハンバーガーメニューが開いた時の背景を疑似要素で作るパターンについて書きました。 mameno-web.hatenablog.com

今回は疑似要素を使うのではなく、JavaScriptで背景用の空タグを生成する方法で書いていきたいと思います。

※1~3(JS編-1)までは前回とほぼ同じです。
メニュー展開時の背景のマスクについてについてだけ変更しています。

今回のゴール

今回作るものは、前回同様こちらのハンバーガーメニュー。

動きとしては、 1. 三本線(ハンバーガーメニュー)をクリック(タップ) 2. 上からメニューが出てきて、背景が暗くなり、ハンバーガーは×ボタンになる 3. ×ボタン or 暗い背景をクリック(タップ)するとメニューが閉じる となります。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

この時の暗い背景を、三本線(ハンバーガーメニュー)をクリックしたときにJavaScriptspanタグを生成して作っちゃおうというお話です。

1. ハンバーガーメニューを作る(HTML編)

まずは、HTMLを記述します。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

今回、ハンバーガメニューを作るにあたって大切にしたことは、2つ。

  • そもそものHTMLに空の要素は作らない
  • スマホバージョンとPCバージョンのメニューは分けない

順に説明していきます。

空の要素を作らない

三本線アイコンについて

ハンバーガーメニューのアイコンというと、HTML上にspanタグを3つ並べて作るパターンをよく見かけます。
が、できればhtml上には不要な要素は置いておきたくない。
ということで、今回はbuttonタグの中にspanタグを一つ入れそこに一本線を作り、 :before:afterで残り2本を作るという方法にしました。

<button type="button" class="c-button p-hamburger js-hamburger" aria-controls="global-nav" aria-expanded="false">
  <span class="p-hamburger__line">
          <span class="u-screenReaderText">メニューを開閉する</span> //ここはスクリーンリーダー用で画面上には表示されない
  </span>
</button>

メニュー展開時の背景のマスクについて

ハンバーガーメニューが開いた時の背景のマスクについては、ハンバーガーメニューが押されたらJS側でspanタグを入れるようにするので、厳密に空要素が全くないとは言えませんが、初めからあるということは避けるようにしました。

スマホバージョンとPCバージョンのメニューは分けない

メニューを分けると、HTML上にスマホバージョンと PCバージョンのメニューが2つ存在することになるわけです。
見た目上は出し分けているので問題ないかもしれませんが、こちらも音声読みあげソフトを使用した時には、 2回メニューが読まれるわけなので鬱陶しいかなということで、HTMLは最小限の記述でcssでなんとかしていくという方法にしました。 (今回は非常にシンプルなメニューだったということもあり)

音声読み上げ用クラスについて

ちなみに、.u-screenReaderTextクラスがついているspanは、 音声読み上げソフトを使用した際に「ここを押すとナビゲーションメニューが開閉できますよ〜」と教えるためのもので、 画面上には必要ないので、後述するcssで見えなくしておきます。

2. ハンバーガーメニューを作る(CSS編)

次はCSSです。

  1. .u-screenReaderText用に隠す記述
  2. ナビゲーションのスタイル
  3. ハンバーガーボタンのスタイル

を順番に記述します。 (767px以下でハンバーガーメニューに変わります。まだ動きません。)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

ハンバーガーメニューのcssについては、いろいろな方が書かれているのでここでは割愛します。

3.ハンバーガーメニューの挙動を作る(JS編-1)

  1. CSSに、.is-active-drawerがついた時の要素について追記(CodePen CSSの下にまとめてあります)
  2. JSにて、ハンバーガーボタンに.is-active-drawerをつける(外す)処理と、aria-expandedのtrue/falseを入れ替える処理の記述(この時点ではハンバーガーボタンのみでメニューの開閉)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

aria-expandedって?

要素、またはそれが制御する別のグループ化要素が現在展開されているか折りたたまれているかを示します。
W3Cリファレンス より

なくても見た目には影響しませんが、アクセシビリティ的な観点から記述した方がいいものです。多分。
クラスの付け替えだけだと、aria-expandedがfalseなのに見た目では展開している感じになってしまうのできちんと書き換える処理を加えています。

4.JavaScriptで背景を生成して制御する(JS編-2)

  1. CSSに、is-active-maskがついたspan要素について記述(先ほどのさらに下に追記)
  2. JavaScriptに、ハンバーガーボタンを押したときの背景の動きについて記述

ハンバーガーボタンを押した時の処理の中に、以下を追記します。

//マスク用spanタグの生成
 const newEl = document.createElement("span");
 newEl.classList.add("is-active-mask");

if (document.querySelector(".is-active-mask") === null) {
//is-active-maskが存在しなければ処理をする
    globalNav.appendChild(newEl).addEventListener("click", () => {
      body.classList.remove("is-active-drawer");
      hamburgerButton.setAttribute("aria-expanded", false);
    });
}

動きとしては、
ハンバーガーメニューがクリック(タップ)される→
is-active-maskというクラスがついたspanタグを生成する→
is-active-maskというクラスがあるかどうかをif文で判別し、存在しなければglobalNav(.js-globalNavクラスがついた要素)の一番最後の子要素に追加する→
そしてその要素にそのままクリックイベントを定義する
という流れになります。

以下はそのデモになります。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

終わりに

いかがでしたでしょうか。
今回は、JSで要素を生成してクリックイベントを定義する方法で作ってみました。
このコードのポイントは、作った要素があるかどうかを判別するのを忘れないことでしょうか。
これをしないと、「そんな要素ないよ!!」って怒られます。

ということで、これが何かの参考になれば嬉しいです。
ご覧いただき、ありがとうございましたー

ハンバーガーメニューの作り方(疑似要素で頑張ろう編)

こんにちは。webデザイン学習中、mamenoです。
今回は、ハンバーガーメニューとその挙動について、私なりに考えて作ってみたのでそのことについて書いていきたいと思います。

今回のゴール

今回作るものはこちらのハンバーガーメニュー。

動きとしては、
1. 三本線(ハンバーガーメニュー)をクリック(タップ)
2. 上からメニューが出てきて、背景が暗くなり、ハンバーガーは×ボタンになる
3. ×ボタン or 暗い背景をクリック(タップ)するとメニューが閉じる
というものになります。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

なんの変哲もないハンバーガーメニューですが、一般的に調べるとよく出てくるものと違う点が1つ。
それは、背景のマスク部分を疑似要素で作っているということ。

疑似要素は、JacaScriptでは直接指定ができないので、マスク部分をクリックした時の処理を書くのにちょっとした工夫が必要でした。
ので、備忘録的に記事にしていきたいと思います。

1. ハンバーガーメニューを作る(HTML編)

まずは、HTMLを記述します。

See the Pen Untitled by mameno (@mameno_design) on CodePen.

今回、ハンバーガメニューを作るにあたって大切にしたことは、2つ。

  • そもそものHTMLに空の要素は作らない
  • スマホバージョンとPCバージョンのメニューは分けない

順に説明していきます。

空の要素を作らない

三本線アイコンについて

ハンバーガーメニューのアイコンというと、HTML上にspanタグを3つ並べて作るパターンをよく見かけます。
が、できればhtml上には不要な要素は置いておきたくない。
ということで、今回はbuttonタグの中にspanタグを一つ入れそこに一本線を作り、 :before:afterで残り2本を作るという方法にしました。

<button type="button" class="c-button p-hamburger js-hamburger" aria-controls="global-nav" aria-expanded="false">
  <span class="p-hamburger__line">
          <span class="u-screenReaderText">メニューを開閉する</span> //ここはスクリーンリーダー用で画面上には表示されない
  </span>
</button>

メニュー展開時の背景のマスクについて

ハンバーガーメニューが開いた時の背景のマスクについても、<span class="mask"></span>みたいな空要素を入れることはせずbodyの疑似要素で対応することにしています。

音声読み上げ用クラスについて

ちなみに、.u-screenReaderTextクラスがついているspanは、
音声読み上げソフトを使用した際に「ここを押すとナビゲーションメニューが開閉できますよ〜」と教えるためのもので、
画面上には必要ないので、後述するcssで見えなくしておきます。

スマホバージョンとPCバージョンのメニューは分けない

メニューを分けると、HTML上にスマホバージョンと PCバージョンのメニューが2つ存在することになるわけです。
見た目上は出し分けているので問題ないかもしれませんが、こちらも音声読みあげソフトを使用した時には、 2回メニューが読まれるわけなので鬱陶しいかなということで、HTMLは最小限の記述でcssでなんとかしていくという方法にしました。
(今回は非常にシンプルなメニューだったということもあり)

2. ハンバーガーメニューを作る(CSS編)

次はCSSです。

  1. .u-screenReaderText用に隠す記述
  2. ナビゲーションのスタイル
  3. ハンバーガーボタンのスタイル

を順番に記述します。 (767px以下でハンバーガーメニューに変わります。まだ動きません。)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

ハンバーガーメニューのcssについては、いろいろな方が書かれているのでここでは割愛します。

3.ハンバーガーメニューの挙動を作る(JS編-1)

  1. CSSに、.is-active-drawerがついた時の要素について追記(CodePen CSSの下にまとめてあります)
  2. JSにて、ハンバーガーボタンに.is-active-drawerをつける(外す)処理と、aria-expandedのtrue/falseを入れ替える処理の記述(この時点ではハンバーガーボタンのみでメニューの開閉)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

aria-expandedって?

要素、またはそれが制御する別のグループ化要素が現在展開されているか折りたたまれているかを示します。
W3Cリファレンス より

なくても見た目には影響しませんが、アクセシビリティ的な観点から記述した方がいいものです。多分。
クラスの付け替えだけだと、aria-expandedがfalseなのに見た目では展開している感じになってしまうのできちんと書き換える処理を加えています。

4.背景をクリックした時でもメニューが閉じるようにする(JS編-2)

ここがとっても難しかったです。
なぜかというと、前述した通り擬似要素はJavaScriptで直接指定できないから。

なので、擬似要素を指定するのではなく、ナビゲーション以外のbody部分を指定するという方法で背景を選択します。

     //背景部分をクリックしたときに閉じる処理
       body.addEventListener("click", (e) => {

      //.js-globalNav(ナビゲーション)部分だけを除外する処置
      const hasClass = e.path.filter((el) => {
        //クラスがついている要素かつクラスに「js-globalNav」がついている要素を変数「hasClass」に配列で取得
        return el.classList && el.classList.contains("js-globalNav");
      });

      if (hasClass.length) {
        //hasClass.lengthの値が1以上(true)だったら何もしない
        return;
      }

      //hasClass.lengthの値が0(false)だったら、js-globalNavは含まれていないので以下の処理を実行する
      // bodyのクラスとarea-expandedの処理
      body.classList.remove("is-active-drawer");
      hamburgerButton.setAttribute("aria-expanded", false);
    });

細かく解説してみる。

  1. bodyにクリックイベントを定義
  2. ナビゲーション部分を除外する処理をする

    • イベントリスナー(e)の中にある、pathプロパティにアクセスすると、その要素が何に包まれているのかを全て取得してくることができるので、それを利用します。
      試しにconsole.loge.pathの値を出してみました。

アセット 1-80.jpg

↑配列の中に「.js-globalNav」が含まれています。

アセット 2-80.jpg

↑配列の中に「.js-globalNav」が含まれていません。

  • e.pathで取得できる上記の配列に、filter()メソッドを使って当てはまる要素だけを抽出し、hasClassで受け取ります。
  • hasClassに値が入っている = .js-globalNav が含まれている場合は、メニューを閉じたくないので何もせずreturn

  • ナビゲーション部分の除外ができたら、あとはbodyをクリックしたらクラスを外す&aria-expandedをfalseにする記述をするだけ

で、以下のように動きます。 (背景部分をクリックしてみてください)

See the Pen Untitled by mameno (@mameno_design) on CodePen.

おまけ(header要素もクリックから除外したいとき)

今回のこのデモだとheaderがそもそもないのですが、headerもクリックの対象から除外したい場合、普通にありますよね。
この場合は、2パターン解決策を考えました。

<パターン1>

HTMLを記述する際に、headerの中にナビゲーションを入れちゃう

そうすれば、el.classList.contains("js-globalNav")としていたところをel.classList.contains("js-header")みたいにしたらheader部分も除外対象に含まれます。

<パターン2>

以下のコードを除外する処理の前に記述する

    [headerを取得するための変数].addEventListener("click", (e) => {
        e.stopPropagation();
     });

stopPropagationを使えば、イベントが止まります。

まとめ

いかがでしたでしょうか。
4つ目の要素の除外については私だけでは思いつかず、入会しているコミュニティの先生にご教授いただき、ここまで作ることができました。
空のタグを使えば簡単な処理ですが、きれいなマークアップを考えるとこういう書き方もできたらいいかなと思い、チャレンジしてみました。
他にもJavaScriptで空の要素(mask用)を生成する方法も考えたので、後日ブログにまとめたいと思います。

ご覧いただき、ありがとうございました。