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