mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-18 11:18:14 +08:00
refactor: Anchor.Link with FC (#37957)
* fix: fix * fix: fix * fix: revert * fix: fix * test: fix test case * test: fix * test: fix * fix: fix * fix: fix
This commit is contained in:
parent
e3abd1e77f
commit
b3a37b7ca9
@ -35,10 +35,10 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
|||||||
|
|
||||||
const sharpMatcherRegx = /#([\S ]+)$/;
|
const sharpMatcherRegx = /#([\S ]+)$/;
|
||||||
|
|
||||||
type Section = {
|
interface Section {
|
||||||
link: string;
|
link: string;
|
||||||
top: number;
|
top: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface AnchorProps {
|
export interface AnchorProps {
|
||||||
prefixCls?: string;
|
prefixCls?: string;
|
||||||
@ -89,11 +89,6 @@ export interface AntAnchor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigConsumerProps> {
|
class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigConsumerProps> {
|
||||||
static defaultProps = {
|
|
||||||
affix: true,
|
|
||||||
showInkInFixed: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = ConfigContext;
|
static contextType = ConfigContext;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -111,20 +106,20 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
|
|
||||||
private links: string[] = [];
|
private links: string[] = [];
|
||||||
|
|
||||||
private scrollEvent: any;
|
private scrollEvent: ReturnType<typeof addEventListener>;
|
||||||
|
|
||||||
private animating: boolean;
|
private animating: boolean;
|
||||||
|
|
||||||
private prefixCls?: string;
|
private prefixCls?: string;
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
registerLink = (link: string) => {
|
registerLink: AntAnchor['registerLink'] = link => {
|
||||||
if (!this.links.includes(link)) {
|
if (!this.links.includes(link)) {
|
||||||
this.links.push(link);
|
this.links.push(link);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
unregisterLink = (link: string) => {
|
unregisterLink: AntAnchor['unregisterLink'] = link => {
|
||||||
const index = this.links.indexOf(link);
|
const index = this.links.indexOf(link);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.links.splice(index, 1);
|
this.links.splice(index, 1);
|
||||||
@ -174,7 +169,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
const linkSections: Array<Section> = [];
|
const linkSections: Array<Section> = [];
|
||||||
const container = this.getContainer();
|
const container = this.getContainer();
|
||||||
this.links.forEach(link => {
|
this.links.forEach(link => {
|
||||||
const sharpLinkMatch = sharpMatcherRegx.exec(link.toString());
|
const sharpLinkMatch = sharpMatcherRegx.exec(link?.toString());
|
||||||
if (!sharpLinkMatch) {
|
if (!sharpLinkMatch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,10 +177,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
if (target) {
|
if (target) {
|
||||||
const top = getOffsetTop(target, container);
|
const top = getOffsetTop(target, container);
|
||||||
if (top < offsetTop + bounds) {
|
if (top < offsetTop + bounds) {
|
||||||
linkSections.push({
|
linkSections.push({ link, top });
|
||||||
link,
|
|
||||||
top,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -259,10 +251,9 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
updateInk = () => {
|
updateInk = () => {
|
||||||
const { prefixCls, wrapperRef } = this;
|
const { prefixCls, wrapperRef } = this;
|
||||||
const anchorNode = wrapperRef.current;
|
const anchorNode = wrapperRef.current;
|
||||||
const linkNode = anchorNode?.getElementsByClassName(`${prefixCls}-link-title-active`)[0];
|
const linkNode = anchorNode?.querySelector<HTMLElement>(`.${prefixCls}-link-title-active`);
|
||||||
|
|
||||||
if (linkNode) {
|
if (linkNode) {
|
||||||
this.inkNode.style.top = `${(linkNode as any).offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
|
this.inkNode.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2 - 4.5}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -283,8 +274,8 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
className = '',
|
className = '',
|
||||||
style,
|
style,
|
||||||
offsetTop,
|
offsetTop,
|
||||||
affix,
|
affix = true,
|
||||||
showInkInFixed,
|
showInkInFixed = false,
|
||||||
children,
|
children,
|
||||||
onClick,
|
onClick,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@ -311,7 +302,7 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
|
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapperStyle = {
|
const wrapperStyle: React.CSSProperties = {
|
||||||
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
|
||||||
...style,
|
...style,
|
||||||
};
|
};
|
||||||
@ -331,33 +322,26 @@ class Anchor extends React.Component<InternalAnchorProps, AnchorState, ConfigCon
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnchorContext.Provider value={contextValue}>
|
<AnchorContext.Provider value={contextValue}>
|
||||||
{!affix ? (
|
{affix ? (
|
||||||
anchorContent
|
|
||||||
) : (
|
|
||||||
<Affix offsetTop={offsetTop} target={this.getContainer}>
|
<Affix offsetTop={offsetTop} target={this.getContainer}>
|
||||||
{anchorContent}
|
{anchorContent}
|
||||||
</Affix>
|
</Affix>
|
||||||
|
) : (
|
||||||
|
anchorContent
|
||||||
)}
|
)}
|
||||||
</AnchorContext.Provider>
|
</AnchorContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// just use in test
|
// just use in test
|
||||||
export type InternalAnchorClass = Anchor;
|
export type InternalAnchorClass = Anchor;
|
||||||
|
|
||||||
const AnchorFC = React.forwardRef<Anchor, AnchorProps>((props, ref) => {
|
const AnchorFC = React.forwardRef<Anchor, AnchorProps>((props, ref) => {
|
||||||
const { prefixCls: customizePrefixCls } = props;
|
const { prefixCls: customizePrefixCls } = props;
|
||||||
const { getPrefixCls } = React.useContext(ConfigContext);
|
const { getPrefixCls } = React.useContext(ConfigContext);
|
||||||
|
|
||||||
const anchorPrefixCls = getPrefixCls('anchor', customizePrefixCls);
|
const anchorPrefixCls = getPrefixCls('anchor', customizePrefixCls);
|
||||||
|
return <Anchor {...props} ref={ref} anchorPrefixCls={anchorPrefixCls} />;
|
||||||
const anchorProps: InternalAnchorProps = {
|
|
||||||
...props,
|
|
||||||
|
|
||||||
anchorPrefixCls,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Anchor {...anchorProps} ref={ref} />;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default AnchorFC;
|
export default AnchorFC;
|
||||||
|
@ -14,71 +14,53 @@ export interface AnchorLinkProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnchorLink extends React.Component<AnchorLinkProps, any, AntAnchor> {
|
const AnchorLink: React.FC<AnchorLinkProps> = props => {
|
||||||
static defaultProps = {
|
const { href = '#', title, prefixCls: customizePrefixCls, children, className, target } = props;
|
||||||
href: '#',
|
|
||||||
};
|
|
||||||
|
|
||||||
static contextType = AnchorContext;
|
const context = React.useContext<AntAnchor | undefined>(AnchorContext);
|
||||||
|
|
||||||
context: AntAnchor;
|
const { registerLink, unregisterLink, scrollTo, onClick, activeLink } = context || {};
|
||||||
|
|
||||||
componentDidMount() {
|
React.useEffect(() => {
|
||||||
this.context.registerLink(this.props.href);
|
registerLink?.(href);
|
||||||
}
|
return () => {
|
||||||
|
unregisterLink?.(href);
|
||||||
|
};
|
||||||
|
}, [href]);
|
||||||
|
|
||||||
componentDidUpdate({ href: prevHref }: AnchorLinkProps) {
|
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
const { href } = this.props;
|
|
||||||
if (prevHref !== href) {
|
|
||||||
this.context.unregisterLink(prevHref);
|
|
||||||
this.context.registerLink(href);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.context.unregisterLink(this.props.href);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
||||||
const { scrollTo, onClick } = this.context;
|
|
||||||
const { href, title } = this.props;
|
|
||||||
onClick?.(e, { title, href });
|
onClick?.(e, { title, href });
|
||||||
scrollTo(href);
|
scrollTo?.(href);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderAnchorLink = ({ getPrefixCls }: ConfigConsumerProps) => {
|
return (
|
||||||
const { prefixCls: customizePrefixCls, href, title, children, className, target } = this.props;
|
<ConfigConsumer>
|
||||||
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
|
{({ getPrefixCls }: ConfigConsumerProps) => {
|
||||||
const active = this.context.activeLink === href;
|
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
|
||||||
const wrapperClassName = classNames(
|
const active = activeLink === href;
|
||||||
`${prefixCls}-link`,
|
const wrapperClassName = classNames(`${prefixCls}-link`, className, {
|
||||||
{
|
[`${prefixCls}-link-active`]: active,
|
||||||
[`${prefixCls}-link-active`]: active,
|
});
|
||||||
},
|
const titleClassName = classNames(`${prefixCls}-link-title`, {
|
||||||
className,
|
[`${prefixCls}-link-title-active`]: active,
|
||||||
);
|
});
|
||||||
const titleClassName = classNames(`${prefixCls}-link-title`, {
|
return (
|
||||||
[`${prefixCls}-link-title-active`]: active,
|
<div className={wrapperClassName}>
|
||||||
});
|
<a
|
||||||
return (
|
className={titleClassName}
|
||||||
<div className={wrapperClassName}>
|
href={href}
|
||||||
<a
|
title={typeof title === 'string' ? title : ''}
|
||||||
className={titleClassName}
|
target={target}
|
||||||
href={href}
|
onClick={handleClick}
|
||||||
title={typeof title === 'string' ? title : ''}
|
>
|
||||||
target={target}
|
{title}
|
||||||
onClick={this.handleClick}
|
</a>
|
||||||
>
|
{children}
|
||||||
{title}
|
</div>
|
||||||
</a>
|
);
|
||||||
{children}
|
}}
|
||||||
</div>
|
</ConfigConsumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
return <ConfigConsumer>{this.renderAnchorLink}</ConfigConsumer>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnchorLink;
|
export default AnchorLink;
|
||||||
|
@ -41,11 +41,17 @@ describe('Anchor Render', () => {
|
|||||||
getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList);
|
getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllTimers();
|
jest.clearAllTimers();
|
||||||
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
jest.clearAllTimers();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
getBoundingClientRectMock.mockRestore();
|
getBoundingClientRectMock.mockRestore();
|
||||||
getClientRectsMock.mockRestore();
|
getClientRectsMock.mockRestore();
|
||||||
@ -69,7 +75,7 @@ describe('Anchor Render', () => {
|
|||||||
expect(anchorInstance!.state).not.toBe(null);
|
expect(anchorInstance!.state).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Anchor render perfectly for complete href - click', () => {
|
it('Anchor render perfectly for complete href - click', async () => {
|
||||||
const hash = getHashUrl();
|
const hash = getHashUrl();
|
||||||
let anchorInstance: InternalAnchorClass;
|
let anchorInstance: InternalAnchorClass;
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@ -82,6 +88,7 @@ describe('Anchor Render', () => {
|
|||||||
</Anchor>,
|
</Anchor>,
|
||||||
);
|
);
|
||||||
fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!);
|
fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!);
|
||||||
|
await waitFakeTimer();
|
||||||
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,13 +107,12 @@ describe('Anchor Render', () => {
|
|||||||
</Anchor>,
|
</Anchor>,
|
||||||
);
|
);
|
||||||
anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1');
|
anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1');
|
||||||
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
|
|
||||||
expect(scrollToSpy).not.toHaveBeenCalled();
|
|
||||||
await waitFakeTimer();
|
await waitFakeTimer();
|
||||||
|
expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1');
|
||||||
expect(scrollToSpy).toHaveBeenCalled();
|
expect(scrollToSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Anchor render perfectly for complete href - scroll', () => {
|
it('Anchor render perfectly for complete href - scroll', async () => {
|
||||||
const hash = getHashUrl();
|
const hash = getHashUrl();
|
||||||
const root = createDiv();
|
const root = createDiv();
|
||||||
render(<div id={hash}>Hello</div>, { container: root });
|
render(<div id={hash}>Hello</div>, { container: root });
|
||||||
@ -121,6 +127,7 @@ describe('Anchor Render', () => {
|
|||||||
</Anchor>,
|
</Anchor>,
|
||||||
);
|
);
|
||||||
anchorInstance!.handleScroll();
|
anchorInstance!.handleScroll();
|
||||||
|
await waitFakeTimer();
|
||||||
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -141,10 +148,10 @@ describe('Anchor Render', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
anchorInstance!.handleScrollTo(`##${hash}`);
|
anchorInstance!.handleScrollTo(`##${hash}`);
|
||||||
|
await waitFakeTimer();
|
||||||
expect(anchorInstance!.state.activeLink).toBe(`##${hash}`);
|
expect(anchorInstance!.state.activeLink).toBe(`##${hash}`);
|
||||||
const calls = scrollToSpy.mock.calls.length;
|
const calls = scrollToSpy.mock.calls.length;
|
||||||
await waitFakeTimer();
|
expect(scrollToSpy.mock.calls.length).toBe(calls);
|
||||||
expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove listener when unmount', async () => {
|
it('should remove listener when unmount', async () => {
|
||||||
@ -373,7 +380,7 @@ describe('Anchor Render', () => {
|
|||||||
|
|
||||||
it('Anchor targetOffset prop', async () => {
|
it('Anchor targetOffset prop', async () => {
|
||||||
const hash = getHashUrl();
|
const hash = getHashUrl();
|
||||||
let dateNowMock;
|
let dateNowMock: jest.SpyInstance;
|
||||||
|
|
||||||
function dataNowMockFn() {
|
function dataNowMockFn() {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
@ -438,7 +445,7 @@ describe('Anchor Render', () => {
|
|||||||
// https://github.com/ant-design/ant-design/issues/31941
|
// https://github.com/ant-design/ant-design/issues/31941
|
||||||
it('Anchor targetOffset prop when contain spaces', async () => {
|
it('Anchor targetOffset prop when contain spaces', async () => {
|
||||||
const hash = `${getHashUrl()} s p a c e s`;
|
const hash = `${getHashUrl()} s p a c e s`;
|
||||||
let dateNowMock;
|
let dateNowMock: jest.SpyInstance;
|
||||||
|
|
||||||
function dataNowMockFn() {
|
function dataNowMockFn() {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
@ -543,13 +550,9 @@ describe('Anchor Render', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('test edge case when getBoundingClientRect return zero size', async () => {
|
it('test edge case when getBoundingClientRect return zero size', async () => {
|
||||||
getBoundingClientRectMock.mockReturnValue({
|
getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000 } as DOMRect);
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
top: 1000,
|
|
||||||
} as DOMRect);
|
|
||||||
const hash = getHashUrl();
|
const hash = getHashUrl();
|
||||||
let dateNowMock;
|
let dateNowMock: jest.SpyInstance;
|
||||||
|
|
||||||
function dataNowMockFn() {
|
function dataNowMockFn() {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
@ -615,7 +618,7 @@ describe('Anchor Render', () => {
|
|||||||
|
|
||||||
it('test edge case when container is not windows', async () => {
|
it('test edge case when container is not windows', async () => {
|
||||||
const hash = getHashUrl();
|
const hash = getHashUrl();
|
||||||
let dateNowMock;
|
let dateNowMock: jest.SpyInstance;
|
||||||
|
|
||||||
function dataNowMockFn() {
|
function dataNowMockFn() {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
@ -758,5 +761,15 @@ describe('Anchor Render', () => {
|
|||||||
rerender(<Demo current={hash2} />);
|
rerender(<Demo current={hash2} />);
|
||||||
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
|
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
|
||||||
});
|
});
|
||||||
|
it('should correct render when href is null', () => {
|
||||||
|
expect(() => {
|
||||||
|
render(
|
||||||
|
<Anchor>
|
||||||
|
<Link href={null as unknown as string} title="test" />
|
||||||
|
</Anchor>,
|
||||||
|
);
|
||||||
|
fireEvent.scroll(window || document);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { AntAnchor } from './Anchor';
|
import type { AntAnchor } from './Anchor';
|
||||||
|
|
||||||
const AnchorContext = React.createContext<AntAnchor>(null as any);
|
const AnchorContext = React.createContext<AntAnchor | undefined>(undefined);
|
||||||
|
|
||||||
export default AnchorContext;
|
export default AnchorContext;
|
||||||
|
Loading…
Reference in New Issue
Block a user