コード例
引き出し(drawer)はよく使われるUIです。ページ遷移をせずに詳細を表示するのに使います。モーダルと似ていますが、モーダルは一般に情報を少しだけ見せ、パッと閉じるものが多いのに対して、引き出しは情報が多く、データ更新も可能で複雑な処理をさせているものをよく見かけます。
aria-selected
属性も変更する必要がありますので、CSS擬似セレクタだけでは不十分ですなお、本来であればモーダル表示で解説しているポイントもすべて検討する必要がありますが、今回は省略しています。UI/UXで満たすべき項目を全て実装する場合は、モーダルの解説もご確認ください。
<!DOCTYPE html> <html> <!-- ... --> <body> <div data-controller="slide-drawer" data-slide-drawer-shown-value="false" class="group/slide-drawer" > <div class="w-full min-w-[768px]"> <nav class="flex h-16"> <!-- ... --> </nav> <div><%= content_for(:breadcrumbs) %></div> <%= yield %> </div> <div id="slide-drawer"> <div class="peer transition-all duration-500 ease-out fixed h-full w-[768px] z-20 bg-white top-0 right-0 overflow-auto aria-hidden:translate-x-[768px]" aria-hidden="true" data-slide-drawer-target="drawer"> <%= button_tag type: :button, data: { action: "click->slide-drawer#hide" }, class: "absolute top-2 right-2 h-14 w-14 p-1 bg-gray-500 text-white hover:bg-gray-400 active:bg-gray-600" do %> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-12"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/> </svg> <% end %> <%= turbo_frame_tag :slide_drawer %> </div> <div class="transition-all duration-500 fixed h-full w-full z-19 bg-black opacity-50 top-0 left-0 peer-aria-hidden:opacity-0 peer-aria-hidden:invisible " aria-hidden="true" data-slide-drawer-target="backdrop" data-action="click->slide-drawer#hide:prevent"> </div> </div> </div> </body> </html>
<div data-controller="slide-drawer" ...>
の箇所でSlideDrawerController
Stimulus controllerと接続しています。初期状態では引き出しは閉じていますので、data-slide-drawer-shown-value="false"
でStimulusのValuesステートの初期状態を記述しています<div id="slide-drawer">
の箇所が引き出しのコードです。ここに記載しているのは引き出しの「枠」であり、引き出しの中身はturbo-frame_tag id="slide_drawer">
の挿入されますbutton_tag
は「閉じる」ボタンで、data-action="click->slide-drawer#hide"
がアクションになります。クリックするとSlideDrawerController
のhide()
メソッドを呼びます。<div ... data-action="click->slide-drawer#hide:prevent">
の箇所は引き出しの背景の黒い幕です。ここをクリックしたときに引き出しを閉じる必要がありますので、SlideDrawerController
のhide()
メソッドを呼ぶようにしていますaria-hidden
属性をCSS擬似セレクタで監視して実現しています(初期設定はaria-hidden="true"
)<div data-controller="carousel" class="relative"> <!-- ... --> </div> <div role="tablist" class="flex flex-row shadow-md border"> <%= link_to "部屋・プラン", "", data: { slide_drawer_target: "tab" }, aria: { selected: true }, class: "px-4 inline-block py-4 h-16 text-lg aria-selected:border-b-4 aria-selected:border-blue-500" %> <%= link_to "宿の紹介", hotel_features_path(@hotel), data: { action: "click->slide-drawer#show", slide_drawer_target: "tab", aria: { selected: false }, turbo_frame: :slide_drawer }, class: "px-4 inline-block py-4 h-16 text-lg aria-selected:border-b-4 aria-selected:border-blue-500" %> <!-- ... --> <%= link_to "---", "", data: { slide_drawer_target: "tab" }, aria: { selected: false }, class: "px-4 inline-block py-4 h-16 text-lg aria-selected:border-b-4 aria-selected:border-blue-500" %> </div> <main class="mt-8 container mx-auto"> <!-- ... --> </main>
data-action="click->slide-drawer#show"
となっているところがタブボタンのActionで、これをクリックするとSlideDrawerController
のshow()
メソッドが呼ばれ、引き出すが表示されます<a>
タグにdata-turbo-frame="slide_drawer"
属性をつけていますので、クリックすると自動的にTurbo Framesでリクエストが送信され、<turbo-frame id="slide_drawer">
にレスポンスが挿入されますaria-selected
属性も変更する必要があります(アクセシビリティの要件)。そのためdata-slide-drawer-target="tab"
として、SlideDrawerController
からtargetとして制御できるようにしなければなりませんaria-selected
をCSS擬似セレクタで監視することによって実装しています<%= turbo_frame_tag :slide_drawer do %> <section class="max-w-[768px] "> <header class="h-16 py-1 px-4 flex justify-between items-center"> <h2 class="text-3xl font-bold">宿の紹介</h2> </header> <div class="px-4"> <nav class="mt-8 h-16 bg-gray-100 flex justify-between items-center"> <%= link_to "トピックス", hotel_features_path(@hotel), class: "text-center py-5 flex-1 h-full block px-4 border-blue-500 border-b-4" %> <%= link_to "お部屋", room_hotel_features_path(@hotel), class: "text-center py-5 flex-1 h-full block px-4" %> <!-- ... --> </nav> <!-- ... --> </div> </section> <% end %>
<turbo-frame id="slide_drawer">
に囲まれていますので、これが上記のTurbo Framesに挿入されます<turbo-frame id="slide_drawer">
に囲まれていますので、デフォルトではここのTurbo Frameの中だけを置換します(つまりページ全体をナビゲーションするのではなく、Turbo Frameの中だけをナビゲーションします)import { Controller } from "@hotwired/stimulus" // Connects to data-controller="slide-drawer" export default class extends Controller { static values = { shown: {type: Boolean, default: false}, selectedTab: {type: Number, default: 0}, }; static targets = ["drawer", "tab"] connect() { } show(event) { this.shownValue = true this.selectedTabValue = this.tabTargets.indexOf(event.currentTarget) } hide() { this.shownValue = false this.selectedTabValue = 0 } shownValueChanged() { this.#render() } #render() { if (this.shownValue) { document.body.style.overflow = "hidden" this.drawerTarget.ariaHidden = false } else { document.body.style.overflow = "auto" this.drawerTarget.ariaHidden = true } this.tabTargets.forEach((target, i) => { target.ariaSelected = (i === this.selectedTabValue) }) } }
shown
は引き出しの開閉状態、selectedTab
は何番目のタブボタンが選択されているかを保管していますtargets
を定義しています。引き出しの"drawer"とタブボタンの"tab"を指定しています
data-slide-drawer-shown-value
)aria-*
属性を変更するには、JavaScriptでDOMを書き換える必要があります。このためにtargets
を使用していますshow()
, hide()
はタブボタンや「閉じる」ボタン、背景の黒い幕をクリックしたときに引き出しを開閉するアクションです。アクションの中ではValuesステートだけを変更して、ここではDOM操作を行いませんshowValueChanged()
は、Valuesステートが変更された時に自動的に呼ばれるコールバックです。ここでは#render()
メソッドを呼び出します。#render()
でValuesステートに応じて、実際にDOMを変更します。眼にみえる表示状態はCSS擬似セレクタですでに制御されていますので、ここではaria-*
属性のみを変更しますaria-*
属性設定箇所があるため、気をつけないとコードは複雑になりがちですaria-*
属性を変更する場合は、Stimulusのtarget
を使って、JavaScriptでDOM操作をすることになります