소스 검색

feat(a11y): [Popconfirm] supports a11y keyboard and focus (#1425)

走鹃 2 년 전
부모
커밋
b8a70ccefa

+ 84 - 19
content/feedback/popconfirm/index-en-US.md

@@ -153,31 +153,79 @@ import { Popconfirm, Button, Toast } from '@douyinfe/semi-ui';
 };
 ```
 
+### Initialize the Focus Position of Popup Layer
+
+`okButtonProps` and `cancelButtonProps` support passing in the `autoFocus` parameter, which will automatically focus at this position when the panel is opened. Version 2.30.0 supported.
+
+`content` supports function, and its parameter is an object, which binds `initialFocusRef` to the focusable DOM or component, and it will automatically focus at this position when the panel is opened. Version 2.30.0 supported.
+
+```jsx live=true
+import React from 'react';
+import { Button, Popconfirm, Space } from '@douyinfe/semi-ui';
+
+() => {
+    return (
+        <Space>
+            <Popconfirm
+                title="Are you sure you want to save this edit?"
+                content="This modification will be irreversible"
+                okButtonProps={{
+                    autoFocus: true,
+                    type: 'danger',
+                }}
+            >
+                <Button>Confirm focus</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="Are you sure you want to save this edit?"
+                content="This modification will be irreversible"
+                cancelButtonProps={{
+                    autoFocus: true,
+                }}
+            >
+                <Button>Cancel focus</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="Are you sure you want to save this edit?"
+                content={({ initialFocusRef }) => {
+                    return <input ref={initialFocusRef} placeholder="focus here" />;
+                }}
+            >
+                <Button>Content focus</Button>
+            </Popconfirm>
+        </Space>
+    );
+};
+```
+
 ### Use with Tooltip or Popover
 
 Please refer to [Use with Tooltip/Popover](/en-US/show/tooltip#Use-with-Popver-or-Popconfirm)
 
 ## API Reference
 
-| Properties         | Instructions                                                                                                                                                          | Type                     | Default             | Version           |
-| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------------------- | ----------------- |
-| arrowPointAtCenter | Whether the "small triangle" points to the center of the element, you need to pass in "showArrow = true" at the same time                                             | boolean                  | false               | **0.34.0** |
-| cancelText         | Cancel button text                                                                                                                                                    | string                   | "Cancel"            |
-| cancelButtonProps  | Properties for cancel button                                                                                                                                          | object                   |                     | **0.29.0**        |
-| cancelType         | Cancel button type                                                                                                                                                    | string                   | "tertiary"          |
-| content            | Content displayed                                                                                                                                                     | string \| ReactNode        |                     |
-| defaultVisible     | Bubble box is displayed by default                                                                                                                                    | boolean                  |                     | **0.19.0**        |
-| disabled           | Click on the Pop confirmation box to see if the bubbles pop up.                                                                                                       | boolean                  | false               |
-| getPopupContainer  | Specify the parent DOM, and the pop-up layer will be rendered into the DOM. Customization needs to set `position: relative`                                                                                                       | Function():HTMLElement         | () => document.body |
-| icon               | Custom pop bubble Icon icon                                                                                                                                           |  ReactNode      | <IconAlertTriangle size="extra-large" />    |
+| Properties         | Instructions                                                                                                                                                          | Type                       | Default             | Version           |
+| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------- | ----------------- |
+| arrowPointAtCenter | Whether the "small triangle" points to the center of the element, you need to pass in "showArrow = true" at the same time                                             | boolean                    | false               | **0.34.0** |
+| cancelText         | Cancel button text                                                                                                                                                    | string                     | "Cancel"            |
+| cancelButtonProps  | Properties for cancel button                                                                                                                                          | object                     |                     | **0.29.0**        |
+| cancelType         | Cancel button type                                                                                                                                                    | string                     | "tertiary"          |
+| closeOnEsc         | Whether to close the panel by pressing the Esc key in the trigger or popup layer. It does not take effect when visible is under controlled | boolean | true | **2.8.0** |
+| content            | Content displayed (function type, supported in version 2.30.0)                                                                                                         | ReactNode\|({ initialFocusRef }) => ReactNode        |                     |
+| defaultVisible     | Bubble box is displayed by default                                                                                                                                    | boolean                    |                     | **0.19.0**        |
+| disabled           | Click on the Pop confirmation box to see if the bubbles pop up.                                                                                                       | boolean                    | false               |
+| getPopupContainer  | Specify the parent DOM, and the pop-up layer will be rendered into the DOM. Customization needs to set `position: relative`                                                                                                       | Function():HTMLElement           | () => document.body |
+| guardFocus         | When the focus is in the popup layer, toggle whether the Tab makes the focus loop in the popup layer | boolean | true | **2.8.0** |
+| icon               | Custom pop bubble Icon icon                                                                                                                                           |  ReactNode        | <IconAlertTriangle size="extra-large" />    |
 | motion             | Whether there is animation when the drop-down list appears/hidden. You can customize animation by passing in an object that conforms to the structure | boolean | true |
-| position           | Directions, optional values: `top`, `topLeft`, `topRight`, `leftTop`, `leftBottom`, `rightTop`, `rightTop`, `rightBottom`, `bottomLeft`, `bottomRight`, `bottomRight` | string                   | "bottomLeft"        |
-| okText             | Confirm button text                                                                                                                                | string                         | "Confirm"              |
-| okType             | Confirm button type                                                                                                                                | string                         | "primary"           |
-| okButtonProps      | Confirm button props                                                                                                                            | object                         |                     | **0.29.0**        |
-| showArrow          | Whether to show arrow triangle                                                                                                                         | boolean                        | false               |                   |
-| stopPropagation    | Whether to prevent the click event on the bomb layer from bubbling                                                                                                                | boolean                        | true                | **0.34.0** |
-| position           | Popup layer position,Optional value:`top`,`topLeft`,`topRight`,`left`,`leftTop`,`leftBottom`,<br/>`right`,`rightTop`,`rightBottom`,`bottom`,`bottomLeft`,`bottomRight` | string                         | "bottomLeft"        |
+| position           | Directions, optional values: `top`, `topLeft`, `topRight`, `leftTop`, `leftBottom`, `rightTop`, `rightTop`, `rightBottom`, `bottomLeft`, `bottomRight`, `bottomRight` | string                     | "bottomLeft"        |
+| okText             | Confirm button text                                                                                                                                | string                           | "Confirm"              |
+| okType             | Confirm button type                                                                                                                                | string                           | "primary"           |
+| okButtonProps      | Confirm button props                                                                                                                            | object                           |                     | **0.29.0**        |
+| showArrow          | Whether to show arrow triangle                                                                                                                         | boolean                          | false               |                   |
+| stopPropagation    | Whether to prevent the click event on the bomb layer from bubbling                                                                                                                | boolean                          | true                | **0.34.0** |
+| position           | Popup layer position,Optional value:`top`,`topLeft`,`topRight`,`left`,`leftTop`,`leftBottom`,<br/>`right`,`rightTop`,`rightBottom`,`bottom`,`bottomLeft`,`bottomRight` | string                           | "bottomLeft"        |
+| returnFocusOnClose | After pressing the Esc key, whether the focus returns to the trigger, it only takes effect when the trigger is set to click | boolean | true | **2.8.0** |
 | title              | Displayed title                                                                                                                                  | string\|ReactNode                |                     |
 | trigger            | Timing to trigger the display, optional value:hover / focus / click / custom                                                                                         | string              |   'click'                  |
 | visible            | Whether the bubble box displays controlled attributes                                                                                                                   | boolean                        |                     | **0.19.0**        |
@@ -185,7 +233,24 @@ Please refer to [Use with Tooltip/Popover](/en-US/show/tooltip#Use-with-Popver-o
 | onConfirm          | Click the confirmation button to call back. Promise support after v2.19                                                                                                                           | (e) => void \| Promise                |                     |
 | onCancel           | Click the Cancel button to call back. Promise support after v2.19                                                                                                                          | (e) => void \| Promise                |                     |
 | onVisibleChange    | Bubble box toggle shows hidden callbacks                                                                                                                              | (visible: boolean) => void | () => {}            | **0.19.0**        |
-| onClickOutSide     | Callback when the pop-up layer is in the display state and the non-Children, non-floating layer inner area is clicked                                                 | (e: event) => void       |                     | **2.1.0**        |
+| onEscKeyDown | Called when Esc key is pressed in trigger or popup layer | function(e:event) | | **2.8.0** |
+| onClickOutSide     | Callback when the pop-up layer is in the display state and the non-Children, non-floating layer inner area is clicked                                                 | (e: event) => void         |                     | **2.1.0**        |
+
+## Accessibility
+
+### ARIA
+
+For ARIA, please refer to [Popover](https://semi.design/zh-CN/show/popover#ARIA)
+
+### Keyboard and focus
+
+- Popconfirm must have trigger, trigger can be focused, use `Enter` key to open Popconfirm
+- After Popconfirm is activated, press the arrow key ⬇️ to move the focus to Popconfirm. The initial focus of Popconfirm should follow several principles:
+    - If the Popconfirm contains the last step of an irreversible process, such as: deleting data, etc., then this initial focus is preferably on the least destructive interactable element, such as: the cancel button (by passing the `autoFocus` to the object `cancelButtonProps`)
+    - If you only read text in Popconfirm, it is recommended to set the initial focus on the most likely interactive elements, such as: confirm button (implemented by passing `autoFocus` to the object `okButtonProps` )
+- Keyboard users can dismiss Popconfirm by pressing `Esc` and focus should return to the trigger. After the user closes the Pop through the interactive element within the Popconfirm, the focus should also return to the trigger (only when trigger is `click`)
+- When it is opened, after the user clicks `Esc` in the blank space in Popconfirm, the focus will also return to the trigger (only when trigger is `click`)
+
 ## Design Tokens
 <DesignToken/>
 

+ 89 - 26
content/feedback/popconfirm/index.md

@@ -151,6 +151,50 @@ import { Popconfirm, Button, Toast } from '@douyinfe/semi-ui';
 };
 ```
 
+### 初始化弹出层焦点位置
+
+okButtonProps 和 cancelButtonProps 支持传入 `autoFocus` 参数,传入后打开面板时会自动聚焦在该位置。2.30.0 版本支持。
+
+content 支持传入函数,它的入参是一个对象,将 `initialFocusRef` 绑定在可聚焦 DOM 或组件上,打开面板时会自动聚焦在该位置。2.30.0 版本支持。
+
+```jsx live=true
+import React from 'react';
+import { Button, Popconfirm, Space } from '@douyinfe/semi-ui';
+
+() => {
+    return (
+        <Space>
+            <Popconfirm
+                title="确定是否要保存此修改?"
+                content="此修改将不可逆"
+                okButtonProps={{
+                    autoFocus: true,
+                    type: 'danger',
+                }}
+            >
+                <Button>确认聚焦</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="确定是否要保存此修改?"
+                content="此修改将不可逆"
+                cancelButtonProps={{
+                    autoFocus: true,
+                }}
+            >
+                <Button>取消聚焦</Button>
+            </Popconfirm>
+            <Popconfirm
+                title="确定是否要保存此修改?"
+                content={({ initialFocusRef }) => {
+                    return <input ref={initialFocusRef} placeholder="focus here" />;
+                }}
+            >
+                <Button>内容聚焦</Button>
+            </Popconfirm>
+        </Space>
+    );
+};
+```
 
 ### 搭配 Tooltip 或 Popover 使用
 
@@ -158,32 +202,51 @@ import { Popconfirm, Button, Toast } from '@douyinfe/semi-ui';
 
 ## API 参考
 
-| 属性               | 说明                                                                                                                                        | 类型                           | 默认值              | 版本              |
-| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------- | ----------------- |
-| arrowPointAtCenter | “小三角”是否指向元素中心,需要同时传入"showArrow=true"                                                                                      | boolean                        | false               | **0.34.0** |
-| cancelText         | 取消按钮文字                                                                                                                                | string                         | "取消"              |
-| cancelButtonProps  | 取消按钮的 props                                                                                                                            | object                         |                     | **0.29.0**        |
-| cancelType         | 取消按钮类型                                                                                                                                | string                         | "tertiary"          |
-| content            | 显示的内容                                                                                                                                  | string\|ReactNode                |                     |
-| defaultVisible     | 气泡框默认是否展示                                                                                                                          | boolean                        |                     | **0.19.0**        |
-| disabled           | 点击 Popconfirm 子元素是否弹出气泡确认框                                                                                                    | boolean                        | false               |
-| getPopupContainer  | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义时容器需要设置 `position: relative`                                                                                                       | Function():HTMLElement         | () => document.body |
-| icon               | 自定义弹出气泡 Icon 图标                                                                                                                    | ReactNode              | <IconAlertTriangle size="extra-large" />    |
-| motion             | 下拉列表出现/隐藏时,是否有动画 | boolean | true |
-| okText             | 确认按钮文字                                                                                                                                | string                         | "确认"              |
-| okType             | 确认按钮类型                                                                                                                                | string                         | "primary"           |
-| okButtonProps      | 确认按钮的 props                                                                                                                            | object                         |                     | **0.29.0**        |
-| position           | 方向,可选值:`top`,`topLeft`,`topRight`,`left`,`leftTop`,`leftBottom`,<br/>`right`,`rightTop`,`rightBottom`,`bottom`,`bottomLeft`,`bottomRight` | string                         | "bottomLeft"        |
-| showArrow          | 是否显示箭头三角形                                                                                                                          | boolean                        | false               |                   |
-| stopPropagation    | 是否阻止弹层上的点击事件冒泡                                                                                                                | boolean                        | true                | **0.34.0** |
-| title              | 显示的标题                                                                                                                                  | string\|ReactNode                |                     |
-| trigger            | 触发展示的时机,可选值:hover / focus / click / custom                                                                                         | string              |   'click'                  |
-| visible            | 气泡框是否展示的受控属性                                                                                                                    | boolean                        |                     | **0.19.0**        |
-| zIndex             | 浮层 z-index 值                                                                                                                             | number                         | 1030                |
-| onConfirm          | 点击确认按钮回调,  Promise类型于 v 2.19后支持                                                                                                                          | Function(e): void \| Promise                      |                     |
-| onCancel           | 点击取消按钮回调,Promise类型于 v 2.19后支持                                                                                                                            | Function(e): void \| Promise                      |                     |
-| onClickOutSide     | 当弹出层处于展示状态,点击非Children、非浮层内部区域时的回调                                                                                      | Function(e)                    |  **2.1.0**      |
-| onVisibleChange    | 气泡框切换显示隐藏的回调                                                                                                               | Function(visible: boolean): void | () => {}            | **0.19.0**        |
+| 属性 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| arrowPointAtCenter | “小三角”是否指向元素中心,需要同时传入"showArrow=true" | boolean | false | **0.34.0** |
+| cancelText | 取消按钮文字 | string | "取消" |
+| cancelButtonProps | 取消按钮的 props | object |  | **0.29.0** |
+| cancelType | 取消按钮类型 | string | "tertiary" |
+| closeOnEsc | 在 trigger 聚焦时或在弹出层内聚焦元素上按 Esc 键是否关闭面板,受控时不生效 | boolean | true | **2.8.0** |
+| content | 显示的内容(函数类型,2.10.0 版本支持) | ReactNode\|({ initialFocusRef }) => ReactNode |  |
+| defaultVisible | 气泡框默认是否展示 | boolean |  | **0.19.0** |
+| disabled | 点击 Popconfirm 子元素是否弹出气泡确认框 | boolean | false |
+| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义时容器需要设置 `position: relative` | Function():HTMLElement | () => document.body |
+| guardFocus | 当焦点处于弹出层内时,切换 Tab 是否让焦点在弹出层内循环 | boolean | true | **2.8.0** |
+| icon | 自定义弹出气泡 Icon 图标 | ReactNode | <IconAlertTriangle size="extra-large" /> |
+| motion | 下拉列表出现/隐藏时,是否有动画 | boolean | true |
+| okText | 确认按钮文字 | string | "确认" |
+| okType | 确认按钮类型 | string | "primary" |
+| okButtonProps | 确认按钮的 props | object |  | **0.29.0** |
+| position | 方向,可选值:`top`,`topLeft`,`topRight`,`left`,`leftTop`,`leftBottom`,<br/>`right`,`rightTop`,`rightBottom`,`bottom`,`bottomLeft`,`bottomRight` | string | "bottomLeft" |
+| returnFocusOnClose | 按下 Esc 键后,焦点是否回到 trigger 上,只有设置 trigger 为 click 时生效 | boolean | true | **2.8.0** |
+| showArrow | 是否显示箭头三角形 | boolean | false |  |
+| stopPropagation | 是否阻止弹层上的点击事件冒泡 | boolean | true | **0.34.0** |
+| title | 显示的标题 | string\|ReactNode |  |
+| trigger | 触发展示的时机,可选值:hover / focus / click / custom | string | 'click' |
+| visible | 气泡框是否展示的受控属性 | boolean |  | **0.19.0** |
+| zIndex | 浮层 z-index 值 | number | 1030 |
+| onConfirm | 点击确认按钮回调 | Function(e) |  |
+| onCancel | 点击取消按钮回调 | Function(e) |  |
+| onClickOutSide | 当弹出层处于展示状态,点击非 Children、非浮层内部区域时的回调 | Function(e) | **2.1.0** |
+| onEscKeyDown | 在 trigger 或弹出层按 Esc 键时调用 | function(e:event) |  | **2.8.0** |
+| onVisibleChange | 气泡框切换显示隐藏的回调 | Function(visible: boolean): void | () => {} | **0.19.0** |
+
+## Accessibility
+
+### ARIA
+
+语义化请参考 [Popover](https://semi.design/zh-CN/show/popover#ARIA)
+
+### 键盘和焦点
+
+- Popconfirm 必须带有触发器,触发器可被聚焦,使用 Enter 键打开 Popconfirm
+- Popconfirm 激活后,按下方向键 ⬇️ 将焦点移动到 Popconfirm 上。Popconfirm 的初始焦点应当遵循以下几个原则:
+    - 如果 Popconfirm 内包含一个不可逆转过程的最后一个步骤,比如:删除数据等,那么这个初始焦点最好放在破坏性最小的可交互元素上,如:关闭按钮 ( 通过向对象 cancelButtonProps 中传入 autoFocus 实现)
+    - 如果 Popconfirm 内仅为阅读文本,那么建议将初始焦点设置在最可能常用的交互元素上,如:确定按钮 ( 通过向对象 okButtonProps 中传入 autoFocus 实现)
+- 键盘用户能够通过按 Esc 关闭 Popconfirm,并且焦点应该返回到触发器上。用户通过 Popconfirm 内的交互元素关闭该 Pop 后,焦点也应当返回到触发器上(仅当 trigger 为 click 时)
+- 打开的情况下,用户点击 Popconfirm 内的空白处 Esc 后,焦点也会回到触发器上 (仅当 trigger 为 click 时)
 
 ## 设计变量
 <DesignToken/>

+ 58 - 0
cypress/integration/popconfirm.spec.js

@@ -0,0 +1,58 @@
+// popConfirm.spec.js created with Cypress
+//
+// Start writing your Cypress tests below!
+// If you're unfamiliar with how Cypress works,
+// check out the link below and learn how to write your first test:
+// https://on.cypress.io/writing-first-test
+
+// Start writing your Cypress tests below!
+// If you're unfamiliar with how Cypress works,
+// check out the link below and learn how to write your first test:
+// https://on.cypress.io/writing-first-test
+
+describe('popConfirm', () => {
+    it('confirm focus', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popconfirm--keyboard-and-focus&args=&viewMode=story');
+        cy.get('[data-cy=initial-focus-confirm]').click();
+        cy.get('.semi-popconfirm-footer .semi-button').eq(1).should('be.focused');
+        cy.get('.semi-popconfirm-footer .semi-button').eq(1).click();
+        // return focus to trigger
+        cy.get('[data-cy=initial-focus-confirm] .semi-button').should('be.focused');
+    });
+
+    it('cancel focus', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popconfirm--keyboard-and-focus&args=&viewMode=story');
+        cy.get('[data-cy=initial-focus-cancel]').click();
+        cy.get('.semi-popconfirm-footer .semi-button').eq(0).should('be.focused');
+        cy.get('.semi-popconfirm-footer .semi-button').eq(0).click();
+        cy.get('[data-cy=initial-focus-cancel] .semi-button').should('be.focused');
+    });
+
+    it('content focus', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popconfirm--keyboard-and-focus&args=&viewMode=story');
+        cy.get('[data-cy=initial-focus-content]').click();
+        cy.get('.semi-popconfirm-body input').eq(0).should('be.focused');
+        cy.get('.semi-popconfirm-header .semi-popconfirm-btn-close').eq(0).click();
+        cy.get('[data-cy=initial-focus-content] .semi-button').should('be.focused');
+    });
+
+    it('content esc keydown', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popconfirm--esc-key-down&viewMode=story');
+        cy.get('[data-cy=content]').click();
+        cy.get('.test-ok').type('{esc}');
+        cy.get('.test-ok').should('not.exist');
+    });
+
+    it('content esc keydown', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popconfirm--esc-key-down&viewMode=story', {
+            onBeforeLoad(win) {
+                cy.stub(win.console, 'log').as('consoleLog');
+            },
+        });
+        cy.get('[data-cy=trigger]').click();
+        cy.get('.test-text').click();
+        cy.get('@consoleLog').should('be.calledWith', 'clicked');
+        cy.get('.test-text').type('{esc}');
+        cy.get('.test-ok').should('not.exist');
+    });
+});

+ 20 - 1
packages/semi-foundation/popconfirm/popconfirmFoundation.ts

@@ -1,5 +1,7 @@
 /* eslint-disable @typescript-eslint/no-empty-function */
 
+import { get } from 'lodash';
+
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import isPromise from '../utils/isPromise';
 
@@ -10,7 +12,10 @@ export interface PopconfirmAdapter<P = Record<string, any>, S = Record<string, a
     notifyConfirm: (e: any) => Promise<any> | void;
     notifyCancel: (e: any) => Promise<any> | void;
     notifyVisibleChange: (visible: boolean) => void;
-    notifyClickOutSide: (e: any) => void
+    notifyClickOutSide: (e: any) => void;
+    focusCancelButton: () => void;
+    focusOkButton: () => void;
+    focusPrevFocusElement: () => void
 }
 
 export default class PopConfirmFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<PopconfirmAdapter<P, S>, P, S> {
@@ -63,6 +68,20 @@ export default class PopConfirmFoundation<P = Record<string, any>, S = Record<st
         if (!this._isControlledComponent('visible')) {
             this._adapter.setVisible(visible);
         }
+        if (visible) {
+            this.handleFocusOperateButton();
+        } else {
+            this._adapter.focusPrevFocusElement();
+        }
         this._adapter.notifyVisibleChange(visible);
     }
+
+    handleFocusOperateButton() {
+        const { cancelButtonProps, okButtonProps } = this._adapter.getProps() as any;
+        if (get(cancelButtonProps, 'autoFocus') && !get(cancelButtonProps, 'disabled')) {
+            this._adapter.focusCancelButton();
+        } else if (get(okButtonProps, 'autoFocus') && !get(okButtonProps, 'disabled')) {
+            this._adapter.focusOkButton();
+        }
+    }
 }

+ 2 - 2
packages/semi-foundation/tooltip/foundation.ts

@@ -1126,7 +1126,7 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
      * 如果 trigger 是 focus 或者 hover,则它绑定了 onFocus,这里我们如果重新 focus 的话,popup 会再次打开
      * 因此 returnFocusOnClose 只支持 click trigger
      */
-    _focusTrigger() {
+    focusTrigger() {
         const { trigger, returnFocusOnClose, preventScroll } = this.getProps();
         if (returnFocusOnClose && trigger !== 'custom') {
             const triggerNode = this._adapter.getTriggerNode();
@@ -1141,7 +1141,7 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
         if (trigger !== 'custom') {
             // Move the focus into the trigger first and then close the pop-up layer 
             // to avoid the problem of opening the pop-up layer again when the focus returns to the trigger in the case of hover and focus
-            this._focusTrigger();
+            this.focusTrigger();
             this.hide();
         }
         this._adapter.notifyEscKeydown(event);

+ 76 - 0
packages/semi-ui/popconfirm/_story/popconfirm.stories.jsx

@@ -5,6 +5,7 @@ import Button from '../../button';
 import Input from '../../input';
 import Table from '../../table';
 import Toast from '../../toast';
+import { Space } from '../../index';
 
 import TypesConfrimDemo from './TypesConfirm';
 import DynamicDisableDemo from './DynamicDisable';
@@ -204,3 +205,78 @@ PromiseCallback.story = {
   name: 'PromiseCallbackDemo',
 };
 
+export const KeyboardAndFocus = () => {
+  return (
+    <div style={{ height: '150vh', marginTop: 200 }}>
+      <Space>
+        <div data-cy="initial-focus-confirm">
+          <Popconfirm
+              title="确定是否要保存此修改?"
+              content="此修改将不可逆"
+              okButtonProps={{
+                autoFocus: true,
+                type: 'danger',
+                className: 'test-ok',
+              }}
+          >
+              <Button>确认聚焦</Button>
+          </Popconfirm>
+        </div>
+        <div data-cy="initial-focus-cancel">
+          <Popconfirm
+              title="确定是否要保存此修改?"
+              content="此修改将不可逆"
+              cancelButtonProps={{
+                autoFocus: true,
+                className: 'test-cancel',
+              }}
+          >
+              <Button>取消聚焦</Button>
+          </Popconfirm>
+        </div>
+        <div data-cy="initial-focus-content">
+          <Popconfirm
+              title="确定是否要保存此修改?"
+              content={({ initialFocusRef }) => {
+                return <input ref={initialFocusRef} placeholder="focus here" />;
+              }}
+          >
+              <Button>内容聚焦</Button>
+          </Popconfirm>
+        </div>
+      </Space>
+    </div>
+  );
+};
+KeyboardAndFocus.storyName = "a11y focus";
+
+export const ESCKeyDown = () => {
+  return (
+    <div style={{ height: '150vh', marginTop: 200 }}>
+      <Space>
+        <div data-cy="content">
+          <Popconfirm
+              title="确定是否要保存此修改?"
+              content="此修改将不可逆"
+              okButtonProps={{
+                autoFocus: true,
+                className: 'test-ok',
+              }}
+          >
+              <Button>content</Button>
+          </Popconfirm>
+        </div>
+        <div data-cy="trigger">
+          <Popconfirm
+                title="确定是否要保存此修改?"
+                content={<div onClick={() => console.log('clicked')} className='test-text'>此修改将不可逆</div>}
+                okButtonProps={{ autoFocus: true }}
+            >
+              <Button>trigger</Button>
+          </Popconfirm>
+        </div>
+      </Space>
+    </div>
+  );
+};
+ESCKeyDown.storyName = "a11y esc keydown";

+ 41 - 13
packages/semi-ui/popconfirm/index.tsx

@@ -2,13 +2,13 @@
 import React from 'react';
 import cls from 'classnames';
 import PropTypes from 'prop-types';
-import { noop, get } from 'lodash';
+import { noop, get, isFunction, omit } from 'lodash';
 import { cssClasses, numbers } from '@douyinfe/semi-foundation/popconfirm/constants';
 import PopconfirmFoundation, { PopconfirmAdapter } from '@douyinfe/semi-foundation/popconfirm/popconfirmFoundation';
 import { IconClose, IconAlertTriangle } from '@douyinfe/semi-icons';
 import BaseComponent from '../_base/baseComponent';
 import Popover, { PopoverProps } from '../popover';
-import { Position, Trigger } from '../tooltip';
+import { Position, Trigger, RenderContentProps } from '../tooltip';
 import Button, { ButtonProps } from '../button';
 import { Type as ButtonType } from '../button/Button';
 import ConfigContext, { ContextValue } from '../configProvider/context';
@@ -20,7 +20,6 @@ export interface PopconfirmProps extends PopoverProps {
     cancelText?: string;
     cancelButtonProps?: ButtonProps;
     cancelType?: ButtonType;
-    content?: React.ReactNode;
     defaultVisible?: boolean;
     disabled?: boolean;
     icon?: React.ReactNode;
@@ -55,7 +54,7 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
     static propTypes = {
         motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
         disabled: PropTypes.bool,
-        content: PropTypes.any,
+        content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
         title: PropTypes.any,
         prefixCls: PropTypes.string,
         className: PropTypes.string,
@@ -96,6 +95,9 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
         onClickOutSide: noop,
     };
 
+    footerRef: React.RefObject<HTMLDivElement | null>;
+    popoverRef: React.RefObject<Popover | null>;
+    foundation: PopconfirmFoundation;
     constructor(props: PopconfirmProps) {
         super(props);
 
@@ -106,6 +108,8 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
         };
 
         this.foundation = new PopconfirmFoundation(this.adapter);
+        this.footerRef = React.createRef();
+        this.popoverRef = React.createRef();
     }
 
     context: ContextValue;
@@ -131,6 +135,17 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
             notifyCancel: (e: React.MouseEvent): Promise<any> | void => this.props.onCancel(e),
             notifyVisibleChange: (visible: boolean): void => this.props.onVisibleChange(visible),
             notifyClickOutSide: (e: React.MouseEvent) => this.props.onClickOutSide(e),
+            focusCancelButton: () => {
+                const buttonNode = this.footerRef?.current?.querySelector('[data-type=cancel]') as HTMLElement;
+                buttonNode?.focus({ preventScroll: true });
+            },
+            focusOkButton: () => {
+                const buttonNode = this.footerRef?.current?.querySelector('[data-type=ok]') as HTMLElement;
+                buttonNode?.focus({ preventScroll: true });
+            },
+            focusPrevFocusElement: () => {
+                this.popoverRef.current?.focusTrigger();
+            }
         };
     }
 
@@ -151,10 +166,23 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
             <LocaleConsumer componentName="Popconfirm">
                 {(locale: LocaleObject['Popconfirm'], localeCode: string) => (
                     <>
-                        <Button type={cancelType} onClick={this.handleCancel} loading={cancelLoading} {...cancelButtonProps}>
+                        <Button
+                            data-type="cancel"
+                            type={cancelType}
+                            onClick={this.handleCancel}
+                            loading={cancelLoading}
+                            {...omit(cancelButtonProps, 'autoFocus')}
+                        >
                             {cancelText || get(locale, 'cancel')}
                         </Button>
-                        <Button type={okType} theme="solid" onClick={this.handleConfirm} loading={confirmLoading} {...okButtonProps}>
+                        <Button
+                            data-type="ok"
+                            type={okType}
+                            theme="solid"
+                            onClick={this.handleConfirm}
+                            loading={confirmLoading}
+                            {...omit(okButtonProps, 'autoFocus')}
+                        >
                             {okText || get(locale, 'confirm')}
                         </Button>
                     </>
@@ -163,7 +191,7 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
         );
     }
 
-    renderConfirmPopCard() {
+    renderConfirmPopCard = ({ initialFocusRef }: { initialFocusRef?: RenderContentProps<any>['initialFocusRef'] }) => {
         const { content, title, className, style, cancelType, icon, prefixCls } = this.props;
         const { direction } = this.context;
         const popCardCls = cls(
@@ -177,7 +205,7 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
         const showContent = !(content === null || typeof content === 'undefined');
 
         return (
-            // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
+            /* eslint-disable-next-line jsx-a11y/no-static-element-interactions */
             <div className={popCardCls} onClick={this.stopImmediatePropagation} style={style}>
                 <div className={`${prefixCls}-inner`}>
                     <div className={`${prefixCls}-header`}>
@@ -202,10 +230,10 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
                     </div>
                     {showContent ? (
                         <div className={`${prefixCls}-body`} x-semi-prop="content">
-                            {content}
+                            {isFunction(content) ? content({ initialFocusRef }) : content}
                         </div>
-                    ) : null} 
-                    <div className={`${prefixCls}-footer`}>{this.renderControls()}</div>
+                    ) : null}
+                    <div className={`${prefixCls}-footer`} ref={this.footerRef}>{this.renderControls()}</div>
                 </div>
             </div>
         );
@@ -230,7 +258,6 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
         }
 
         const { visible } = this.state;
-        const popContent = this.renderConfirmPopCard();
         const popProps: PopProps = {
             onVisibleChange: this.handleVisibleChange,
             className: cssClasses.POPOVER,
@@ -243,8 +270,9 @@ export default class Popconfirm extends BaseComponent<PopconfirmProps, Popconfir
 
         return (
             <Popover
+                ref={this.popoverRef}
                 {...attrs}
-                content={popContent}
+                content={this.renderConfirmPopCard}
                 visible={visible}
                 position={position}
                 {...popProps}

+ 13 - 0
packages/semi-ui/popover/index.tsx

@@ -109,6 +109,18 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
     };
 
     context: ContextValue;
+    tooltipRef: React.RefObject<Tooltip | null>;
+    constructor(props: PopoverProps) {
+        super(props);
+        this.tooltipRef = React.createRef();
+    }
+
+    /**
+     * focus on tooltip trigger
+     */
+    public focusTrigger = () => {
+        this.tooltipRef.current?.focusTrigger();
+    }
 
     renderPopCard = ({ initialFocusRef }: { initialFocusRef: RenderContentProps['initialFocusRef'] }) => {
         const { content, contentClassName, prefixCls } = this.props;
@@ -166,6 +178,7 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
         return (
             <Tooltip
                 guardFocus
+                ref={this.tooltipRef}
                 {...(attr as any)}
                 trigger={trigger}
                 position={position}

+ 12 - 3
packages/semi-ui/tooltip/index.tsx

@@ -34,11 +34,11 @@ export interface ArrowBounding {
     height?: number
 }
 
-export interface RenderContentProps {
-    initialFocusRef?: React.RefObject<HTMLElement>
+export interface RenderContentProps<T = HTMLElement> {
+    initialFocusRef?: React.RefObject<T>
 }
 
-export type RenderContent = (props: RenderContentProps) => React.ReactNode;
+export type RenderContent<T = HTMLElement> = (props: RenderContentProps<T>) => React.ReactNode;
 
 export interface TooltipProps extends BaseProps {
     children?: React.ReactNode;
@@ -449,6 +449,13 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         this.mounted = false;
         this.foundation.destroy();
     }
+    
+    /**
+     * focus on tooltip trigger
+     */
+    public focusTrigger() {
+        this.foundation.focusTrigger();
+    }
 
     isSpecial = (elem: React.ReactNode | HTMLElement | any) => {
         if (isHTMLElement(elem)) {
@@ -612,6 +619,8 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
             <Portal getPopupContainer={this.props.getPopupContainer} style={{ zIndex }}>
                 {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
                 <div
+                    // listen keyboard event, don't move tabIndex -1
+                    tabIndex={-1}
                     className={`${BASE_CLASS_PREFIX}-portal-inner`}
                     style={portalInnerStyle}
                     ref={this.setContainerEl}