@douyinfe/semi-ui 2.7.1 → 2.8.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.
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
2
2
 
3
3
  import Popover from '../index';
4
4
  import { strings } from '@douyinfe/semi-foundation/tooltip/constants';
5
- import { Button, Input, Table, IconButton, Modal, Tag } from '@douyinfe/semi-ui';
5
+ import { Button, Input, Table, IconButton, Modal, Tag, Space } from '@douyinfe/semi-ui';
6
6
  import SelectInPopover from './SelectInPopover';
7
7
  import BtnClose from './BtnClose';
8
8
  import PopRight from './PopRight';
@@ -572,3 +572,77 @@ export const ArrowPointAtCenterDemo = () => <ArrowPointAtCenter />;
572
572
  ArrowPointAtCenterDemo.story = {
573
573
  name: 'arrow point at center'
574
574
  }
575
+
576
+ export const A11yKeyboard = () => {
577
+ const [visible, setVisible] = React.useState(false);
578
+ const popStyle = { height: 200, width: 200 };
579
+
580
+ const renderContent = ({ initialFocusRef }) => {
581
+ return (
582
+ <div style={popStyle} data-cy="pop">
583
+ <button data-cy="pop-focusable-first">first focusable</button>
584
+ <a href="https://semi.design">link</a>
585
+ {/* <input ref={initialFocusRef} placeholder="init focus" /> */}
586
+ <input placeholder="" defaultValue="semi" />
587
+ <a href="https://semi.design">link2</a>
588
+ <button data-cy="pop-focusable-last">last focusable</button>
589
+ </div>
590
+ );
591
+ };
592
+
593
+ const noFocusableContent = (
594
+ <div style={popStyle}>没有可聚焦元素</div>
595
+ );
596
+
597
+ const initFocusContent = ({ initialFocusRef }) => {
598
+ return (
599
+ <div style={popStyle} data-cy="pop">
600
+ <button data-cy="pop-focusable-first">first focusable</button>
601
+ <input placeholder="" defaultValue="semi" ref={initialFocusRef} data-cy="initial-focus-input" />
602
+ <button data-cy="pop-focusable-last">last focusable</button>
603
+ </div>
604
+ );
605
+ };
606
+
607
+ return (
608
+ <div style={{ paddingLeft: 100, paddingTop: 100 }}>
609
+ <Space spacing={100}>
610
+ <Popover content={renderContent} trigger="click" motion={false}>
611
+ <Button data-cy="click">click</Button>
612
+ </Popover>
613
+ <Popover content={renderContent} trigger="hover">
614
+ <span data-cy="hover">hover</span>
615
+ </Popover>
616
+ <Popover content={renderContent} trigger="focus">
617
+ <Input data-cy="focus" defaultValue="focus" style={{ width: 150 }} />
618
+ </Popover>
619
+ <Popover
620
+ content={renderContent}
621
+ trigger="custom"
622
+ visible={visible}
623
+ onEscKeyDown={() => {
624
+ console.log('esc key down');
625
+ setVisible(false);
626
+ }}
627
+ >
628
+ <Button onClick={() => setVisible(!visible)} data-cy="custom">
629
+ custom trigger + click me toggle show
630
+ </Button>
631
+ </Popover>
632
+ <Popover content={noFocusableContent} trigger="click" data-cy="click-pop-contains-no-focusable">
633
+ <Button>pop内没有可聚焦元素</Button>
634
+ </Popover>
635
+ <Popover content={initFocusContent} trigger="click" motion={false}>
636
+ <Button data-cy="initial-focus">custom initialFocus</Button>
637
+ </Popover>
638
+ <Popover content={renderContent} trigger="click" motion={false} closeOnEsc={false}>
639
+ <Button data-cy="closeOnEsc-false">closeOnEsc=false</Button>
640
+ </Popover>
641
+ <Popover content={renderContent} trigger="click" motion={false} returnFocusOnClose={false}>
642
+ <Button data-cy="returnFocusOnClose-false">returnFocusOnClose=false</Button>
643
+ </Popover>
644
+ </Space>
645
+ </div>
646
+ );
647
+ };
648
+ A11yKeyboard.storyName = "a11y keyboard and focus";
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}
@@ -504,6 +504,22 @@ describe('Select', () => {
504
504
  expect(optionList.at(0).text()).toEqual('Abc');
505
505
  });
506
506
 
507
+ it('filter = true,label includes regex special character and key it at first', () => {
508
+ let props = {
509
+ filter: true,
510
+ optionList: [{label: 'label++',value: ''}]
511
+ };
512
+ const select = getSelect(props);
513
+ // click to show input
514
+ select.find(`.${BASE_CLASS_PREFIX}-select`).simulate('click', {});
515
+ let inputValue = '+';
516
+ let event = { target: { value: inputValue } };
517
+ select.find('input').simulate('change', event);
518
+ let optionList = select.find(`.${BASE_CLASS_PREFIX}-select-option-list`).children();
519
+ expect(optionList.length).toEqual(1);
520
+ expect(optionList.at(0).text()).toEqual('label++');
521
+ });
522
+
507
523
  it('filter = custom function', () => {
508
524
  let customFilter = (sugInput, option) => {
509
525
  return option.label == 'Hotsoon';
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
@@ -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 };