Skip to content
This repository was archived by the owner on Nov 4, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 30 additions & 47 deletions src/Align.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -26,11 +26,6 @@ export interface AlignProps {
children: React.ReactElement;
}

interface MonitorRef {
element?: HTMLElement;
cancel: () => void;
}

export interface RefAlign {
forceAlign: () => void;
}
Expand All @@ -52,6 +47,8 @@ const Align: React.ForwardRefRenderFunction<RefAlign, AlignProps> = (
const cacheRef = React.useRef<{ element?: HTMLElement; point?: TargetPoint; align?: AlignType }>(
{},
);

/** Popup node ref */
const nodeRef = React.useRef();
let childNode = React.Children.only(children);

Expand All @@ -75,9 +72,10 @@ const Align: React.ForwardRefRenderFunction<RefAlign, AlignProps> = (
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);
Expand Down Expand Up @@ -110,40 +108,32 @@ const Align: React.ForwardRefRenderFunction<RefAlign, AlignProps> = (
}, monitorBufferTime);

// ===================== Effect =====================
// Listen for target updated
const resizeMonitor = React.useRef<MonitorRef>({
cancel: () => {},
});
// Listen for source updated
const sourceResizeMonitor = React.useRef<MonitorRef>({
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) {
Expand All @@ -154,24 +144,17 @@ const Align: React.ForwardRefRenderFunction<RefAlign, AlignProps> = (
}, [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();
},
[],
Expand Down
5 changes: 3 additions & 2 deletions tests/element.test.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -16,6 +16,7 @@ describe('element align', () => {
});

afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});

Expand Down
78 changes: 78 additions & 0 deletions tests/strict.test.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div ref={targetRef} />
<Align target={() => targetRef.current} align={{ points: ['bc', 'tc'] }}>
<div
style={{
position: 'absolute',
width: 50,
height: 80,
}}
/>
</Align>
</>
);
};

const { unmount } = render(
<React.StrictMode>
<Demo />
</React.StrictMode>,
);

act(() => {
jest.runAllTimers();
});

expect((global as any).watchCnt).toBeGreaterThan(0);

unmount();
expect((global as any).watchCnt).toEqual(0);
});
});
/* eslint-enable */