diff --git a/components/Affix/index.tsx b/components/Affix/index.tsx index dd4d280a3a..2c1ad37875 100644 --- a/components/Affix/index.tsx +++ b/components/Affix/index.tsx @@ -166,10 +166,13 @@ function Affix(baseProps: PropsWithChildren, ref) { useImperativeHandle(ref, () => ({ updatePosition, + getRootDOMNode: () => { + return wrapperRef.current; + }, })); return ( - + wrapperRef.current}>
{isFixed &&
}
)}
- + ); } diff --git a/components/Anchor/anchor.tsx b/components/Anchor/anchor.tsx index d19e4caa00..b7db43e1c2 100644 --- a/components/Anchor/anchor.tsx +++ b/components/Anchor/anchor.tsx @@ -88,6 +88,7 @@ function Anchor(baseProps: AnchorPropsWithChildren, ref) { ref, () => ({ dom: wrapperRef.current, + getRootDOMNode: () => wrapperRef.current, }), [] ); diff --git a/components/BackTop/index.tsx b/components/BackTop/index.tsx index 275cd65389..77c18a9c94 100644 --- a/components/BackTop/index.tsx +++ b/components/BackTop/index.tsx @@ -1,5 +1,4 @@ import React, { PropsWithChildren, forwardRef, useState, useEffect, useContext, memo } from 'react'; -import { CSSTransition } from 'react-transition-group'; import BTween from 'b-tween'; import { pickDataAttributes } from '../_util/pick'; import cs from '../_util/classNames'; @@ -10,6 +9,7 @@ import throttleByRaf from '../_util/throttleByRaf'; import { BackTopProps } from './interface'; import useMergeProps from '../_util/hooks/useMergeProps'; import useKeyboardEvent from '../_util/hooks/useKeyboardEvent'; +import ArcoCSSTransition from '../_util/CSSTransition'; const defaultProps: BackTopProps = { visibleHeight: 400, @@ -81,13 +81,13 @@ function BackTop(baseProps: PropsWithChildren, ref) { onPressEnter: scrollToTop, })} > - + {props.children || ( )} - +
); } diff --git a/components/Badge/count.tsx b/components/Badge/count.tsx index 9812864b97..bdcdefa7d4 100644 --- a/components/Badge/count.tsx +++ b/components/Badge/count.tsx @@ -1,15 +1,15 @@ import React, { useState } from 'react'; -import { CSSTransition } from 'react-transition-group'; import cs from '../_util/classNames'; import usePrevious from '../_util/hooks/usePrevious'; +import ArcoCSSTransition from '../_util/CSSTransition'; -export default function Count({ prefixCls, maxCount, count, className, style }) { +function Count({ prefixCls, maxCount, count, className, style }) { const [isEntered, setIsEntered] = useState(false); const oldCount = usePrevious(count); const isChanged = count !== oldCount; return ( - 0} timeout={300} @@ -25,6 +25,8 @@ export default function Count({ prefixCls, maxCount, count, className, style }) {maxCount && count > maxCount ? `${maxCount}+` : count} - + ); } + +export default Count; diff --git a/components/Badge/index.tsx b/components/Badge/index.tsx index e3e52d265f..12fc2d3d1f 100644 --- a/components/Badge/index.tsx +++ b/components/Badge/index.tsx @@ -1,11 +1,11 @@ import React, { useContext, forwardRef } from 'react'; -import { CSSTransition } from 'react-transition-group'; import cs from '../_util/classNames'; import { ConfigContext } from '../ConfigProvider'; import { isObject } from '../_util/is'; import Count from './count'; import { BadgeProps } from './interface'; import useMergeProps from '../_util/hooks/useMergeProps'; +import ArcoCSSTransition from '../_util/CSSTransition'; const InnerColors = [ 'red', @@ -95,7 +95,7 @@ function Badge(baseProps: BadgeProps, ref) { } if ((dot || color) && count > 0) { return ( - - + ); } return ( diff --git a/components/Carousel/index.tsx b/components/Carousel/index.tsx index 62f3d850d2..1cd2049527 100644 --- a/components/Carousel/index.tsx +++ b/components/Carousel/index.tsx @@ -122,6 +122,7 @@ function Carousel(baseProps: CarouselProps, ref) { useImperativeHandle(carousel, () => { return { dom: refDom.current, + getRootDOMNode: () => refDom.current, goto: ({ index, isNegative, isManual, resetAutoPlayInterval }) => { slideTo({ targetIndex: getValidIndex(index), @@ -272,7 +273,7 @@ function Carousel(baseProps: CarouselProps, ref) { } return ( - + refDom.current}>
{ ref = _ref; diff --git a/components/Cascader/panel/list.tsx b/components/Cascader/panel/list.tsx index 330af0a31c..f0fc019319 100644 --- a/components/Cascader/panel/list.tsx +++ b/components/Cascader/panel/list.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import isEqualWith from 'lodash/isEqualWith'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { TransitionGroup } from 'react-transition-group'; import cs from '../../_util/classNames'; import Option from './option'; import { isFunction, isObject, isString } from '../../_util/is'; @@ -13,6 +13,7 @@ import Node from '../base/node'; import { getMultipleCheckValue } from '../util'; import VirtualList, { VirtualListHandle } from '../../_class/VirtualList'; import { on, off } from '../../_util/dom'; +import ArcoCSSTransition from '../../_util/CSSTransition'; const getLegalActiveNode = (options) => { for (let index = 0; index < options.length; index++) { @@ -269,7 +270,7 @@ const ListPanel = (props: CascaderPanelProps) => { const footer = renderFooter ? renderFooter(level, activeNode || null) : null; return list.length === 0 && !showEmptyChildren ? null : ( - (props: CascaderPanelProps) => { }} classNames="cascaderSlide" onEnter={(e: HTMLDivElement) => { + if (!e) return; e.style.marginLeft = `-${e.scrollWidth}px`; }} onEntering={(e: HTMLDivElement) => { + if (!e) return; e.style.marginLeft = `0px`; }} onEntered={(e) => { + if (!e) return; e.style.marginLeft = ''; }} > @@ -397,7 +401,7 @@ const ListPanel = (props: CascaderPanelProps) => { level )}
- + ); })} diff --git a/components/Drawer/index.tsx b/components/Drawer/index.tsx index a08edb6e07..3f759499d6 100644 --- a/components/Drawer/index.tsx +++ b/components/Drawer/index.tsx @@ -7,9 +7,9 @@ import React, { useState, useImperativeHandle, } from 'react'; -import { CSSTransition } from 'react-transition-group'; import FocusLock from 'react-focus-lock'; -import { findDOMNode } from 'react-dom'; +import ArcoCSSTransition from '../_util/CSSTransition'; +import { findDOMNode } from '../_util/react-dom'; import IconClose from '../../icon/react-icon/IconClose'; import cs from '../_util/classNames'; import Button from '../Button'; @@ -235,7 +235,7 @@ function Drawer(baseProps: DrawerProps, ref) { } > {mask ? ( - - + ) : null} - { + if (!e) return; e.parentNode.style.display = 'block'; setInExit(false); }} @@ -282,6 +283,7 @@ function Drawer(baseProps: DrawerProps, ref) { setInExit(true); }} onExited={(e) => { + if (!e) return; setInExit(false); e.parentNode.style.display = ''; // don't set display='none' afterClose?.(); @@ -301,7 +303,7 @@ function Drawer(baseProps: DrawerProps, ref) {
- + ); diff --git a/components/Form/form-item.tsx b/components/Form/form-item.tsx index d25d3ae763..ab269f60e8 100644 --- a/components/Form/form-item.tsx +++ b/components/Form/form-item.tsx @@ -10,7 +10,6 @@ import React, { ReactNode, useRef, } from 'react'; -import { CSSTransition } from 'react-transition-group'; import cs from '../_util/classNames'; import { isArray, isFunction, isUndefined, isObject } from '../_util/is'; import Grid from '../Grid'; @@ -33,6 +32,7 @@ import { ConfigContext } from '../ConfigProvider'; import omit from '../_util/omit'; import FormItemLabel from './form-label'; import { formatValidateMsg } from './utils'; +import ArcoCSSTransition from '../_util/CSSTransition'; const Row = Grid.Row; const Col = Grid.Col; @@ -71,7 +71,7 @@ const FormItemTip: React.FC = ({ return ( visible && ( - +
= ({ )}
-
+ ) ); }; diff --git a/components/Image/image-preview.tsx b/components/Image/image-preview.tsx index 5e354aea46..7d9c4b6069 100644 --- a/components/Image/image-preview.tsx +++ b/components/Image/image-preview.tsx @@ -9,8 +9,8 @@ import React, { useMemo, WheelEvent, } from 'react'; -import { CSSTransition } from 'react-transition-group'; -import { findDOMNode } from 'react-dom'; +import ArcoCSSTransition from '../_util/CSSTransition'; +import { findDOMNode } from '../_util/react-dom'; import useMergeProps from '../_util/hooks/useMergeProps'; import useMergeValue from '../_util/hooks/useMergeValue'; import cs from '../_util/classNames'; @@ -110,6 +110,7 @@ function Preview(baseProps: ImagePreviewProps, ref) { const refImage = useRef(); const refImageContainer = useRef(); const refWrapper = useRef(); + const refRootWrapper = useRef(); const keyboardEventOn = useRef(false); const refMoveData = useRef({ @@ -149,6 +150,7 @@ function Preview(baseProps: ImagePreviewProps, ref) { useImperativeHandle(ref, () => ({ reset, + getRootDOMNode: () => refRootWrapper.current, })); const [container, setContainer] = useState(); @@ -489,8 +491,9 @@ function Preview(baseProps: ImagePreviewProps, ref) { ...(style || {}), ...(isFixed ? {} : { zIndex: 'inherit', position: 'absolute' }), }} + ref={refRootWrapper} > - { + if (!e) return; e.parentNode.style.display = 'block'; e.style.display = 'block'; }} onExited={(e) => { + if (!e) return; e.parentNode.style.display = ''; e.style.display = 'none'; }} >
- + {visible && ( - + refWrapper.current}>
)}
- {(scale * 100).toFixed(0)}%
-
+ {isLoaded && ( ( () => { return { dom: refInput.current, + getRootDOMNode: () => refInput.current, focus: () => { refInput.current && refInput.current.focus && refInput.current.focus(); }, @@ -272,6 +273,7 @@ const InputComponent = React.forwardRef( )} {autoFitWidth && ( refInputMirror.current} onResize={() => { const inputWidth = refInputMirror.current.offsetWidth; if (typeof autoFitWidth === 'object') { diff --git a/components/Input/textarea.tsx b/components/Input/textarea.tsx index 272420e2ea..19cdf4b433 100644 --- a/components/Input/textarea.tsx +++ b/components/Input/textarea.tsx @@ -121,6 +121,9 @@ const TextArea = (props: TextAreaProps, ref) => { blur: () => { textareaRef.current && textareaRef.current.blur && textareaRef.current.blur(); }, + getRootDOMNode: () => { + return textareaRef.current; + }, }), [] ); diff --git a/components/InputTag/__demo__/basic.md b/components/InputTag/__demo__/basic.md index 84ed26e9d9..44d9eb53ca 100644 --- a/components/InputTag/__demo__/basic.md +++ b/components/InputTag/__demo__/basic.md @@ -1,5 +1,6 @@ --- order: 0 +skip: true title: zh-CN: 基本用法 en-US: Basic diff --git a/components/InputTag/__demo__/max-tag-count.md b/components/InputTag/__demo__/max-tag-count.md index c010d38b08..42031ef6ab 100644 --- a/components/InputTag/__demo__/max-tag-count.md +++ b/components/InputTag/__demo__/max-tag-count.md @@ -29,10 +29,10 @@ const App = () => { {invisibleTagCount} More, + render: (invisibleTagCount) => {invisibleTagCount} More, }} /> diff --git a/components/InputTag/__test__/__snapshots__/demo.test.ts.snap b/components/InputTag/__test__/__snapshots__/demo.test.ts.snap index 08f5f4a22d..1999259b6b 100644 --- a/components/InputTag/__test__/__snapshots__/demo.test.ts.snap +++ b/components/InputTag/__test__/__snapshots__/demo.test.ts.snap @@ -574,12 +574,12 @@ exports[`renders InputTag/demo/max-tag-count.md correctly 1`] = ` >
- 1 + 11
- 2 + 22
- 3 + 33 , ref) { const refInput = useRef>(); const refTSLastSeparateTriggered = useRef(null); + const refRootWrapper = useRef(); + const [focused, setFocused] = useState(false); const [value, setValue] = useMergeValue([], { defaultValue: 'defaultValue' in props ? formatValue(props.defaultValue) : undefined, @@ -164,6 +167,9 @@ function InputTag(baseProps: InputTagProps, ref) { return { blur: refInput.current?.blur, focus: refInput.current?.focus, + getRootDOMNode: () => { + return refRootWrapper.current; + }, }; }, [] @@ -381,13 +387,13 @@ function InputTag(baseProps: InputTagProps, ref) { valueCountMap[x.value] = valueCount + 1; const { dom: eleTag, valueKey } = mergedRenderTag(x, i); return React.isValidElement(eleTag) ? ( - {eleTag} - + ) : ( eleTag ); @@ -396,7 +402,11 @@ function InputTag(baseProps: InputTagProps, ref) { }, [value]); const suffixInput = [ - + , ref) { handleTokenSeparators(event.clipboardData.getData('text')); }} /> - , + , ]; const hasPrefix = !isEmptyNode(prefix); @@ -490,6 +500,7 @@ function InputTag(baseProps: InputTagProps, ref) { onClick(e); } }} + ref={!needWrapper ? refRootWrapper : undefined} >
{hasPrefix && ( @@ -559,6 +570,7 @@ function InputTag(baseProps: InputTagProps, ref) { }, propsAppliedToRoot.className )} + ref={refRootWrapper} > {needAddBefore &&
{addBefore}
} {eleInputTagCore} diff --git a/components/List/index.tsx b/components/List/index.tsx index 504b519cfb..03bc067ce6 100644 --- a/components/List/index.tsx +++ b/components/List/index.tsx @@ -102,6 +102,7 @@ function List(baseProps: ListProps, ref) { }); } }, + getRootDOMNode: () => refDom.current, }; }); diff --git a/components/Menu/overflow-wrap.tsx b/components/Menu/overflow-wrap.tsx index 1dc33d55c4..0a95738cbe 100644 --- a/components/Menu/overflow-wrap.tsx +++ b/components/Menu/overflow-wrap.tsx @@ -143,7 +143,7 @@ const OverflowWrap = (props: OverflowWrapProps) => { }; return ( - + refUl.current}>
{renderChildren()}
diff --git a/components/Menu/sub-menu/inline.tsx b/components/Menu/sub-menu/inline.tsx index 8e2d4dc59f..5da4bd4401 100644 --- a/components/Menu/sub-menu/inline.tsx +++ b/components/Menu/sub-menu/inline.tsx @@ -1,5 +1,4 @@ import React, { CSSProperties, useContext, ReactNode } from 'react'; -import { CSSTransition } from 'react-transition-group'; import cs from '../../_util/classNames'; import useStateWithPromise from '../../_util/hooks/useStateWithPromise'; import { MenuSubMenuProps } from '../interface'; @@ -11,6 +10,7 @@ import pick from '../../_util/pick'; import omit from '../../_util/omit'; import { Enter } from '../../_util/keycode'; import useId from '../../_util/hooks/useId'; +import ArcoCSSTransition from '../../_util/CSSTransition'; // Use visibility: hidden to avoid Menu.Item get focused by Tab key const CONTENT_HIDDEN_STYLE: CSSProperties = { height: 0, visibility: 'hidden' }; @@ -92,12 +92,13 @@ const SubMenuInline = (props: MenuSubMenuProps & { forwardedRef }) => { {...omit(rest, ['key', 'popup', 'triggerProps'])} > {header} - { + if (!element) return; await setContentStyle(CONTENT_HIDDEN_STYLE); await setContentStyle({ height: element.scrollHeight }); }} @@ -105,12 +106,13 @@ const SubMenuInline = (props: MenuSubMenuProps & { forwardedRef }) => { setContentStyle({ height: 'auto' }); }} onExit={async (element) => { + if (!element) return; await setContentStyle({ height: element.scrollHeight }); await setContentStyle(CONTENT_HIDDEN_STYLE); }} > {content} - +
); }; diff --git a/components/Message/index.tsx b/components/Message/index.tsx index 9fa9bb3ae5..88ac64b026 100644 --- a/components/Message/index.tsx +++ b/components/Message/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { TransitionGroup } from 'react-transition-group'; +import ArcoCSSTransition from '../_util/CSSTransition'; import { render } from '../_util/react-dom'; import BaseNotification from '../_class/notification'; import Notice from '../_class/notice'; @@ -196,17 +197,20 @@ class Message extends BaseNotification {
{notices.map((notice) => ( - { + if (!e) return; e.style.height = `${e.scrollHeight}px`; }} onExiting={(e) => { + if (!e) return; e.style.height = 0; }} onExited={(e) => { + if (!e) return; e.style.height = 0; notice.onClose && notice.onClose(); }} @@ -221,7 +225,7 @@ class Message extends BaseNotification { rtl={mergedRtl} {...(isUndefined(mergeClosable) ? {} : { closable: mergeClosable })} /> - + ))}
diff --git a/components/Modal/modal.tsx b/components/Modal/modal.tsx index c48ebeaf42..f1b7df35ed 100644 --- a/components/Modal/modal.tsx +++ b/components/Modal/modal.tsx @@ -1,4 +1,3 @@ -import { findDOMNode } from 'react-dom'; import React, { useState, PropsWithChildren, @@ -10,7 +9,6 @@ import React, { useCallback, ReactElement, } from 'react'; -import { CSSTransition } from 'react-transition-group'; import FocusLock from 'react-focus-lock'; import IconClose from '../../icon/react-icon/IconClose'; import cs from '../_util/classNames'; @@ -29,6 +27,8 @@ import useModal from './useModal'; import { ModalProps, ModalReturnProps } from './interface'; import useMergeValue from '../_util/hooks/useMergeValue'; import useMergeProps from '../_util/hooks/useMergeProps'; +import { findDOMNode } from '../_util/react-dom'; +import ArcoCSSTransition from '../_util/CSSTransition'; type CursorPositionType = { left: number; top: number } | null; let cursorPosition: CursorPositionType | null = null; @@ -342,7 +342,7 @@ function Modal(baseProps: PropsWithChildren, ref) {
{mask ? ( - , ref) { classNames="fadeModal" unmountOnExit={unmountOnExit} onEnter={(e) => { + if (!e) return; e.style.display = 'block'; }} onExited={(e) => { + if (!e) return; e.style.display = 'none'; }} >
- + ) : null}
, ref) { }} onClick={onClickMask} > - , ref) { unmountOnExit={unmountOnExit} mountOnEnter={mountOnEnter} onEnter={(e: HTMLDivElement) => { + if (!e) return; setWrapperVisible(true); cursorPositionRef.current = cursorPosition; haveOriginTransformOrigin.current = !!e.style.transformOrigin; @@ -415,6 +418,7 @@ function Modal(baseProps: PropsWithChildren, ref) { modalRef.current = e; }} onEntered={(e: HTMLDivElement) => { + if (!e) return; setTransformOrigin(e); cursorPositionRef.current = null; afterOpen?.(); @@ -423,6 +427,7 @@ function Modal(baseProps: PropsWithChildren, ref) { inExit.current = true; }} onExited={(e) => { + if (!e) return; setWrapperVisible(false); setTransformOrigin(e); afterClose?.(); @@ -443,7 +448,7 @@ function Modal(baseProps: PropsWithChildren, ref) { }, } )} - +
diff --git a/components/Notification/index.tsx b/components/Notification/index.tsx index 3a43f282aa..98d48cb611 100644 --- a/components/Notification/index.tsx +++ b/components/Notification/index.tsx @@ -1,5 +1,5 @@ import React, { ReactInstance } from 'react'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { TransitionGroup } from 'react-transition-group'; import { createPortal } from 'react-dom'; import { render as ReactDOMRender } from '../_util/react-dom'; import BaseNotification from '../_class/notification'; @@ -8,6 +8,7 @@ import cs from '../_util/classNames'; import { isNumber, isUndefined } from '../_util/is'; import { NotificationProps, NotificationHookReturnType } from './interface'; import useNotification from './useNotification'; +import ArcoCSSTransition from '../_util/CSSTransition'; const notificationTypes = ['info', 'success', 'error', 'warning', 'normal']; @@ -192,7 +193,7 @@ class Notification extends BaseNotification {
{notices.map((notice) => ( - { + if (!e) return; e.style.height = `${e.scrollHeight}px`; }} onExiting={(e) => { + if (!e) return; e.style.height = 0; }} onExited={(e) => { + if (!e) return; e.style.height = 0; notice.onClose && notice.onClose(); }} @@ -219,7 +223,7 @@ class Notification extends BaseNotification { noticeType="notification" rtl={mergedRtl} /> - + ))}
diff --git a/components/PageHeader/index.tsx b/components/PageHeader/index.tsx index 5f4585d252..45d0583ad5 100644 --- a/components/PageHeader/index.tsx +++ b/components/PageHeader/index.tsx @@ -33,6 +33,7 @@ function PageHeader(baseProps: PropsWithChildren) { setPageWrap(pageRef.current.offsetWidth < 768); } }} + getTargetDOMNode={() => pageRef.current} >
(); const verticalTriggerIcon = rtlReverse ? [, ] @@ -153,8 +154,8 @@ export default function ResizeTrigger(props: PropsWithChildren -
+ refDiv.current}> +
{isFunction(renderChildren) ? renderChildren(prev, trigger, next) : children || renderIcon()} diff --git a/components/Select/select.tsx b/components/Select/select.tsx index 85778b2c6f..cbbb2f7f4e 100644 --- a/components/Select/select.tsx +++ b/components/Select/select.tsx @@ -768,6 +768,7 @@ function Select(baseProps: SelectProps, ref) { getOptionInfoByValue, getOptionInfoList: () => [...optionInfoMap.values()].filter((info) => info._valid), scrollIntoView, + getRootDOMNode: refSelectView.current?.getRootDOMNode, }), [hotkeyHandler, optionInfoMap, valueActive, getOptionInfoByValue, scrollIntoView] ); diff --git a/components/Statistic/index.tsx b/components/Statistic/index.tsx index a5eae4e654..d519a9170b 100644 --- a/components/Statistic/index.tsx +++ b/components/Statistic/index.tsx @@ -48,6 +48,8 @@ function Statistic(baseProps: StatisticProps, ref) { } = props; const tween = useRef(); + const wrapperRef = useRef(); + const [value, setValue] = useState( 'value' in props ? props.value : undefined ); @@ -99,6 +101,7 @@ function Statistic(baseProps: StatisticProps, ref) { useImperativeHandle(ref, () => ({ countUp, + getRootDOMNode: () => wrapperRef.current, })); const { int, decimal } = useMemo(() => { @@ -139,6 +142,7 @@ function Statistic(baseProps: StatisticProps, ref) { className={cs(`${prefixCls}`, { [`${prefixCls}-rtl`]: rtl }, className)} style={style} {...omit(rest, ['value', 'countUp', 'countFrom', 'countDuration'])} + ref={wrapperRef} > {title &&
{title}
}
diff --git a/components/Switch/index.tsx b/components/Switch/index.tsx index 703c4c1e4f..f4e1de955d 100644 --- a/components/Switch/index.tsx +++ b/components/Switch/index.tsx @@ -1,5 +1,5 @@ import React, { useState, useContext, forwardRef, ReactElement } from 'react'; -import { SwitchTransition, CSSTransition } from 'react-transition-group'; +import { SwitchTransition } from 'react-transition-group'; import cs from '../_util/classNames'; import { isArray, isObject } from '../_util/is'; import omit from '../_util/omit'; @@ -7,6 +7,7 @@ import { ConfigContext } from '../ConfigProvider'; import IconLoading from '../../icon/react-icon/IconLoading'; import { SwitchProps } from './interface'; import useMergeProps from '../_util/hooks/useMergeProps'; +import ArcoCSSTransition from '../_util/CSSTransition'; export interface SwitchState { checked: boolean; @@ -96,7 +97,7 @@ function Switch(baseProps: SwitchProps, ref) {
{!loading && (checkedIcon || uncheckedIcon) && ( - {mergedChecked ? checkedIcon : uncheckedIcon} - + )} @@ -120,12 +121,12 @@ function Switch(baseProps: SwitchProps, ref) { {checkedElement && mergedChecked && checkedElement} {unCheckedElement && !mergedChecked && unCheckedElement}
- +
{checkedElement && mergedChecked && checkedElement} {unCheckedElement && !mergedChecked && unCheckedElement}
-
+ )} diff --git a/components/Table/interface.tsx b/components/Table/interface.tsx index 297820c04d..2f35f99609 100644 --- a/components/Table/interface.tsx +++ b/components/Table/interface.tsx @@ -658,6 +658,7 @@ export interface TbodyProps { getRowKey?: GetRowKeyType; placeholder?: ReactNode; saveVirtualListRef?: (ref: VirtualListHandle) => void; + saveRef?: (ref: HTMLElement | HTMLDivElement) => void; } export interface TfootProps { diff --git a/components/Table/table.tsx b/components/Table/table.tsx index b92626b978..d506114a09 100644 --- a/components/Table/table.tsx +++ b/components/Table/table.tsx @@ -130,6 +130,7 @@ function Table(baseProps: TableProps, ref: React.Ref -1 ? ctxSize : 'default'); const refTableHead = useRef(null); const refTableBody = useRef(null); + const refTBody = useRef(null); const refTableFoot = useRef(null); const refTable = useRef(null); const refVirtualList = useRef(null); @@ -525,6 +526,7 @@ function Table(baseProps: TableProps, ref: React.Ref
(baseProps: TableProps, ref: React.Ref
{...props} + saveRef={(node) => (refTBody.current = node)} selectedRowKeys={selectedRowKeys} indeterminateKeys={indeterminateKeys} expandedRowKeys={expandedRowKeys} @@ -876,7 +879,10 @@ function Table(baseProps: TableProps, ref: React.Ref
0; return ( - + refTableBody.current || refTBody.current} + > {fixedHeader && !virtualized ? ( (props: TbodyProps) { saveVirtualListRef, } = props; + const saveRef = (node) => { + props.saveRef?.(node); + }; + const { ComponentTbody } = useComponent(components); let scrollStyleX: CSSProperties = {}; @@ -209,14 +213,17 @@ function TBody(props: TbodyProps) { outerStyle={{ ...scrollStyleX, minWidth: '100%', overflow: 'visible' }} innerStyle={{ right: 'auto', minWidth: '100%' }} className={`${prefixCls}-body`} - ref={(ref) => saveVirtualListRef(ref)} + ref={(ref) => { + saveVirtualListRef(ref); + saveRef(ref?.dom); + }} itemKey={getRowKey} {...virtualListProps} > {renderDataRecord} ) : ( -
+
{noDataTr}
@@ -224,7 +231,11 @@ function TBody(props: TbodyProps) { ); } - return {data.length > 0 ? data.map(renderDataRecord) : noDataTr}; + return ( + + {data.length > 0 ? data.map(renderDataRecord) : noDataTr} + + ); } export default TBody; diff --git a/components/Tooltip/index.tsx b/components/Tooltip/index.tsx index 803550108f..58f38fa4ef 100644 --- a/components/Tooltip/index.tsx +++ b/components/Tooltip/index.tsx @@ -79,6 +79,9 @@ function Tooltip(baseProps: PropsWithChildren, ref) { ref, () => ({ updatePopupPosition, + getRootDOMNode: () => { + return refTrigger.current?.getRootDOMNode?.(); + }, }), [] ); diff --git a/components/Tree/animation.tsx b/components/Tree/animation.tsx index 550e3afdc7..7aea0f20ca 100644 --- a/components/Tree/animation.tsx +++ b/components/Tree/animation.tsx @@ -4,13 +4,13 @@ import React, { PropsWithChildren, useMemo, useContext, useEffect, useRef } from 'react'; -import { CSSTransition } from 'react-transition-group'; import { TreeContext } from './context'; import { NodeProps } from './interface'; import VirtualList from '../_class/VirtualList'; import { ConfigContext } from '../ConfigProvider'; import Node from './node'; import { isNumber } from '../_util/is'; +import ArcoCSSTransition from '../_util/CSSTransition'; function getKey(option) { return option.key || option._key; @@ -95,7 +95,7 @@ const TreeAnimation = (props: PropsWithChildren) => { }, [filtedData, currentExpandKeys]); return ( - -1 && filtedData.length > 0} unmountOnExit classNames="tree-slide-expand" @@ -104,18 +104,22 @@ const TreeAnimation = (props: PropsWithChildren) => { exit: 0, }} onEnter={(e) => { + if (!e) return; const scrollHeight = e.scrollHeight; e.style.height = expanded ? 0 : `${Math.min(realHeight || scrollHeight, e.scrollHeight)}px`; }} onEntering={(e) => { + if (!e) return; const scrollHeight = e.scrollHeight; e.style.height = expanded ? `${Math.min(realHeight || scrollHeight, scrollHeight)}px` : 0; }} onEntered={(e) => { + if (!e) return; e.style.height = props.expanded ? '' : 0; treeContext.onExpandEnd(props._key); }} onExit={(e) => { + if (!e) return; e.style.display = 'none'; }} > @@ -132,7 +136,7 @@ const TreeAnimation = (props: PropsWithChildren) => { return ; }} - + ); }; diff --git a/components/TreeSelect/__demo__/basic.md b/components/TreeSelect/__demo__/basic.md index 9054405342..bcb23eb3d4 100644 --- a/components/TreeSelect/__demo__/basic.md +++ b/components/TreeSelect/__demo__/basic.md @@ -19,9 +19,7 @@ const TreeNode = TreeSelect.Node; const App = () => { return ( - { - console.log('a') - }}> + diff --git a/components/TreeSelect/tree-select.tsx b/components/TreeSelect/tree-select.tsx index 4c9e8d58c7..d0aa7498c6 100644 --- a/components/TreeSelect/tree-select.tsx +++ b/components/TreeSelect/tree-select.tsx @@ -251,6 +251,9 @@ const TreeSelect: ForwardRefRenderFunction< blur() { refSelectView.current && refSelectView.current.blur(); }, + getRootDOMNode: () => { + return refSelectView.current?.getRootDOMNode?.(); + }, })); const filterNode = useCallback( diff --git a/components/Trigger/__demo__/basic.md b/components/Trigger/__demo__/basic.md index 02d6830351..ae565cb356 100644 --- a/components/Trigger/__demo__/basic.md +++ b/components/Trigger/__demo__/basic.md @@ -16,13 +16,6 @@ The basic usage. The popup layer has no style by default. ```js import { Trigger, Button, Tooltip, Input, Skeleton, Typography,Space } from '@arco-design/web-react'; -function Element(props) { - return ( - - Hover me - - ); -} function Popup() { return ( @@ -44,7 +37,9 @@ function App() { mouseLeaveDelay={400} position="bottom" > - + + Hover me + } trigger="click" position="bottom" classNames="zoomInTop"> diff --git a/components/Trigger/index.tsx b/components/Trigger/index.tsx index 3f24183427..00188e801a 100644 --- a/components/Trigger/index.tsx +++ b/components/Trigger/index.tsx @@ -1,9 +1,16 @@ -import React, { PureComponent, ReactElement, PropsWithChildren, CSSProperties } from 'react'; -import { findDOMNode } from 'react-dom'; -import { CSSTransition } from 'react-transition-group'; +import React, { + PureComponent, + ReactElement, + PropsWithChildren, + CSSProperties, + createRef, + RefObject, +} from 'react'; import ResizeObserverPolyfill from 'resize-observer-polyfill'; +import { CSSTransition } from 'react-transition-group'; +import { callbackOriginRef, findDOMNode } from '../_util/react-dom'; import { on, off, contains, getScrollElements, isScrollElement } from '../_util/dom'; -import { isFunction, isObject, isArray } from '../_util/is'; +import { isFunction, isObject, isArray, supportRef } from '../_util/is'; import { pickDataAttributes } from '../_util/pick'; import { Esc } from '../_util/keycode'; import Portal from './portal'; @@ -116,7 +123,12 @@ class Trigger extends PureComponent { popupContainer; - triggerRef: HTMLSpanElement | null; + rootElementRef: any; + + triggerRef: RefObject; + + // 标志 popup 是否被销毁 + triggerRefDestoried: boolean; delayTimer: any = null; @@ -190,6 +202,8 @@ class Trigger extends PureComponent { 'popupVisible' in mergedProps ? mergedProps.popupVisible : mergedProps.defaultPopupVisible; this.popupOpen = !!popupVisible; + this.triggerRef = createRef(); + this.state = { popupVisible: !!popupVisible, popupStyle: {}, @@ -197,10 +211,14 @@ class Trigger extends PureComponent { } getRootElement = (): HTMLElement => { - this.childrenDom = findDOMNode(this) as HTMLElement; + this.childrenDom = findDOMNode(this.props.getTargetDOMNode?.() || this.rootElementRef, this); return this.childrenDom; }; + getPopupElement = (): HTMLSpanElement | null => { + return this.triggerRef?.current || null; + }; + componentDidMount() { this.componentDidUpdate(this.getMergedProps()); this.isDidMount = true; @@ -421,7 +439,7 @@ class Trigger extends PureComponent { }; getTransformOrigin = (position) => { - const content = this.triggerRef as HTMLElement; + const content = this.getPopupElement() as HTMLElement; if (!content) return {}; const { showArrow, classNames } = this.getMergedProps(['showArrow', 'classNames']); @@ -488,7 +506,7 @@ class Trigger extends PureComponent { } const mountContainer = this.popupContainer as Element; - const content = this.triggerRef; + const content = this.triggerRef.current; const child: HTMLElement = this.getRootElement(); // offsetParent=null when display:none or position: fixed @@ -539,6 +557,10 @@ class Trigger extends PureComponent { ); }); + getRootDOMNode = () => { + return this.getRootElement(); + }; + updatePopupPosition = (delay = 0, callback?: () => void) => { const currentVisible = this.state.popupVisible; if (!currentVisible) { @@ -624,7 +646,7 @@ class Trigger extends PureComponent { 'onClickOutside', 'clickOutsideToClose', ]); - const triggerNode = this.triggerRef; + const triggerNode = this.getPopupElement(); const childrenDom = this.getRootElement(); if ( @@ -1010,9 +1032,20 @@ class Trigger extends PureComponent { ); const childrenComponent = isExistChildren && ( - + { + return this.rootElementRef; + }} + > {React.cloneElement(child, { ...mergeProps, + ref: !supportRef(child) + ? undefined + : (node) => { + this.rootElementRef = node; + callbackOriginRef(child, node); + }, })} ); @@ -1025,7 +1058,12 @@ class Trigger extends PureComponent { unmountOnExit={unmountOnExit} appear mountOnEnter - onEnter={(e) => { + onEnter={() => { + this.triggerRefDestoried = false; + const e = this.getPopupElement(); + if (!e) { + return; + } e.style.display = 'initial'; e.style.pointerEvents = 'none'; if (classNames === 'slideDynamicOrigin') { @@ -1033,35 +1071,52 @@ class Trigger extends PureComponent { e.style.transform = this.getTransformTranslate(); } }} - onEntering={(e) => { + onEntering={() => { + const e = this.getPopupElement(); + if (!e) { + return; + } if (classNames === 'slideDynamicOrigin') { // 下拉菜单 e.style.transform = ''; } }} - onEntered={(e) => { + onEntered={() => { + const e = this.getPopupElement(); + if (!e) { + return; + } e.style.pointerEvents = 'auto'; this.forceUpdate(); }} - onExit={(e) => { + onExit={() => { + const e = this.getPopupElement(); + if (!e) { + return; + } // 避免消失动画时对元素的快速点击触发意外的操作 e.style.pointerEvents = 'none'; __onExit?.(e); }} - onExited={(e) => { + onExited={() => { + const e = this.getPopupElement(); + if (!e) { + return; + } e.style.display = 'none'; // 这里立即设置为null是为了在setState popupStyle引起重新渲染时,能触发 Portal的卸载事件。移除父节点。 // 否则只有在下个循环中 triggerRef 才会变为null,需要重新forceUpdate,才能触发Portal的unmount。 if (unmountOnExit) { - this.triggerRef = null; + this.triggerRefDestoried = true; } this.setState({ popupStyle: {} }); __onExited?.(e); }} + nodeRef={this.triggerRef} > { - const target = this.triggerRef; + const target = this.triggerRef.current; if (target) { // Avoid the flickering problem caused by the size change and positioning not being recalculated in time. // TODO: Consider changing the popup style directly in the next major version @@ -1072,9 +1127,10 @@ class Trigger extends PureComponent { } this.onResize(); }} + getTargetDOMNode={() => this.getPopupElement()} > (this.triggerRef = node)} + ref={this.triggerRef} trigger-placement={this.realPosition} style={ { @@ -1124,7 +1180,7 @@ class Trigger extends PureComponent { // 如果 triggerRef 不存在,说明弹出层内容被销毁,可以隐藏portal。 const portal = - popupVisible || this.triggerRef ? ( + popupVisible || (this.getPopupElement() && !this.triggerRefDestoried) ? ( {portalContent} ) : null; diff --git a/components/Trigger/interface.ts b/components/Trigger/interface.ts index 2363f5b417..acc8069611 100644 --- a/components/Trigger/interface.ts +++ b/components/Trigger/interface.ts @@ -240,6 +240,7 @@ export interface TriggerProps { */ updateOnScroll?: boolean; children?: ReactNode; + getTargetDOMNode?: () => HTMLElement; __onExit?: (event) => void; __onExited?: (event) => void; } diff --git a/components/Trigger/portal.tsx b/components/Trigger/portal.tsx index c37779adb4..ae730a7225 100644 --- a/components/Trigger/portal.tsx +++ b/components/Trigger/portal.tsx @@ -29,6 +29,7 @@ const Portal = (props: PortalProps) => { } }; }, []); + return containerRef.current ? ReactDOM.createPortal(children, containerRef.current) : null; }; diff --git a/components/Typography/base.tsx b/components/Typography/base.tsx index dc4e72e779..fabd258465 100644 --- a/components/Typography/base.tsx +++ b/components/Typography/base.tsx @@ -1,4 +1,11 @@ -import React, { useState, useContext, PropsWithChildren } from 'react'; +import React, { + useState, + useContext, + PropsWithChildren, + forwardRef, + useRef, + useImperativeHandle, +} from 'react'; import { ConfigContext } from '../ConfigProvider'; import ResizeObserverComponent from '../_util/resizeObserver'; import { @@ -62,7 +69,7 @@ function getClassNameAndComponentName(props: BaseProps, prefixCls: string) { }; } -function Base(props: BaseProps) { +function Base(props: BaseProps, ref) { const { componentType, style, @@ -78,6 +85,7 @@ function Base(props: BaseProps) { const { getPrefixCls, rtl } = configContext; const prefixCls = getPrefixCls('typography'); + const rootDOMRef = useRef(); const { component, className: componentClassName } = getClassNameAndComponentName( props, prefixCls @@ -163,6 +171,8 @@ function Base(props: BaseProps) { ellipsisConfig.onEllipsis && ellipsisConfig.onEllipsis(isEllipsis); }, [isEllipsis]); + useImperativeHandle(ref, () => rootDOMRef.current); + function wrap(content, component, props, innerProps = {}) { let currentContent = content; component.forEach((c, _index) => { @@ -208,8 +218,14 @@ function Base(props: BaseProps) { const addTooltip = isEllipsis && showTooltip && !expanding; const node = ( - + { + return rootDOMRef.current; + }} + > +
); } + +export default forwardRef(EditContent); diff --git a/components/Typography/paragraph.tsx b/components/Typography/paragraph.tsx index 82b99638c8..9d09713e90 100644 --- a/components/Typography/paragraph.tsx +++ b/components/Typography/paragraph.tsx @@ -1,18 +1,20 @@ -import React, { useContext } from 'react'; +import React, { forwardRef, useContext } from 'react'; import { TypographyParagraphProps } from './interface'; import Base from './base'; import cs from '../_util/classNames'; import { ConfigContext } from '../ConfigProvider'; -function Paragraph(props: TypographyParagraphProps) { +function ParagraphComponent(props: TypographyParagraphProps, ref) { const { spacing = 'default', className } = props; const { getPrefixCls } = useContext(ConfigContext); const prefixCls = getPrefixCls('typography'); const classNames = spacing === 'close' ? cs(`${prefixCls}-spacing-close`, className) : className; - return ; + return ; } +const Paragraph = forwardRef(ParagraphComponent); + Paragraph.displayName = 'Paragraph'; export default Paragraph; diff --git a/components/Typography/text.tsx b/components/Typography/text.tsx index 522fa84a58..65fd833b52 100644 --- a/components/Typography/text.tsx +++ b/components/Typography/text.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { TypographyTextProps } from './interface'; import Base from './base'; -function Text(props: TypographyTextProps) { - return ; +function TextComponent(props: TypographyTextProps, ref) { + return ; } +const Text = forwardRef(TextComponent); + Text.displayName = 'Text'; export default Text; diff --git a/components/Typography/title.tsx b/components/Typography/title.tsx index 93f24428b6..86b9b802f0 100644 --- a/components/Typography/title.tsx +++ b/components/Typography/title.tsx @@ -1,12 +1,14 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { TypographyTitleProps } from './interface'; import Base from './base'; -function Title(props: TypographyTitleProps) { +function TitleComponent(props: TypographyTitleProps, ref) { const { heading = 1, ...rest } = props; - return ; + return ; } +const Title = forwardRef(TitleComponent); + Title.displayName = 'Title'; export default Title; diff --git a/components/Upload/list/index.tsx b/components/Upload/list/index.tsx index 48834a66ef..63e3f4fbed 100644 --- a/components/Upload/list/index.tsx +++ b/components/Upload/list/index.tsx @@ -1,5 +1,5 @@ import React, { useContext, ReactNode, useState, useMemo } from 'react'; -import { CSSTransition, TransitionGroup } from 'react-transition-group'; +import { TransitionGroup } from 'react-transition-group'; import cs from '../../_util/classNames'; import PictureItem from './pictureItem'; import TextItem from './textItem'; @@ -7,6 +7,7 @@ import { ConfigContext } from '../../ConfigProvider'; import { STATUS, UploadListProps } from '../interface'; import { isFunction } from '../../_util/is'; import ImagePreviewGroup from '../../Image/image-preview-group'; +import ArcoCSSTransition from '../../_util/CSSTransition'; export const FileList = (props: UploadListProps) => { const { locale, rtl } = useContext(ConfigContext); @@ -69,42 +70,49 @@ export const FileList = (props: UploadListProps) => { originNode = renderUploadItem(originNode, file, fileList); } return listType === 'picture-card' ? ( - { + if (!e) return; e.style.width = ''; }} onExit={(e) => { + if (!e) return; e.style.width = `${e.scrollWidth}px`; }} onExiting={(e) => { + if (!e) return; e.style.width = 0; }} onExited={(e) => { + if (!e) return; e.style.width = 0; }} > {originNode} - + ) : ( - { + if (!e) return; e.style.height = `${e.scrollHeight}px`; }} onExiting={(e) => { + if (!e) return; e.style.height = 0; }} onExited={(e) => { + if (!e) return; e.style.height = 0; }} > {originNode} - + ); })} diff --git a/components/Upload/list/pictureItem.tsx b/components/Upload/list/pictureItem.tsx index 5cb7ec46f5..0caa618e50 100644 --- a/components/Upload/list/pictureItem.tsx +++ b/components/Upload/list/pictureItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { ConfigProviderProps } from '../../ConfigProvider'; import { UploadListProps, STATUS, CustomIconType } from '../interface'; import { UploadItem } from '../upload'; @@ -11,7 +11,8 @@ import IconUpload from '../../../icon/react-icon/IconUpload'; import useKeyboardEvent from '../../_util/hooks/useKeyboardEvent'; const PictureItem = ( - props: UploadListProps & { file: UploadItem; locale: ConfigProviderProps['locale'] } + props: UploadListProps & { file: UploadItem; locale: ConfigProviderProps['locale'] }, + ref ) => { const { disabled, prefixCls, file, showUploadList, locale } = props; const keyboardEvents = useKeyboardEvent(); @@ -25,7 +26,7 @@ const PictureItem = ( const actionIcons = isObject(showUploadList) ? (showUploadList as CustomIconType) : {}; return ( -
+
{status === STATUS.uploading ? ( (PictureItem); diff --git a/components/Upload/list/textItem.tsx b/components/Upload/list/textItem.tsx index b1271d36ff..0ee34cdb58 100644 --- a/components/Upload/list/textItem.tsx +++ b/components/Upload/list/textItem.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import IconFile from '../../../icon/react-icon/IconFile'; import IconFilePdf from '../../../icon/react-icon/IconFilePdf'; import IconFileImage from '../../../icon/react-icon/IconFileImage'; @@ -49,7 +49,8 @@ const getIconType = (file: UploadItem) => { }; const TextItem = ( - props: UploadListProps & { file: UploadItem; locale: ConfigProviderProps['locale'] } + props: UploadListProps & { file: UploadItem; locale: ConfigProviderProps['locale'] }, + ref ) => { const { prefixCls, disabled, file, locale } = props; const cls = `${prefixCls}-list-item-text`; @@ -79,7 +80,7 @@ const TextItem = ( } return ( -
+
{props.listType === 'picture-list' && (
@@ -159,4 +160,7 @@ const TextItem = ( ); }; -export default TextItem; +export default forwardRef< + HTMLDivElement, + UploadListProps & { file: UploadItem; locale: ConfigProviderProps['locale'] } +>(TextItem); diff --git a/components/Upload/trigger-node.tsx b/components/Upload/trigger-node.tsx index 819940a04e..59078a68aa 100644 --- a/components/Upload/trigger-node.tsx +++ b/components/Upload/trigger-node.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext, PropsWithChildren, useEffect } from 'react'; +import React, { useState, useContext, forwardRef, PropsWithChildren, useEffect } from 'react'; import cs from '../_util/classNames'; import Button from '../Button'; import IconUpload from '../../icon/react-icon/IconUpload'; @@ -8,7 +8,7 @@ import { ConfigContext } from '../ConfigProvider'; import { getFiles, loopDirectory } from './util'; import useKeyboardEvent from '../_util/hooks/useKeyboardEvent'; -const TriggerNode = (props: PropsWithChildren) => { +const TriggerNode = (props: PropsWithChildren, ref) => { const getKeyboardEvents = useKeyboardEvent(); const { locale } = useContext(ConfigContext); const [isDragging, setIsDragging] = useState(false); @@ -33,6 +33,7 @@ const TriggerNode = (props: PropsWithChildren) => { !disabled && props.onClick?.(); }, })} + ref={ref} onDragEnter={() => { setDragEnterCount(dragEnterCount + 1); }} @@ -138,4 +139,4 @@ const TriggerNode = (props: PropsWithChildren) => { ); }; -export default TriggerNode; +export default forwardRef>(TriggerNode); diff --git a/components/Upload/upload.tsx b/components/Upload/upload.tsx index 7246fa5656..95a6f1674d 100644 --- a/components/Upload/upload.tsx +++ b/components/Upload/upload.tsx @@ -76,6 +76,7 @@ const Upload: React.ForwardRefRenderFunction(); + const inputWrapperRef = useRef(); const [innerUploadState, setInnerUploadState] = useState(() => { return 'fileList' in props @@ -156,6 +157,9 @@ const Upload: React.ForwardRefRenderFunction { reuploadFile(file); }, + getRootDOMNode: () => { + return inputWrapperRef.current; + }, }; }); @@ -226,6 +230,7 @@ const Upload: React.ForwardRefRenderFunction void; @@ -243,7 +243,7 @@ class Uploader extends React.Component, U e.stopPropagation(); }} /> - , U > {isFunction(children) ? children({ fileList: this.props.fileList }) : children} - + {tip && listType !== 'picture-card' && !drag ? (
{tip} diff --git a/components/_class/VirtualList/index.tsx b/components/_class/VirtualList/index.tsx index afff3ef12e..2d099de252 100644 --- a/components/_class/VirtualList/index.tsx +++ b/components/_class/VirtualList/index.tsx @@ -498,6 +498,7 @@ const VirtualList: React.ForwardRefExoticComponent< ref, () => ({ dom: refList.current, + getRootDOMNode: () => refList.current, // Scroll to a certain height or an element scrollTo: (arg) => { refRafId.current && caf(refRafId.current); @@ -673,6 +674,7 @@ const VirtualList: React.ForwardRefExoticComponent< setStateHeight(clientHeight); } }} + getTargetDOMNode={() => refList.current} > { timer: any; + rootDOMRef; + + constructor(props) { + super(props); + + this.rootDOMRef = createRef(); + } + + getRootDOMNode = () => { + return this.rootDOMRef.current; + }; + componentDidMount() { this.startTimer(); } @@ -181,6 +193,7 @@ class Notice extends Component { style={{ textAlign: 'center' }} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + ref={this.rootDOMRef} >
{shouldRenderIcon && this.renderIcon()} @@ -208,7 +221,11 @@ class Notice extends Component { if (noticeType === 'notification') { return ( -
+
{shouldRenderIcon &&
{this.renderIcon()}
}
diff --git a/components/_class/picker/input-range.tsx b/components/_class/picker/input-range.tsx index bf954b86a5..1017c9199e 100644 --- a/components/_class/picker/input-range.tsx +++ b/components/_class/picker/input-range.tsx @@ -82,6 +82,7 @@ function DateInput( const { getPrefixCls, size: ctxSize, locale, rtl } = useContext(ConfigContext); const input0 = useRef(null); const input1 = useRef(null); + const refRootWrapper = useRef(null); const disabled1 = isArray(disabled) ? disabled[0] : disabled; const disabled2 = isArray(disabled) ? disabled[1] : disabled; @@ -102,6 +103,9 @@ function DateInput( input1.current && input1.current.blur && input1.current.blur(); } }, + getRootDOMNode: () => { + return refRootWrapper.current; + }, })); function changeFocusedInput(e, index: number) { @@ -173,7 +177,12 @@ function DateInput( } return ( -
+
{prefix &&
{prefix}
}
input.current, })); function onKeyDown(e) { diff --git a/components/_class/select-view.tsx b/components/_class/select-view.tsx index 73d12c8a8e..b461b0ab10 100644 --- a/components/_class/select-view.tsx +++ b/components/_class/select-view.tsx @@ -192,6 +192,7 @@ const DUMMY_TAG_COUNT = 1; export type SelectViewHandle = { dom: HTMLDivElement; + getRootDOMNode: () => HTMLDivElement; focus: () => void; blur: () => void; getWidth: () => number; @@ -311,6 +312,7 @@ const CoreSelectView = React.forwardRef( useImperativeHandle(ref, () => ({ dom: refWrapper.current, + getRootDOMNode: () => refWrapper.current, focus: handleFocus.bind(null, 'focus'), blur: handleFocus.bind(null, 'blur'), getWidth: () => refWrapper.current && refWrapper.current.clientWidth, diff --git a/components/_util/CSSTransition.tsx b/components/_util/CSSTransition.tsx new file mode 100644 index 0000000000..a9f2d88f48 --- /dev/null +++ b/components/_util/CSSTransition.tsx @@ -0,0 +1,42 @@ +// just to resolve CssTransition findDOMNode +import React, { ReactElement, cloneElement, isValidElement, useMemo, useRef } from 'react'; +import { CSSTransition } from 'react-transition-group'; +import { supportRef } from './is'; +import { callbackOriginRef, findDOMNode } from './react-dom'; + +export default function ArcoCSSTransition(props: CSSTransition) { + const { children, ...rest } = props; + const nodeRef = useRef(); + const flagRef = useRef(); + + const dom = useMemo(() => { + // 只处理 div, span 之类的 children 即可 + if (props.nodeRef === undefined && supportRef(children) && isValidElement(children)) { + flagRef.current = true; + return cloneElement(children as ReactElement, { + ref: (node) => { + nodeRef.current = findDOMNode(node); + callbackOriginRef(children, node); + }, + }); + } + flagRef.current = false; + return children; + }, [children, props.nodeRef]); + + if (flagRef.current) { + ['onEnter', 'onEntering', 'onEntered', 'onExit', 'onExiting', 'onExited'].forEach((key) => { + if (props[key]) { + rest[key] = (_maybeNode, ...args) => { + props[key](nodeRef.current, ...args); + }; + } + }); + } + + return ( + + {dom} + + ); +} diff --git a/components/_util/hooks/useOverrideRef.ts b/components/_util/hooks/useOverrideRef.ts new file mode 100644 index 0000000000..41f4f4cb26 --- /dev/null +++ b/components/_util/hooks/useOverrideRef.ts @@ -0,0 +1,24 @@ +import React, { ReactElement, ReactNode, cloneElement, isValidElement, useCallback } from 'react'; +import { callbackOriginRef } from '../react-dom'; +import { supportRef } from '../is'; + +export default function useOverrideRef(): [ + (originNode) => ReactNode, + React.MutableRefObject +] { + const ref = React.useRef(null); + + const overrideNode = useCallback((originNode) => { + if (isValidElement(originNode) && supportRef(originNode)) { + return cloneElement(originNode as ReactElement, { + ref: (node: T) => { + ref.current = node; + callbackOriginRef(originNode, node); + }, + }); + } + return originNode; + }, []); + + return [overrideNode, ref]; +} diff --git a/components/_util/is.ts b/components/_util/is.ts index 56ce6437d3..656342010f 100644 --- a/components/_util/is.ts +++ b/components/_util/is.ts @@ -1,4 +1,6 @@ import { Dayjs } from 'dayjs'; +import { isValidElement } from 'react'; +import { isForwardRef } from 'react-is'; const opt = Object.prototype.toString; @@ -100,3 +102,33 @@ export function isDayjs(time): time is Dayjs { export function isBoolean(value: any): value is Boolean { return typeof value === 'boolean'; } + +export const isReactComponent = (element: any): boolean => { + return element && isValidElement(element) && typeof element.type === 'function'; +}; + +export const isClassComponent = (element: any): boolean => { + return isReactComponent(element) && !!element.type.prototype?.isReactComponent; +}; + +// element 是合成的 dom 元素或者字符串,数字等 +export const isDOMElement = (element: any): boolean => { + return isValidElement(element) && typeof element.type === 'string'; +}; + +// 传入的元素是否可以设置 ref 饮用 +export const supportRef = (element: any): boolean => { + if (isDOMElement(element)) { + return true; + } + + if (isForwardRef(element)) { + return true; + } + + if (isReactComponent(element)) { + return isClassComponent(element); // 函数组件且没有被 forwardRef,无法设置 ref + } + + return false; +}; diff --git a/components/_util/react-dom.ts b/components/_util/react-dom.ts index 2f03883d51..c3d800ce30 100644 --- a/components/_util/react-dom.ts +++ b/components/_util/react-dom.ts @@ -1,6 +1,7 @@ -import { ReactElement } from 'react'; +import { Component, ReactElement, ReactInstance } from 'react'; import ReactDOM from 'react-dom'; -import { isObject } from './is'; +import { isObject, isFunction } from './is'; +import warning from './warning'; type CreateRootFnType = (container: Element | DocumentFragment) => { render: (container: ReactElement) => void; @@ -73,4 +74,53 @@ if (isReact18 && createRoot) { }; } +/** + * + * @param element + * @param instance: 兜底 findDOMNode 查找,一般都是 this + * @returns + */ +export const findDOMNode = (element: any, instance?: ReactInstance) => { + // 类组件,非 forwardRef(function component) 都拿不到真实dom + if (element && element instanceof Element) { + return element; + } + + if (element && element.current && element.current instanceof Element) { + return element.current; + } + + if (element instanceof Component) { + return ReactDOM.findDOMNode(element); + } + + if (element && isFunction(element.getRootDOMNode)) { + return element.getRootDOMNode(); + } + + // 一般 useImperativeHandle 的元素拿到的 ref 不是 dom 元素且不存在 getRootDOMNode ,会走到这里。 + if (instance) { + warning( + true, + 'Element does not define the `getRootDOMNode` method causing a call to React.findDOMNode. but findDOMNode is deprecated in StrictMode. Please check the code logic', + { element, instance } + ); + return ReactDOM.findDOMNode(instance); + } + + return null; +}; + +// 回调children的原始 ref ,适配函数 ref or ref.current 场景 +export const callbackOriginRef = (children: any, node) => { + if (children && children.ref) { + if (isFunction(children.ref)) { + children?.ref(node); + } + if ('current' in children.ref) { + children.ref.current = node; + } + } +}; + export const render = copyRender; diff --git a/components/_util/resizeObserver.tsx b/components/_util/resizeObserver.tsx index 1775c065a2..e1a388c09e 100644 --- a/components/_util/resizeObserver.tsx +++ b/components/_util/resizeObserver.tsx @@ -1,16 +1,29 @@ -import React from 'react'; +import React, { ReactElement, cloneElement, isValidElement } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import lodashThrottle from 'lodash/throttle'; -import { findDOMNode } from 'react-dom'; +import { callbackOriginRef, findDOMNode } from '../_util/react-dom'; +import { supportRef } from './is'; export interface ResizeProps { throttle?: boolean; onResize?: (entry: ResizeObserverEntry[]) => void; children?: React.ReactNode; + getTargetDOMNode?: () => any; } class ResizeObserverComponent extends React.Component { - resizeObserver: any; + resizeObserver: ResizeObserver; + + rootDOMRef: any; + + getRootElement = () => { + const { getTargetDOMNode } = this.props; + return findDOMNode(getTargetDOMNode?.() || this.rootDOMRef, this); + }; + + getRootDOMNode = () => { + return this.getRootElement(); + }; componentDidMount() { if (!React.isValidElement(this.props.children)) { @@ -21,7 +34,7 @@ class ResizeObserverComponent extends React.Component { } componentDidUpdate() { - if (!this.resizeObserver && findDOMNode(this)) { + if (!this.resizeObserver && this.getRootElement()) { this.createResizeObserver(); } } @@ -48,7 +61,8 @@ class ResizeObserverComponent extends React.Component { } resizeHandler(entry); }); - this.resizeObserver.observe(findDOMNode(this) as Element); + const targetNode = this.getRootElement(); + targetNode && this.resizeObserver.observe(targetNode as Element); }; destroyResizeObserver = () => { @@ -57,6 +71,18 @@ class ResizeObserverComponent extends React.Component { }; render() { + const { children } = this.props; + + if (supportRef(children) && isValidElement(children) && !this.props.getTargetDOMNode) { + return cloneElement(children as ReactElement, { + ref: (node) => { + this.rootDOMRef = node; + + callbackOriginRef(children, node); + }, + }); + } + this.rootDOMRef = null; return this.props.children; } } diff --git a/components/_util/warning.ts b/components/_util/warning.ts index 9545627ad5..9274e5a9b4 100644 --- a/components/_util/warning.ts +++ b/components/_util/warning.ts @@ -1,7 +1,10 @@ -export default function warning(condition, message: string) { +export default function warning(condition, message: string, ...extra) { if (process.env.NODE_ENV !== 'production' && console) { if (condition) { - console.error(`[@arco-design/web-react]: ${message}`); + return console.error( + `[@arco-design/web-react]: ${message}`, + extra ? { detail: extra } : undefined + ); } } } diff --git a/site/src/index.js b/site/src/index.js index 4968974277..6f11bc4b98 100644 --- a/site/src/index.js +++ b/site/src/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { StrictMode, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import axios from 'axios'; @@ -77,7 +77,12 @@ if (isProduction) { // Don't render itself if flag is set under which case it will be rendered by its host app if (typeof process === 'undefined' || process?.env?.RENDER_BY_HOST !== 'true') { - ReactDOM.render(, document.getElementById('root')); + ReactDOM.render( + + + , + document.getElementById('root') + ); } tea({ name: 'site_components_zh' }); diff --git a/stories/Trigger.story.tsx b/stories/Trigger.story.tsx new file mode 100644 index 0000000000..4e8f8b7d02 --- /dev/null +++ b/stories/Trigger.story.tsx @@ -0,0 +1,78 @@ +/* eslint-disable no-console */ +import React, { StrictMode, forwardRef } from 'react'; +import { Trigger, Space, Typography, Tooltip } from '@self'; + +function Son(props) { + return
Son
; +} + +function Son2(props, ref) { + return ( +
+ ForardRefSon +
+ ); +} + +function Popup() { + return ( +
+ + 123123 + +
+ ); +} + +const ForardRefSon = forwardRef(Son2); + +class Child extends React.Component { + render() { + return
Child
; + } +} + +export const Demo = () => ( + +
+ + {/* + {``} + + + string + {1} */} + + ( +
+ + 123123 + +
+ )} + > + 123 +
+ + {/* + + + + + */} +
+
+
+); + +export default { + title: ' Trigger', +};