Browse Source

Merge branch 'release' into main

He Cheng 3 years ago
parent
commit
a21784e0fb
34 changed files with 787 additions and 210 deletions
  1. 3 3
      .github/workflows/cypress.yml
  2. 10 10
      content/feedback/toast/index-en-US.md
  3. 16 16
      content/feedback/toast/index.md
  4. 39 1
      content/show/popover/index-en-US.md
  5. 43 2
      content/show/popover/index.md
  6. 14 0
      content/start/changelog/index-en-US.md
  7. 15 0
      content/start/changelog/index.md
  8. 82 0
      cypress/integration/popover.spec.js
  9. 1 2
      cypress/integration/tooltip.spec.js
  10. 1 1
      lerna.json
  11. 1 1
      package.json
  12. 3 3
      packages/semi-animation-react/package.json
  13. 1 1
      packages/semi-animation-styled/package.json
  14. 1 1
      packages/semi-animation/package.json
  15. 1 1
      packages/semi-foundation/inputNumber/foundation.ts
  16. 2 2
      packages/semi-foundation/package.json
  17. 131 3
      packages/semi-foundation/tooltip/foundation.ts
  18. 8 0
      packages/semi-foundation/utils/isEscPress.ts
  19. 1 0
      packages/semi-foundation/utils/keyCode.ts
  20. 2 2
      packages/semi-icons/package.json
  21. 1 1
      packages/semi-illustrations/package.json
  22. 2 2
      packages/semi-next/package.json
  23. 1 1
      packages/semi-scss-compile/package.json
  24. 1 1
      packages/semi-theme-default/package.json
  25. 29 1
      packages/semi-ui/_utils/index.ts
  26. 4 0
      packages/semi-ui/inputNumber/_story/inputNumber.stories.js
  27. 1 1
      packages/semi-ui/notification/useNotification/index.tsx
  28. 8 8
      packages/semi-ui/package.json
  29. 75 1
      packages/semi-ui/popover/_story/popover.stories.js
  30. 24 8
      packages/semi-ui/popover/index.tsx
  31. 71 21
      packages/semi-ui/tooltip/index.tsx
  32. 1 1
      packages/semi-webpack/package.json
  33. 4 1
      src/templates/toUEDUtils/toUED.ts
  34. 190 114
      yarn.lock

+ 3 - 3
.github/workflows/cypress.yml

@@ -21,7 +21,7 @@ on:
 jobs:
   install:
     runs-on: ubuntu-latest
-    container: cypress/browsers:node14.16.0-chrome90-ff88
+    container: cypress/browsers:node16.5.0-chrome94-ff93
     if: ${{ github.repository_owner == 'DouyinFE' }}
     steps:
       - name: Checkout
@@ -46,7 +46,7 @@ jobs:
           runTests: false
   chrome-tests:
     runs-on: ubuntu-latest
-    container: cypress/browsers:node14.16.0-chrome90-ff88
+    container: cypress/browsers:node16.5.0-chrome94-ff93
     needs: install
     strategy:
       fail-fast: false
@@ -82,7 +82,7 @@ jobs:
   firefox-tests:
     runs-on: ubuntu-latest
     container: 
-      image: cypress/browsers:node14.16.0-chrome90-ff88
+      image: cypress/browsers:node16.5.0-chrome94-ff93
       options: --user 1001
     needs: install
     strategy:

+ 10 - 10
content/feedback/toast/index-en-US.md

@@ -349,7 +349,7 @@ Close Manually ( `toastId` is the return value of the display methods)
 | bottom | Pop-up position bottom | number \| string | - | 0.25.0 |
 | content | Toast content | string | ReactNode | '' |  |
 | duration | Automatic close delay, no auto-close when set to 0 | number | 3 |  |
-| getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative` | () => HTMLElement \| null | () => document.body | 0.34.0 |
+| getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set container and inner .semi-toast-wrapper  'position: relative` | () => HTMLElement \| null | () => document.body | 0.34.0 |
 | icon | Custom icons | ReactNode |  | 0.25.0 |
 | left | Pop-up position left | number \| string | - | 0.25.0 |
 | right | Pop-up position right | number \| string | - | 0.25.0 |
@@ -364,15 +364,15 @@ The global configuration is set before any method call, and takes effect only on
 
 -   `Toast.config(config)`
 
-| Properties | Instructions | type | Default | version |
-| --- | --- | --- | --- | --- |
-| bottom | Bottom, absolute position | number \| string | - | 0.25.0 |
-| duration | Automatic close delay, no auto-close when set to 0 | number(second) | 3 | 0.25.0 |
-| getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative` | () => HTMLElement \| null | () => document.body | 1.23.0 |
-| left | Left, absolute position | number \| string | - | 0.25.0 |
-| right | Right, absolute position | number \| string | - | 0.25.0 |
-| top | Top, absolute position | number \| string | - | 0.25.0 |
-| zIndex | Z-index | number | 1010 | 0.25.0 |
+| Properties | Instructions                                                                                                                                              | type | Default | version |
+| --- |-----------------------------------------------------------------------------------------------------------------------------------------------------------| --- | --- | --- |
+| bottom | Bottom, absolute position                                                                                                                                 | number \| string | - | 0.25.0 |
+| duration | Automatic close delay, no auto-close when set to 0                                                                                                        | number(second) | 3 | 0.25.0 |
+| getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set container and inner .semi-toast-wrapper  'position: relative` | () => HTMLElement \| null | () => document.body | 1.23.0 |
+| left | Left, absolute position                                                                                                                                   | number \| string | - | 0.25.0 |
+| right | Right, absolute position                                                                                                                                  | number \| string | - | 0.25.0 |
+| top | Top, absolute position                                                                                                                                    | number \| string | - | 0.25.0 |
+| zIndex | Z-index                                                                                                                                                   | number | 1010 | 0.25.0 |
 
 -   `ToastFactory.create(config) => Toast`  
     If you need Toast with different configs in your application, you can use ToastFactory.create(config)to create a new Toast (>= 1.23):

+ 16 - 16
content/feedback/toast/index.md

@@ -345,21 +345,21 @@ render(Demo);
 
 -   `Toast.close(toastId)`
 
-| 属性 | 说明 | 类型 | 默认值 | 版本 |
-| --- | --- | --- | --- | --- |
-| bottom | 弹出位置 bottom | number \| string | - | 0.25.0 |
-| content | 提示内容 | ReactNode | '' |  |
-| duration | 自动关闭的延时,单位 s,设为 0 时不自动关闭 | number | 3 |  |
-| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` | () => HTMLElement \| null | () => document.body | 0.34.0 |
-| icon | 自定义图标 | ReactNode |  | 0.25.0 |
-| left | 弹出位置 left | number \| string | - | 0.25.0 |
-| right | 弹出位置 right | number \| string | - | 0.25.0 |
-| showClose | 是否展示关闭按钮 | boolean | true | 0.25.0 |
-| textMaxWidth | 内容的最大宽度 | number \| string | 450 | 0.25.0 |
-| theme | 填充样式,支持 `light`, `normal` | string | `normal` | 1.0.0 |
-| top | 弹出位置 top | number \| string | - | 0.25.0 |
-| zIndex | 弹层 z-index 值 | number | 1010 |  |
-| onClose | toast 关闭的回调函数 | () => void |  |  |
+| 属性 | 说明                                                                                       | 类型 | 默认值 | 版本 |
+| --- |------------------------------------------------------------------------------------------| --- | --- | --- |
+| bottom | 弹出位置 bottom                                                                              | number \| string | - | 0.25.0 |
+| content | 提示内容                                                                                     | ReactNode | '' |  |
+| duration | 自动关闭的延时,单位 s,设为 0 时不自动关闭                                                                 | number | 3 |  |
+| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 container 和 内部的 .semi-toast-wrapper `position: relative` | () => HTMLElement \| null | () => document.body | 0.34.0 |
+| icon | 自定义图标                                                                                    | ReactNode |  | 0.25.0 |
+| left | 弹出位置 left                                                                                | number \| string | - | 0.25.0 |
+| right | 弹出位置 right                                                                               | number \| string | - | 0.25.0 |
+| showClose | 是否展示关闭按钮                                                                                 | boolean | true | 0.25.0 |
+| textMaxWidth | 内容的最大宽度                                                                                  | number \| string | 450 | 0.25.0 |
+| theme | 填充样式,支持 `light`, `normal`                                                                | string | `normal` | 1.0.0 |
+| top | 弹出位置 top                                                                                 | number \| string | - | 0.25.0 |
+| zIndex | 弹层 z-index 值                                                                             | number | 1010 |  |
+| onClose | toast 关闭的回调函数                                                                            | () => void |  |  |
 
 全局配置在调用前提前配置,全局一次生效 ( >= 0.25.0 ):
 
@@ -369,7 +369,7 @@ render(Demo);
 | --- | --- | --- | --- | --- |
 | bottom | 弹出位置 bottom | number \| string | - | 0.25.0 |
 | duration | 自动关闭的延时,单位 s,设为 0 时不自动关闭 | number | 3 | 0.25.0 |
-| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` | () => HTMLElement \| null | () => document.body | 1.23.0 |
+| getPopupContainer | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 container 和 内部的 .semi-toast-wrapper `position: relative` | () => HTMLElement \| null | () => document.body | 1.23.0 |
 | left | 弹出位置 left | number \| string | - | 0.25.0 |
 | right | 弹出位置 right | number \| string | - | 0.25.0 |
 | top | 弹出位置 top | number \| string | - | 0.25.0 |

+ 39 - 1
content/show/popover/index-en-US.md

@@ -450,6 +450,32 @@ function Demo() {
 }
 ```
 
+### Initialize the Focus Position of Popup Layer
+
+Popover `content` also supports functions. Its input parameter is an object, which binds `initialFocusRef` to the focusable DOM or component. When the panel is opened, it will automatically focus at that position.
+
+```jsx live=true
+import React from 'react';
+import { Button, Input, Popover, Space } from '@douyinfe/semi-ui';
+() => {
+    const renderContent = ({ initialFocusRef }) => {
+        return (
+            <div style={{ padding: 12 }}>
+                <Space>
+                    <Button>first focusable element</Button>
+                    <Input ref={initialFocusRef} placeholder="focus here" />
+                </Space>
+            </div>
+        );
+    };
+    return (
+        <Popover content={renderContent} trigger="click">
+            <Button>click me</Button>
+        </Popover>
+    );
+};
+```
+
 ### Use with Tooltip or Popconfirm
 
 Please refer to [Use with Tooltip/Popconfirm](/en-US/show/tooltip#%E6%90%AD%E9%85%8D%20Popover%20%E6%88%96%20Popconfirm%20%E4%BD%BF%E7%94%A8)
@@ -460,12 +486,15 @@ Please refer to [Use with Tooltip/Popconfirm](/en-US/show/tooltip#%E6%90%AD%E9%8
 | --- | --- | --- | --- | --- |
 | autoAdjustOverflow | Whether to automatically adjust the expansion direction of the floating layer for automatic adjustment of the expansion direction during edge occlusion | boolean | true |
 | arrowPointAtCenter | Whether the "small triangle" points to the center of the element, you need to pass in "showArrow = true" at the same time | boolean | true | **0.34.0** |
+| 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 | string \| ReactNode |  |
 | clickToHide | Whether to automatically close the elastic layer when clicking on the floating layer and any element inside | boolean | false | **0.24.0** |
 | getPopupContainer | Specifies the parent DOM, and the bullet layer will be rendered to the DOM, you need to set 'position: relative` | () => 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** |
 | mouseEnterDelay | After the mouse is moved in, the display delay time, in milliseconds (only effective when the trigger is hover/focus) | number | 50 |  |
 | mouseLeaveDelay | The time for the delay to disappear after the mouse is moved out, in milliseconds (only effective when the trigger is hover/focus) | number | 50 |  |
 | rePosKey | You can update the value of this item to manually trigger the repositioning of the pop-up layer | string\|number |  |  |
+| 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** |
 | visible | Display popup or not | boolean |  |
 | position | Directions, optional values: `top`, `topLeft`, `topRight`, `leftTop`, `leftBottom`, `rightTop`, `rightTop`, `rightBottom`, `bottomLeft`, `bottomRight`, `bottomRight` | string | "bottom" |
 | spacing | The distance between the out layer and the children element, in px | number | 4(while showArrow=false) 10(while showArrow=true) |  |
@@ -473,8 +502,9 @@ Please refer to [Use with Tooltip/Popconfirm](/en-US/show/tooltip#%E6%90%AD%E9%8
 | trigger | Trigger mode, optional value: `hover`, `focus`, `click`, `custom` | string | 'hover' |
 | stopPropagation | Whether to prevent click events on the bomb layer from bubbling | boolean | false | **0.34.0** |
 | zIndex | Floating layer z-index value | number | 1030 |
-| onVisibleChange | A callback triggered when the pop-up layer is displayed / hidden | (isVisble: boolean) => void |  |
 | onClickOutSide  | Callback when the pop-up layer is in the display state and the non-Children, non-floating layer inner area is clicked (only valid when trigger is custom, click) | (e:event) => void | | **2.1.0** |
+| onEscKeyDown | Called when Esc key is pressed in trigger or popup layer | function(e:event) | | **2.8.0** |
+| onVisibleChange | A callback triggered when the pop-up layer is displayed / hidden | (isVisible: boolean) => void |  |
 
 ## Accessibility
 
@@ -490,6 +520,14 @@ Please refer to [Use with Tooltip/Popconfirm](/en-US/show/tooltip#%E6%90%AD%E9%8
   - Will be automatically added [aria-haspopup](https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup) attribute, which is `dialog`
   - Will be automatically added [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) attribute, which is the id of the content wrapper
 
+### Keyboard and Focus
+
+- When the Popover trigger method is set to `hover`: Open the Popover when the mouse is hovered or focused
+- When the Popover trigger method is set to `click`: Click the trigger or focus and use the Enter key to open the Popover
+- After the Popover is activated, press the `arrow key` ⬇️ to move the focus to the Popover. At this time, the focus is on the first interactive element in the Popover by default, and the user can also customize the focus position (if there is no interactive element in the Popover, it will appear as No response)
+- Use the `Tab` key when the focus is in the Popover, the focus will cycle in the Popover, and use `Shift + Tab` to move the focus in the opposite direction
+- Keyboard users can close the Popover by pressing `Esc`, after closing the focus returns to the trigger (when the trigger is click)
+
 ## Design Tokens
 
 <DesignToken/>

+ 43 - 2
content/show/popover/index.md

@@ -450,22 +450,54 @@ function Demo() {
 }
 ```
 
+### 初始化弹出层焦点位置
+
+Popover content 支持传入函数,它的入参是一个对象,将 `initialFocusRef` 绑定在可聚焦 DOM 或组件上,打开面板时会自动聚焦在该位置。
+
+```jsx live=true
+import React from 'react';
+import { Button, Input, Popover, Space } from '@douyinfe/semi-ui';
+
+() => {
+    const renderContent = ({ initialFocusRef }) => {
+        return (
+            <div style={{ padding: 12 }}>
+                <Space>
+                    <Button>first focusable element</Button>
+                    <Input ref={initialFocusRef} placeholder="focus here" />
+                </Space>
+            </div>
+        );
+    };
+
+    return (
+        <Popover content={renderContent} trigger="click">
+            <Button>click me</Button>
+        </Popover>
+    );
+};
+```
+
 ### 搭配 Tooltip 或 Popconfirm 使用
 
 请参考[搭配使用](/zh-CN/show/tooltip#%E6%90%AD%E9%85%8D%20Popover%20%E6%88%96%20Popconfirm%20%E4%BD%BF%E7%94%A8)
 
+
 ## API 参考
 
 | 属性               | 说明                                                                                                                                        | 类型                       | 默认值                                      | 版本       |
 | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------- | ---------- |
 | autoAdjustOverflow | 是否自动调整弹出层展开方向,用于边缘遮挡时自动调整展开方向                                                                                  | boolean                    | true                                        |            |
 | arrowPointAtCenter | “小三角”是否指向元素中心,需要同时传入"showArrow=true"                                                                                      | boolean                    | true                                        | **0.34.0** |
-| content            | 显示的内容                                                                                                                                  | string\|ReactNode          |                                             |            |
+| closeOnEsc         | 在 trigger 或 弹出层按 Esc 键是否关闭面板,受控时不生效 | boolean | true | **2.8.0** |
+| content            | 显示的内容(函数类型,2.8.0 版本支持)                                                                                                                                  | ReactNode \| ({ initialFocusRef }) => ReactNode          |                                             |            |
 | clickToHide        | 点击弹出层及内部任一元素时是否自动关闭弹层                                                                                                  | boolean                    | false                                       | **0.24.0** |
 | getPopupContainer  | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative`                                                                  | function():HTMLElement     | () => document.body                         |            |
+| guardFocus         | 当焦点处于弹出层内时,切换 Tab 是否让焦点在弹出层内循环 | boolean | true | **2.8.0** |
 | mouseEnterDelay    | 鼠标移入后,延迟显示的时间,单位毫秒(仅当 trigger 为 hover/focus 时生效)                                                                  | number                     | 50                                          |            |
 | mouseLeaveDelay    | 鼠标移出后,延迟消失的时间,单位毫秒(仅当 trigger 为 hover/focus 时生效)                                                                  | number                     | 50                                          |            |
 | rePosKey           | 可以更新该项值手动触发弹出层的重新定位                                                                                                         | string\|number             |                                            |             |
+| returnFocusOnClose | 按下 Esc 键后,焦点是否回到 trigger 上,只有设置 trigger 为 click 时生效 | boolean | true  | **2.8.0** |
 | position           | 方向,可选值:`top`,`topLeft`,`topRight`,`left`,`leftTop`,`leftBottom`,`right`,`rightTop`,`rightBottom`,`bottom`,`bottomLeft`,`bottomRight` | string                     | "bottom"                                    |            |
 | spacing            | 弹出层与 children 元素的距离,单位 px                                                                                                       | number                     | 4(showArrow=false 时) 10(showArrow=true 时) |            |
 | showArrow          | 是否显示“小三角”                                                                                                                            | boolean                    |                                             |            |
@@ -473,8 +505,9 @@ function Demo() {
 | trigger            | 触发方式,可选值:`hover`, `focus`, `click`, `custom`                                                                                       | string                     | 'hover'                                     |            |
 | visible            | 是否显示,配合trigger='custom'可实现完全受控                                                                                                                                    | boolean                    |                                             |            |
 | zIndex             | 弹出层 z-index 值                                                                                                                             | number                     | 1030                                        |            |
-| onVisibleChange    | 弹出层展示/隐藏时触发的回调                                                                                                                 | function(isVisble:boolean) |                                             |            |
 | onClickOutSide     | 当弹出层处于展示状态,点击非Children、非浮层内部区域时的回调(仅trigger为custom、click时有效)| function(e:event) |  | **2.1.0** |
+| onEscKeyDown       | 在 trigger 或 弹出层按 Esc 键时调用        | function(e:event) | | **2.8.0** |
+| onVisibleChange    | 弹出层展示/隐藏时触发的回调                                                                                                                 | function(isVisble:boolean) |                                             |            |
 
 ## Accessibility
 
@@ -490,5 +523,13 @@ function Demo() {
   - 会被自动添加 [aria-haspopup](https://www.w3.org/TR/wai-aria-1.1/#aria-haspopup) 属性,为 `dialog`
   - 会被自动添加 [aria-controls](https://www.w3.org/TR/wai-aria-1.1/#aria-controls) 属性,为 content 的 wrapper 的 id
 
+### 键盘和焦点
+
+- Popover 触发方式设置为 hover 时:鼠标悬浮或聚焦时打开 Popover
+- Popover 触发方式设置为 click 时:点击触发器或聚焦时并使用 Enter 键打开 Popover
+- Popover 激活后,按下方向键 ⬇️ 将焦点移动到 Popover 上,此时焦点默认处于 Popover 中第一个可交互元素上,用户也可自定义焦点位置(若 Popover 内无可交互元素则表现为无响应)
+- 焦点处于 Popover 内时使用 Tab 键,焦点会在 Popover 内循环,使用 Shift + Tab 会反方向移动焦点
+- 键盘用户能够通过按 Esc 关闭 Popover,关闭后焦点返回到触发器上(仅当 trigger 为 click 时)
+
 ## 设计变量
 <DesignToken/>

+ 14 - 0
content/start/changelog/index-en-US.md

@@ -16,6 +16,20 @@ Version:Major.Minor.Patch
 
 ---
 
+#### 🎉 2.8.0-beta.1 (2022-04-03)
+- 【Fix】
+    - Fixed error throw due to unescaped characters during Select search [#734](https://github.com/DouyinFE/semi-design/issues/734) [@boenfu](https://github.com/boenfu)
+#### 🎉 2.8.0-beta.0 (2022-04-02)
+- 【Fix】
+    - fix the problem that useNotification gets the same ID every time
+    - fix InputNumber value be formated when precision is set and defaultvalue is empty [@rojer95](https://github.com/rojer95)
+    - Fixed the panel rendering error when DatePicker defaultPickerValue passes numbers  [#735](https://github.com/DouyinFE/semi-design/issues/735)
+- 【Feat】
+    - Popover adds A11y keyboard and focus adaptation  [#205](https://github.com/DouyinFE/semi-design/issues/205)
+- 【Style】
+    - Adjust the CSS style of the extra element of Form Label: display: block -> flex, fix the problem of not centering alignment when placing Icon in extra [#324](https://github.com/DouyinFE/semi-design/issues/324)
+
+
 #### 🎉 2.7.1 (2022-03-30)
 - 【Fix】
     - Fixed focus style issue after Button is clicked (Affects 2.5.0 ~ 2.7.0, there is a problem with Safari compatibility, its behavior is the same as before 2.5.0) [#730](https://github.com/DouyinFE/semi-design/pull/730)

+ 15 - 0
content/start/changelog/index.md

@@ -15,6 +15,21 @@ Semi 版本号遵循**Semver**规范(主版本号-次版本号-修订版本号
 
 ---
 
+#### 🎉 2.8.0-beta.1 (2022-04-03)
+- 【Fix】
+    - 修复 Select 搜索时因为字符未转义导致报错的问题 [#734](https://github.com/DouyinFE/semi-design/issues/734) [@boenfu](https://github.com/boenfu)
+
+#### 🎉 2.8.0-beta.0 (2022-04-02)
+- 【Fix】
+    - 修复 useNotification 每次获得ID都相同的问题
+    - 修复当inputnumber初始值为空时,如果设置了precision,内容会被初始化为0且进行精度格式化的问题 [@rojer95](https://github.com/rojer95)
+    - 修复 DatePicker defaultPickerValue 传数字时面板渲染错误问题  [#735](https://github.com/DouyinFE/semi-design/issues/735)
+- 【Feat】
+    - Popover 新增 A11y 键盘和焦点适配  [#205](https://github.com/DouyinFE/semi-design/issues/205)
+- 【Style】
+    - Form Label的extra 元素CSS样式调整:display: block -> flex,修复 extra中放置 Icon时未居中对齐的问题 [#324](https://github.com/DouyinFE/semi-design/issues/324)
+
+
 #### 🎉 2.7.1 (2022-03-30)
 - 【Fix】
     - 修复 Button 点击后聚焦样式问题(影响2.5.0 ~ 2.7.0,Safari 兼容性有问题,其行为与 2.5.0 之前一致)[#730](https://github.com/DouyinFE/semi-design/pull/730)

+ 82 - 0
cypress/integration/popover.spec.js

@@ -0,0 +1,82 @@
+// 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('popover', () => {
+    it('trigger=click + keyboard', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=click]').click();
+        cy.get('[data-cy=click]').type('{downArrow}');
+        cy.get('[data-cy=pop-focusable-first]').should('be.focused');
+        cy.get('[data-cy=pop-focusable-first]').type('{esc}');
+        cy.get('[data-cy=click]').should('be.focused');
+        cy.get('[data-cy=click]').click();
+        cy.get('[data-cy=click]').type('{upArrow}');
+        cy.get('[data-cy=pop-focusable-last]').should('be.focused');
+    });
+
+    it('trigger=hover + keyboard', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=hover]').trigger('focus');
+        cy.get('[data-cy=hover]').type('{downArrow}');
+        cy.get('[data-cy=pop-focusable-first]').should('be.focused');
+        cy.get('[data-cy=pop-focusable-first]').type('{esc}');
+        cy.get('[data-cy=hover]').trigger('focus');
+        cy.get('[data-cy=hover]').type('{upArrow}');
+        cy.get('[data-cy=pop-focusable-last]').should('be.focused');
+    });
+
+    it('trigger=focus + keyboard', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=focus]').trigger('focus');
+        cy.get('[data-cy=focus]').type('{downArrow}');
+        cy.get('[data-cy=pop-focusable-first]').should('be.focused');
+        cy.get('[data-cy=pop-focusable-first]').type('{esc}');
+        cy.get('[data-cy=focus]').trigger('focus');
+        cy.get('[data-cy=focus]').type('{upArrow}');
+        cy.get('[data-cy=pop-focusable-last]').should('be.focused');
+    });
+
+    it('trigger=custom + keyboard', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=custom]').trigger('click');
+        cy.get('[data-cy=custom]').type('{downArrow}');
+        cy.get('[data-cy=pop-focusable-first]').should('be.focused');
+        cy.get('[data-cy=pop-focusable-first]').type('{esc}');
+        cy.get('[data-cy=custom]').trigger('click');
+        cy.get('[data-cy=custom]').type('{upArrow}');
+        cy.get('[data-cy=pop-focusable-last]').should('be.focused');
+    });
+
+    it('trigger=click + focus guard', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=click]').click();
+        cy.get('[data-cy=click]').type('{upArrow}');
+        cy.get('[data-cy=pop]').trigger('keydown', { eventConstructor: 'KeyboardEvent', key: 'Tab' });
+        cy.get('[data-cy=pop-focusable-first]').should('be.focused');
+        cy.get('[data-cy=pop]').trigger('keydown', { eventConstructor: 'KeyboardEvent', key: 'Tab', shiftKey: true });
+        cy.get('[data-cy=pop-focusable-last]').should('be.focused');
+    });
+
+    it('trigger=click + init focus', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=initial-focus]').click({ force: true });
+        cy.get('[data-cy=initial-focus-input]').should('be.focused');
+    });
+
+    it('trigger=click + closeOnEsc=false', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=closeOnEsc-false]').click({ force: true });
+        cy.get('[data-cy=closeOnEsc-false]').type('{esc}');
+        cy.get('[data-cy=pop]').should('be.visible');
+    });
+
+    it('trigger=click + returnFocusOnClose=false', () => {
+        cy.visit('http://localhost:6006/iframe.html?id=popover--a-11-y-keyboard&args=&viewMode=story');
+        cy.get('[data-cy=returnFocusOnClose-false]').click({ force: true });
+        cy.get('[data-cy=returnFocusOnClose-false]').type('{downArrow}');
+        cy.get('[data-cy=pop-focusable-first]').type('{esc}', { force: true });
+        cy.get('[data-cy=returnFocusOnClose-false]').should('not.be.focused');
+    });
+});

+ 1 - 2
cypress/integration/tooltip.spec.js

@@ -9,6 +9,7 @@
  * Cypress will default scroll element into view
  * @see https://docs.cypress.io/guides/core-concepts/interacting-with-elements#Scrolling
  */
+
 describe('tooltip', () => {
     it('leftTopOver autoAdjustOverflow', () => {
         const viewportWidth = 1200;
@@ -26,9 +27,7 @@ describe('tooltip', () => {
         cy.get(dataSelector).scrollIntoView(rightBottomPosition);
         cy.get('[x-placement="rightBottomOver"]').should('have.length', 1);
     });
-});
 
-describe('tooltip', () => {
     it('autoFocusHover', () => {
         cy.visit('http://127.0.0.1:6006/iframe.html?id=tooltip--auto-focus-content-demo&args=&viewMode=story');
         const dataSelector = `[data-cy=hover]`;

+ 1 - 1
lerna.json

@@ -1,5 +1,5 @@
 {
     "useWorkspaces": true,
     "npmClient": "yarn",
-    "version": "2.7.1"
+    "version": "2.8.0-beta.1"
 }

+ 1 - 1
package.json

@@ -150,7 +150,7 @@
     "chromatic": "^6.0.6",
     "crypto": "^1.0.1",
     "css-loader": "^3.6.0",
-    "cypress": "^9.2.1",
+    "cypress": "9.5.2",
     "enzyme": "^3.11.0",
     "enzyme-adapter-react-16": "^1.15.6",
     "enzyme-to-json": "^3.6.2",

+ 3 - 3
packages/semi-animation-react/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-animation-react",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "motion library for semi-ui-react",
   "keywords": [
     "motion",
@@ -26,8 +26,8 @@
   },
   "dependencies": {
     "@babel/runtime-corejs3": "^7.15.4",
-    "@douyinfe/semi-animation": "2.7.1",
-    "@douyinfe/semi-animation-styled": "2.7.1",
+    "@douyinfe/semi-animation": "2.8.0-beta.1",
+    "@douyinfe/semi-animation-styled": "2.8.0-beta.1",
     "classnames": "^2.2.6"
   },
   "peerDependencies": {

+ 1 - 1
packages/semi-animation-styled/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-animation-styled",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "semi styled animation",
   "keywords": [
     "semi",

+ 1 - 1
packages/semi-animation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-animation",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "animation base library for semi-ui",
   "keywords": [
     "animation",

+ 1 - 1
packages/semi-foundation/inputNumber/foundation.ts

@@ -432,7 +432,7 @@ class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
 
     _adjustPrec(num: string | number) {
         const precision = this.getProp('precision');
-        if (typeof precision === 'number') {
+        if (typeof precision === 'number' && num !== '') {
             num = Number(num).toFixed(precision);
         }
         return toString(num);

+ 2 - 2
packages/semi-foundation/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-foundation",
-    "version": "2.7.1",
+    "version": "2.8.0-beta.1",
     "description": "",
     "scripts": {
         "build:lib": "node ./scripts/compileLib.js",
@@ -8,7 +8,7 @@
     },
     "dependencies": {
         "@babel/runtime-corejs3": "^7.15.4",
-        "@douyinfe/semi-animation": "2.7.1",
+        "@douyinfe/semi-animation": "2.8.0-beta.1",
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",
         "date-fns": "^2.9.0",

+ 131 - 3
packages/semi-foundation/tooltip/foundation.ts

@@ -46,6 +46,7 @@ export interface TooltipAdapter<P = Record<string, any>, S = Record<string, any>
         click: string;
         focus: string;
         blur: string;
+        keydown: string;
     };
     registerTriggerEvent(...args: any[]): void;
     getTriggerBounding(...args: any[]): DOMRect;
@@ -61,6 +62,12 @@ export interface TooltipAdapter<P = Record<string, any>, S = Record<string, any>
     updateContainerPosition(): void;
     updatePlacementAttr(placement: Position): void;
     getContainerPosition(): string;
+    getFocusableElements(node: any): any[];
+    getActiveElement(): any;
+    getContainer(): any;
+    setInitialFocus(): void;
+    notifyEscKeydown(event: any): void;
+    getTriggerNode(): any;
 }
 
 export type Position = ArrayElement<typeof strings.POSITION_SET>;
@@ -154,7 +161,12 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
 
     _generateEvent(types: ArrayElement<typeof strings.TRIGGER_SET>) {
         const eventNames = this._adapter.getEventName();
-        const triggerEventSet = {};
+        const triggerEventSet = {
+            // bind esc keydown on trigger for a11y
+            [eventNames.keydown]: (event) => {
+                this._handleTriggerKeydown(event);
+            },
+        };
         let portalEventSet = {};
         switch (types) {
             case 'focus':
@@ -186,6 +198,13 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                     this.delayHide();
                     // this.hide('trigger');
                 };
+                // bind focus to hover trigger for a11y
+                triggerEventSet[eventNames.focus] = () => {
+                    this.delayShow();
+                };
+                triggerEventSet[eventNames.blur] = () => {
+                    this.delayHide();
+                };
 
                 portalEventSet = { ...triggerEventSet };
                 if (this.getProp('clickToHide')) {
@@ -205,7 +224,7 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
                 break;
             case 'custom':
                 // when trigger type is 'custom', no need to bind eventHandler
-                // show/hide completely depond on props.visible which change by user
+                // show/hide completely depend on props.visible which change by user
                 break;
             default:
                 break;
@@ -292,7 +311,12 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
     _togglePortalVisible(isVisible: boolean) {
         const nowVisible = this.getState('visible');
         if (nowVisible !== isVisible) {
-            this._adapter.togglePortalVisible(isVisible, () => this._adapter.notifyVisibleChange(isVisible));
+            this._adapter.togglePortalVisible(isVisible, () => {
+                if (isVisible) {
+                    this._adapter.setInitialFocus();
+                }
+                this._adapter.notifyVisibleChange(isVisible);
+            });
         }
     }
 
@@ -797,4 +821,108 @@ export default class Tooltip<P = Record<string, any>, S = Record<string, any>> e
     _initContainerPosition() {
         this._adapter.updateContainerPosition();
     }
+
+    handleContainerKeydown = (event: any) => {
+        const { guardFocus, closeOnEsc } = this.getProps();
+        switch (event && event.key) {
+            case "Escape":
+                closeOnEsc && this._handleEscKeyDown(event);
+                break;
+            case "Tab":
+                if (guardFocus) {
+                    const container = this._adapter.getContainer();
+                    const focusableElements = this._adapter.getFocusableElements(container);
+                    const focusableNum = focusableElements.length;
+    
+                    if (focusableNum) {
+                        // Shift + Tab will move focus backward
+                        if (event.shiftKey) {
+                            this._handleContainerShiftTabKeyDown(focusableElements, event);
+                        } else {
+                            this._handleContainerTabKeyDown(focusableElements, event);
+                        }
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+    }
+
+    _handleTriggerKeydown(event: any) {
+        const { closeOnEsc } = this.getProps();
+        const container = this._adapter.getContainer();
+        const focusableElements = this._adapter.getFocusableElements(container);
+        const focusableNum = focusableElements.length;
+        
+        switch (event && event.key) {
+            case "Escape":
+                closeOnEsc && this._handleEscKeyDown(event);
+                break;
+            case "ArrowUp":
+                focusableNum && this._handleTriggerArrowUpKeydown(focusableElements, event);
+                break;
+            case "ArrowDown":
+                focusableNum && this._handleTriggerArrowDownKeydown(focusableElements, event);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * focus trigger 
+     * 
+     * when trigger is 'focus' or 'hover', onFocus is bind to show popup
+     * if we focus trigger, popup will show again
+     * 
+     * 如果 trigger 是 focus 或者 hover,则它绑定了 onFocus,这里我们如果重新 focus 的话,popup 会再次打开
+     * 因此 returnFocusOnClose 只支持 click trigger
+     */
+    _focusTrigger() {
+        const { trigger, returnFocusOnClose } = this.getProps();
+        if (returnFocusOnClose && trigger === 'click') {
+            const triggerNode = this._adapter.getTriggerNode();
+            if (triggerNode && 'focus' in triggerNode) {
+                triggerNode.focus();
+            }
+        }
+    }
+
+    _handleEscKeyDown(event: any) {
+        const { trigger } = this.getProps();
+        if (trigger !== 'custom') {
+            this.hide();
+            this._focusTrigger();
+        }
+        this._adapter.notifyEscKeydown(event);
+    }
+
+    _handleContainerTabKeyDown(focusableElements: any[], event: any) {
+        const activeElement = this._adapter.getActiveElement();
+        const isLastCurrentFocus = focusableElements[focusableElements.length - 1] === activeElement;
+        if (isLastCurrentFocus) {
+            focusableElements[0].focus();
+            event.preventDefault(); // prevent browser default tab move behavior
+        }
+    }
+
+    _handleContainerShiftTabKeyDown(focusableElements: any[], event: any) {
+        const activeElement = this._adapter.getActiveElement();
+        const isFirstCurrentFocus = focusableElements[0] === activeElement;
+        if (isFirstCurrentFocus) {
+            focusableElements[focusableElements.length - 1].focus();
+            event.preventDefault(); // prevent browser default tab move behavior
+        }
+    }
+
+    _handleTriggerArrowDownKeydown(focusableElements: any[], event: any) {
+        focusableElements[0].focus();
+        event.preventDefault(); // prevent browser default scroll behavior
+    }
+
+    _handleTriggerArrowUpKeydown(focusableElements: any[], event: any) {
+        focusableElements[focusableElements.length - 1].focus();
+        event.preventDefault(); // prevent browser default scroll behavior
+    }
 }

+ 8 - 0
packages/semi-foundation/utils/isEscPress.ts

@@ -0,0 +1,8 @@
+import { get } from 'lodash';
+import { ESC_KEY } from './keyCode';
+
+function isEscPress<T extends { key: string }>(e: T) {
+    return get(e, 'key') === ESC_KEY ? true : false;
+}
+
+export default isEscPress;

+ 1 - 0
packages/semi-foundation/utils/keyCode.ts

@@ -428,5 +428,6 @@ const keyCode = {
 
 export const ENTER_KEY = 'Enter';
 export const TAB_KEY = 'Tab';
+export const ESC_KEY = 'Escape';
 
 export default keyCode;

+ 2 - 2
packages/semi-icons/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-icons",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "semi icons",
   "keywords": [
     "semi",
@@ -38,7 +38,7 @@
     "@babel/plugin-transform-runtime": "^7.15.8",
     "@babel/preset-env": "^7.15.8",
     "@babel/preset-react": "^7.14.5",
-    "@douyinfe/semi-webpack-plugin": "2.7.1",
+    "@douyinfe/semi-webpack-plugin": "2.8.0-beta.1",
     "babel-loader": "^8.2.2",
     "css-loader": "4.3.0",
     "del": "^6.0.0",

+ 1 - 1
packages/semi-illustrations/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-illustrations",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "semi illustrations",
   "keywords": [
     "semi",

+ 2 - 2
packages/semi-next/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-next",
-    "version": "2.7.1",
+    "version": "2.8.0-beta.1",
     "description": "Plugin that support Semi Design in Next.js",
     "author": "伍浩威 <[email protected]>",
     "homepage": "",
@@ -23,7 +23,7 @@
         "typescript": "^4"
     },
     "dependencies": {
-        "@douyinfe/semi-webpack-plugin": "2.7.1"
+        "@douyinfe/semi-webpack-plugin": "2.8.0-beta.1"
     },
     "gitHead": "eb34a4f25f002bb4cbcfa51f3df93bed868c831a"
 }

+ 1 - 1
packages/semi-scss-compile/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@douyinfe/semi-scss-compile",
-  "version": "2.7.1",
+  "version": "2.8.0-beta.1",
   "description": "compile semi scss to css",
   "author": "[email protected]",
   "license": "MIT",

+ 1 - 1
packages/semi-theme-default/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-theme-default",
-    "version": "2.7.1",
+    "version": "2.8.0-beta.1",
     "description": "semi-theme-default",
     "keywords": [
         "semi-theme",

+ 29 - 1
packages/semi-ui/_utils/index.ts

@@ -4,6 +4,7 @@ import React from 'react';
 import { cloneDeepWith, set, get } from 'lodash';
 import warning from '@douyinfe/semi-foundation/utils/warning';
 import { findAll } from '@douyinfe/semi-foundation/utils/getHighlight';
+import { isHTMLElement } from '@douyinfe/semi-foundation/utils/dom';
 /**
  * stop propagation
  *
@@ -161,6 +162,33 @@ export interface HighLightTextHTMLChunk {
  */
 export const isSemiIcon = (icon: any): boolean => React.isValidElement(icon) && get(icon.type, 'elementType') === 'Icon';
 
-export function getActiveElement() {
+export function getActiveElement(): HTMLElement | null {
     return document ? document.activeElement as HTMLElement : null;
+}
+
+export function isNodeContainsFocus(node: HTMLElement) {
+    const activeElement = getActiveElement();
+    return activeElement === node || node.contains(activeElement);
+}
+
+export function getFocusableElements(node: HTMLElement) {
+    if (!isHTMLElement(node)) {
+        return [];
+    }
+    const focusableSelectorsList = [
+        "input:not([disabled]):not([tabindex='-1'])",
+        "textarea:not([disabled]):not([tabindex='-1'])",
+        "button:not([disabled]):not([tabindex='-1'])",
+        "a[href]:not([tabindex='-1'])",
+        "select:not([disabled]):not([tabindex='-1'])",
+        "area[href]:not([tabindex='-1'])",
+        "iframe:not([tabindex='-1'])",
+        "object:not([tabindex='-1'])",
+        "*[tabindex]:not([tabindex='-1'])",
+        "*[contenteditable]:not([tabindex='-1'])",
+    ];
+    const focusableSelectorsStr = focusableSelectorsList.join(',');
+    // we are not filtered elements which are invisible
+    const focusableElements = Array.from(node.querySelectorAll<HTMLElement>(focusableSelectorsStr));
+    return focusableElements;
 }

+ 4 - 0
packages/semi-ui/inputNumber/_story/inputNumber.stories.js

@@ -63,6 +63,10 @@ export const _InputNumber = () => {
         />
         <br />
 
+        <label>小数(没有初始化值)</label>
+        <InputNumber precision={2} onChange={log} />
+        <br />
+
         <label>小数</label>
         <InputNumber defaultValue={10.08} precision={2} onChange={log} />
         <br />

+ 1 - 1
packages/semi-ui/notification/useNotification/index.tsx

@@ -59,8 +59,8 @@ export default function useNotification() {
     const [elements, patchElement] = usePatchElement();
     const noticeRef = new Map<string, { close: () => void } & ReactElement>();
 
-    const id = getUuid('semi_notice_');
     const addNotice = (config: NoticeProps) => {
+        const id = getUuid('semi_notice_');
         const mergeConfig = {
             ...config,
             id,

+ 8 - 8
packages/semi-ui/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-ui",
-    "version": "2.7.1",
+    "version": "2.8.0-beta.1",
     "description": "",
     "main": "lib/cjs/index.js",
     "module": "lib/es/index.js",
@@ -14,12 +14,12 @@
     },
     "dependencies": {
         "@babel/runtime-corejs3": "^7.15.4",
-        "@douyinfe/semi-animation": "2.7.1",
-        "@douyinfe/semi-animation-react": "2.7.1",
-        "@douyinfe/semi-foundation": "2.7.1",
-        "@douyinfe/semi-icons": "2.7.1",
-        "@douyinfe/semi-illustrations": "2.7.1",
-        "@douyinfe/semi-theme-default": "2.7.1",
+        "@douyinfe/semi-animation": "2.8.0-beta.1",
+        "@douyinfe/semi-animation-react": "2.8.0-beta.1",
+        "@douyinfe/semi-foundation": "2.8.0-beta.1",
+        "@douyinfe/semi-icons": "2.8.0-beta.1",
+        "@douyinfe/semi-illustrations": "2.8.0-beta.1",
+        "@douyinfe/semi-theme-default": "2.8.0-beta.1",
         "@types/react-window": "^1.8.2",
         "async-validator": "^3.5.0",
         "classnames": "^2.2.6",
@@ -75,7 +75,7 @@
         "@babel/plugin-transform-runtime": "^7.15.8",
         "@babel/preset-env": "^7.15.8",
         "@babel/preset-react": "^7.14.5",
-        "@douyinfe/semi-scss-compile": "2.7.1",
+        "@douyinfe/semi-scss-compile": "2.8.0-beta.1",
         "@storybook/addon-knobs": "^6.3.1",
         "@types/lodash": "^4.14.176",
         "babel-loader": "^8.2.2",

+ 75 - 1
packages/semi-ui/popover/_story/popover.stories.js

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
 
 import Popover from '../index';
 import { strings } from '@douyinfe/semi-foundation/tooltip/constants';
-import { Button, Input, Table, IconButton, Modal, Tag } from '@douyinfe/semi-ui';
+import { Button, Input, Table, IconButton, Modal, Tag, Space } from '@douyinfe/semi-ui';
 import SelectInPopover from './SelectInPopover';
 import BtnClose from './BtnClose';
 import PopRight from './PopRight';
@@ -572,3 +572,77 @@ export const ArrowPointAtCenterDemo = () => <ArrowPointAtCenter />;
 ArrowPointAtCenterDemo.story = {
   name: 'arrow point at center'
 }
+
+export const A11yKeyboard = () => {
+  const [visible, setVisible] = React.useState(false);
+  const popStyle = { height: 200, width: 200 };
+
+  const renderContent = ({ initialFocusRef }) => {
+    return (
+      <div style={popStyle} data-cy="pop">
+        <button data-cy="pop-focusable-first">first focusable</button>
+        <a href="https://semi.design">link</a>
+        {/* <input ref={initialFocusRef} placeholder="init focus" /> */}
+        <input placeholder="" defaultValue="semi" />
+        <a href="https://semi.design">link2</a>
+        <button data-cy="pop-focusable-last">last focusable</button>
+      </div>
+    );
+  };
+
+  const noFocusableContent = (
+    <div style={popStyle}>没有可聚焦元素</div>
+  );
+
+  const initFocusContent = ({ initialFocusRef }) => {
+    return (
+      <div style={popStyle} data-cy="pop">
+        <button data-cy="pop-focusable-first">first focusable</button>
+        <input placeholder="" defaultValue="semi" ref={initialFocusRef} data-cy="initial-focus-input" />
+        <button data-cy="pop-focusable-last">last focusable</button>
+      </div>
+    );
+  };
+
+  return (
+      <div style={{ paddingLeft: 100, paddingTop: 100 }}>
+          <Space spacing={100}>
+              <Popover content={renderContent} trigger="click" motion={false}>
+                  <Button data-cy="click">click</Button>
+              </Popover>
+              <Popover content={renderContent} trigger="hover">
+                  <span data-cy="hover">hover</span>
+              </Popover>
+              <Popover content={renderContent} trigger="focus">
+                  <Input data-cy="focus" defaultValue="focus" style={{ width: 150 }} />
+              </Popover>
+              <Popover
+                  content={renderContent}
+                  trigger="custom"
+                  visible={visible}
+                  onEscKeyDown={() => {
+                      console.log('esc key down');
+                      setVisible(false);
+                  }}
+              >
+                  <Button onClick={() => setVisible(!visible)} data-cy="custom">
+                    custom trigger + click me toggle show
+                  </Button>
+              </Popover>
+              <Popover content={noFocusableContent} trigger="click" data-cy="click-pop-contains-no-focusable">
+                  <Button>pop内没有可聚焦元素</Button>
+              </Popover>
+              <Popover content={initFocusContent} trigger="click" motion={false}>
+                  <Button data-cy="initial-focus">custom initialFocus</Button>
+              </Popover>
+              <Popover content={renderContent} trigger="click" motion={false} closeOnEsc={false}>
+                  <Button data-cy="closeOnEsc-false">closeOnEsc=false</Button>
+              </Popover>
+              <Popover content={renderContent} trigger="click" motion={false} returnFocusOnClose={false}>
+                  <Button data-cy="returnFocusOnClose-false">returnFocusOnClose=false</Button>
+              </Popover>
+          </Space>
+      </div>
+  );
+};
+A11yKeyboard.storyName = "a11y keyboard and focus";

+ 24 - 8
packages/semi-ui/popover/index.tsx

@@ -3,12 +3,12 @@ import classNames from 'classnames';
 import PropTypes from 'prop-types';
 import ConfigContext from '../configProvider/context';
 import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/popover/constants';
-import Tooltip, { ArrowBounding, Position, Trigger } from '../tooltip/index';
+import Tooltip, { ArrowBounding, Position, TooltipProps, Trigger, RenderContentProps } from '../tooltip/index';
 import Arrow from './Arrow';
 import '@douyinfe/semi-foundation/popover/popover.scss';
 import { BaseProps } from '../_base/baseComponent';
 import { Motion } from '../_base/base';
-import { noop } from 'lodash';
+import { isFunction, noop } from 'lodash';
 
 export { ArrowProps } from './Arrow';
 declare interface ArrowStyle {
@@ -19,7 +19,7 @@ declare interface ArrowStyle {
 
 export interface PopoverProps extends BaseProps {
     children?: React.ReactNode;
-    content?: React.ReactNode;
+    content?: TooltipProps['content'];
     visible?: boolean;
     autoAdjustOverflow?: boolean;
     motion?: Motion;
@@ -40,6 +40,10 @@ export interface PopoverProps extends BaseProps {
     rePosKey?: string | number;
     getPopupContainer?: () => HTMLElement;
     zIndex?: number;
+    closeOnEsc?: TooltipProps['closeOnEsc'];
+    guardFocus?: TooltipProps['guardFocus'];
+    returnFocusOnClose?: TooltipProps['returnFocusOnClose'];
+    onEscKeyDown?: TooltipProps['onEscKeyDown'];
 }
 
 export interface PopoverState {
@@ -52,7 +56,7 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
     static contextType = ConfigContext;
     static propTypes = {
         children: PropTypes.node,
-        content: PropTypes.node,
+        content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
         visible: PropTypes.bool,
         autoAdjustOverflow: PropTypes.bool,
         motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),
@@ -76,6 +80,7 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
         arrowPointAtCenter: PropTypes.bool,
         arrowBounding: PropTypes.object,
         prefixCls: PropTypes.string,
+        guardFocus: PropTypes.bool,
     };
 
     static defaultProps = {
@@ -90,9 +95,13 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
         position: 'bottom',
         prefixCls: cssClasses.PREFIX,
         onClickOutSide: noop,
+        onEscKeyDown: noop,
+        closeOnEsc: true,
+        returnFocusOnClose: true,
+        guardFocus: true,
     };
 
-    renderPopCard() {
+    renderPopCard = ({ initialFocusRef }: { initialFocusRef: RenderContentProps['initialFocusRef'] }) => {
         const { content, contentClassName, prefixCls } = this.props;
         const { direction } = this.context;
         const popCardCls = classNames(
@@ -102,13 +111,20 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
                 [`${prefixCls}-rtl`]: direction === 'rtl',
             }
         );
+        const contentNode = this.renderContentNode({ initialFocusRef, content });
         return (
             <div className={popCardCls}>
-                <div className={`${prefixCls}-content`}>{content}</div>
+                <div className={`${prefixCls}-content`}>{contentNode}</div>
             </div>
         );
     }
 
+    renderContentNode = (props: { content: TooltipProps['content'], initialFocusRef: RenderContentProps['initialFocusRef'] }) => {
+        const { initialFocusRef, content } = props;
+        const contentProps = { initialFocusRef };
+        return !isFunction(content) ? content : content(contentProps);
+    };
+
     render() {
         const {
             children,
@@ -122,7 +138,6 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
             ...attr
         } = this.props;
         let { spacing } = this.props;
-        const popContent = this.renderPopCard();
 
         const arrowProps = {
             position,
@@ -141,11 +156,12 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
 
         return (
             <Tooltip
+                guardFocus
                 {...(attr as any)}
                 trigger={trigger}
                 position={position}
                 style={style}
-                content={popContent}
+                content={this.renderPopCard}
                 prefixCls={prefixCls}
                 spacing={spacing}
                 showArrow={arrow}

+ 71 - 21
packages/semi-ui/tooltip/index.tsx

@@ -3,7 +3,7 @@ import React, { isValidElement, cloneElement } from 'react';
 import ReactDOM from 'react-dom';
 import classNames from 'classnames';
 import PropTypes from 'prop-types';
-import { throttle, noop, get, omit, each, isEmpty } from 'lodash';
+import { throttle, noop, get, omit, each, isEmpty, isFunction } from 'lodash';
 
 import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
 import warning from '@douyinfe/semi-foundation/utils/warning';
@@ -17,7 +17,7 @@ import '@douyinfe/semi-foundation/tooltip/tooltip.scss';
 
 import BaseComponent, { BaseProps } from '../_base/baseComponent';
 import { isHTMLElement } from '../_base/reactUtils';
-import { stopPropagation } from '../_utils';
+import { getActiveElement, getFocusableElements, stopPropagation } from '../_utils';
 import Portal from '../_portal/index';
 import ConfigContext from '../configProvider/context';
 import TriangleArrow from './TriangleArrow';
@@ -36,6 +36,12 @@ export interface ArrowBounding {
     height?: number;
 }
 
+export interface RenderContentProps {
+    initialFocusRef?: React.RefObject<HTMLElement>;
+}
+
+export type RenderContent = (props: RenderContentProps) => React.ReactNode;
+
 export interface TooltipProps extends BaseProps {
     children?: React.ReactNode;
     motion?: Motion;
@@ -49,7 +55,7 @@ export interface TooltipProps extends BaseProps {
     clickToHide?: boolean;
     visible?: boolean;
     style?: React.CSSProperties;
-    content?: React.ReactNode;
+    content?: React.ReactNode | RenderContent;
     prefixCls?: string;
     onVisibleChange?: (visible: boolean) => void;
     onClickOutSide?: (e: React.MouseEvent) => void;
@@ -65,6 +71,10 @@ export interface TooltipProps extends BaseProps {
     stopPropagation?: boolean;
     clickTriggerToHide?: boolean;
     wrapperClassName?: string;
+    closeOnEsc?: boolean;
+    guardFocus?: boolean;
+    returnFocusOnClose?: boolean;
+    onEscKeyDown?: (e: React.KeyboardEvent) => void;
 }
 interface TooltipState {
     visible: boolean;
@@ -108,7 +118,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         clickTriggerToHide: PropTypes.bool,
         visible: PropTypes.bool,
         style: PropTypes.object,
-        content: PropTypes.node,
+        content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
         prefixCls: PropTypes.string,
         onVisibleChange: PropTypes.func,
         onClickOutSide: PropTypes.func,
@@ -123,6 +133,8 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         // private
         role: PropTypes.string,
         wrapWhenSpecial: PropTypes.bool, // when trigger has special status such as "disabled" or "loading", wrap span
+        guardFocus: PropTypes.bool,
+        returnFocusOnClose: PropTypes.bool,
     };
 
     static defaultProps = {
@@ -143,11 +155,16 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         showArrow: true,
         wrapWhenSpecial: true,
         zIndex: numbers.DEFAULT_Z_INDEX,
+        closeOnEsc: false,
+        guardFocus: false,
+        returnFocusOnClose: false,
+        onEscKeyDown: noop,
     };
 
     eventManager: Event;
     triggerEl: React.RefObject<unknown>;
-    containerEl: React.RefObject<unknown>;
+    containerEl: React.RefObject<HTMLDivElement>;
+    initialFocusRef: React.RefObject<HTMLElement>;
     clickOutsideHandler: any;
     resizeHandler: any;
     isWrapped: boolean;
@@ -155,6 +172,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
     scrollHandler: any;
     getPopupContainer: () => HTMLElement;
     containerPosition: string;
+    foundation: TooltipFoundation;
 
     constructor(props: TooltipProps) {
         super(props);
@@ -180,6 +198,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         this.eventManager = new Event();
         this.triggerEl = React.createRef();
         this.containerEl = React.createRef();
+        this.initialFocusRef = React.createRef();
         this.clickOutsideHandler = null;
         this.resizeHandler = null;
         this.isWrapped = false; // Identifies whether a span element is wrapped
@@ -197,7 +216,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
             // eslint-disable-next-line @typescript-eslint/ban-ts-comment
             // @ts-ignore
             off: (...args: any[]) => this.eventManager.off(...args),
-            insertPortal: (content: string, { position, ...containerStyle }: { position: Position }) => {
+            insertPortal: (content: TooltipProps['content'], { position, ...containerStyle }: { position: Position }) => {
                 this.setState(
                     {
                         isInsert: true,
@@ -223,6 +242,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                 click: 'onClick',
                 focus: 'onFocus',
                 blur: 'onBlur',
+                keydown: 'onKeyDown'
             }),
             registerTriggerEvent: (triggerEventSet: Record<string, any>) => {
                 this.setState({ triggerEventSet });
@@ -236,12 +256,8 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                 // eslint-disable-next-line
                 // It may be a React component or an html element
                 // There is no guarantee that triggerE l.current can get the real dom, so call findDOMNode to ensure that you can get the real dom
-                let triggerDOM = this.triggerEl.current;
-                if (!isHTMLElement(this.triggerEl.current)) {
-                    const realDomNode = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
-                    (this.triggerEl as any).current = realDomNode;
-                    triggerDOM = realDomNode;
-                }
+                const triggerDOM = this.adapter.getTriggerNode();
+                (this.triggerEl as any).current = triggerDOM;
                 return triggerDOM && (triggerDOM as Element).getBoundingClientRect();
             },
             // Gets the outer size of the specified container
@@ -317,7 +333,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                     let el = this.triggerEl && this.triggerEl.current;
                     let popupEl = this.containerEl && this.containerEl.current;
                     el = ReactDOM.findDOMNode(el as React.ReactInstance);
-                    popupEl = ReactDOM.findDOMNode(popupEl as React.ReactInstance);
+                    popupEl = ReactDOM.findDOMNode(popupEl as React.ReactInstance) as HTMLDivElement;
                     if (
                         (el && !(el as any).contains(e.target) && popupEl && !(popupEl as any).contains(e.target)) ||
                         this.props.clickTriggerToHide
@@ -363,10 +379,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                     if (!this.mounted) {
                         return false;
                     }
-                    let triggerDOM = this.triggerEl.current;
-                    if (!isHTMLElement(this.triggerEl.current)) {
-                        triggerDOM = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
-                    }
+                    const triggerDOM = this.adapter.getTriggerNode();
                     const isRelativeScroll = e.target.contains(triggerDOM);
                     if (isRelativeScroll) {
                         const scrollPos = { x: e.target.scrollLeft, y: e.target.scrollTop };
@@ -392,6 +405,29 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                 }
             },
             getContainerPosition: () => this.containerPosition,
+            getContainer: () => this.containerEl && this.containerEl.current,
+            getTriggerNode: () => {
+                let triggerDOM = this.triggerEl.current;
+                if (!isHTMLElement(this.triggerEl.current)) {
+                    triggerDOM = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
+                }
+                return triggerDOM as Element;
+            },
+            getFocusableElements: (node: HTMLDivElement) => {
+                return getFocusableElements(node);
+            },
+            getActiveElement: () => {
+                return getActiveElement();
+            },
+            setInitialFocus: () => {
+                const focusRefNode = get(this, 'initialFocusRef.current');
+                if (focusRefNode && 'focus' in focusRefNode) {
+                    focusRefNode.focus();
+                } 
+            },
+            notifyEscKeydown: (event: React.KeyboardEvent) => {
+                this.props.onEscKeyDown(event);
+            }
         };
     }
 
@@ -491,9 +527,21 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
         }
     };
 
+    handlePortalInnerKeyDown = (e: React.KeyboardEvent) => {
+        this.foundation.handleContainerKeydown(e);
+    }
+
+    renderContentNode = (content: TooltipProps['content']) => {
+        const contentProps = {
+            initialFocusRef: this.initialFocusRef
+        };
+        return !isFunction(content) ? content : content(contentProps);
+    };
+
     renderPortal = () => {
         const { containerStyle = {}, visible, portalEventSet, placement, transitionState, id, isPositionUpdated } = this.state;
         const { prefixCls, content, showArrow, style, motion, role, zIndex } = this.props;
+        const contentNode = this.renderContentNode(content);
         const { className: propClassName } = this.props;
         const direction = this.context.direction;
         const className = classNames(propClassName, {
@@ -524,7 +572,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                                 x-placement={placement}
                                 id={id}
                             >
-                                {content}
+                                {contentNode}
                                 {icon}
                             </div>
                         ) :
@@ -533,7 +581,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
             </TooltipTransition>
         ) : (
             <div className={className} {...portalEventSet} x-placement={placement} style={{ visibility: motion ? undefined : 'visible', ...style }}>
-                {content}
+                {contentNode}
                 {icon}
             </div>
         );
@@ -546,6 +594,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                     style={portalInnerStyle}
                     ref={this.setContainerEl}
                     onClick={this.handlePortalInnerClick}
+                    onKeyDown={this.handlePortalInnerKeyDown}
                 >
                     {inner}
                 </div>
@@ -587,7 +636,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
 
     render() {
         const { isInsert, triggerEventSet, visible, id } = this.state;
-        const { wrapWhenSpecial, role } = this.props;
+        const { wrapWhenSpecial, role, trigger } = this.props;
         let { children } = this.props;
         const childrenStyle = { ...get(children, 'props.style') };
         const extraStyle: React.CSSProperties = {};
@@ -648,6 +697,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
                     ref.current = node;
                 }
             },
+            tabIndex: trigger === 'hover' ? 0 : undefined, // a11y keyboard
         });
 
         // If you do not add a layer of div, in order to bind the events and className in the tooltip, you need to cloneElement children, but this time it may overwrite the children's original ref reference
@@ -661,4 +711,4 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
     }
 }
 
-export { Position };
+export { Position };

+ 1 - 1
packages/semi-webpack/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@douyinfe/semi-webpack-plugin",
-    "version": "2.7.1",
+    "version": "2.8.0-beta.1",
     "description": "",
     "author": "伍浩威 <[email protected]>",
     "homepage": "",

+ 4 - 1
src/templates/toUEDUtils/toUED.ts

@@ -20,7 +20,7 @@ const getAnotherSideUrl=(site:'design'|'main')=>{
 };
 
 
-const cache={scrollHeight:null};
+const cache={ scrollHeight:null };
 
 const transContent=(site:'main'|'design')=>{
     const url=`${getAnotherSideUrl('design')}?concisemode=true`;
@@ -57,6 +57,8 @@ const transContent=(site:'main'|'design')=>{
                         iframeContainer.style['height']=`${data['scrollHeight']}px`;
                         console.log('height===>',data['scrollHeight']);
                         cache['scrollHeight']=`${data['scrollHeight']}px`;
+                        // @ts-ignore
+                        iframeDOM?.contentWindow?.semidoc?.setDarkmode(document.body.getAttribute('theme-mode')==='dark');
                     }
                 } catch (e){
                     console.log('getMessage ====>',e);
@@ -66,6 +68,7 @@ const transContent=(site:'main'|'design')=>{
 
             const contentAreaDOM=document.querySelector('.article-wrapper');
             contentAreaDOM.appendChild(iframeContainer);
+
         }
 
 

File diff suppressed because it is too large
+ 190 - 114
yarn.lock


Some files were not shown because too many files changed in this diff