feat: Anchor horizontal (#39372)

* feat: AnchorInk

* feat: update

* feat: update ink

* test: add test

* feat: handle with nested link

* feat: improve

* fix: lint

* feat: get direction from context in AnchorLink

* docs: update demo

* test: update snapshot

* test: update snapshot

* test: update snapshot

* test: update test cases

* test: update snapshot

* test: update snapshots

* test: update snapshots

* test: update test cases

* test: update test cases

* docs: update

* test: improve

* docs: update demo

* doc: update demos

* doc: update demos again

* feat: use scroll-into-view-if-needed

* fix: hide  scrollbar

* docs: update demos

* fix: active transition for horizontal anchor

* chore: fix terser (#39617)

* chore: fix terser

* chore: clean up

* test: update snapshot

* feat: improve code style

* feat: anchor ink improvement

* feat: improve code style

* fix: lint issue

* test: update snapshots

* docs: simplified the demo

* feat: Merge the AnchorInk component back into the Anchor component

* feat: Adjust DOM order

* test: update snapshots

* docs: Improve the document

* feat: simplify css classnames

* test: update snapshots

Co-authored-by: 二货爱吃白萝卜 <smith3816@gmail.com>
This commit is contained in:
Yuki Zhang 2022-12-27 17:14:35 +08:00 committed by GitHub
parent 00655846d7
commit 4eff22497d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1282 additions and 459 deletions

View File

@ -1,6 +1,8 @@
import classNames from 'classnames';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import * as React from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import Affix from '../affix';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
@ -73,6 +75,7 @@ export interface AnchorProps {
/** Listening event when scrolling change active link */
onChange?: (currentActiveLink: string) => void;
items?: AnchorLinkItemProps[];
direction?: AnchorDirection;
}
interface InternalAnchorProps extends AnchorProps {
@ -91,6 +94,8 @@ export interface AnchorDefaultProps extends AnchorProps {
getContainer: () => AnchorContainer;
}
export type AnchorDirection = 'vertical' | 'horizontal';
export interface AntAnchor {
registerLink: (link: string) => void;
unregisterLink: (link: string) => void;
@ -100,6 +105,7 @@ export interface AntAnchor {
e: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
link: { title: React.ReactNode; href: string },
) => void;
direction: AnchorDirection;
}
const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
@ -113,6 +119,7 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
showInkInFixed = false,
children,
items,
direction: anchorDirection = 'vertical',
bounds,
targetOffset,
onClick,
@ -126,6 +133,14 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
warning(!children, 'Anchor', '`Anchor children` is deprecated. Please use `items` instead.');
}
if (process.env.NODE_ENV !== 'production') {
warning(
!(anchorDirection === 'horizontal' && items?.some((n) => 'children' in n)),
'Anchor',
'`Anchor items#children` is not supported when `Anchor` direction is horizontal.',
);
}
const [links, setLinks] = React.useState<string[]>([]);
const [activeLink, setActiveLink] = React.useState<string | null>(null);
const activeLinkRef = React.useRef<string | null>(activeLink);
@ -163,8 +178,17 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
`.${prefixCls}-link-title-active`,
);
if (linkNode && spanLinkNode.current) {
spanLinkNode.current.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
spanLinkNode.current.style.height = `${linkNode.clientHeight}px`;
if (anchorDirection !== 'horizontal') {
spanLinkNode.current.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
spanLinkNode.current.style.height = `${linkNode.clientHeight}px`;
} else {
spanLinkNode.current.style.left = `${linkNode.offsetLeft}px`;
spanLinkNode.current.style.width = `${linkNode.clientWidth}px`;
scrollIntoView(linkNode, {
scrollMode: 'if-needed',
block: 'nearest',
});
}
}
};
@ -250,17 +274,11 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
[targetOffset, offsetTop],
);
const inkClass = classNames(
{
[`${prefixCls}-ink-ball-visible`]: activeLink,
},
`${prefixCls}-ink-ball`,
);
const wrapperClass = classNames(
rootClassName,
`${prefixCls}-wrapper`,
{
[`${prefixCls}-wrapper-horizontal`]: anchorDirection === 'horizontal',
[`${prefixCls}-rtl`]: direction === 'rtl',
},
className,
@ -270,6 +288,10 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
[`${prefixCls}-fixed`]: !affix && !showInkInFixed,
});
const inkClass = classNames(`${prefixCls}-ink`, {
[`${prefixCls}-ink-visible`]: activeLink,
});
const wrapperStyle: React.CSSProperties = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...style,
@ -279,7 +301,7 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
Array.isArray(options)
? options.map((item) => (
<AnchorLink {...item} key={item.key}>
{createNestedLink(item.children)}
{anchorDirection === 'vertical' && createNestedLink(item.children)}
</AnchorLink>
))
: null;
@ -287,9 +309,7 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
const anchorContent = (
<div ref={wrapperRef} className={wrapperClass} style={wrapperStyle}>
<div className={anchorClass}>
<div className={`${prefixCls}-ink`}>
<span className={inkClass} ref={spanLinkNode} />
</div>
<span className={inkClass} ref={spanLinkNode} />
{'items' in props ? createNestedLink(items) : children}
</div>
</div>
@ -312,7 +332,7 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
React.useEffect(() => {
updateInk();
}, [getCurrentAnchor, dependencyListItem, activeLink]);
}, [anchorDirection, getCurrentAnchor, dependencyListItem, activeLink]);
const memoizedContextValue = React.useMemo<AntAnchor>(
() => ({
@ -321,8 +341,9 @@ const AnchorContent: React.FC<InternalAnchorProps> = (props) => {
scrollTo: handleScrollTo,
activeLink,
onClick,
direction: anchorDirection,
}),
[activeLink, onClick, handleScrollTo],
[activeLink, onClick, handleScrollTo, anchorDirection],
);
return (

View File

@ -1,7 +1,8 @@
import classNames from 'classnames';
import * as React from 'react';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import { ConfigConsumer } from '../config-provider';
import warning from '../_util/warning';
import type { AntAnchor } from './Anchor';
import AnchorContext from './context';
@ -22,7 +23,7 @@ const AnchorLink: React.FC<AnchorLinkProps> = (props) => {
const context = React.useContext<AntAnchor | undefined>(AnchorContext);
const { registerLink, unregisterLink, scrollTo, onClick, activeLink } = context || {};
const { registerLink, unregisterLink, scrollTo, onClick, activeLink, direction } = context || {};
React.useEffect(() => {
registerLink?.(href);
@ -36,31 +37,42 @@ const AnchorLink: React.FC<AnchorLinkProps> = (props) => {
scrollTo?.(href);
};
const { getPrefixCls } = React.useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const wrapperClassName = classNames(`${prefixCls}-link`, className, {
[`${prefixCls}-link-active`]: activeLink === href,
});
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: activeLink === href,
});
// =================== Warning =====================
if (process.env.NODE_ENV !== 'production') {
warning(
!children || direction !== 'horizontal',
'Anchor.Link',
'`Anchor.Link children` is not supported when `Anchor` direction is horizontal',
);
}
return (
<div className={wrapperClassName}>
<a
className={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={handleClick}
>
{title}
</a>
{children}
</div>
<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>
{direction !== 'horizontal' ? children : null}
</div>
);
}}
</ConfigConsumer>
);
};

View File

@ -1,4 +1,7 @@
import React from 'react';
import { resetWarned } from 'rc-util/lib/warning';
import scrollIntoView from 'scroll-into-view-if-needed';
import Anchor from '..';
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
@ -13,12 +16,15 @@ function createDiv() {
let idCounter = 0;
const getHashUrl = () => `Anchor-API-${idCounter++}`;
jest.mock('scroll-into-view-if-needed', () => jest.fn());
describe('Anchor Render', () => {
const getBoundingClientRectMock = jest.spyOn(
HTMLHeadingElement.prototype,
'getBoundingClientRect',
);
const getClientRectsMock = jest.spyOn(HTMLHeadingElement.prototype, 'getClientRects');
const scrollIntoViewMock = jest.createMockFromModule<any>('scroll-into-view-if-needed');
beforeAll(() => {
jest.useFakeTimers();
@ -27,11 +33,12 @@ describe('Anchor Render', () => {
height: 100,
top: 1000,
} as DOMRect);
getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList);
getClientRectsMock.mockReturnValue([1] as unknown as DOMRectList);
});
beforeEach(() => {
jest.useFakeTimers();
scrollIntoViewMock.mockReset();
});
afterEach(() => {
@ -46,22 +53,133 @@ describe('Anchor Render', () => {
getClientRectsMock.mockRestore();
});
it('renders correctly', () => {
const hash = getHashUrl();
const { container } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
it('renders items correctly', () => {
const { container, asFragment } = render(
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
children: [
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
],
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(5);
const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!).map((n) =>
(n as HTMLElement).querySelector('.ant-anchor-link-title'),
);
expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic');
expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static');
expect((linkTitles[3] as HTMLAnchorElement).href).toContain('#api');
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#anchor-props');
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#link-props');
expect(asFragment().firstChild).toMatchSnapshot();
});
it('renders items correctly#horizontal', () => {
const { container, asFragment } = render(
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(3);
const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!).map((n) =>
(n as HTMLElement).querySelector('.ant-anchor-link-title'),
);
expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic');
expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static');
expect((linkTitles[3] as HTMLAnchorElement).href).toContain('#api');
expect(asFragment().firstChild).toMatchSnapshot();
});
it('render items and ignore jsx children', () => {
const { container, asFragment } = render(
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
]}
>
<Link href="#api" title="API" />
</Anchor>,
);
expect(container.querySelector(`a[href="#${hash}"]`)).not.toBe(null);
expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(1);
expect(
(container.querySelector('.ant-anchor .ant-anchor-link-title') as HTMLAnchorElement).href,
).toContain('#components-anchor-demo-basic');
expect(asFragment().firstChild).toMatchSnapshot();
});
it('actives the target when clicking a link', async () => {
const hash = getHashUrl();
const { container } = render(
<Anchor prefixCls="ant-anchor">
<Link href={`http://www.example.com/#${hash}`} title={hash} />
</Anchor>,
<Anchor
prefixCls="ant-anchor"
direction="horizontal"
items={[
{
key: hash,
title: hash,
href: `http://www.example.com/#${hash}`,
},
]}
/>,
);
const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!;
fireEvent.click(link);
@ -74,9 +192,7 @@ describe('Anchor Render', () => {
const scrollToSpy = jest.spyOn(window, 'scrollTo');
render(<div id="/faq?locale=en#Q1">Q1</div>, { container: root });
const { container } = render(
<Anchor>
<Link href="/#/faq?locale=en#Q1" title="Q1" />
</Anchor>,
<Anchor items={[{ key: 'Q1', title: 'Q1', href: '/#/faq?locale=en#Q1' }]} />,
);
const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!;
fireEvent.click(link);
@ -97,10 +213,13 @@ describe('Anchor Render', () => {
{ container: root },
);
const { container } = render(
<Anchor onChange={onChange}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
<Anchor
onChange={onChange}
items={[
{ key: hash1, href: `#${hash1}`, title: hash1 },
{ key: hash2, href: `#${hash2}`, title: hash2 },
]}
/>,
);
onChange.mockClear();
@ -119,9 +238,7 @@ describe('Anchor Render', () => {
it('should update DOM when children are unmounted', () => {
const hash = getHashUrl();
const { container, rerender } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
<Anchor items={[{ key: hash, href: `#${hash}`, title: hash }]} />,
);
expect(container.querySelectorAll('.ant-anchor-link-title')).toHaveLength(1);
@ -134,11 +251,7 @@ describe('Anchor Render', () => {
it('should update DOM when link href is changed', async () => {
const hash = getHashUrl();
function AnchorUpdate({ href }: { href: string }) {
return (
<Anchor>
<Link href={href} title={hash} />
</Anchor>
);
return <Anchor items={[{ key: hash, href, title: hash }]} />;
}
const { container, rerender } = render(<AnchorUpdate href={`#${hash}`} />);
@ -154,17 +267,11 @@ describe('Anchor Render', () => {
const root = createDiv();
render(<h1 id={hash}>Hello</h1>, { container: root });
const { container, rerender } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
<Anchor items={[{ key: hash, href: `#${hash}`, title: hash }]} />,
);
const setProps = (props: Record<string, any>) =>
rerender(
<Anchor {...props}>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
);
rerender(<Anchor {...props} items={[{ key: hash, href: `#${hash}`, title: hash }]} />);
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
await waitFakeTimer();
@ -191,17 +298,11 @@ describe('Anchor Render', () => {
const root = createDiv();
render(<h1 id={hash}>Hello</h1>, { container: root });
const { container, rerender } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
<Anchor items={[{ key: hash, href: `#${hash}`, title: hash }]} />,
);
const setProps = (props: Record<string, any>) =>
rerender(
<Anchor {...props}>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
);
rerender(<Anchor {...props} items={[{ key: hash, href: `#${hash}`, title: hash }]} />);
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
await waitFakeTimer();
@ -233,9 +334,7 @@ describe('Anchor Render', () => {
const href = `#${hash}`;
const title = hash;
const { container } = render(
<Anchor onClick={handleClick}>
<Link href={href} title={title} />
</Anchor>,
<Anchor onClick={handleClick} items={[{ key: hash, href, title }]} />,
);
fireEvent.click(container.querySelector(`a[href="${href}"]`)!);
@ -248,10 +347,21 @@ describe('Anchor Render', () => {
const hash2 = getHashUrl();
const onChange = jest.fn();
const { container } = render(
<Anchor onChange={onChange}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
<Anchor
onChange={onChange}
items={[
{
key: hash1,
href: `#${hash1}`,
title: hash1,
},
{
key: hash2,
href: `#${hash2}`,
title: hash2,
},
]}
/>,
// https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0
// @ts-ignore
{ legacyRoot: true },
@ -265,9 +375,7 @@ describe('Anchor Render', () => {
it('handles invalid hash correctly', () => {
const { container } = render(
<Anchor>
<Link href="notexsited" title="title" />
</Anchor>,
<Anchor items={[{ key: 'title', href: 'notexsited', title: 'title' }]} />,
);
const link = container.querySelector(`a[href="notexsited"]`)!;
@ -357,10 +465,13 @@ describe('Anchor Render', () => {
const hash2 = getHashUrl();
const getCurrentAnchor = () => `#${hash2}`;
const { container } = render(
<Anchor getCurrentAnchor={getCurrentAnchor}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
<Anchor
getCurrentAnchor={getCurrentAnchor}
items={[
{ key: hash1, href: `#${hash1}`, title: hash1 },
{ key: hash2, href: `#${hash2}`, title: hash2 },
]}
/>,
);
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2);
@ -372,10 +483,14 @@ describe('Anchor Render', () => {
const hash2 = getHashUrl();
const onChange = jest.fn();
const { container } = render(
<Anchor onChange={onChange} getCurrentAnchor={() => hash1}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
<Anchor
onChange={onChange}
getCurrentAnchor={() => hash1}
items={[
{ key: hash1, href: `#${hash1}`, title: hash1 },
{ key: hash2, href: `#${hash2}`, title: hash2 },
]}
/>,
// https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0
// @ts-ignore
{ legacyRoot: true },
@ -393,10 +508,13 @@ describe('Anchor Render', () => {
const hash2 = getHashUrl();
const getCurrentAnchor = jest.fn();
const { container } = render(
<Anchor getCurrentAnchor={getCurrentAnchor}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
<Anchor
getCurrentAnchor={getCurrentAnchor}
items={[
{ key: hash1, href: `#${hash1}`, title: hash1 },
{ key: hash2, href: `#${hash2}`, title: hash2 },
]}
/>,
);
fireEvent.click(container.querySelector(`a[href="#${hash1}"]`)!);
@ -410,10 +528,13 @@ describe('Anchor Render', () => {
const hash1 = getHashUrl();
const hash2 = getHashUrl();
const Demo: React.FC<{ current: string }> = ({ current }) => (
<Anchor getCurrentAnchor={() => `#${current}`}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>
<Anchor
getCurrentAnchor={() => `#${current}`}
items={[
{ key: hash1, href: `#${hash1}`, title: hash1 },
{ key: hash2, href: `#${hash2}`, title: hash2 },
]}
/>
);
const { container, rerender } = render(<Demo current={hash1} />);
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash1);
@ -424,72 +545,348 @@ describe('Anchor Render', () => {
it('should render correctly when href is null', () => {
expect(() => {
render(
<Anchor>
<Link href={null as unknown as string} title="test" />
</Anchor>,
<Anchor items={[{ key: 'test', href: null as unknown as string, title: 'test' }]} />,
);
fireEvent.scroll(window || document);
}).not.toThrow();
});
});
it('renders items correctly', () => {
const { container, asFragment } = render(
<Anchor
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
describe('horizontal anchor', () => {
describe('scroll x', () => {
it('targetOffset horizontal', async () => {
const hash = getHashUrl();
const scrollToSpy = jest.spyOn(window, 'scrollTo');
const root = createDiv();
render(<h1 id={hash}>Hello</h1>, { container: root });
const { container, rerender } = render(
<Anchor
direction="horizontal"
items={[
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
children: [
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
key: hash,
href: `#${hash}`,
title: hash,
},
],
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(5);
const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!)
.slice(1)
.map((n) => (n as HTMLElement).querySelector('.ant-anchor-link-title'));
expect((linkTitles[0] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic');
expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static');
expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#api');
expect(asFragment().firstChild).toMatchSnapshot();
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#anchor-props');
expect(
(
container.querySelector(
'.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link .ant-anchor-link-title',
) as HTMLAnchorElement
)?.href,
).toContain('#link-props');
]}
/>,
);
const setProps = (props: Record<string, any>) =>
rerender(
<Anchor
{...props}
direction="horizontal"
items={[
{
key: hash,
href: `#${hash}`,
title: hash,
},
]}
/>,
);
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
await waitFakeTimer();
expect(scrollIntoView).toHaveBeenCalled();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000);
setProps({ offsetTop: 100 });
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900);
setProps({ targetOffset: 200 });
fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800);
});
});
it('test direction prop', () => {
const { container } = render(
<Anchor
direction="horizontal"
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor-ink').length).toBe(1);
expect(
container
.querySelector('.ant-anchor-wrapper')
?.classList.contains('ant-anchor-wrapper-horizontal'),
).toBeTruthy();
});
it('nested children via items should be filtered out when direction is horizontal', () => {
const { container } = render(
<Anchor
direction="horizontal"
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
{
key: '5',
href: '#link-props',
title: 'Link Props',
},
],
},
]}
/>,
);
expect(container.querySelectorAll('.ant-anchor-link').length).toBe(3);
});
it('nested children via jsx should be filtered out when direction is horizontal', () => {
const { container } = render(
<Anchor direction="horizontal">
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
<Link href="#link-props" title="Link Props" />
</Link>
</Anchor>,
);
expect(container.querySelectorAll('.ant-anchor-link').length).toBe(3);
});
});
describe('deprecated/legacy jsx syntax', () => {
it('renders jsx correctly', () => {
const hash = getHashUrl();
const { container } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
);
expect(container.querySelector(`a[href="#${hash}"]`)).not.toBe(null);
});
it('actives the target when clicking a link', async () => {
const hash = getHashUrl();
const { container } = render(
<Anchor prefixCls="ant-anchor">
<Link href={`http://www.example.com/#${hash}`} title={hash} />
</Anchor>,
);
const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!;
fireEvent.click(link);
await waitFakeTimer();
expect(link.classList).toContain('ant-anchor-link-title-active');
});
it('scrolls the page when clicking a link', async () => {
const root = createDiv();
const scrollToSpy = jest.spyOn(window, 'scrollTo');
render(<div id="/faq?locale=en#Q1">Q1</div>, { container: root });
const { container } = render(
<Anchor>
<Link href="/#/faq?locale=en#Q1" title="Q1" />
</Anchor>,
);
const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!;
fireEvent.click(link);
await waitFakeTimer();
expect(scrollToSpy).toHaveBeenCalled();
});
it('handleScroll should not be triggered when scrolling caused by clicking a link', async () => {
const hash1 = getHashUrl();
const hash2 = getHashUrl();
const root = createDiv();
const onChange = jest.fn();
render(
<div>
<div id={hash1}>Hello</div>
<div id={hash2}>World</div>
</div>,
{ container: root },
);
const { container } = render(
<Anchor onChange={onChange}>
<Link href={`#${hash1}`} title={hash1} />
<Link href={`#${hash2}`} title={hash2} />
</Anchor>,
);
onChange.mockClear();
const link = container.querySelector(`a[href="#${hash2}"]`)!;
// this will trigger 1 onChange
fireEvent.click(link);
// smooth scroll caused by clicking needs time to finish.
// we scroll the window before it finish, the scroll listener should not be triggered,
fireEvent.scroll(window);
await waitFakeTimer();
// if the scroll listener is triggered, we will get 2 onChange, now we expect only 1.
expect(onChange).toHaveBeenCalledTimes(1);
});
it('should update DOM when children are unmounted', () => {
const hash = getHashUrl();
const { container, rerender } = render(
<Anchor>
<Link href={`#${hash}`} title={hash} />
</Anchor>,
);
expect(container.querySelectorAll('.ant-anchor-link-title')).toHaveLength(1);
expect(container.querySelector('.ant-anchor-link-title')).toHaveAttribute('href', `#${hash}`);
rerender(<Anchor />);
expect(container.querySelector('.ant-anchor-link-title')).toBeFalsy();
});
it('should update DOM when link href is changed', async () => {
const hash = getHashUrl();
function AnchorUpdate({ href }: { href: string }) {
return (
<Anchor>
<Link href={href} title={hash} />
</Anchor>
);
}
const { container, rerender } = render(<AnchorUpdate href={`#${hash}`} />);
expect(container.querySelector(`a[href="#${hash}"]`)).toBeTruthy();
rerender(<AnchorUpdate href={`#${hash}_1`} />);
expect(container.querySelector(`a[href="#${hash}_1"]`)).toBeTruthy();
});
it('handles invalid hash correctly', () => {
const { container } = render(
<Anchor>
<Link href="notexsited" title="title" />
</Anchor>,
);
const link = container.querySelector(`a[href="notexsited"]`)!;
fireEvent.click(link);
expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe('title');
});
});
describe('warning', () => {
let errSpy: jest.SpyInstance;
beforeEach(() => {
resetWarned();
errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
errSpy.mockRestore();
});
it('warning nested children when direction is horizontal ', () => {
render(
<Anchor
direction="horizontal"
items={[
{
key: '1',
href: '#components-anchor-demo-basic',
title: 'Item Basic Demo',
},
{
key: '2',
href: '#components-anchor-demo-static',
title: 'Static demo',
},
{
key: '3',
href: '#api',
title: 'API',
children: [
{
key: '4',
href: '#anchor-props',
title: 'Anchor Props',
},
],
},
]}
/>,
);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Anchor] `Anchor items#children` is not supported when `Anchor` direction is horizontal.',
);
});
it('deprecated jsx style', () => {
render(
<Anchor direction="horizontal">
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
</Anchor>,
);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.',
);
});
it('deprecated jsx style for direction#vertical', () => {
render(
<Anchor>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
</Anchor>,
);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.',
);
});
it('deprecated jsx style for direction#vertical 1: with nested children', () => {
render(
<Anchor direction="horizontal">
<Link href="#api" title="API">
<Link href="#anchor-props" title="Anchor Props" />
</Link>
</Anchor>,
);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.',
);
expect(errSpy).toHaveBeenCalledWith(
'Warning: [antd: Anchor.Link] `Anchor.Link children` is not supported when `Anchor` direction is horizontal',
);
});
});
});

View File

@ -1,5 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Anchor Render render items and ignore jsx children 1`] = `
<div>
<div
class=""
>
<div
class="ant-anchor-wrapper"
style="max-height: 100vh;"
>
<div
class="ant-anchor"
>
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Item Basic Demo"
>
Item Basic Demo
</a>
</div>
</div>
</div>
</div>
</div>
`;
exports[`Anchor Render renders items correctly 1`] = `
<div>
<div
@ -12,13 +44,9 @@ exports[`Anchor Render renders items correctly 1`] = `
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -79,3 +107,57 @@ exports[`Anchor Render renders items correctly 1`] = `
</div>
</div>
`;
exports[`Anchor Render renders items correctly#horizontal 1`] = `
<div>
<div
class=""
>
<div
class="ant-anchor-wrapper"
style="max-height: 100vh;"
>
<div
class="ant-anchor"
>
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Item Basic Demo"
>
Item Basic Demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -34,13 +34,9 @@ exports[`renders ./components/anchor/demo/basic.tsx extend context correctly 1`]
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -90,13 +86,9 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx extend context
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -156,82 +148,191 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx extend context
</div>
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx extend context correctly 1`] = `
<div>
exports[`renders ./components/anchor/demo/horizontal.tsx extend context correctly 1`] = `
Array [
<div
class=""
style="padding:20px"
>
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div>
<div
class="ant-anchor"
class=""
>
<div
class="ant-anchor-ink"
class="ant-anchor-wrapper ant-anchor-wrapper-horizontal"
style="max-height:100vh"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
class="ant-anchor"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
<a
class="ant-anchor-link-title"
href="#part-1"
title="Part 1"
>
Part 1
</a>
</div>
<div
class="ant-anchor-link"
>
Link Props
</a>
<a
class="ant-anchor-link-title"
href="#part-2"
title="Part 2"
>
Part 2
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-3"
title="Part 3"
>
Part 3
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-4"
title="Part 4"
>
Part 4
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-5"
title="Part 5"
>
Part 5
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-6"
title="Part 6"
>
Part 6
</a>
</div>
</div>
</div>
</div>
</div>
</div>,
<div>
<div
id="part-1"
style="width:100vw;height:100vh;text-align:center;background:rgba(0,255,0,0.02)"
/>
<div
id="part-2"
style="width:100vw;height:100vh;text-align:center;background:rgba(0,0,255,0.02)"
/>
<div
id="part-3"
style="width:100vw;height:100vh;text-align:center;background:#FFFBE9"
/>
<div
id="part-4"
style="width:100vw;height:100vh;text-align:center;background:#F4EAD5"
/>
<div
id="part-5"
style="width:100vw;height:100vh;text-align:center;background:#DAE2B6"
/>
<div
id="part-6"
style="width:100vw;height:100vh;text-align:center;background:#CCD6A6"
/>
</div>,
]
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx extend context correctly 1`] = `
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div
class="ant-anchor ant-anchor-fixed"
>
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
>
Link Props
</a>
</div>
</div>
</div>
</div>
`;
@ -244,13 +345,9 @@ exports[`renders ./components/anchor/demo/onChange.tsx extend context correctly
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -318,13 +415,9 @@ exports[`renders ./components/anchor/demo/onClick.tsx extend context correctly 1
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -392,13 +485,9 @@ exports[`renders ./components/anchor/demo/static.tsx extend context correctly 1`
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -499,13 +588,9 @@ exports[`renders ./components/anchor/demo/targetOffset.tsx extend context correc
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>

View File

@ -34,13 +34,9 @@ exports[`renders ./components/anchor/demo/basic.tsx correctly 1`] = `
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -90,13 +86,9 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx correctly 1`] =
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -156,82 +148,191 @@ exports[`renders ./components/anchor/demo/customizeHighlight.tsx correctly 1`] =
</div>
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx correctly 1`] = `
<div>
exports[`renders ./components/anchor/demo/horizontal.tsx correctly 1`] = `
Array [
<div
class=""
style="padding:20px"
>
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div>
<div
class="ant-anchor"
class=""
>
<div
class="ant-anchor-ink"
class="ant-anchor-wrapper ant-anchor-wrapper-horizontal"
style="max-height:100vh"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
class="ant-anchor"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
<a
class="ant-anchor-link-title"
href="#part-1"
title="Part 1"
>
Part 1
</a>
</div>
<div
class="ant-anchor-link"
>
Link Props
</a>
<a
class="ant-anchor-link-title"
href="#part-2"
title="Part 2"
>
Part 2
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-3"
title="Part 3"
>
Part 3
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-4"
title="Part 4"
>
Part 4
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-5"
title="Part 5"
>
Part 5
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#part-6"
title="Part 6"
>
Part 6
</a>
</div>
</div>
</div>
</div>
</div>
</div>,
<div>
<div
id="part-1"
style="width:100vw;height:100vh;text-align:center;background:rgba(0,255,0,0.02)"
/>
<div
id="part-2"
style="width:100vw;height:100vh;text-align:center;background:rgba(0,0,255,0.02)"
/>
<div
id="part-3"
style="width:100vw;height:100vh;text-align:center;background:#FFFBE9"
/>
<div
id="part-4"
style="width:100vw;height:100vh;text-align:center;background:#F4EAD5"
/>
<div
id="part-5"
style="width:100vw;height:100vh;text-align:center;background:#DAE2B6"
/>
<div
id="part-6"
style="width:100vw;height:100vh;text-align:center;background:#CCD6A6"
/>
</div>,
]
`;
exports[`renders ./components/anchor/demo/legacy-anchor.tsx correctly 1`] = `
<div
class="ant-anchor-wrapper"
style="max-height:100vh"
>
<div
class="ant-anchor ant-anchor-fixed"
>
<span
class="ant-anchor-ink"
/>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-basic"
title="Basic demo"
>
Basic demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#components-anchor-demo-static"
title="Static demo"
>
Static demo
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#api"
title="API"
>
API
</a>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#anchor-props"
title="Anchor Props"
>
Anchor Props
</a>
</div>
<div
class="ant-anchor-link"
>
<a
class="ant-anchor-link-title"
href="#link-props"
title="Link Props"
>
Link Props
</a>
</div>
</div>
</div>
</div>
`;
@ -244,13 +345,9 @@ exports[`renders ./components/anchor/demo/onChange.tsx correctly 1`] = `
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -318,13 +415,9 @@ exports[`renders ./components/anchor/demo/onClick.tsx correctly 1`] = `
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -392,13 +485,9 @@ exports[`renders ./components/anchor/demo/static.tsx correctly 1`] = `
<div
class="ant-anchor ant-anchor-fixed"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -499,13 +588,9 @@ exports[`renders ./components/anchor/demo/targetOffset.tsx correctly 1`] = `
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>

View File

@ -0,0 +1,7 @@
## zh-CN
横向 Anchor。
## en-US
Horizontally aligned anchors

View File

@ -0,0 +1,82 @@
import React from 'react';
import { Anchor } from 'antd';
const App: React.FC = () => (
<>
<div style={{ padding: '20px' }}>
<Anchor
direction="horizontal"
items={[
{
key: 'part-1',
href: '#part-1',
title: 'Part 1',
},
{
key: 'part-2',
href: '#part-2',
title: 'Part 2',
},
{
key: 'part-3',
href: '#part-3',
title: 'Part 3',
},
{
key: 'part-4',
href: '#part-4',
title: 'Part 4',
},
{
key: 'part-5',
href: '#part-5',
title: 'Part 5',
},
{
key: 'part-6',
href: '#part-6',
title: 'Part 6',
},
]}
/>
</div>
<div>
<div
id="part-1"
style={{
width: '100vw',
height: '100vh',
textAlign: 'center',
background: 'rgba(0,255,0,0.02)',
}}
/>
<div
id="part-2"
style={{
width: '100vw',
height: '100vh',
textAlign: 'center',
background: 'rgba(0,0,255,0.02)',
}}
/>
<div
id="part-3"
style={{ width: '100vw', height: '100vh', textAlign: 'center', background: '#FFFBE9' }}
/>
<div
id="part-4"
style={{ width: '100vw', height: '100vh', textAlign: 'center', background: '#F4EAD5' }}
/>
<div
id="part-5"
style={{ width: '100vw', height: '100vh', textAlign: 'center', background: '#DAE2B6' }}
/>
<div
id="part-6"
style={{ width: '100vw', height: '100vh', textAlign: 'center', background: '#CCD6A6' }}
/>
</div>
</>
);
export default App;

View File

@ -4,7 +4,7 @@ import { Anchor } from 'antd';
const { Link } = Anchor;
const App: React.FC = () => (
<Anchor>
<Anchor affix={false}>
<Link href="#components-anchor-demo-basic" title="Basic demo" />
<Link href="#components-anchor-demo-static" title="Static demo" />
<Link href="#api" title="API">

View File

@ -3,7 +3,6 @@ category: Components
title: Anchor
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: Navigation
order: 3
@ -23,7 +22,8 @@ For displaying anchor hyperlinks on page and jumping between them.
<!-- prettier-ignore -->
<code src="./demo/basic.tsx" iframe="200">Basic</code>
<code src="./demo/static.tsx">Static Anchor</code>
<code src="./demo/horizontal.tsx" iframe="200">Horizontal Anchor</code>
<code src="./demo/static.tsx" >Static Anchor</code>
<code src="./demo/onClick.tsx">Customize the onClick event</code>
<code src="./demo/customizeHighlight.tsx">Customize the anchor highlight</code>
<code src="./demo/targetOffset.tsx" iframe="200">Set Anchor scroll offset</code>
@ -45,10 +45,23 @@ For displaying anchor hyperlinks on page and jumping between them.
| targetOffset | Anchor scroll offset, default as `offsetTop`, [example](#components-anchor-demo-targetoffset) | number | - | |
| onChange | Listening for anchor link change | (currentActiveLink: string) => void | | |
| onClick | Set the handler to handle `click` event | (e: MouseEvent, link: object) => void | - | |
| items | Data configuration option content, support nesting through children | { href, title, target, children }\[] | - | |
| items | Data configuration option content, support nesting through children | { key, href, title, target, children }\[] [see](#anchoritem) | - | 5.1.0 |
| direction | Set Anchor direction | `vertical` \| `horizontal` | `vertical` | 5.2.0 |
### AnchorItem
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| key | The unique identifier of the Anchor Link | string \| number | - | |
| href | The target of hyperlink | string | | |
| target | Specifies where to display the linked URL | string | | |
| title | The content of hyperlink | ReactNode | | |
| children | Nested Anchor Link, `Attention: This attribute does not support horizontal orientation` | [AnchorItem](#anchoritem)\[] | - | |
### Link Props
We recommend using the items form instead.
| Property | Description | Type | Default | Version |
| -------- | ----------------------------------------- | --------- | ------- | ------- |
| href | The target of hyperlink | string | | |

View File

@ -4,7 +4,6 @@ title: Anchor
subtitle: 锚点
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original
demo:
cols: 2
group:
title: 导航
order: 3
@ -24,6 +23,7 @@ group:
<!-- prettier-ignore -->
<code src="./demo/basic.tsx" iframe="200">基本</code>
<code src="./demo/horizontal.tsx" iframe="200">横向 Anchor</code>
<code src="./demo/static.tsx">静态位置</code>
<code src="./demo/onClick.tsx">自定义 onClick 事件</code>
<code src="./demo/customizeHighlight.tsx">自定义锚点高亮</code>
@ -46,12 +46,25 @@ group:
| targetOffset | 锚点滚动偏移量,默认与 offsetTop 相同,[例子](#components-anchor-demo-targetoffset) | number | - | |
| onChange | 监听锚点链接改变 | (currentActiveLink: string) => void | - | |
| onClick | `click` 事件的 handler | (e: MouseEvent, link: object) => void | - | |
| items | 数据化配置选项内容,支持通过 children 嵌套 | { href, title, target, children }\[] | - | |
| items | 数据化配置选项内容,支持通过 children 嵌套 | { key, href, title, target, children }\[] [具体见](#anchoritem) | - | 5.1.0 |
| direction | 设置导航方向 | `vertical` \| `horizontal` | `vertical` | 5.2.0 |
### AnchorItem
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| key | 唯一标志 | string \| number | - | |
| href | 锚点链接 | string | - | |
| target | 该属性指定在何处显示链接的资源 | string | - | |
| title | 文字内容 | ReactNode | - | |
| children | 嵌套的 Anchor Link`注意:水平方向该属性不支持` | [AnchorItem](#anchoritem)\[] | - | |
### Link Props
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| ------ | -------------------------------- | --------- | ------ | ---- |
| href | 锚点链接 | string | - | |
| target | 该属性指定在何处显示链接的资源。 | string | - | |
| title | 文字内容 | ReactNode | - | |
建议使用 items 形式。
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| ------ | ------------------------------ | --------- | ------ | ---- |
| href | 锚点链接 | string | - | |
| target | 该属性指定在何处显示链接的资源 | string | - | |
| title | 文字内容 | ReactNode | - | |

View File

@ -16,8 +16,15 @@ interface AnchorToken extends FullToken<'Anchor'> {
// ============================== Shared ==============================
const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
const { componentCls, holderOffsetBlock, motionDurationSlow, lineWidthBold, colorPrimary } =
token;
const {
componentCls,
holderOffsetBlock,
motionDurationSlow,
lineWidthBold,
colorPrimary,
lineType,
colorSplit,
} = token;
return {
[`${componentCls}-wrapper`]: {
@ -34,40 +41,6 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
position: 'relative',
paddingInlineStart: lineWidthBold,
[`${componentCls}-ink`]: {
position: 'absolute',
insetBlockStart: 0,
insetInlineStart: 0,
height: '100%',
'&::before': {
position: 'relative',
display: 'block',
width: lineWidthBold,
height: '100%',
margin: '0 auto',
backgroundColor: token.colorSplit,
content: '" "',
},
},
[`${componentCls}-ink-ball`]: {
position: 'absolute',
left: {
_skip_check_: true,
value: 0,
},
display: 'none',
transform: 'translateY(-50%)',
transition: `top ${motionDurationSlow} ease-in-out`,
width: lineWidthBold,
backgroundColor: colorPrimary,
[`&${componentCls}-ink-ball-visible`]: {
display: 'inline-block',
},
},
[`${componentCls}-link`]: {
paddingBlock: token.anchorPaddingBlock,
paddingInline: `${token.anchorPaddingInline}px 0`,
@ -96,13 +69,94 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
},
},
[`${componentCls}-fixed ${componentCls}-ink ${componentCls}-ink-ball`]: {
[`&:not(&-horizontal)`]: {
[componentCls]: {
'&::before': {
position: 'absolute',
left: {
_skip_check_: true,
value: 0,
},
top: 0,
height: '100%',
borderInlineStart: `${lineWidthBold}px ${lineType} ${colorSplit}`,
content: '" "',
},
[`${componentCls}-ink`]: {
position: 'absolute',
left: {
_skip_check_: true,
value: 0,
},
display: 'none',
transform: 'translateY(-50%)',
transition: `top ${motionDurationSlow} ease-in-out`,
width: lineWidthBold,
backgroundColor: colorPrimary,
[`&${componentCls}-ink-visible`]: {
display: 'inline-block',
},
},
},
},
[`${componentCls}-fixed ${componentCls}-ink ${componentCls}-ink`]: {
display: 'none',
},
},
};
};
const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token;
return {
[`${componentCls}-wrapper-horizontal`]: {
position: 'relative',
'&::before': {
position: 'absolute',
left: {
_skip_check_: true,
value: 0,
},
right: {
_skip_check_: true,
value: 0,
},
bottom: 0,
borderBottom: `1px ${token.lineType} ${token.colorSplit}`,
content: '" "',
},
[componentCls]: {
overflowX: 'scroll',
position: 'relative',
display: 'flex',
scrollbarWidth: 'none' /* Firefox */,
'&::-webkit-scrollbar': {
display: 'none' /* Safari and Chrome */,
},
[`${componentCls}-link:first-of-type`]: {
paddingInline: 0,
},
[`${componentCls}-ink`]: {
position: 'absolute',
bottom: 0,
transition: `left ${motionDurationSlow} ease-in-out, width ${motionDurationSlow} ease-in-out`,
height: lineWidthBold,
backgroundColor: colorPrimary,
},
},
},
};
};
// ============================== Export ==============================
export default genComponentStyleHook('Anchor', (token) => {
const { fontSize, fontSizeLG, padding, paddingXXS } = token;
@ -115,5 +169,5 @@ export default genComponentStyleHook('Anchor', (token) => {
anchorTitleBlock: (fontSize / 14) * 3,
anchorBallSize: fontSizeLG / 2,
});
return [genSharedAnchorStyle(anchorToken)];
return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)];
});

View File

@ -138,13 +138,9 @@ exports[`ConfigProvider components Anchor configProvider 1`] = `
<div
class="config-anchor"
>
<div
<span
class="config-anchor-ink"
>
<span
class="config-anchor-ink-ball"
/>
</div>
/>
<div
class="config-anchor-link"
>
@ -174,13 +170,9 @@ exports[`ConfigProvider components Anchor configProvider componentDisabled 1`] =
<div
class="config-anchor"
>
<div
<span
class="config-anchor-ink"
>
<span
class="config-anchor-ink-ball"
/>
</div>
/>
<div
class="config-anchor-link"
>
@ -210,13 +202,9 @@ exports[`ConfigProvider components Anchor configProvider componentSize large 1`]
<div
class="config-anchor"
>
<div
<span
class="config-anchor-ink"
>
<span
class="config-anchor-ink-ball"
/>
</div>
/>
<div
class="config-anchor-link"
>
@ -246,13 +234,9 @@ exports[`ConfigProvider components Anchor configProvider componentSize middle 1`
<div
class="config-anchor"
>
<div
<span
class="config-anchor-ink"
>
<span
class="config-anchor-ink-ball"
/>
</div>
/>
<div
class="config-anchor-link"
>
@ -282,13 +266,9 @@ exports[`ConfigProvider components Anchor configProvider virtual and dropdownMat
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -318,13 +298,9 @@ exports[`ConfigProvider components Anchor normal 1`] = `
<div
class="ant-anchor"
>
<div
<span
class="ant-anchor-ink"
>
<span
class="ant-anchor-ink-ball"
/>
</div>
/>
<div
class="ant-anchor-link"
>
@ -354,13 +330,9 @@ exports[`ConfigProvider components Anchor prefixCls 1`] = `
<div
class="prefix-Anchor"
>
<div
<span
class="prefix-Anchor-ink"
>
<span
class="prefix-Anchor-ink-ball"
/>
</div>
/>
<div
class="prefix-Anchor-link"
>