123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- /* eslint-disable prefer-destructuring, max-lines-per-function, react/no-find-dom-node, max-len, @typescript-eslint/no-empty-function */
- 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, isFunction, isEqual } from 'lodash';
- import { BASE_CLASS_PREFIX } from '@douyinfe/semi-foundation/base/constants';
- import warning from '@douyinfe/semi-foundation/utils/warning';
- import Event from '@douyinfe/semi-foundation/utils/Event';
- import { ArrayElement } from '@douyinfe/semi-foundation/utils/type';
- import { convertDOMRectToObject, DOMRectLikeType } from '@douyinfe/semi-foundation/utils/dom';
- import TooltipFoundation, {
- TooltipAdapter,
- Position,
- PopupContainerDOMRect
- } from '@douyinfe/semi-foundation/tooltip/foundation';
- import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/tooltip/constants';
- import { getUuidShort } from '@douyinfe/semi-foundation/utils/uuid';
- import '@douyinfe/semi-foundation/tooltip/tooltip.scss';
- import BaseComponent, { BaseProps } from '../_base/baseComponent';
- import { isHTMLElement } from '../_base/reactUtils';
- import { getActiveElement, getFocusableElements, stopPropagation } from '../_utils';
- import Portal from '../_portal/index';
- import ConfigContext, { ContextValue } from '../configProvider/context';
- import TriangleArrow from './TriangleArrow';
- import TriangleArrowVertical from './TriangleArrowVertical';
- import ArrowBoundingShape from './ArrowBoundingShape';
- import CSSAnimation from "../_cssAnimation";
- export type Trigger = ArrayElement<typeof strings.TRIGGER_SET>;
- export type { Position };
- export interface ArrowBounding {
- offsetX?: number;
- offsetY?: number;
- width?: number;
- height?: number
- }
- export interface RenderContentProps<T = HTMLElement> {
- initialFocusRef?: React.RefObject<T>
- }
- export type RenderContent<T = HTMLElement> = (props: RenderContentProps<T>) => React.ReactNode;
- export interface TooltipProps extends BaseProps {
- children?: React.ReactNode;
- motion?: boolean;
- autoAdjustOverflow?: boolean;
- position?: Position;
- getPopupContainer?: () => HTMLElement;
- mouseEnterDelay?: number;
- mouseLeaveDelay?: number;
- trigger?: Trigger;
- className?: string;
- clickToHide?: boolean;
- visible?: boolean;
- style?: React.CSSProperties;
- content?: React.ReactNode | RenderContent;
- prefixCls?: string;
- onVisibleChange?: (visible: boolean) => void;
- onClickOutSide?: (e: React.MouseEvent) => void;
- spacing?: number;
- margin?: number | { marginLeft: number; marginTop: number; marginRight: number; marginBottom: number };
- showArrow?: boolean | React.ReactNode;
- zIndex?: number;
- rePosKey?: string | number;
- role?: string;
- arrowBounding?: ArrowBounding;
- transformFromCenter?: boolean;
- arrowPointAtCenter?: boolean;
- wrapWhenSpecial?: boolean;
- stopPropagation?: boolean;
- clickTriggerToHide?: boolean;
- wrapperClassName?: string;
- closeOnEsc?: boolean;
- guardFocus?: boolean;
- returnFocusOnClose?: boolean;
- onEscKeyDown?: (e: React.KeyboardEvent) => void;
- disableArrowKeyDown?: boolean;
- wrapperId?: string;
- preventScroll?: boolean;
- disableFocusListener?: boolean;
- afterClose?: () => void;
- keepDOM?: boolean
- }
- interface TooltipState {
- visible: boolean;
- transitionState: string;
- triggerEventSet: {
- [key: string]: any
- };
- portalEventSet: {
- [key: string]: any
- };
- containerStyle: React.CSSProperties;
- isInsert: boolean;
- placement: Position;
- transitionStyle: Record<string, any>;
- isPositionUpdated: boolean;
- id: string;
- displayNone: boolean
- }
- const prefix = cssClasses.PREFIX;
- const positionSet = strings.POSITION_SET;
- const triggerSet = strings.TRIGGER_SET;
- const blockDisplays = ['flex', 'block', 'table', 'flow-root', 'grid'];
- const defaultGetContainer = () => document.body;
- export default class Tooltip extends BaseComponent<TooltipProps, TooltipState> {
- static contextType = ConfigContext;
- static propTypes = {
- children: PropTypes.node,
- motion: PropTypes.bool,
- autoAdjustOverflow: PropTypes.bool,
- position: PropTypes.oneOf(positionSet),
- getPopupContainer: PropTypes.func,
- mouseEnterDelay: PropTypes.number,
- mouseLeaveDelay: PropTypes.number,
- trigger: PropTypes.oneOf(triggerSet).isRequired,
- className: PropTypes.string,
- wrapperClassName: PropTypes.string,
- clickToHide: PropTypes.bool,
- // used with trigger === hover, private
- clickTriggerToHide: PropTypes.bool,
- visible: PropTypes.bool,
- style: PropTypes.object,
- content: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
- prefixCls: PropTypes.string,
- onVisibleChange: PropTypes.func,
- onClickOutSide: PropTypes.func,
- spacing: PropTypes.number,
- margin: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
- showArrow: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
- zIndex: PropTypes.number,
- rePosKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- arrowBounding: ArrowBoundingShape,
- transformFromCenter: PropTypes.bool, // Whether to change from the center of the trigger (for dynamic effects)
- arrowPointAtCenter: PropTypes.bool,
- stopPropagation: PropTypes.bool,
- // 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,
- preventScroll: PropTypes.bool,
- keepDOM: PropTypes.bool,
- };
- static defaultProps = {
- arrowBounding: numbers.ARROW_BOUNDING,
- autoAdjustOverflow: true,
- arrowPointAtCenter: true,
- trigger: 'hover',
- transformFromCenter: true,
- position: 'top',
- prefixCls: prefix,
- role: 'tooltip',
- mouseEnterDelay: numbers.MOUSE_ENTER_DELAY,
- mouseLeaveDelay: numbers.MOUSE_LEAVE_DELAY,
- motion: true,
- onVisibleChange: noop,
- onClickOutSide: noop,
- spacing: numbers.SPACING,
- margin: numbers.MARGIN,
- showArrow: true,
- wrapWhenSpecial: true,
- zIndex: numbers.DEFAULT_Z_INDEX,
- closeOnEsc: false,
- guardFocus: false,
- returnFocusOnClose: false,
- onEscKeyDown: noop,
- disableFocusListener: false,
- disableArrowKeyDown: false,
- keepDOM: false
- };
- eventManager: Event;
- triggerEl: React.RefObject<unknown>;
- containerEl: React.RefObject<HTMLDivElement>;
- initialFocusRef: React.RefObject<HTMLElement>;
- clickOutsideHandler: any;
- resizeHandler: any;
- isWrapped: boolean;
- mounted: any;
- scrollHandler: any;
- getPopupContainer: () => HTMLElement;
- containerPosition: string;
- foundation: TooltipFoundation;
- context: ContextValue;
- constructor(props: TooltipProps) {
- super(props);
- this.state = {
- visible: false,
- /**
- *
- * Note: The transitionState parameter is equivalent to isInsert
- */
- transitionState: '',
- triggerEventSet: {},
- portalEventSet: {},
- containerStyle: {
- // zIndex: props.zIndex,
- },
- isInsert: false,
- placement: props.position || 'top',
- transitionStyle: {},
- isPositionUpdated: false,
- id: props.wrapperId, // auto generate id, will be used by children.aria-describedby & content.id, improve a11y,
- displayNone: false
- };
- this.foundation = new TooltipFoundation(this.adapter);
- 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
- this.containerPosition = undefined;
- }
- setContainerEl = (node: HTMLDivElement) => (this.containerEl = { current: node });
- get adapter(): TooltipAdapter<TooltipProps, TooltipState> {
- return {
- ...super.adapter,
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- on: (...args: any[]) => this.eventManager.on(...args),
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- off: (...args: any[]) => this.eventManager.off(...args),
- insertPortal: (content: TooltipProps['content'], { position, ...containerStyle }: { position: Position }) => {
- this.setState(
- {
- isInsert: true,
- transitionState: 'enter',
- containerStyle: { ...this.state.containerStyle, ...containerStyle },
- },
- () => {
- setTimeout(() => {
- // waiting child component mounted
- this.eventManager.emit('portalInserted');
- }, 0);
- }
- );
- },
- removePortal: () => {
- this.setState({ isInsert: false, isPositionUpdated: false });
- },
- getEventName: () => ({
- mouseEnter: 'onMouseEnter',
- mouseLeave: 'onMouseLeave',
- mouseOut: 'onMouseOut',
- mouseOver: 'onMouseOver',
- click: 'onClick',
- focus: 'onFocus',
- blur: 'onBlur',
- keydown: 'onKeyDown'
- }),
- registerTriggerEvent: (triggerEventSet: Record<string, any>) => {
- this.setState({ triggerEventSet });
- },
- registerPortalEvent: (portalEventSet: Record<string, any>) => {
- this.setState({ portalEventSet });
- },
- getTriggerBounding: () => {
- // 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
- 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
- getPopupContainerRect: () => {
- const container = this.getPopupContainer();
- let rect: PopupContainerDOMRect = null;
- if (container && isHTMLElement(container)) {
- const boundingRect: DOMRectLikeType = convertDOMRectToObject(container.getBoundingClientRect());
- rect = {
- ...boundingRect,
- scrollLeft: container.scrollLeft,
- scrollTop: container.scrollTop,
- };
- }
- return rect;
- },
- containerIsBody: () => {
- const container = this.getPopupContainer();
- return container === document.body;
- },
- containerIsRelative: () => {
- const container = this.getPopupContainer();
- const computedStyle = window.getComputedStyle(container);
- return computedStyle.getPropertyValue('position') === 'relative';
- },
- containerIsRelativeOrAbsolute: () => ['relative', 'absolute'].includes(this.containerPosition),
- // Get the size of the pop-up layer
- getWrapperBounding: () => {
- const el = this.containerEl && this.containerEl.current;
- return el && (el as Element).getBoundingClientRect();
- },
- getDocumentElementBounding: () => document.documentElement.getBoundingClientRect(),
- setPosition: ({ position, ...style }: { position: Position }) => {
- this.setState(
- {
- containerStyle: { ...this.state.containerStyle, ...style },
- placement: position,
- isPositionUpdated: true
- },
- () => {
- this.eventManager.emit('positionUpdated');
- }
- );
- },
- setDisplayNone: (displayNone: boolean, cb: () => void) => {
- this.setState({ displayNone }, cb);
- },
- updatePlacementAttr: (placement: Position) => {
- this.setState({ placement });
- },
- togglePortalVisible: (visible: boolean, cb: () => void) => {
- const willUpdateStates: Partial<TooltipState> = {};
- willUpdateStates.transitionState = visible ? 'enter' : 'leave';
- willUpdateStates.visible = visible;
- this.mounted && this.setState(willUpdateStates as TooltipState, () => {
- cb();
- });
- },
- registerClickOutsideHandler: (cb: () => void) => {
- if (this.clickOutsideHandler) {
- this.adapter.unregisterClickOutsideHandler();
- }
- this.clickOutsideHandler = (e: React.MouseEvent): any => {
- if (!this.mounted) {
- return false;
- }
- 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) as HTMLDivElement;
- if (
- (el && !(el as any).contains(e.target) && popupEl && !(popupEl as any).contains(e.target)) ||
- (this.props.clickTriggerToHide && el && (el as any).contains(e.target))
- ) {
- this.props.onClickOutSide(e);
- cb();
- }
- };
- window.addEventListener('mousedown', this.clickOutsideHandler);
- },
- unregisterClickOutsideHandler: () => {
- if (this.clickOutsideHandler) {
- window.removeEventListener('mousedown', this.clickOutsideHandler);
- this.clickOutsideHandler = null;
- }
- },
- registerResizeHandler: (cb: (e: any) => void) => {
- if (this.resizeHandler) {
- this.adapter.unregisterResizeHandler();
- }
- this.resizeHandler = throttle((e): any => {
- if (!this.mounted) {
- return false;
- }
- cb(e);
- }, 10);
- window.addEventListener('resize', this.resizeHandler, false);
- },
- unregisterResizeHandler: () => {
- if (this.resizeHandler) {
- window.removeEventListener('resize', this.resizeHandler, false);
- this.resizeHandler = null;
- }
- },
- notifyVisibleChange: (visible: boolean) => {
- this.props.onVisibleChange(visible);
- },
- registerScrollHandler: (rePositionCb: (arg: { x: number; y: number }) => void) => {
- if (this.scrollHandler) {
- this.adapter.unregisterScrollHandler();
- }
- this.scrollHandler = throttle((e): any => {
- if (!this.mounted) {
- return false;
- }
- const triggerDOM = this.adapter.getTriggerNode();
- const isRelativeScroll = e.target.contains(triggerDOM);
- if (isRelativeScroll) {
- const scrollPos = { x: e.target.scrollLeft, y: e.target.scrollTop };
- rePositionCb(scrollPos);
- }
- }, 10); // When it is greater than 16ms, it will be very obvious
- window.addEventListener('scroll', this.scrollHandler, true);
- },
- unregisterScrollHandler: () => {
- if (this.scrollHandler) {
- window.removeEventListener('scroll', this.scrollHandler, true);
- this.scrollHandler = null;
- }
- },
- canMotion: () => Boolean(this.props.motion),
- updateContainerPosition: () => {
- const container = this.getPopupContainer();
- if (container && isHTMLElement(container)) {
- // getComputedStyle need first parameter is Element type
- const computedStyle = window.getComputedStyle(container);
- const position = computedStyle.getPropertyValue('position');
- this.containerPosition = position;
- }
- },
- 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 { preventScroll } = this.props;
- const focusRefNode = get(this, 'initialFocusRef.current');
- if (focusRefNode && 'focus' in focusRefNode) {
- focusRefNode.focus({ preventScroll });
- }
- },
- notifyEscKeydown: (event: React.KeyboardEvent) => {
- this.props.onEscKeyDown(event);
- },
- setId: () => {
- this.setState({ id: getUuidShort() });
- }
- };
- }
- componentDidMount() {
- this.mounted = true;
- this.getPopupContainer = this.props.getPopupContainer || this.context.getPopupContainer || defaultGetContainer;
- this.foundation.init();
- }
- componentWillUnmount() {
- this.mounted = false;
- this.foundation.destroy();
- }
- /**
- * focus on tooltip trigger
- */
- public focusTrigger() {
- this.foundation.focusTrigger();
- }
- isSpecial = (elem: React.ReactNode | HTMLElement | any) => {
- if (isHTMLElement(elem)) {
- return Boolean(elem.disabled);
- } else if (isValidElement(elem)) {
- const disabled = get(elem, 'props.disabled');
- if (disabled) {
- return strings.STATUS_DISABLED;
- }
- const loading = get(elem, 'props.loading');
- /* Only judge the loading state of the Button, and no longer judge other components */
- const isButton = !isEmpty(elem)
- && !isEmpty(elem.type)
- && (get(elem, 'type.elementType') === 'Button' || get(elem, 'type.elementType') === 'IconButton');
- if (loading && isButton) {
- return strings.STATUS_LOADING;
- }
- }
- return false;
- };
- // willEnter = () => {
- // this.foundation.calcPosition();
- // this.setState({ visible: true });
- // };
- didLeave = () => {
- if (this.props.keepDOM) {
- this.foundation.setDisplayNone(true);
- } else {
- this.foundation.removePortal();
- }
- this.foundation.unBindEvent();
- };
- /** for transition - end */
- rePosition() {
- return this.foundation.calcPosition();
- }
- componentDidUpdate(prevProps: TooltipProps, prevState: TooltipState) {
- warning(
- this.props.mouseLeaveDelay < this.props.mouseEnterDelay,
- "[Semi Tooltip] 'mouseLeaveDelay' cannot be less than 'mouseEnterDelay', which may cause the dropdown layer to not be hidden."
- );
- if (prevProps.visible !== this.props.visible) {
- if (['hover', 'focus'].includes(this.props.trigger)) {
- this.props.visible ? this.foundation.delayShow() : this.foundation.delayHide();
- } else {
- this.props.visible ? this.foundation.show() : this.foundation.hide();
- }
- }
- if (!isEqual(prevProps.rePosKey, this.props.rePosKey)) {
- this.rePosition();
- }
- }
- renderIcon = () => {
- const { placement } = this.state;
- const { showArrow, prefixCls, style } = this.props;
- let icon = null;
- const triangleCls = classNames([`${prefixCls}-icon-arrow`]);
- const bgColor = get(style, 'backgroundColor');
- const iconComponent = placement.includes('left') || placement.includes('right') ?
- <TriangleArrowVertical/> :
- <TriangleArrow/>;
- if (showArrow) {
- if (isValidElement(showArrow)) {
- icon = showArrow;
- } else {
- icon = React.cloneElement(iconComponent, {
- className: triangleCls,
- style: { color: bgColor, fill: 'currentColor' }
- });
- }
- }
- return icon;
- };
- handlePortalInnerClick = (e: React.MouseEvent) => {
- if (this.props.clickToHide) {
- this.foundation.hide();
- }
- if (this.props.stopPropagation) {
- stopPropagation(e);
- }
- };
- handlePortalMouseDown = (e: React.MouseEvent) => {
- if (this.props.stopPropagation) {
- stopPropagation(e);
- }
- }
- handlePortalFocus = (e: React.FocusEvent<HTMLElement>) => {
- if (this.props.stopPropagation) {
- stopPropagation(e);
- }
- }
- handlePortalBlur = (e: React.FocusEvent<HTMLElement>) => {
- if (this.props.stopPropagation) {
- stopPropagation(e);
- }
- }
- 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,
- displayNone,
- 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, {
- [`${prefixCls}-wrapper`]: true,
- [`${prefixCls}-wrapper-show`]: visible,
- [`${prefixCls}-with-arrow`]: Boolean(showArrow),
- [`${prefixCls}-rtl`]: direction === 'rtl',
- });
- const icon = this.renderIcon();
- const portalInnerStyle = omit(containerStyle, motion ? ['transformOrigin'] : undefined);
- const transformOrigin = get(containerStyle, 'transformOrigin');
- const userOpacity = get(style, 'opacity');
- const opacity = userOpacity ? userOpacity : 1;
- const inner =
- <CSSAnimation
- fillMode="forwards"
- animationState={transitionState as "enter" | "leave"}
- motion={motion && isPositionUpdated}
- startClassName={transitionState === 'enter' ? `${prefix}-animation-show` : `${prefix}-animation-hide`}
- onAnimationEnd={() => {
- if (transitionState === 'leave') {
- this.didLeave();
- this.props.afterClose?.();
- }
- }}>
- {
- ({ animationStyle, animationClassName, animationEventsNeedBind }) => {
- return <div
- className={classNames(className, animationClassName)}
- style={{
- ...animationStyle,
- ...(displayNone ? { display: "none" } : {}),
- transformOrigin,
- ...style,
- opacity: isPositionUpdated ? opacity : "0",
- }}
- {...portalEventSet}
- {...animationEventsNeedBind}
- role={role}
- x-placement={placement}
- id={id}
- >
- <div className={`${prefix}-content`} >{contentNode}</div>
- {icon}
- </div>;
- }
- }
- </CSSAnimation>;
- return (
- <Portal getPopupContainer={this.props.getPopupContainer} style={{ zIndex }}>
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
- <div
- // listen keyboard event, don't move tabIndex -1
- tabIndex={-1}
- className={`${BASE_CLASS_PREFIX}-portal-inner`}
- style={portalInnerStyle}
- ref={this.setContainerEl}
- onClick={this.handlePortalInnerClick}
- onFocus={this.handlePortalFocus}
- onBlur={this.handlePortalBlur}
- onMouseDown={this.handlePortalMouseDown}
- onKeyDown={this.handlePortalInnerKeyDown}
- >
- {inner}
- </div>
- </Portal>
- );
- };
- wrapSpan = (elem: React.ReactNode | React.ReactElement) => {
- const { wrapperClassName } = this.props;
- const display = get(elem, 'props.style.display');
- const block = get(elem, 'props.block');
- const style: React.CSSProperties = {
- display: 'inline-block',
- };
- if (block || blockDisplays.includes(display)) {
- style.width = '100%';
- }
- // eslint-disable-next-line jsx-a11y/no-static-element-interactions
- return <span className={wrapperClassName} style={style}>{elem}</span>;
- };
- mergeEvents = (rawEvents: Record<string, any>, events: Record<string, any>) => {
- const mergedEvents = {};
- each(events, (handler: any, key) => {
- if (typeof handler === 'function') {
- mergedEvents[key] = (...args: any[]) => {
- handler(...args);
- if (rawEvents && typeof rawEvents[key] === 'function') {
- rawEvents[key](...args);
- }
- };
- }
- });
- return mergedEvents;
- };
- getPopupId = () => {
- return this.state.id;
- }
- render() {
- const { isInsert, triggerEventSet, visible, id } = this.state;
- const { wrapWhenSpecial, role, trigger } = this.props;
- let { children } = this.props;
- const childrenStyle = { ...get(children, 'props.style') as React.CSSProperties } ;
- const extraStyle: React.CSSProperties = {};
- if (wrapWhenSpecial) {
- const isSpecial = this.isSpecial(children);
- if (isSpecial) {
- childrenStyle.pointerEvents = 'none';
- if (isSpecial === strings.STATUS_DISABLED) {
- extraStyle.cursor = 'not-allowed';
- }
- children = cloneElement(children as React.ReactElement, { style: childrenStyle });
- if (trigger !== 'custom') {
- // no need to wrap span when trigger is custom, cause it don't need bind event
- children = this.wrapSpan(children);
- }
- this.isWrapped = true;
- } else if (!isValidElement(children)) {
- children = this.wrapSpan(children);
- this.isWrapped = true;
- }
- }
- // eslint-disable-next-line prefer-const
- let ariaAttribute = {};
- // Take effect when used by Popover component
- if (role === 'dialog') {
- ariaAttribute['aria-expanded'] = visible ? 'true' : 'false';
- ariaAttribute['aria-haspopup'] = 'dialog';
- ariaAttribute['aria-controls'] = id;
- } else {
- ariaAttribute['aria-describedby'] = id;
- }
- // The incoming children is a single valid element, otherwise wrap a layer with span
- const newChild = React.cloneElement(children as React.ReactElement, {
- ...ariaAttribute,
- ...(children as React.ReactElement).props,
- ...this.mergeEvents((children as React.ReactElement).props, triggerEventSet),
- style: {
- ...get(children, 'props.style') as React.CSSProperties,
- ...extraStyle,
- },
- className: classNames(
- get(children, 'props.className')
- ),
- // to maintain refs with callback
- ref: (node: React.ReactNode) => {
- // Keep your own reference
- (this.triggerEl as any).current = node;
- // Call the original ref, if any
- const { ref } = children as any;
- // this.log('tooltip render() - get ref', ref);
- if (typeof ref === 'function') {
- ref(node);
- } else if (ref && typeof ref === 'object') {
- ref.current = node;
- }
- },
- tabIndex: (children as React.ReactElement).props.tabIndex || 0, // a11y keyboard, in some condition select's tabindex need to -1 or 0
- 'data-popupid': id
- });
- // 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
- // So if the user adds ref to the content, you need to use callback ref: https://github.com/facebook/react/issues/8873
- return (
- <React.Fragment>
- {isInsert ? this.renderPortal() : null}
- {newChild}
- </React.Fragment>
- );
- }
- }
|