diff --git a/src/Align.tsx b/src/Align.tsx index d8b4048..636b493 100644 --- a/src/Align.tsx +++ b/src/Align.tsx @@ -3,16 +3,16 @@ * - childrenProps */ -import React from 'react'; -import { composeRef } from 'rc-util/lib/ref'; -import isVisible from 'rc-util/lib/Dom/isVisible'; import { alignElement, alignPoint } from 'dom-align'; -import addEventListener from 'rc-util/lib/Dom/addEventListener'; import isEqual from 'lodash/isEqual'; +import addEventListener from 'rc-util/lib/Dom/addEventListener'; +import isVisible from 'rc-util/lib/Dom/isVisible'; +import { composeRef } from 'rc-util/lib/ref'; +import React from 'react'; -import { isSamePoint, restoreFocus, monitorResize } from './util'; -import type { AlignType, AlignResult, TargetType, TargetPoint } from './interface'; import useBuffer from './hooks/useBuffer'; +import type { AlignResult, AlignType, TargetPoint, TargetType } from './interface'; +import { isSamePoint, monitorResize, restoreFocus } from './util'; type OnAlign = (source: HTMLElement, result: AlignResult) => void; @@ -26,11 +26,6 @@ export interface AlignProps { children: React.ReactElement; } -interface MonitorRef { - element?: HTMLElement; - cancel: () => void; -} - export interface RefAlign { forceAlign: () => void; } @@ -52,6 +47,8 @@ const Align: React.ForwardRefRenderFunction = ( const cacheRef = React.useRef<{ element?: HTMLElement; point?: TargetPoint; align?: AlignType }>( {}, ); + + /** Popup node ref */ const nodeRef = React.useRef(); let childNode = React.Children.only(children); @@ -75,9 +72,10 @@ const Align: React.ForwardRefRenderFunction = ( align: latestAlign, onAlign: latestOnAlign, } = forceAlignPropsRef.current; - if (!latestDisabled && latestTarget) { - const source = nodeRef.current; + const source = nodeRef.current; + + if (!latestDisabled && latestTarget && source) { let result: AlignResult; const element = getElement(latestTarget); const point = getPoint(latestTarget); @@ -110,40 +108,32 @@ const Align: React.ForwardRefRenderFunction = ( }, monitorBufferTime); // ===================== Effect ===================== - // Listen for target updated - const resizeMonitor = React.useRef({ - cancel: () => {}, - }); - // Listen for source updated - const sourceResizeMonitor = React.useRef({ - cancel: () => {}, - }); - React.useEffect(() => { - const element = getElement(target); - const point = getPoint(target); - - if (nodeRef.current !== sourceResizeMonitor.current.element) { - sourceResizeMonitor.current.cancel(); - sourceResizeMonitor.current.element = nodeRef.current; - sourceResizeMonitor.current.cancel = monitorResize(nodeRef.current, forceAlign); - } + // Handle props change + const element = getElement(target); + const point = getPoint(target); + React.useEffect(() => { if ( cacheRef.current.element !== element || !isSamePoint(cacheRef.current.point, point) || !isEqual(cacheRef.current.align, align) ) { forceAlign(); - - // Add resize observer - if (resizeMonitor.current.element !== element) { - resizeMonitor.current.cancel(); - resizeMonitor.current.element = element; - resizeMonitor.current.cancel = monitorResize(element, forceAlign); - } } }); + // Watch popup element resize + React.useEffect(() => { + const cancelFn = monitorResize(nodeRef.current, forceAlign); + return cancelFn; + }, [nodeRef.current]); + + // Watch target element resize + React.useEffect(() => { + const cancelFn = monitorResize(element, forceAlign); + return cancelFn; + }, [element]); + // Listen for disabled change React.useEffect(() => { if (!disabled) { @@ -154,24 +144,17 @@ const Align: React.ForwardRefRenderFunction = ( }, [disabled]); // Listen for window resize - const winResizeRef = React.useRef<{ remove: Function }>(null); React.useEffect(() => { if (monitorWindowResize) { - if (!winResizeRef.current) { - winResizeRef.current = addEventListener(window, 'resize', forceAlign); - } - } else if (winResizeRef.current) { - winResizeRef.current.remove(); - winResizeRef.current = null; + const cancelFn = addEventListener(window, 'resize', forceAlign); + + return cancelFn.remove; } }, [monitorWindowResize]); // Clear all if unmount React.useEffect( () => () => { - resizeMonitor.current.cancel(); - sourceResizeMonitor.current.cancel(); - if (winResizeRef.current) winResizeRef.current.remove(); cancelForceAlign(); }, [], diff --git a/tests/element.test.tsx b/tests/element.test.tsx index 742761c..86d68ab 100644 --- a/tests/element.test.tsx +++ b/tests/element.test.tsx @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this */ -import React from 'react'; -import { render } from '@testing-library/react'; +import { act, render } from '@testing-library/react'; import { spyElementPrototype } from 'rc-util/lib/test/domHook'; +import React from 'react'; import Align from '../src'; describe('element align', () => { @@ -16,6 +16,7 @@ describe('element align', () => { }); afterEach(() => { + jest.clearAllTimers(); jest.useRealTimers(); }); diff --git a/tests/strict.test.tsx b/tests/strict.test.tsx new file mode 100644 index 0000000..fa87689 --- /dev/null +++ b/tests/strict.test.tsx @@ -0,0 +1,78 @@ +/* eslint-disable class-methods-use-this */ +import { act, render } from '@testing-library/react'; +import { spyElementPrototype } from 'rc-util/lib/test/domHook'; +import React from 'react'; +import Align from '../src'; + +(global as any).watchCnt = 0; + +jest.mock('../src/util', () => { + const originUtil = jest.requireActual('../src/util'); + + return { + ...originUtil, + monitorResize: (...args: any[]) => { + (global as any).watchCnt += 1; + const cancelFn = originUtil.monitorResize(...args); + + return () => { + (global as any).watchCnt -= 1; + cancelFn(); + }; + }, + }; +}); + +describe('element align', () => { + beforeAll(() => { + spyElementPrototype(HTMLElement, 'offsetParent', { + get: () => ({}), + }); + }); + + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('StrictMode should keep resize observer', () => { + const Demo = () => { + const targetRef = React.useRef(null); + + return ( + <> +
+ targetRef.current} align={{ points: ['bc', 'tc'] }}> +
+ + + ); + }; + + const { unmount } = render( + + + , + ); + + act(() => { + jest.runAllTimers(); + }); + + expect((global as any).watchCnt).toBeGreaterThan(0); + + unmount(); + expect((global as any).watchCnt).toEqual(0); + }); +}); +/* eslint-enable */