Skip to content

[Dialog] 支持焦点落点可配置、Enter 键确认与焦点循环(含 DialogPlugin 对齐) #4430

Open

Description

@xiongbao

这个功能解决了什么问题

环境

  • tdesign-react: 1.18.2
  • React: 18
  • 浏览器: Chrome 140 / Safari 18(均复现)

背景与诉求

  1. 不同弹窗诉求不同:二次确认希望焦点落「确定」,危险操作(删除/锁定)希望落「取消」,表单类弹窗希望落输入框。落点应由参数决定,并支持全站默认值。
  2. 可配置的键盘确认:只要弹窗打开,按 Enter 就应等同于点击「确定」。当前焦点默认落在弹窗容器上,回车完全无响应,键盘链路断裂。

期望行为

  1. 焦点落点可配置:提供 defaultFocus,支持 'confirm' | 'cancel' | 'none' | (() => HTMLElement | null);组件默认值建议保持现状('none')以兼容存量,但通过 globalConfig.dialog.defaultFocus 可全站统一策略。
  2. Enter 触发确认,与焦点解耦:弹窗打开期间按 Enter 等同于点击「确定」,不要求焦点位于主按钮;Esc 仍关闭。需内置边界:焦点在 textarea/可换行输入控件、IME 组合输入过程中不触发,confirmLoading 期间不重复触发。可用 confirmOnEnter 开关。
  3. Tab 焦点循环:Tab / Shift+Tab 在弹窗内循环,不逃逸;关闭后焦点归还触发元素(restoreFocus)。
  4. 上述能力在声明式 Dialog 与命令式 DialogPlugin 上行为一致。

你建议的方案是什么

建议 API(P0,其余能力可另开 issue)

interface DialogProps {
  /** 打开后焦点落点;组件默认建议保持现状('none'),由业务按需指定 */
  defaultFocus?: 'confirm' | 'cancel' | 'none' | (() => HTMLElement | null);
  /** Enter 触发确认,默认 true */
  confirmOnEnter?: boolean;
  /** 焦点循环,默认 true */
  focusTrap?: boolean;
  /** 关闭后焦点归还触发元素,默认 true */
  restoreFocus?: boolean;
}

配套要求:

  • globalConfig.dialog 暴露 defaultFocus / confirmOnEnter / focusTrap 的默认值,便于全站统一;
  • 为「主操作按钮」提供稳定语义标记(例如统一 data-role="confirm",自定义 footer 下由业务显式声明),让 defaultFocus: 'confirm' 与 Enter 确认在任何 footer 形态下都能命中,而不是靠类名匹配。

根因(源码定位)

  1. dialog/hooks/useDialogEsc.js 的 activateDialog() 只做 lastDialog.current.focus(),把焦点交给容器 .t-dialog__ctx,既不涉及按钮,也没有任何落点配置。
  2. 组件层只监听 Esc,无 Enter、无 focus trap;autofocus 在 Dialog 内不生效(参考 [Input] 自动聚焦无效 #2794),消费方只能在 onOpened 里补刀,且开场动画期间 TDesign 会抢焦点,时序脆弱。
  3. dialog/DialogCard.js 的 renderConfirmBtn 硬编码 theme: "primary"(theme="danger" 弹窗也渲染 primary),renderFooter 对自定义 footer 直接 parseTNode,不提供「主操作按钮」语义标记。于是:
    • 默认 footer:主按钮是 .t-button--theme-primary;
    • 自定义 footer:业务常用 <Button theme="danger">,类名是 .t-button--theme-danger。
      我们的补丁只认 --theme-primary 时,一批自定义 footer 弹窗(如分支锁定)主按钮依然拿不到焦点——只要组件不给语义,消费方就只能不断猜选择器。
  4. config-provider/ConfigContext.js 中 dialog.confirmBtnTheme 已声明默认值,但全量检索 es/ 无任何消费方,属未生效配置。

我们当前的 workaround(供参考,非长期方案)

在业务封装层:① onOpened 里按类名猜主按钮并 focus();② 在 document 上挂 keydown 捕获实现 Tab 循环;③ 用 :focus 补焦点环(程序化 focus 不命中 :focus-visible,Button 无可见焦点);④ 对 DialogPlugin.confirm 做同构包装复制同一套逻辑。

Enter 确认目前只能自己绑,还得自行判断 textarea、IME 组合输入、confirmLoading 等边界,规则与组件内部(如 Esc 的 dialogStack)不同步就会出现误触发或漏触发。这套补丁必须随组件实现变化持续维护,因此希望能力下沉到组件层。

Activity

  1. github-actions commented on Oct 15, 2026

    @github-actions
    Contributor

    👋 @xiongbao,感谢给 TDesign 提出了 issue。
    请根据 issue 模版确保背景信息的完善,我们将调查并尽快回复你。

  2. RylanBot commented on Nov 2, 2026

    @RylanBot
    Collaborator

    可安装测试包验证是否符合预期:

    npm i https://pkg.pr.new/tdesign-react@4448

    相关变动:

    • 新增 API focusable,且其与 confirmOnEnter 均支持全局配置
      export interface DialogFocusable {
        /**
         * 打开后的默认焦点。
         * 未设置时默认聚焦对话框容器;`null` 不移动焦点。`confirm-button` 和 `cancel-button` 聚焦对应按钮;函数返回自定义元素。
         */
        defaultFocus?: null | 'confirm-button' | 'cancel-button' | (() => HTMLElement | null);
        /**
         * 是否将焦点限制在当前对话框内
         */
        focusTrap?: boolean;
        /**
         * 关闭后是否将焦点交还给打开对话框前的触发元素
         */
        focusTriggerAfterClose?: boolean;
      }
    • 修复开启 confirmOnEnter时,textarea、IME 组合输入等场景异常的问题
    • 修复全局 confirmBtnTheme 不生效的问题

    关于 data-role="confirm" 的需求,暂时不进行实现
    现在可通过 class t-dialog__confirm / t-dialog__cancel 区分

  3. xiongbao commented on Nov 4, 2026

    @xiongbao
    Author

    符合预期👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions