【もりけん塾】JS課題30 – Vanilla JSでドロワーメニューを作成 –

JavaScript

Vanilla JSでドロワーメニューの基本機能を作成しました。

こんにちは。Webコーダーのはるです。

現在、所属している「もりけん塾」でJavaScriptのハンズオン課題 に取り組み、レビューをいただいています。

今日は、課題30 についてアウトプットをしていきます。
(前回までのアウトプットは、こちらです。)

実装する過程で学んだことを、学習ノートとして記録していきます。

認識の間違えている箇所がありましたら、お問い合わせからご指摘いただけるとうれしいです。

JavaScript課題30の仕様

    (もりけんさんのJavaScriptハンズオン課題より)

    JavaScript課題30の学習記録

    主にレビューいただいた箇所の復習として記録を残します。

    制作物

    HTML

    ログイン・会員登録画面の header に <nav> を追加しました。

    <header class="header" id="js-header">
     <div class="header__inner">
      <h1 class="title title--top title--center" id="js-title"><a href="./" class="header__link">haru news</a></h1>
      <nav class="header__nav" id="global-nav" data-name="drawer-menu" aria-hidden="true">
       <div class="header__nav-inner">
        <ul class="header__nav-list">
         <li class="header__nav-item"><a href="./register.html" class="header__link" tabindex="2">Sign up</a></li>
         <li class="header__nav-item"><a href="./login.html" class="header__link" tabindex="3">Login</a></li>
        </ul>
        <ul class="header__nav-list">
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
         <li class="header__nav-item"><a href="#" class="header__link">Dummy link</a></li>
        </ul>
       </div>
      </nav>
      <button type="button" id="js-hamburger-button" class="header__hamburger" aria-controls="global-nav" aria-expanded="false" tabindex="1">
       <span class="header__hamburger-bar"><span class="header__hamburger-text">メニューを開閉する</span></span>
      </button>
     </div>
    </header>

     ドロワーメニューの開閉

    ハンバーガーボタンのクリックで、メニューを開閉するようにしました。

    hamburgerButton.addEventListener("click", (e) => {
     if(isOpen(e.target)){
      closeMenu(e.target, drawerMenu);
     } else {
      openMenu(e.target, drawerMenu);
     }
    })

    メニューの開閉状態を検証

    メニューが今開閉どちらなのかは、isOpen()関数で検証します。

    const isOpen = (button) => button.getAttribute("aria-expanded") === "true";

    ハンバーガーボタンにaria-expanded属性をつけて、開いているときはtrue、閉じているときはfalseに変更するようにしたので、getAttributeで値を確認しました。

    コントロールが展開されているか折りたたまれているか、および制御された要素が表示されているか非表示になっているかを示すために、要素に設定されます。(MDN)

    レビュー①
    元々isOpenState()関数という名前にしていましたが、よりシンプルなisOpen()に修正しました。is openというのが既に状態なのでstateはなくてよかった。
    レビュー②
    e.target部分を全て変数hamburgerButtonにしていたのですが、e.targetとしたほうがスマートなコードになるとご指摘いただきました。読みやすい、かつ、targetが変更になったときも最初のhamburgerButtonだけへ変更すれば良いのでそういった意味でもスマートになるのだと解釈しました。

    メニューを開くとき

    const openMenu = (button, menu) => {
     body.classList.add("is-drawer-active");
     menu.classList.add("is-open");
     menu.setAttribute("aria-hidden", false);
     button.setAttribute("aria-expanded", true);
     toggleInertAttribute(focusControlTargets,true); //後ほど記録します
    }
    • <body>にis-drawer-activeクラスをつけて、背景を固定&オーバーレイ(Safariまだ対応できていない)
    body.is-drawer-active{
     overflow-y: hidden;
    }
    
    body.is-drawer-active:after{
     display: block;
     content: "";
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     height: 100%;
     background-color: rgb(48, 55, 63, 0.9);
    }
    • メニューにis-openクラスをつけて、メニューを表示
    .header__nav.is-open{
     transform: translateX(0); //translateX(-100%)から移動
    }
    • メニューのaria-hidden属性をfalseに変更
    • ハンバーガーボタンのaria-expanded属性をtrueに変更
    • toggleInertAttribute(focusControlTargets,true);については後ほど記載

    メニューを閉じるとき

    const closeMenu = (button, menu) => {
     body.classList.remove("is-drawer-active");
     menu.classList.remove("is-open");
     menu.setAttribute("aria-hidden", true);
     button.setAttribute("aria-expanded", false);
     toggleInertAttribute(focusControlTargets,false);
    }

    openMenu()の逆です。

    inert属性

    メニューが開いているときに裏側のコンテンツにフォーカスを当てない・読み上げを行わないために、新しい属性inertを学習しました。

    フォーカスイベントや支援技術からのイベントを含む、要素に対するユーザーの入力イベントをブラウザーが「無視」するようにします。ブラウザーは、要素でのページ検索やテキスト選択も無視することができます。これは、モーダルのような UI を構築する際に、モーダルが表示されているときにフォーカスをモーダル内に「閉じ込める」場合に便利です。(MDN)

    HTMLの不活性にさせたい要素にinert属性をつけると、その要素とサブツリーの要素が無視されます。

    厳密には、2023年3月時点でfirefox非対応のためPollyfillを読み込む必要があります。

    しかし、Interop 2023にinert属性も取り上げられていたため年内に対応になるのではと思い、学習として使用してみることにしました。

    メニューが開いているとき、裏側のタイトルとフォームを不活性にしたいので、それらを実現する関数を作成しました。

    const toggleInertAttribute = (targets, boolean) => {
     targets.forEach(target => {
      target.inert = boolean;
     })
    };
    
    // 使用
    toggleInertAttribute(focusControlTargets,true);
    toggleInertAttribute(focusControlTargets,false);
    
    
    focusControlTargetには、不活性にしたい要素の配列を渡しました。
    
    const focusControlTargets = [document.getElementById("js-form"), document.getElementById("js-title")];

    論理値で、その要素が不活性である場合は true、それ以外の場合はこの値は false になります。(MDN)

    メニュー以外の部分をクリックするとメニューを閉じる

    仕様外ですが、メニュー外を押してもメニューが閉じるようにしました。

    document.addEventListener("click", (e) => {
     if (e.target.classList.contains("is-drawer-active")) {
      closeMenu(hamburgerButton, drawerMenu);
     }
    });
    

    e.targetに is-drawer-acrtiveクラスがついているときはメニューを閉じます。

    <body>にis-drawer-activeクラスがついているときはメニューが開いているときであり、メニュー以外の部分を指します。

    Escapeキーでメニューを閉じる

    こちらも仕様外ですが実装しました。

    Escapeキーを特定するために、KeybordEventのプロパティをいくつか調べました。

    • KeyboardEvent.keyCode -> 非推奨になった。キーに番号が割り当てられていた
    • KeyboardEvent.key -> ユーザーが押したキーの名前を文字列で返す
    • KeyboardEvent.code -> イベントを発生させたボタンに対応するコードを文字列で返す

    実装方法を調べていると、KeyboardEvent.keyCodeを仕様している記事が多くあったのですが、MDNで確認したところ現在は非推奨のプロパティになっていました。

    それ以外のKeyboardEvent.keyとKeyboardEvent.codeの違いは、キーの名前を返すか、キーの位置に対応するコードを返すかでした。

    後者は、

    返ってきた code が “KeyQ” は QWERTY レイアウトのキーボードでは Q キーですが、同じ Dvorak キーボードでは同じ code の値が ‘ キーを表し、 AZERTY キーボードでは A キーを表すものでもあります。したがって、すべてのユーザーが特定のキーボードレイアウトを使用しているわけではないため、 code の値を用いてユーザーが認識しているキーの名前が何であるかを特定することはできません。(MDN

    とあり、キーボードを使用するゲームなどに適していると知りました。

    今回は、KeyboardEvent.keyを使用しました。

    document.addEventListener("keydown", (e) => {
     if(e.key === "Escape") closeMenu(hamburgerButton, drawerMenu);
    })

    Escapeキーを押すと、e.keyで“Escape”という文字列が返ってきます。

    キーが押された時に発火するkeydownイベントも知りました。

     

     

    //
    学習に使用している本は、JavaScript本格入門・独習JavaScriptです。

      

    あとがき

    もりけんさん(@terrace_tec)、さえさん(@sae_prog)、まいさん(@mai2022web)、ちひろさん(@chihiro7029) 、takakoさん(@wattwattwatt)お忙しい中レビューいただきありがとうございました!

    今回は5回に分けて細切れにPRを出したのですが、毎回丁寧に動作確認していただきました。感謝です。

    TabキーやEscキーでの遷移や、アクセシビリティに対応する実装を調べて試すことができました。

    実装方法の記事を検索して知ったプロパティを調べてみると非推奨になっていることもあり、改めて使用するコードは自分でMDNなどで調べて根拠を持って使用していきたいと思いました。

    教えていただいたことを生かして、次の課題に進みたいと思います!

    今日は以上です。

    //

    【もりけん塾で勉強しています】

    もりけんさん(@terrace_tec)のHPはこちら

    タイトルとURLをコピーしました