mcpField
D
dialog-patterns

dialog-patterns

github.com/majesticlabs-devデザイン・UI5063 回インストール7 週間前に更新
リポジトリ

dialog-patterns について

dialog-patternsは、アクセシブルなダイアログやオーバーレイを作るRails開発者向けのスキルです。ネイティブHTML dialog、Turbo Frames、Stimulusを使う方法に加え、JavaScript不要の確認パターンも扱います。

主な機能

  1. 01ネイティブダイアログのアクセシビリティ
  2. 02JavaScript不要の確認ダイアログ
  3. 03Turbo Frameによる非同期モーダル
  4. 04トーストとスライドオーバーのパターン
  5. 05フォーム検証への対応

活用シーン

  1. 01Railsでモーダルフォームを構築
  2. 02破壊的操作の確認
  3. 03トースト通知の表示
  4. 04フィルター用サイドパネルの作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/majesticlabs-dev/majestic-marketplace --skill dialog-patterns

インストール後、Claude Code で/dialog-patternsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
dialog-examples.md
toast-slideover-patterns.md
zero-js-patterns.md

詳細

Rails 向けネイティブダイアログのパターン

ネイティブ HTML の <dialog> 要素を Turbo Frames および Stimulus と組み合わせて、アクセシブルでモダンなダイアログ UI を構築できます。JavaScript フレームワークや大規模なライブラリは不要です。

このスキルを使用する場面

  • フォーム、確認、コンテンツ用のモーダルダイアログを構築するとき
  • トースト通知やアラート通知を作成するとき
  • 確認ダイアログ(削除、破壊的な操作)を実装するとき
  • フォーカス管理とアクセシビリティが必要なオーバーレイ UI を実装するとき

ネイティブの <dialog> を使用する理由

機能ネイティブの <dialog>カスタムモーダル
フォーカストラップ組み込み手動で実装
ESC キーで閉じる組み込み手動で実装
背景組み込み(::backdrop)手動でオーバーレイを実装
アクセシビリティネイティブの role="dialog"ARIA を手動で実装
最前面レイヤー自動(すべてのコンテンツの上)z-index の調整が必要
スクロールロック自動overflow: hidden を手動で設定

JavaScript 不要の確認ダイアログ(推奨)

最新のブラウザーは、宣言的にダイアログを制御できる Invoker Commands API に対応しています。JavaScript は不要です。詳しい例は references/zero-js-patterns.md を参照してください。

クイックリファレンス
<%= button_tag "Delete", commandfor: "delete-#{post.id}", command: "show-modal" %>

<dialog id="delete-<%= post.id %>" closedby="any" role="alertdialog">
  <h3>Delete "<%= post.title %>"?</h3>
  <button commandfor="delete-<%= post.id %>" command="close">Cancel</button>
  <%= button_to "Delete", post, method: :delete %>
</dialog>
主な属性
属性用途
commandfor="id"制御するダイアログを指定します
command="show-modal"モーダルとして開きます(背景、フォーカストラップ)
command="close"ダイアログを閉じます
closedby="any"背景クリックと ESC キーで閉じられるようにします
JavaScript 不要の方法と Stimulus の使い分け
シナリオ方法
シンプルな確認JavaScript 不要(Invoker Commands)
非同期コンテンツを含むモーダルStimulus + Turbo Frames
複雑な複数ステップのダイアログStimulus コントローラー
アニメーションCSS @starting-style
その他のパターン(references/ を参照)
  • @starting-style を使用した、表示・非表示の切り替え用 CSS アニメーション
  • 見づらいブラウザーのダイアログを置き換える Turbo.config.forms.confirm
  • クロスブラウザー互換性のための プログレッシブエンハンスメント

基本パターン:Turbo Frames を使った非同期モーダル

Rails のモーダルには、次の 3 つの技術を組み合わせるパターンを推奨します。

  1. Turbo Frame - ページを再読み込みせずに非同期でコンテンツを読み込みます
  2. ネイティブの <dialog> - アクセシブルなモーダル表示を実現します
  3. Stimulus コントローラー - ライフサイクルを管理します
ステップ 1:レイアウトコンテナー

レイアウトにモーダル用の turbo-frame を追加します。

<%# app/views/layouts/application.html.erb %>
<body>
  <%= yield %>

  <%# Modal injection point %>
  <%= turbo_frame_tag :modal %>
</body>
ステップ 2:トリガーリンク

任意のリンクからモーダルフレームをターゲットにします。

<%# Any view %>
<%= link_to "New Post", new_post_path, data: { turbo_frame: :modal } %>
<%= link_to "Edit", edit_post_path(@post), data: { turbo_frame: :modal } %>
<%= link_to "Confirm Delete", confirm_delete_post_path(@post), data: { turbo_frame: :modal } %>
ステップ 3:モーダルコンテンツのビュー

モーダルコンテンツを、内側のフレームを含む一致する turbo-frame で囲みます。

<%# app/views/posts/new.html.erb %>
<%= turbo_frame_tag :modal do %>
  <%# Inner frame prevents flash during form validation %>
  <%= turbo_frame_tag :modal_content do %>
    <dialog data-controller="dialog" data-action="click->dialog#clickOutside" open>
      <article>
        <header>
          <h2>New Post</h2>
          <button data-action="dialog#close" aria-label="Close">&times;</button>
        </header>

        <%= render "form", post: @post %>
      </article>
    </dialog>
  <% end %>
<% end %>
ステップ 4:Stimulus コントローラー

主な動作:接続時に showModal() を呼び出し、切断時に replaceChildren() を呼び出して古いコンテンツを防ぎ、clickOutside で背景クリックによる閉じる操作に対応します。

Stimulus コントローラー全体、CSS のスタイル設定、Tailwind バリアントについては、references/dialog-examples.md を参照してください。

Turbo Frames をネストする理由

フレームをネストするパターン(modal > modal_content)を使うと、コンテンツのちらつきを防げます。

<%= turbo_frame_tag :modal do %>
  <%= turbo_frame_tag :modal_content do %>
    <dialog>...</dialog>
  <% end %>
<% end %>

ネストしたフレームを使わない場合の問題: モーダル内のフォームでバリデーションエラーが発生して再描画されると、置き換え前に外側のフレームに古いコンテンツが一時的に表示されます。

ネストしたフレームを使う場合の解決策: 内側のフレームがフォームの再描画を個別に処理するため、モーダルの構造が安定します。

モーダル内のフォーム処理

送信成功時

Turbo でリダイレクトしてモーダルを閉じ、ページを更新します。

# app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  if @post.save
    redirect_to posts_path, notice: "Post created!"
  else
    render :new, status: :unprocessable_entity
  end
end

リダイレクトは _top(ページ全体)に移動し、実質的にモーダルを閉じます。

バリデーションエラー

422 ステータスでフォームを再描画し、モーダルを開いたままにします。

render :new, status: :unprocessable_entity
Turbo Stream レスポンス(モーダル内にとどまる)

turbo_stream.update("modal", "") を使用して、全体をリダイレクトせずにモーダルをクリアします。詳しい例は references/dialog-examples.md を参照してください。

確認ダイアログのパターン

破壊的な操作では、confirm_delete メンバールートを追加し、turbo frame 内にダイアログを描画して、data: { turbo_frame: :modal } を指定した link_to で起動します。

確認ダイアログのビュー、ルート、起動方法の全例については、references/dialog-examples.md を参照してください。

アラート/トーストのパターン

フラッシュメッセージや通知に使用します。モーダルではない表示には、showModal() ではなく show() を使用します。実装の全例については、references/toast-slideover-patterns.md を参照してください。

<dialog class="toast" data-controller="toast" data-toast-duration-value="5000">
  <p><%= message %></p>
</dialog>

主な違いは、show() は背景やフォーカストラップなしで開き(トースト)、showModal() は背景付きで中央に表示される(モーダル)ことです。

スライドオーバーパネルのパターン

サイドパネル(設定、フィルター、詳細)に使用します。スタイルとアニメーションについては、references/toast-slideover-patterns.md を参照してください。

<dialog class="slideover" data-controller="dialog" data-action="click->dialog#clickOutside">
  <aside>
    <header><h2>Filters</h2></header>
    <%= render "filters" %>
  </aside>
</dialog>

アクセシビリティ

ネイティブの <dialog> は、フォーカストラップ、ESC キーでのクローズ、背景の非アクティブ化、トップレイヤーへの配置を自動で提供します。さらに、以下も確認してください。

  • 常に表示される閉じるボタンを設置する(ESC キーだけに頼らない)
  • 説明を補うコンテキストとして aria-labelledby/aria-describedby を設定する
  • 閉じたときにフォーカスを起動要素に戻す(connect() で document.activeElement を保存する)

アクセシビリティの強化とフォーカスを戻す例については、references/dialog-examples.md を参照してください。

よく使うパターンの概要

パターンコンテナーStimulusshow メソッド
モーダルフォームturbo_frame_tag :modaldialogshowModal()
確認turbo_frame_tag :modaldialogshowModal()
トースト/アラート固定位置toastshow()
スライドオーバーturbo_frame_tag :modaldialogshowModal()

避けるべきアンチパターン

アンチパターン問題解決策
<dialog> を使わないカスタムモーダルネイティブのアクセシビリティがないネイティブの <dialog> を使用する
ネストした turbo-frame がないバリデーション時にコンテンツがちらつく内側の frame を追加する
閉じるときに frame をクリアしない再表示時に古いコンテンツが残るdisconnect() で replaceChildren() を使ってクリアする
重なり順に z-index を使う他の要素との競合を招く<dialog> はトップレイヤーを使用する
フォーカストラップを手動で実装する複雑で、エラーが起きやすいshowModal() に任せる
インラインの backdrop divマークアップが増える::backdrop 疑似要素を使用する

ダイアログのテスト

# System test - use `within "dialog"` to scope assertions
within "dialog" do
  fill_in "Title", with: "My Post"
  click_button "Create"
end
expect(page).not_to have_selector("dialog[open]")  # Modal closed

ブラウザーのサポート状況

パターンChromeFirefoxSafari
ネイティブの <dialog>37+98+15.4+
Invoker Commands135+144+26.2+
@starting-style117+129+17.5+

古いブラウザー向け:dialog polyfill、invokers polyfill。プログレッシブエンハンスメントの方法については、references/zero-js-patterns.md を参照してください。

コメント