@douyinfe/semi-ui 2.7.0-beta.0 → 2.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/_base/_story/index.stories.js +2 -6
  2. package/_portal/_story/portal.stories.js +1 -5
  3. package/_utils/hooks/usePrevFocus.ts +1 -0
  4. package/_utils/index.ts +29 -1
  5. package/datePicker/_story/v2/FixDefaultPickerValue.jsx +31 -0
  6. package/datePicker/_story/v2/InsetInput.jsx +1 -1
  7. package/datePicker/_story/v2/index.js +1 -0
  8. package/datePicker/monthsGrid.tsx +3 -13
  9. package/dist/css/semi.css +30 -21
  10. package/dist/css/semi.min.css +1 -1
  11. package/dist/umd/semi-ui.js +627 -287
  12. package/dist/umd/semi-ui.js.map +1 -1
  13. package/dist/umd/semi-ui.min.js +1 -1
  14. package/dist/umd/semi-ui.min.js.map +1 -1
  15. package/form/hooks/useFormApi.tsx +3 -2
  16. package/input/_story/input.stories.js +10 -1
  17. package/inputNumber/_story/inputNumber.stories.js +4 -0
  18. package/lib/cjs/_utils/hooks/usePrevFocus.js +1 -0
  19. package/lib/cjs/_utils/index.d.ts +3 -1
  20. package/lib/cjs/_utils/index.js +25 -1
  21. package/lib/cjs/datePicker/monthsGrid.js +11 -19
  22. package/lib/cjs/form/hooks/useFormApi.d.ts +2 -1
  23. package/lib/cjs/modal/useModal/HookModal.js +2 -0
  24. package/lib/cjs/notification/useNotification/index.js +1 -1
  25. package/lib/cjs/popover/index.d.ts +18 -3
  26. package/lib/cjs/popover/index.js +53 -23
  27. package/lib/cjs/radio/radioGroup.js +6 -0
  28. package/lib/cjs/select/index.js +5 -2
  29. package/lib/cjs/tag/group.d.ts +2 -0
  30. package/lib/cjs/tag/group.js +4 -2
  31. package/lib/cjs/tooltip/index.d.ts +22 -4
  32. package/lib/cjs/tooltip/index.js +65 -27
  33. package/lib/cjs/tree/nodeList.js +1 -0
  34. package/lib/cjs/treeSelect/index.js +4 -0
  35. package/lib/es/_utils/hooks/usePrevFocus.js +2 -0
  36. package/lib/es/_utils/index.d.ts +3 -1
  37. package/lib/es/_utils/index.js +18 -0
  38. package/lib/es/datePicker/monthsGrid.js +11 -19
  39. package/lib/es/form/hooks/useFormApi.d.ts +2 -1
  40. package/lib/es/modal/useModal/HookModal.js +2 -0
  41. package/lib/es/notification/useNotification/index.js +2 -1
  42. package/lib/es/popover/index.d.ts +18 -3
  43. package/lib/es/popover/index.js +52 -23
  44. package/lib/es/radio/radioGroup.js +6 -0
  45. package/lib/es/select/index.js +5 -2
  46. package/lib/es/tag/group.d.ts +2 -0
  47. package/lib/es/tag/group.js +4 -2
  48. package/lib/es/tooltip/index.d.ts +22 -4
  49. package/lib/es/tooltip/index.js +65 -27
  50. package/lib/es/tree/nodeList.js +1 -0
  51. package/lib/es/treeSelect/index.js +4 -0
  52. package/modal/_story/modal.stories.js +93 -1
  53. package/modal/useModal/HookModal.tsx +1 -0
  54. package/notification/_story/useNotification/index.jsx +21 -7
  55. package/notification/useNotification/index.tsx +1 -1
  56. package/package.json +9 -9
  57. package/popover/_story/popover.stories.js +75 -1
  58. package/popover/index.tsx +24 -8
  59. package/radio/__test__/radioGroup.test.jsx +9 -1
  60. package/radio/_story/radio.stories.js +22 -1
  61. package/radio/radioGroup.tsx +9 -0
  62. package/select/_story/select.stories.js +73 -2
  63. package/select/index.tsx +5 -3
  64. package/table/_story/v2/FixedMemoryLeak/index.jsx +33 -0
  65. package/table/_story/v2/index.js +2 -1
  66. package/tag/group.tsx +5 -3
  67. package/toast/_story/toast.stories.js +41 -0
  68. package/tooltip/index.tsx +72 -22
  69. package/tree/nodeList.tsx +1 -0
  70. package/treeSelect/index.tsx +3 -0
package/popover/index.tsx CHANGED
@@ -3,12 +3,12 @@ import classNames from 'classnames';
3
3
  import PropTypes from 'prop-types';
4
4
  import ConfigContext from '../configProvider/context';
5
5
  import { cssClasses, strings, numbers } from '@douyinfe/semi-foundation/popover/constants';
6
- import Tooltip, { ArrowBounding, Position, Trigger } from '../tooltip/index';
6
+ import Tooltip, { ArrowBounding, Position, TooltipProps, Trigger, RenderContentProps } from '../tooltip/index';
7
7
  import Arrow from './Arrow';
8
8
  import '@douyinfe/semi-foundation/popover/popover.scss';
9
9
  import { BaseProps } from '../_base/baseComponent';
10
10
  import { Motion } from '../_base/base';
11
- import { noop } from 'lodash';
11
+ import { isFunction, noop } from 'lodash';
12
12
 
13
13
  export { ArrowProps } from './Arrow';
14
14
  declare interface ArrowStyle {
@@ -19,7 +19,7 @@ declare interface ArrowStyle {
19
19
 
20
20
  export interface PopoverProps extends BaseProps {
21
21
  children?: React.ReactNode;
22
- content?: React.ReactNode;
22
+ content?: TooltipProps['content'];
23
23
  visible?: boolean;
24
24
  autoAdjustOverflow?: boolean;
25
25
  motion?: Motion;
@@ -40,6 +40,10 @@ export interface PopoverProps extends BaseProps {
40
40
  rePosKey?: string | number;
41
41
  getPopupContainer?: () => HTMLElement;
42
42
  zIndex?: number;
43
+ closeOnEsc?: TooltipProps['closeOnEsc'];
44
+ guardFocus?: TooltipProps['guardFocus'];
45
+ returnFocusOnClose?: TooltipProps['returnFocusOnClose'];
46
+ onEscKeyDown?: TooltipProps['onEscKeyDown'];
43
47
  }
44
48
 
45
49
  export interface PopoverState {
@@ -52,7 +56,7 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
52
56
  static contextType = ConfigContext;
53
57
  static propTypes = {
54
58
  children: PropTypes.node,
55
- content: PropTypes.node,
59
+ content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
56
60
  visible: PropTypes.bool,
57
61
  autoAdjustOverflow: PropTypes.bool,
58
62
  motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),
@@ -76,6 +80,7 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
76
80
  arrowPointAtCenter: PropTypes.bool,
77
81
  arrowBounding: PropTypes.object,
78
82
  prefixCls: PropTypes.string,
83
+ guardFocus: PropTypes.bool,
79
84
  };
80
85
 
81
86
  static defaultProps = {
@@ -90,9 +95,13 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
90
95
  position: 'bottom',
91
96
  prefixCls: cssClasses.PREFIX,
92
97
  onClickOutSide: noop,
98
+ onEscKeyDown: noop,
99
+ closeOnEsc: true,
100
+ returnFocusOnClose: true,
101
+ guardFocus: true,
93
102
  };
94
103
 
95
- renderPopCard() {
104
+ renderPopCard = ({ initialFocusRef }: { initialFocusRef: RenderContentProps['initialFocusRef'] }) => {
96
105
  const { content, contentClassName, prefixCls } = this.props;
97
106
  const { direction } = this.context;
98
107
  const popCardCls = classNames(
@@ -102,13 +111,20 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
102
111
  [`${prefixCls}-rtl`]: direction === 'rtl',
103
112
  }
104
113
  );
114
+ const contentNode = this.renderContentNode({ initialFocusRef, content });
105
115
  return (
106
116
  <div className={popCardCls}>
107
- <div className={`${prefixCls}-content`}>{content}</div>
117
+ <div className={`${prefixCls}-content`}>{contentNode}</div>
108
118
  </div>
109
119
  );
110
120
  }
111
121
 
122
+ renderContentNode = (props: { content: TooltipProps['content'], initialFocusRef: RenderContentProps['initialFocusRef'] }) => {
123
+ const { initialFocusRef, content } = props;
124
+ const contentProps = { initialFocusRef };
125
+ return !isFunction(content) ? content : content(contentProps);
126
+ };
127
+
112
128
  render() {
113
129
  const {
114
130
  children,
@@ -122,7 +138,6 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
122
138
  ...attr
123
139
  } = this.props;
124
140
  let { spacing } = this.props;
125
- const popContent = this.renderPopCard();
126
141
 
127
142
  const arrowProps = {
128
143
  position,
@@ -141,11 +156,12 @@ class Popover extends React.PureComponent<PopoverProps, PopoverState> {
141
156
 
142
157
  return (
143
158
  <Tooltip
159
+ guardFocus
144
160
  {...(attr as any)}
145
161
  trigger={trigger}
146
162
  position={position}
147
163
  style={style}
148
- content={popContent}
164
+ content={this.renderPopCard}
149
165
  prefixCls={prefixCls}
150
166
  spacing={spacing}
151
167
  showArrow={arrow}
@@ -196,4 +196,12 @@ describe('RadioGroup', () => {
196
196
  expect(middleRadio.exists(`.${BASE_CLASS_PREFIX}-radio-addon-buttonRadio-middle`)).toEqual(true);
197
197
  expect(largeRadio.exists(`.${BASE_CLASS_PREFIX}-radio-addon-buttonRadio-large`)).toEqual(true);
198
198
  });
199
- });
199
+
200
+ it('does not trigger Maximum update exceeded when setting radio-group\'s value to NaN', () => {
201
+ const radioGroup = mount(
202
+ createRadioGroup({ value: NaN }),
203
+ );
204
+
205
+ expect(radioGroup.exists(`${BASE_CLASS_PREFIX}-radio-checked`)).toEqual(false);
206
+ });
207
+ });
@@ -865,4 +865,25 @@ export const FixWithFieldLossRef = () => {
865
865
  </Form>
866
866
  );
867
867
  }
868
- FixWithFieldLossRef.storyName = '修复 Form Field 丢失 ref 问题 #384';
868
+ FixWithFieldLossRef.storyName = '修复 Form Field 丢失 ref 问题 #384';
869
+
870
+
871
+ export const SwitchValueToNaN = () => {
872
+ const [val, setVal] = useState(1);
873
+
874
+ return (
875
+ <>
876
+ <RadioGroup direction="vertical" aria-label="单选组合示例" value={val}>
877
+ <Radio value={1}>A</Radio>
878
+ <Radio value={2}>B</Radio>
879
+ <Radio value={3}>C</Radio>
880
+ <Radio value={4}>D</Radio>
881
+ </RadioGroup>
882
+ <Space>
883
+ <Button onClick={() => setVal(NaN)}>NaN</Button>
884
+ <Button onClick={() => setVal(2)}>2</Button>
885
+ </Space>
886
+ </>
887
+ );
888
+ }
889
+ SwitchValueToNaN.storyName = 'SwitchValueToNaN';
@@ -97,6 +97,15 @@ class RadioGroup extends BaseComponent<RadioGroupProps, RadioGroupState> {
97
97
  }
98
98
 
99
99
  componentDidUpdate(prevProps: RadioGroupProps) {
100
+ if (typeof prevProps.value === 'number'
101
+ && isNaN(prevProps.value)
102
+ && typeof this.props.value === 'number'
103
+ && isNaN(this.props.value)
104
+ ) {
105
+ // `NaN === NaN` returns false, and this will fail the next if check
106
+ // therefore triggering an infinite loop
107
+ return;
108
+ }
100
109
  if (prevProps.value !== this.props.value) {
101
110
  this.foundation.handlePropValueChange(this.props.value);
102
111
  }
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useRef, useEffect } from 'react';
2
2
 
3
3
  import './select.scss';
4
- import { Input, Select, Button, Icon, Avatar, Checkbox, Form, withField, Space } from '../../index';
4
+ import { Input, Select, Button, Icon, Avatar, Checkbox, Form, withField, Space, Tag } from '../../index';
5
5
  import CustomTrigger from './CustomTrigger';
6
6
  import classNames from 'classnames';
7
7
  import { getHighLightTextHTML } from '../../_utils/index';
@@ -2903,4 +2903,75 @@ export const AutoClearSearchValue = () => {
2903
2903
 
2904
2904
  SelectInputPropsDemo.story = {
2905
2905
  name: 'AutoClearSearchValue',
2906
- };
2906
+ };
2907
+
2908
+
2909
+ export const RenderSelectedItemCallCount = () => {
2910
+ const list = [
2911
+ { "name": "夏可漫", "email": "xiakeman@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/avatarDemo.jpeg" },
2912
+ { "name": "申悦", "email": "shenyue@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/bf8647bffab13c38772c9ff94bf91a9d.jpg" },
2913
+ { "name": "曲晨一", "email": "quchenyi@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/8bd8224511db085ed74fea37205aede5.jpg" },
2914
+ { "name": "文嘉茂", "email": "wenjiamao@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/6fbafc2d-e3e6-4cff-a1e2-17709c680624.png" },
2915
+ { "name": "文嘉茂2", "email": "wenjiamao@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/6fbafc2d-e3e6-4cff-a1e2-17709c680624.png" },
2916
+ { "name": "文嘉茂3", "email": "wenjiamao@example.com", "avatar": "https://sf6-cdn-tos.douyinstatic.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/6fbafc2d-e3e6-4cff-a1e2-17709c680624.png" },
2917
+ ]
2918
+
2919
+ const renderMultipleWithCustomTag = (optionNode, { onClose }) => {
2920
+ console.count('rerender')
2921
+ const content = (
2922
+ <Tag
2923
+ avatarSrc={optionNode.avatar}
2924
+ avatarShape='circle'
2925
+ closable={true}
2926
+ onClose={onClose}
2927
+ size='large'
2928
+ >
2929
+ {optionNode.name}
2930
+ </Tag>
2931
+ );
2932
+ return {
2933
+ isRenderInTag: false,
2934
+ content
2935
+ };
2936
+ }
2937
+
2938
+ const renderCustomOption = (item, index) => {
2939
+ const optionStyle = {
2940
+ display: 'flex',
2941
+ paddingLeft: 24,
2942
+ paddingTop: 10,
2943
+ paddingBottom: 10
2944
+ }
2945
+ return (
2946
+ <Select.Option key={index} value={item.name} style={optionStyle} showTick={true} {...item} key={item.email}>
2947
+ <Avatar size="small" src={item.avatar} />
2948
+ <div style={{ marginLeft: 8 }}>
2949
+ <div style={{ fontSize: 14 }}>{item.name}</div>
2950
+ <div style={{ color: 'var(--color-text-2)', fontSize: 12, lineHeight: '16px', fontWeight: 'normal' }}>{item.email}</div>
2951
+ </div>
2952
+ </Select.Option>
2953
+ )
2954
+ }
2955
+
2956
+ return (
2957
+ <>
2958
+ <Select
2959
+ placeholder='请选择'
2960
+ showClear
2961
+ multiple
2962
+ maxTagCount={2}
2963
+ style={{ width: 280, height: 40 }}
2964
+ onChange={v => console.log(v)}
2965
+ defaultValue={'夏可漫'}
2966
+ renderSelectedItem={renderMultipleWithCustomTag}
2967
+ >
2968
+ {list.map((item, index) => renderCustomOption(item, index))}
2969
+ </Select>
2970
+ </>
2971
+ );
2972
+ }
2973
+
2974
+
2975
+ RenderSelectedItemCallCount.story = {
2976
+ name: 'RenderSelectedItemCallCount',
2977
+ };
package/select/index.tsx CHANGED
@@ -898,8 +898,10 @@ class Select extends BaseComponent<SelectProps, SelectState> {
898
898
  content: optionNode.label,
899
899
  });
900
900
  }
901
+
902
+ const mapItems = maxTagCount ? selectedItems.slice(0, maxTagCount) : selectedItems; // no need to render rest tag when maxTagCount is setting
901
903
 
902
- const tags = selectedItems.map((item, i) => {
904
+ const tags = mapItems.map((item, i) => {
903
905
  const label = item[0];
904
906
  const { value } = item[1];
905
907
  const disabled = item[1].disabled || selectDisabled;
@@ -939,11 +941,11 @@ class Select extends BaseComponent<SelectProps, SelectState> {
939
941
  // [prefixcls + '-selection-text-inactive']: !inputValue && !tags.length,
940
942
  });
941
943
  const placeholderText = placeholder && !inputValue ? <span className={spanCls}>{placeholder}</span> : null;
942
- const n = tags.length > maxTagCount ? maxTagCount : undefined;
944
+ const n = selectedItems.length > maxTagCount ? maxTagCount : undefined;
943
945
 
944
946
  const NotOneLine = !maxTagCount; // Multiple lines (that is, do not set maxTagCount), do not use TagGroup, directly traverse with Tag, otherwise Input cannot follow the correct position
945
947
 
946
- const tagContent = NotOneLine ? tags : <TagGroup tagList={tags} maxTagCount={n} size="large" mode="custom" />;
948
+ const tagContent = NotOneLine ? tags : <TagGroup tagList={tags} maxTagCount={n} restCount={maxTagCount ? selectedItems.length - maxTagCount : undefined} size="large" mode="custom" />;
947
949
 
948
950
  return (
949
951
  <>
@@ -0,0 +1,33 @@
1
+ import React, { useState } from 'react';
2
+ import { Popconfirm, Table } from "@douyinfe/semi-ui";
3
+
4
+ export default function App() {
5
+ const [data, setData] = useState([{ a: 1 }]);
6
+ return (
7
+ <Table
8
+ dataSource={data}
9
+ columns={[
10
+ {
11
+ dataIndex: "a",
12
+ title: "a",
13
+ },
14
+ {
15
+ dataIndex: "b",
16
+ render: () => {
17
+ return (
18
+ <Popconfirm
19
+ onConfirm={() => {
20
+ setTimeout(() => {
21
+ setData([]);
22
+ });
23
+ }}
24
+ >
25
+ 删除
26
+ </Popconfirm>
27
+ );
28
+ },
29
+ },
30
+ ]}
31
+ />
32
+ );
33
+ }
@@ -3,4 +3,5 @@ export { default as FixedColumnsChange } from './FixedColumnsChange';
3
3
  export { default as FixedZIndex } from './FixedZIndex';
4
4
  export { default as FixedHeaderMerge } from './FixedHeaderMerge';
5
5
  export { default as FixedResizable } from './FixedResizable';
6
- export { default as FixedExpandedRow } from './FixedExpandedRow';
6
+ export { default as FixedExpandedRow } from './FixedExpandedRow';
7
+ export { default as FixedMemoryLeak } from './FixedMemoryLeak';
package/tag/group.tsx CHANGED
@@ -14,12 +14,13 @@ export interface TagGroupProps {
14
14
  style?: React.CSSProperties;
15
15
  className?: string;
16
16
  maxTagCount?: number;
17
+ restCount?: number;
17
18
  tagList?: (TagProps | React.ReactNode)[];
18
19
  size?: 'small' | 'large';
19
20
  showPopover?: boolean;
20
21
  popoverProps?: PopoverProps;
21
22
  avatarShape?: AvatarShape;
22
- mode?: string; // TODO: This API is not in the check file
23
+ mode?: string;
23
24
  }
24
25
 
25
26
  export default class TagGroup extends PureComponent<TagGroupProps> {
@@ -35,6 +36,7 @@ export default class TagGroup extends PureComponent<TagGroupProps> {
35
36
  style: PropTypes.object,
36
37
  className: PropTypes.string,
37
38
  maxTagCount: PropTypes.number,
39
+ restCount: PropTypes.number,
38
40
  tagList: PropTypes.array,
39
41
  size: PropTypes.oneOf(tagSize),
40
42
  mode: PropTypes.string,
@@ -77,8 +79,8 @@ export default class TagGroup extends PureComponent<TagGroupProps> {
77
79
  }
78
80
 
79
81
  renderMergeTags(tags: (Tag | React.ReactNode)[]) {
80
- const { maxTagCount, tagList } = this.props;
81
- const n = tagList.length - maxTagCount;
82
+ const { maxTagCount, tagList, restCount } = this.props;
83
+ const n = restCount ? restCount : tagList.length - maxTagCount;
82
84
  let renderTags: (Tag | React.ReactNode)[] = tags;
83
85
 
84
86
  const normalTags: (Tag | React.ReactNode)[] = tags.slice(0, maxTagCount);
@@ -96,3 +96,44 @@ export const _Toast = () => (
96
96
  _Toast.story = {
97
97
  name: 'toast',
98
98
  };
99
+
100
+ const ReachableContext = React.createContext();
101
+
102
+ /**
103
+ * test with cypress
104
+ * @returns
105
+ */
106
+ export const useToastDemo = () => {
107
+ const [toast, contextHolder] = Toast.useToast();
108
+ const config = {
109
+ duration: 0,
110
+ title: 'This is a success message',
111
+ content: <ReachableContext.Consumer>{name => `ReachableContext: ${name}`}</ReachableContext.Consumer>,
112
+ };
113
+
114
+ return (
115
+ <ReachableContext.Provider value="Light">
116
+ <div>
117
+ <Button
118
+ onClick={() => {
119
+ toast.success(config);
120
+ toast.info(config);
121
+ toast.error(config);
122
+ toast.warning(config);
123
+ const id = toast.open(config);
124
+
125
+ setTimeout(() => {
126
+ toast.close(id);
127
+ }, 100);
128
+ }}
129
+ >
130
+ Hook Toast
131
+ </Button>
132
+ </div>
133
+ <div data-cy="context-holder">
134
+ {contextHolder}
135
+ </div>
136
+ </ReachableContext.Provider>
137
+ );
138
+ };
139
+ useToastDemo.storyName = "useToast";
package/tooltip/index.tsx CHANGED
@@ -3,7 +3,7 @@ import React, { isValidElement, cloneElement } from 'react';
3
3
  import ReactDOM from 'react-dom';
4
4
  import classNames from 'classnames';
5
5
  import PropTypes from 'prop-types';
6
- import { throttle, noop, get, omit, each, isEmpty } from 'lodash';
6
+ import { throttle, noop, get, omit, each, isEmpty, isFunction } from 'lodash';
7
7
 
8
8
  import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
9
9
  import warning from '@douyinfe/semi-foundation/utils/warning';
@@ -17,7 +17,7 @@ import '@douyinfe/semi-foundation/tooltip/tooltip.scss';
17
17
 
18
18
  import BaseComponent, { BaseProps } from '../_base/baseComponent';
19
19
  import { isHTMLElement } from '../_base/reactUtils';
20
- import { stopPropagation } from '../_utils';
20
+ import { getActiveElement, getFocusableElements, stopPropagation } from '../_utils';
21
21
  import Portal from '../_portal/index';
22
22
  import ConfigContext from '../configProvider/context';
23
23
  import TriangleArrow from './TriangleArrow';
@@ -36,6 +36,12 @@ export interface ArrowBounding {
36
36
  height?: number;
37
37
  }
38
38
 
39
+ export interface RenderContentProps {
40
+ initialFocusRef?: React.RefObject<HTMLElement>;
41
+ }
42
+
43
+ export type RenderContent = (props: RenderContentProps) => React.ReactNode;
44
+
39
45
  export interface TooltipProps extends BaseProps {
40
46
  children?: React.ReactNode;
41
47
  motion?: Motion;
@@ -49,7 +55,7 @@ export interface TooltipProps extends BaseProps {
49
55
  clickToHide?: boolean;
50
56
  visible?: boolean;
51
57
  style?: React.CSSProperties;
52
- content?: React.ReactNode;
58
+ content?: React.ReactNode | RenderContent;
53
59
  prefixCls?: string;
54
60
  onVisibleChange?: (visible: boolean) => void;
55
61
  onClickOutSide?: (e: React.MouseEvent) => void;
@@ -65,6 +71,10 @@ export interface TooltipProps extends BaseProps {
65
71
  stopPropagation?: boolean;
66
72
  clickTriggerToHide?: boolean;
67
73
  wrapperClassName?: string;
74
+ closeOnEsc?: boolean;
75
+ guardFocus?: boolean;
76
+ returnFocusOnClose?: boolean;
77
+ onEscKeyDown?: (e: React.KeyboardEvent) => void;
68
78
  }
69
79
  interface TooltipState {
70
80
  visible: boolean;
@@ -108,7 +118,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
108
118
  clickTriggerToHide: PropTypes.bool,
109
119
  visible: PropTypes.bool,
110
120
  style: PropTypes.object,
111
- content: PropTypes.node,
121
+ content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
112
122
  prefixCls: PropTypes.string,
113
123
  onVisibleChange: PropTypes.func,
114
124
  onClickOutSide: PropTypes.func,
@@ -123,6 +133,8 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
123
133
  // private
124
134
  role: PropTypes.string,
125
135
  wrapWhenSpecial: PropTypes.bool, // when trigger has special status such as "disabled" or "loading", wrap span
136
+ guardFocus: PropTypes.bool,
137
+ returnFocusOnClose: PropTypes.bool,
126
138
  };
127
139
 
128
140
  static defaultProps = {
@@ -143,11 +155,16 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
143
155
  showArrow: true,
144
156
  wrapWhenSpecial: true,
145
157
  zIndex: numbers.DEFAULT_Z_INDEX,
158
+ closeOnEsc: false,
159
+ guardFocus: false,
160
+ returnFocusOnClose: false,
161
+ onEscKeyDown: noop,
146
162
  };
147
163
 
148
164
  eventManager: Event;
149
165
  triggerEl: React.RefObject<unknown>;
150
- containerEl: React.RefObject<unknown>;
166
+ containerEl: React.RefObject<HTMLDivElement>;
167
+ initialFocusRef: React.RefObject<HTMLElement>;
151
168
  clickOutsideHandler: any;
152
169
  resizeHandler: any;
153
170
  isWrapped: boolean;
@@ -155,6 +172,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
155
172
  scrollHandler: any;
156
173
  getPopupContainer: () => HTMLElement;
157
174
  containerPosition: string;
175
+ foundation: TooltipFoundation;
158
176
 
159
177
  constructor(props: TooltipProps) {
160
178
  super(props);
@@ -180,6 +198,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
180
198
  this.eventManager = new Event();
181
199
  this.triggerEl = React.createRef();
182
200
  this.containerEl = React.createRef();
201
+ this.initialFocusRef = React.createRef();
183
202
  this.clickOutsideHandler = null;
184
203
  this.resizeHandler = null;
185
204
  this.isWrapped = false; // Identifies whether a span element is wrapped
@@ -197,7 +216,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
197
216
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
198
217
  // @ts-ignore
199
218
  off: (...args: any[]) => this.eventManager.off(...args),
200
- insertPortal: (content: string, { position, ...containerStyle }: { position: Position }) => {
219
+ insertPortal: (content: TooltipProps['content'], { position, ...containerStyle }: { position: Position }) => {
201
220
  this.setState(
202
221
  {
203
222
  isInsert: true,
@@ -223,6 +242,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
223
242
  click: 'onClick',
224
243
  focus: 'onFocus',
225
244
  blur: 'onBlur',
245
+ keydown: 'onKeyDown'
226
246
  }),
227
247
  registerTriggerEvent: (triggerEventSet: Record<string, any>) => {
228
248
  this.setState({ triggerEventSet });
@@ -236,12 +256,8 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
236
256
  // eslint-disable-next-line
237
257
  // It may be a React component or an html element
238
258
  // 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
239
- let triggerDOM = this.triggerEl.current;
240
- if (!isHTMLElement(this.triggerEl.current)) {
241
- const realDomNode = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
242
- (this.triggerEl as any).current = realDomNode;
243
- triggerDOM = realDomNode;
244
- }
259
+ const triggerDOM = this.adapter.getTriggerNode();
260
+ (this.triggerEl as any).current = triggerDOM;
245
261
  return triggerDOM && (triggerDOM as Element).getBoundingClientRect();
246
262
  },
247
263
  // Gets the outer size of the specified container
@@ -302,7 +318,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
302
318
  } else {
303
319
  willUpdateStates.visible = visible;
304
320
  }
305
- this.setState(willUpdateStates as TooltipState, () => {
321
+ this.mounted && this.setState(willUpdateStates as TooltipState, () => {
306
322
  cb();
307
323
  });
308
324
  },
@@ -317,7 +333,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
317
333
  let el = this.triggerEl && this.triggerEl.current;
318
334
  let popupEl = this.containerEl && this.containerEl.current;
319
335
  el = ReactDOM.findDOMNode(el as React.ReactInstance);
320
- popupEl = ReactDOM.findDOMNode(popupEl as React.ReactInstance);
336
+ popupEl = ReactDOM.findDOMNode(popupEl as React.ReactInstance) as HTMLDivElement;
321
337
  if (
322
338
  (el && !(el as any).contains(e.target) && popupEl && !(popupEl as any).contains(e.target)) ||
323
339
  this.props.clickTriggerToHide
@@ -363,10 +379,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
363
379
  if (!this.mounted) {
364
380
  return false;
365
381
  }
366
- let triggerDOM = this.triggerEl.current;
367
- if (!isHTMLElement(this.triggerEl.current)) {
368
- triggerDOM = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
369
- }
382
+ const triggerDOM = this.adapter.getTriggerNode();
370
383
  const isRelativeScroll = e.target.contains(triggerDOM);
371
384
  if (isRelativeScroll) {
372
385
  const scrollPos = { x: e.target.scrollLeft, y: e.target.scrollTop };
@@ -392,6 +405,29 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
392
405
  }
393
406
  },
394
407
  getContainerPosition: () => this.containerPosition,
408
+ getContainer: () => this.containerEl && this.containerEl.current,
409
+ getTriggerNode: () => {
410
+ let triggerDOM = this.triggerEl.current;
411
+ if (!isHTMLElement(this.triggerEl.current)) {
412
+ triggerDOM = ReactDOM.findDOMNode(this.triggerEl.current as React.ReactInstance);
413
+ }
414
+ return triggerDOM as Element;
415
+ },
416
+ getFocusableElements: (node: HTMLDivElement) => {
417
+ return getFocusableElements(node);
418
+ },
419
+ getActiveElement: () => {
420
+ return getActiveElement();
421
+ },
422
+ setInitialFocus: () => {
423
+ const focusRefNode = get(this, 'initialFocusRef.current');
424
+ if (focusRefNode && 'focus' in focusRefNode) {
425
+ focusRefNode.focus();
426
+ }
427
+ },
428
+ notifyEscKeydown: (event: React.KeyboardEvent) => {
429
+ this.props.onEscKeyDown(event);
430
+ }
395
431
  };
396
432
  }
397
433
 
@@ -491,9 +527,21 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
491
527
  }
492
528
  };
493
529
 
530
+ handlePortalInnerKeyDown = (e: React.KeyboardEvent) => {
531
+ this.foundation.handleContainerKeydown(e);
532
+ }
533
+
534
+ renderContentNode = (content: TooltipProps['content']) => {
535
+ const contentProps = {
536
+ initialFocusRef: this.initialFocusRef
537
+ };
538
+ return !isFunction(content) ? content : content(contentProps);
539
+ };
540
+
494
541
  renderPortal = () => {
495
542
  const { containerStyle = {}, visible, portalEventSet, placement, transitionState, id, isPositionUpdated } = this.state;
496
543
  const { prefixCls, content, showArrow, style, motion, role, zIndex } = this.props;
544
+ const contentNode = this.renderContentNode(content);
497
545
  const { className: propClassName } = this.props;
498
546
  const direction = this.context.direction;
499
547
  const className = classNames(propClassName, {
@@ -524,7 +572,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
524
572
  x-placement={placement}
525
573
  id={id}
526
574
  >
527
- {content}
575
+ {contentNode}
528
576
  {icon}
529
577
  </div>
530
578
  ) :
@@ -533,7 +581,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
533
581
  </TooltipTransition>
534
582
  ) : (
535
583
  <div className={className} {...portalEventSet} x-placement={placement} style={{ visibility: motion ? undefined : 'visible', ...style }}>
536
- {content}
584
+ {contentNode}
537
585
  {icon}
538
586
  </div>
539
587
  );
@@ -546,6 +594,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
546
594
  style={portalInnerStyle}
547
595
  ref={this.setContainerEl}
548
596
  onClick={this.handlePortalInnerClick}
597
+ onKeyDown={this.handlePortalInnerKeyDown}
549
598
  >
550
599
  {inner}
551
600
  </div>
@@ -587,7 +636,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
587
636
 
588
637
  render() {
589
638
  const { isInsert, triggerEventSet, visible, id } = this.state;
590
- const { wrapWhenSpecial, role } = this.props;
639
+ const { wrapWhenSpecial, role, trigger } = this.props;
591
640
  let { children } = this.props;
592
641
  const childrenStyle = { ...get(children, 'props.style') };
593
642
  const extraStyle: React.CSSProperties = {};
@@ -648,6 +697,7 @@ export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
648
697
  ref.current = node;
649
698
  }
650
699
  },
700
+ tabIndex: trigger === 'hover' ? 0 : undefined, // a11y keyboard
651
701
  });
652
702
 
653
703
  // 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> {
661
711
  }
662
712
  }
663
713
 
664
- export { Position };
714
+ export { Position };
package/tree/nodeList.tsx CHANGED
@@ -55,6 +55,7 @@ export default class NodeList extends PureComponent<NodeListProps, NodeListState
55
55
  }
56
56
 
57
57
  onMotionEnd = () => {
58
+ typeof this.props.onMotionEnd === 'function' && this.props.onMotionEnd();
58
59
  this.setState({ transitionNodes: [] });
59
60
  };
60
61