mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 20:49:53 +08:00
refactor: rewrite Affix CC => FC (#42674)
* fix * test * Update index.ts * fix * fix * fix * fix: fix * fix: fix * fix: fix * fix: fix * fix: fix * fix: fix * update docs * Update components/affix/index.tsx Co-authored-by: kiner-tang(文辉) <1127031143@qq.com> Signed-off-by: lijianan <574980606@qq.com> * fix * test: add test case * Update components/affix/index.zh-CN.md Co-authored-by: afc163 <afc163@gmail.com> Signed-off-by: lijianan <574980606@qq.com> * update demo --------- Signed-off-by: lijianan <574980606@qq.com> Co-authored-by: kiner-tang(文辉) <1127031143@qq.com> Co-authored-by: afc163 <afc163@gmail.com>
This commit is contained in:
parent
e76606f771
commit
be92498f15
@ -1,9 +1,5 @@
|
||||
import raf from 'rc-util/lib/raf';
|
||||
|
||||
type throttledFn = (...args: any[]) => void;
|
||||
|
||||
type throttledCancelFn = { cancel: () => void };
|
||||
|
||||
function throttleByAnimationFrame<T extends any[]>(fn: (...args: T) => void) {
|
||||
let requestId: number | null;
|
||||
|
||||
@ -12,7 +8,7 @@ function throttleByAnimationFrame<T extends any[]>(fn: (...args: T) => void) {
|
||||
fn(...args);
|
||||
};
|
||||
|
||||
const throttled: throttledFn & throttledCancelFn = (...args: T) => {
|
||||
const throttled = (...args: T) => {
|
||||
if (requestId == null) {
|
||||
requestId = raf(later(args));
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import type { InternalAffixClass } from '..';
|
||||
|
||||
import Affix from '..';
|
||||
import accessibilityTest from '../../../tests/shared/accessibilityTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
@ -12,13 +11,12 @@ const events: Partial<Record<keyof HTMLElementEventMap, (ev: Partial<Event>) =>
|
||||
interface AffixProps {
|
||||
offsetTop?: number;
|
||||
offsetBottom?: number;
|
||||
style?: CSSProperties;
|
||||
style?: React.CSSProperties;
|
||||
onChange?: () => void;
|
||||
onTestUpdatePosition?: () => void;
|
||||
getInstance?: (inst: InternalAffixClass) => void;
|
||||
}
|
||||
|
||||
const AffixMounter: React.FC<AffixProps> = ({ getInstance, ...restProps }) => {
|
||||
const AffixMounter: React.FC<AffixProps> = (props) => {
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (container.current) {
|
||||
@ -31,7 +29,7 @@ const AffixMounter: React.FC<AffixProps> = ({ getInstance, ...restProps }) => {
|
||||
}, []);
|
||||
return (
|
||||
<div ref={container} className="container">
|
||||
<Affix className="fixed" ref={getInstance} target={() => container.current} {...restProps}>
|
||||
<Affix className="fixed" target={() => container.current} {...props}>
|
||||
<Button type="primary">Fixed at the top of container</Button>
|
||||
</Affix>
|
||||
</div>
|
||||
@ -124,34 +122,14 @@ describe('Affix Render', () => {
|
||||
});
|
||||
|
||||
describe('updatePosition when target changed', () => {
|
||||
it('function change', async () => {
|
||||
document.body.innerHTML = '<div id="mounter" />';
|
||||
const container = document.getElementById('mounter');
|
||||
const getTarget = () => container;
|
||||
let affixInstance: InternalAffixClass;
|
||||
const { rerender } = render(
|
||||
<Affix
|
||||
ref={(node) => {
|
||||
affixInstance = node as InternalAffixClass;
|
||||
}}
|
||||
target={getTarget}
|
||||
>
|
||||
{null}
|
||||
</Affix>,
|
||||
);
|
||||
rerender(
|
||||
<Affix
|
||||
ref={(node) => {
|
||||
affixInstance = node as InternalAffixClass;
|
||||
}}
|
||||
target={() => null}
|
||||
>
|
||||
{null}
|
||||
</Affix>,
|
||||
);
|
||||
expect(affixInstance!.state.status).toBe(0);
|
||||
expect(affixInstance!.state.affixStyle).toBe(undefined);
|
||||
expect(affixInstance!.state.placeholderStyle).toBe(undefined);
|
||||
it('function change', () => {
|
||||
document.body.innerHTML = `<div id="mounter" />`;
|
||||
const target = document.getElementById('mounter');
|
||||
const getTarget = () => target;
|
||||
const { container, rerender } = render(<Affix target={getTarget}>{null}</Affix>);
|
||||
rerender(<Affix target={() => null}>{null}</Affix>);
|
||||
expect(container.querySelector(`div[aria-hidden="true"]`)).toBeNull();
|
||||
expect(container.querySelector('.ant-affix')?.getAttribute('style')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('check position change before measure', async () => {
|
||||
@ -167,34 +145,18 @@ describe('Affix Render', () => {
|
||||
);
|
||||
await waitFakeTimer();
|
||||
await movePlaceholder(1000);
|
||||
expect(container.querySelector('.ant-affix')).toBeTruthy();
|
||||
expect(container.querySelector<HTMLDivElement>('.ant-affix')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('do not measure when hidden', async () => {
|
||||
let affixInstance: InternalAffixClass | null = null;
|
||||
|
||||
const { rerender } = render(
|
||||
<AffixMounter
|
||||
getInstance={(inst) => {
|
||||
affixInstance = inst;
|
||||
}}
|
||||
offsetBottom={0}
|
||||
/>,
|
||||
);
|
||||
const { container, rerender } = render(<AffixMounter offsetBottom={0} />);
|
||||
await waitFakeTimer();
|
||||
const firstAffixStyle = affixInstance!.state.affixStyle;
|
||||
const affixStyleEle = container.querySelector('.ant-affix');
|
||||
const firstAffixStyle = affixStyleEle ? affixStyleEle.getAttribute('style') : null;
|
||||
|
||||
rerender(
|
||||
<AffixMounter
|
||||
getInstance={(inst) => {
|
||||
affixInstance = inst;
|
||||
}}
|
||||
offsetBottom={0}
|
||||
style={{ display: 'none' }}
|
||||
/>,
|
||||
);
|
||||
rerender(<AffixMounter offsetBottom={0} style={{ display: 'none' }} />);
|
||||
await waitFakeTimer();
|
||||
const secondAffixStyle = affixInstance!.state.affixStyle;
|
||||
const secondAffixStyle = affixStyleEle ? affixStyleEle.getAttribute('style') : null;
|
||||
|
||||
expect(firstAffixStyle).toEqual(secondAffixStyle);
|
||||
});
|
||||
@ -204,36 +166,23 @@ describe('Affix Render', () => {
|
||||
it('add class automatically', async () => {
|
||||
document.body.innerHTML = '<div id="mounter" />';
|
||||
|
||||
let affixInstance: InternalAffixClass | null = null;
|
||||
render(
|
||||
<AffixMounter
|
||||
getInstance={(inst) => {
|
||||
affixInstance = inst;
|
||||
}}
|
||||
offsetBottom={0}
|
||||
/>,
|
||||
{
|
||||
container: document.getElementById('mounter')!,
|
||||
},
|
||||
);
|
||||
const { container } = render(<AffixMounter offsetBottom={0} />, {
|
||||
container: document.getElementById('mounter')!,
|
||||
});
|
||||
|
||||
await waitFakeTimer();
|
||||
await movePlaceholder(300);
|
||||
expect(affixInstance!.state.affixStyle).toBeTruthy();
|
||||
expect(container.querySelector(`div[aria-hidden="true"]`)).toBeTruthy();
|
||||
expect(container.querySelector('.ant-affix')?.getAttribute('style')).toBeTruthy();
|
||||
});
|
||||
|
||||
// Trigger inner and outer element for the two <ResizeObserver>s.
|
||||
[
|
||||
'.ant-btn', // inner
|
||||
'.fixed', // outer
|
||||
].forEach((selector) => {
|
||||
['.ant-btn', '.fixed'].forEach((selector) => {
|
||||
it(`trigger listener when size change: ${selector}`, async () => {
|
||||
const updateCalled = jest.fn();
|
||||
const { container } = render(
|
||||
<AffixMounter offsetBottom={0} onTestUpdatePosition={updateCalled} />,
|
||||
{
|
||||
container: document.getElementById('mounter')!,
|
||||
},
|
||||
{ container: document.getElementById('mounter')! },
|
||||
);
|
||||
|
||||
updateCalled.mockReset();
|
||||
|
@ -90,10 +90,10 @@ exports[`renders components/affix/demo/on-change.tsx extend context correctly 2`
|
||||
|
||||
exports[`renders components/affix/demo/target.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="scrollable-container"
|
||||
style="width: 100%; height: 100px; overflow: auto;"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
style="width: 100%; height: 1000px;"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -84,10 +84,10 @@ exports[`renders components/affix/demo/on-change.tsx correctly 1`] = `
|
||||
|
||||
exports[`renders components/affix/demo/target.tsx correctly 1`] = `
|
||||
<div
|
||||
class="scrollable-container"
|
||||
style="width:100%;height:100px;overflow:auto"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
style="width:100%;height:1000px"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Affix, Button } from 'antd';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [top, setTop] = useState(10);
|
||||
const [bottom, setBottom] = useState(10);
|
||||
|
||||
const [top, setTop] = React.useState<number>(100);
|
||||
const [bottom, setBottom] = React.useState<number>(100);
|
||||
return (
|
||||
<>
|
||||
<Affix offsetTop={top}>
|
||||
|
@ -1,12 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { Affix, Button } from 'antd';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
const containerStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: 100,
|
||||
overflow: 'auto',
|
||||
};
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: 1000,
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [container, setContainer] = React.useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<div className="scrollable-container" ref={setContainer}>
|
||||
<div className="background">
|
||||
<div style={containerStyle} ref={setContainer}>
|
||||
<div style={style}>
|
||||
<Affix target={() => container}>
|
||||
<Button type="primary">Fixed at the top of container</Button>
|
||||
</Affix>
|
||||
|
@ -18,6 +18,10 @@ On longer web pages, it's helpful to stick component into the viewport. This is
|
||||
|
||||
Please note that Affix should not cover other content on the page, especially when the size of the viewport is small.
|
||||
|
||||
> Notes for developers
|
||||
>
|
||||
> After version `5.10.0`, we rewrite Affix use FC, Some methods of obtaining `ref` and calling internal instance methods will invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { createRef, forwardRef, useContext } from 'react';
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
@ -41,16 +40,16 @@ export interface AffixProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface InternalAffixProps extends AffixProps {
|
||||
affixPrefixCls: string;
|
||||
}
|
||||
|
||||
enum AffixStatus {
|
||||
None,
|
||||
Prepare,
|
||||
}
|
||||
|
||||
export interface AffixState {
|
||||
interface InternalAffixProps extends AffixProps {
|
||||
affixPrefixCls: string;
|
||||
}
|
||||
|
||||
interface AffixState {
|
||||
affixStyle?: React.CSSProperties;
|
||||
placeholderStyle?: React.CSSProperties;
|
||||
status: AffixStatus;
|
||||
@ -58,117 +57,58 @@ export interface AffixState {
|
||||
prevTarget: Window | HTMLElement | null;
|
||||
}
|
||||
|
||||
class InternalAffix extends React.Component<InternalAffixProps, AffixState> {
|
||||
static contextType = ConfigContext;
|
||||
interface AffixRef {
|
||||
updatePosition: ReturnType<typeof throttleByAnimationFrame>;
|
||||
}
|
||||
|
||||
state: AffixState = {
|
||||
status: AffixStatus.None,
|
||||
lastAffix: false,
|
||||
prevTarget: null,
|
||||
};
|
||||
const InternalAffix = React.forwardRef<AffixRef, InternalAffixProps>((props, ref) => {
|
||||
const {
|
||||
style,
|
||||
offsetTop,
|
||||
offsetBottom,
|
||||
affixPrefixCls,
|
||||
rootClassName,
|
||||
children,
|
||||
target,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
private placeholderNodeRef = createRef<HTMLDivElement>();
|
||||
const [lastAffix, setLastAffix] = React.useState(false);
|
||||
const [affixStyle, setAffixStyle] = React.useState<React.CSSProperties>();
|
||||
const [placeholderStyle, setPlaceholderStyle] = React.useState<React.CSSProperties>();
|
||||
|
||||
private fixedNodeRef = createRef<HTMLDivElement>();
|
||||
const status = React.useRef<AffixStatus>(AffixStatus.None);
|
||||
|
||||
private timer: ReturnType<typeof setTimeout> | null;
|
||||
const prevTarget = React.useRef<Window | HTMLElement | null>(null);
|
||||
const prevListener = React.useRef<EventListener>();
|
||||
|
||||
context: ConfigConsumerProps;
|
||||
const placeholderNodeRef = React.useRef<HTMLDivElement>(null);
|
||||
const fixedNodeRef = React.useRef<HTMLDivElement>(null);
|
||||
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
private getTargetFunc() {
|
||||
const { getTargetContainer } = this.context;
|
||||
const { target } = this.props;
|
||||
const { getTargetContainer } = React.useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
if (target !== undefined) {
|
||||
return target;
|
||||
}
|
||||
const targetFunc = target ?? getTargetContainer ?? getDefaultTarget;
|
||||
|
||||
return getTargetContainer ?? getDefaultTarget;
|
||||
}
|
||||
|
||||
addListeners = () => {
|
||||
const targetFunc = this.getTargetFunc();
|
||||
const target = targetFunc?.();
|
||||
const { prevTarget } = this.state;
|
||||
if (prevTarget !== target) {
|
||||
TRIGGER_EVENTS.forEach((eventName) => {
|
||||
prevTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
|
||||
target?.addEventListener(eventName, this.lazyUpdatePosition);
|
||||
});
|
||||
this.updatePosition();
|
||||
this.setState({ prevTarget: target });
|
||||
}
|
||||
};
|
||||
|
||||
removeListeners = () => {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
const { prevTarget } = this.state;
|
||||
const targetFunc = this.getTargetFunc();
|
||||
const newTarget = targetFunc?.();
|
||||
TRIGGER_EVENTS.forEach((eventName) => {
|
||||
newTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
|
||||
prevTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
|
||||
});
|
||||
this.updatePosition.cancel();
|
||||
// https://github.com/ant-design/ant-design/issues/22683
|
||||
this.lazyUpdatePosition.cancel();
|
||||
};
|
||||
|
||||
// Event handler
|
||||
componentDidMount() {
|
||||
// [Legacy] Wait for parent component ref has its value.
|
||||
// We should use target as directly element instead of function which makes element check hard.
|
||||
this.timer = setTimeout(this.addListeners);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: AffixProps) {
|
||||
this.addListeners();
|
||||
if (
|
||||
prevProps.offsetTop !== this.props.offsetTop ||
|
||||
prevProps.offsetBottom !== this.props.offsetBottom
|
||||
) {
|
||||
this.updatePosition();
|
||||
}
|
||||
this.measure();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
getOffsetTop = () => {
|
||||
const { offsetBottom, offsetTop } = this.props;
|
||||
return offsetBottom === undefined && offsetTop === undefined ? 0 : offsetTop;
|
||||
};
|
||||
|
||||
getOffsetBottom = () => this.props.offsetBottom;
|
||||
const internalOffsetTop = offsetBottom === undefined && offsetTop === undefined ? 0 : offsetTop;
|
||||
|
||||
// =================== Measure ===================
|
||||
measure = () => {
|
||||
const { status, lastAffix } = this.state;
|
||||
const { onChange } = this.props;
|
||||
const targetFunc = this.getTargetFunc();
|
||||
const measure = () => {
|
||||
if (
|
||||
status !== AffixStatus.Prepare ||
|
||||
!this.fixedNodeRef.current ||
|
||||
!this.placeholderNodeRef.current ||
|
||||
status.current !== AffixStatus.Prepare ||
|
||||
!fixedNodeRef.current ||
|
||||
!placeholderNodeRef.current ||
|
||||
!targetFunc
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offsetTop = this.getOffsetTop();
|
||||
const offsetBottom = this.getOffsetBottom();
|
||||
|
||||
const targetNode = targetFunc();
|
||||
if (targetNode) {
|
||||
const newState: Partial<AffixState> = {
|
||||
status: AffixStatus.None,
|
||||
};
|
||||
const placeholderRect = getTargetRect(this.placeholderNodeRef.current);
|
||||
const placeholderRect = getTargetRect(placeholderNodeRef.current);
|
||||
|
||||
if (
|
||||
placeholderRect.top === 0 &&
|
||||
@ -180,7 +120,7 @@ class InternalAffix extends React.Component<InternalAffixProps, AffixState> {
|
||||
}
|
||||
|
||||
const targetRect = getTargetRect(targetNode);
|
||||
const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop);
|
||||
const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop);
|
||||
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
|
||||
|
||||
if (fixedTop !== undefined) {
|
||||
@ -208,46 +148,38 @@ class InternalAffix extends React.Component<InternalAffixProps, AffixState> {
|
||||
}
|
||||
|
||||
newState.lastAffix = !!newState.affixStyle;
|
||||
if (onChange && lastAffix !== newState.lastAffix) {
|
||||
onChange(newState.lastAffix);
|
||||
|
||||
if (lastAffix !== newState.lastAffix) {
|
||||
onChange?.(newState.lastAffix);
|
||||
}
|
||||
this.setState(newState as AffixState);
|
||||
|
||||
status.current = newState.status!;
|
||||
setAffixStyle(newState.affixStyle);
|
||||
setPlaceholderStyle(newState.placeholderStyle);
|
||||
setLastAffix(newState.lastAffix);
|
||||
}
|
||||
};
|
||||
|
||||
prepareMeasure = () => {
|
||||
// event param is used before. Keep compatible ts define here.
|
||||
this.setState({
|
||||
status: AffixStatus.Prepare,
|
||||
affixStyle: undefined,
|
||||
placeholderStyle: undefined,
|
||||
});
|
||||
|
||||
// Test if `updatePosition` called
|
||||
const prepareMeasure = () => {
|
||||
status.current = AffixStatus.Prepare;
|
||||
measure();
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const { onTestUpdatePosition } = this.props as any;
|
||||
onTestUpdatePosition?.();
|
||||
(props as any)?.onTestUpdatePosition?.();
|
||||
}
|
||||
};
|
||||
|
||||
updatePosition = throttleByAnimationFrame(() => {
|
||||
this.prepareMeasure();
|
||||
const updatePosition = throttleByAnimationFrame(() => {
|
||||
prepareMeasure();
|
||||
});
|
||||
|
||||
lazyUpdatePosition = throttleByAnimationFrame(() => {
|
||||
const targetFunc = this.getTargetFunc();
|
||||
const { affixStyle } = this.state;
|
||||
|
||||
const lazyUpdatePosition = throttleByAnimationFrame(() => {
|
||||
// Check position change before measure to make Safari smooth
|
||||
if (targetFunc && affixStyle) {
|
||||
const offsetTop = this.getOffsetTop();
|
||||
const offsetBottom = this.getOffsetBottom();
|
||||
|
||||
const targetNode = targetFunc();
|
||||
if (targetNode && this.placeholderNodeRef.current) {
|
||||
if (targetNode && placeholderNodeRef.current) {
|
||||
const targetRect = getTargetRect(targetNode);
|
||||
const placeholderRect = getTargetRect(this.placeholderNodeRef.current);
|
||||
const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop);
|
||||
const placeholderRect = getTargetRect(placeholderNodeRef.current);
|
||||
const fixedTop = getFixedTop(placeholderRect, targetRect, internalOffsetTop);
|
||||
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
|
||||
|
||||
if (
|
||||
@ -260,49 +192,91 @@ class InternalAffix extends React.Component<InternalAffixProps, AffixState> {
|
||||
}
|
||||
|
||||
// Directly call prepare measure since it's already throttled.
|
||||
this.prepareMeasure();
|
||||
prepareMeasure();
|
||||
});
|
||||
|
||||
// =================== Render ===================
|
||||
render() {
|
||||
const { affixStyle, placeholderStyle } = this.state;
|
||||
const { affixPrefixCls, rootClassName, children } = this.props;
|
||||
const className = classNames(affixStyle && rootClassName, {
|
||||
[affixPrefixCls]: !!affixStyle,
|
||||
const addListeners = () => {
|
||||
const listenerTarget = targetFunc?.();
|
||||
TRIGGER_EVENTS.forEach((eventName) => {
|
||||
if (prevListener.current) {
|
||||
prevTarget.current?.removeEventListener(eventName, prevListener.current);
|
||||
}
|
||||
listenerTarget?.addEventListener(eventName, lazyUpdatePosition);
|
||||
});
|
||||
prevTarget.current = listenerTarget;
|
||||
prevListener.current = lazyUpdatePosition;
|
||||
};
|
||||
|
||||
let props = omit(this.props, [
|
||||
'prefixCls',
|
||||
'offsetTop',
|
||||
'offsetBottom',
|
||||
'target',
|
||||
'onChange',
|
||||
'affixPrefixCls',
|
||||
'rootClassName',
|
||||
]);
|
||||
// Omit this since `onTestUpdatePosition` only works on test.
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
props = omit(props as typeof props & { onTestUpdatePosition: any }, ['onTestUpdatePosition']);
|
||||
const removeListeners = () => {
|
||||
if (timer.current) {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
}
|
||||
const newTarget = targetFunc?.();
|
||||
TRIGGER_EVENTS.forEach((eventName) => {
|
||||
newTarget?.removeEventListener(eventName, lazyUpdatePosition);
|
||||
if (prevListener.current) {
|
||||
prevTarget.current?.removeEventListener(eventName, prevListener.current);
|
||||
}
|
||||
});
|
||||
updatePosition.cancel();
|
||||
lazyUpdatePosition.cancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizeObserver onResize={this.updatePosition}>
|
||||
<div {...props} ref={this.placeholderNodeRef}>
|
||||
{affixStyle && <div style={placeholderStyle} aria-hidden="true" />}
|
||||
<div className={className} ref={this.fixedNodeRef} style={affixStyle}>
|
||||
<ResizeObserver onResize={this.updatePosition}>{children}</ResizeObserver>
|
||||
</div>
|
||||
</div>
|
||||
</ResizeObserver>
|
||||
);
|
||||
React.useImperativeHandle(ref, () => ({ updatePosition }));
|
||||
|
||||
// mount & unmount
|
||||
React.useEffect(() => {
|
||||
// [Legacy] Wait for parent component ref has its value.
|
||||
// We should use target as directly element instead of function which makes element check hard.
|
||||
timer.current = setTimeout(addListeners);
|
||||
return () => removeListeners();
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
addListeners();
|
||||
}, [target, affixStyle]);
|
||||
|
||||
React.useEffect(() => {
|
||||
updatePosition();
|
||||
}, [target, offsetTop, offsetBottom]);
|
||||
|
||||
const className = classNames({
|
||||
[affixPrefixCls]: affixStyle,
|
||||
[rootClassName!]: affixStyle && rootClassName,
|
||||
});
|
||||
|
||||
let otherProps = omit(props, [
|
||||
'prefixCls',
|
||||
'offsetTop',
|
||||
'offsetBottom',
|
||||
'target',
|
||||
'onChange',
|
||||
'affixPrefixCls',
|
||||
'rootClassName',
|
||||
]);
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
otherProps = omit(otherProps as typeof otherProps & { onTestUpdatePosition: any }, [
|
||||
'onTestUpdatePosition',
|
||||
]);
|
||||
}
|
||||
}
|
||||
// just use in test
|
||||
export type InternalAffixClass = InternalAffix;
|
||||
|
||||
const Affix = forwardRef<InternalAffix, AffixProps>((props, ref) => {
|
||||
return (
|
||||
<ResizeObserver onResize={updatePosition}>
|
||||
<div style={style} ref={placeholderNodeRef} {...otherProps}>
|
||||
{affixStyle && <div style={placeholderStyle} aria-hidden="true" />}
|
||||
<div className={className} ref={fixedNodeRef} style={affixStyle}>
|
||||
<ResizeObserver onResize={updatePosition}>{children}</ResizeObserver>
|
||||
</div>
|
||||
</div>
|
||||
</ResizeObserver>
|
||||
);
|
||||
});
|
||||
|
||||
const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
|
||||
const { prefixCls: customizePrefixCls, rootClassName } = props;
|
||||
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const { getPrefixCls } = React.useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const affixPrefixCls = getPrefixCls('affix', customizePrefixCls);
|
||||
|
||||
const [wrapSSR, hashId] = useStyle(affixPrefixCls);
|
||||
|
@ -19,6 +19,10 @@ group:
|
||||
|
||||
页面可视范围过小时,慎用此功能以免遮挡页面内容。
|
||||
|
||||
> 开发者注意事项:
|
||||
>
|
||||
> 自 `5.10.0` 起,由于 Affix 组件由 class 重构为 FC,之前获取 `ref` 并调用内部实例方法的写法都会失效。
|
||||
|
||||
## 代码演示
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
Loading…
Reference in New Issue
Block a user