关于 dialog-patterns
dialog-patterns 面向 Rails 开发者,提供构建无障碍对话框和覆盖界面的模式。内容涵盖原生 HTML dialog、Turbo Frames 与 Stimulus,也包括无需 JavaScript 的确认方案。
主要功能
- 01原生对话框无障碍支持
- 02无需 JavaScript 的确认框
- 03Turbo Frame 异步模态框
- 04Toast 与侧滑面板模式
- 05表单验证处理
适用场景
- 01在 Rails 中构建模态表单
- 02确认破坏性操作
- 03显示 Toast 通知
- 04创建筛选器侧边面板
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ majesticlabs-dev/ majestic-marketplace --skill dialog-patterns安装后在 Claude Code 中输入/dialog-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/dialog-examples.mdtoast-slideover-patterns.mdzero-js-patterns.md详细介绍
Rails 的原生对话框模式
使用原生 HTML <dialog> 元素、Turbo Frames 和 Stimulus,构建现代、易于访问的对话框 UI。无需 JavaScript 框架或重量级库。
何时使用此技能
- 为表单、确认操作或内容构建模态对话框
- 创建 Toast/提醒通知
- 实现确认对话框(删除、破坏性操作)
- 构建任何需要焦点管理和无障碍支持的叠加层 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 模态框的推荐模式结合了三种技术:
- Turbo Frame - 无需重新加载页面即可异步加载内容
- 原生
<dialog>- 无障碍的模态呈现 - Stimulus 控制器 - 生命周期管理
步骤 1:布局容器
在布局中添加一个 modal turbo-frame:
<%# app/views/layouts/application.html.erb %>
<body>
<%= yield %>
<%# Modal injection point %>
<%= turbo_frame_tag :modal %>
</body>
步骤 2:触发链接
从任意链接指向 modal frame:
<%# 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 中,并嵌套内层 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">×</button>
</header>
<%= render "form", post: @post %>
</article>
</dialog>
<% end %>
<% end %>
步骤 4:Stimulus 控制器
关键行为:连接时调用 showModal(),断开连接时调用 replaceChildren()(防止内容过时),使用 clickOutside 关闭背景遮罩。
完整的 Stimulus 控制器、CSS 样式和 Tailwind 变体,请参见 references/dialog-examples.md。
为什么要嵌套 Turbo Frames?
嵌套 frame 模式(modal > modal_content)可避免内容闪烁:
<%= turbo_frame_tag :modal do %>
<%= turbo_frame_tag :modal_content do %>
<dialog>...</dialog>
<% end %>
<% end %>
未使用嵌套 frame 时的问题: 模态框中的表单出现验证错误并重新渲染时,外层 frame 会先短暂显示旧内容,然后才替换内容。
使用嵌套 frame 的解决方案: 内层 frame 独立处理表单重新渲染,使模态框结构保持稳定。
在模态框中处理表单
提交成功
使用 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。
提示消息/Toast 模式
用于 flash 消息和通知。非模态呈现时使用 show(),而不是 showModal()。完整实现请参阅 references/toast-slideover-patterns.md。
<dialog class="toast" data-controller="toast" data-toast-duration-value="5000">
<p><%= message %></p>
</dialog>
主要区别:show() 打开时没有背景遮罩或焦点陷阱(用于 Toast),而 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。
常见模式摘要
| 模式 | 容器 | Stimulus | show 方法 |
|---|---|---|---|
| 模态表单 | turbo_frame_tag :modal | dialog | showModal() |
| 确认对话框 | turbo_frame_tag :modal | dialog | showModal() |
| Toast/提示消息 | 固定位置 | toast | show() |
| 滑出面板 | turbo_frame_tag :modal | dialog | showModal() |
应避免的反模式
| 反模式 | 问题 | 解决方案 |
|---|---|---|
不使用 <dialog> 的自定义模态框 | 缺少原生无障碍支持 | 使用原生 <dialog> |
| 缺少嵌套 turbo-frame | 验证时内容闪烁 | 添加内层 frame |
| 关闭时不清空 frame | 重新打开时显示过时内容 | 在 disconnect() 中使用 replaceChildren() 清空 |
| 使用 z-index 控制层叠 | 与其他元素发生冲突 | <dialog> 使用顶层 |
| 手动设置焦点陷阱 | 复杂且容易出错 | 由 showModal() 处理 |
| 内联背景遮罩 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
浏览器支持
| 模式 | Chrome | Firefox | Safari |
|---|---|---|---|
原生 <dialog> | 37+ | 98+ | 15.4+ |
| Invoker Commands | 135+ | 144+ | 26.2+ |
@starting-style | 117+ | 129+ | 17.5+ |
对于较旧的浏览器:dialog polyfill、invokers polyfill。有关渐进增强策略,请参阅 references/zero-js-patterns.md。