feat(anchor): add direction action (#6447)

* refactor(anchor): direction show

* refactor(anchor): update anchor css

* feat(anchor): update demo

* test(anchor): update demo test snap

* feat(anchor): update docs

* Update index.zh-CN.md

* Update index.en-US.md

---------

Co-authored-by: tangjinzhou <415800467@qq.com>
pull/6514/head
Cherry7 2023-04-21 17:33:38 +08:00 committed by GitHub
parent de00607dc7
commit 8932aff13f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 312 additions and 98 deletions

View File

@ -1,5 +1,6 @@
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import {
watch,
defineComponent,
nextTick,
onBeforeUnmount,
@ -9,6 +10,7 @@ import {
ref,
computed,
} from 'vue';
import scrollIntoView from 'scroll-into-view-if-needed';
import classNames from '../_util/classNames';
import addEventListener from '../vc-util/Dom/addEventListener';
import Affix from '../affix';
@ -20,6 +22,8 @@ import useStyle from './style';
import type { AnchorLinkProps } from './AnchorLink';
import AnchorLink from './AnchorLink';
import type { Key } from '../_util/type';
import PropTypes from '../_util/vue-types';
import devWarning from '../vc-util/devWarning';
export interface AnchorLinkItemProps extends AnchorLinkProps {
key: Key;
@ -27,6 +31,9 @@ export interface AnchorLinkItemProps extends AnchorLinkProps {
style?: CSSProperties;
children?: AnchorLinkItemProps[];
}
export type AnchorDirection = 'vertical' | 'horizontal';
function getDefaultContainer() {
return window;
}
@ -73,6 +80,7 @@ export const anchorProps = () => ({
type: Array as PropType<AnchorLinkItemProps[]>,
default: undefined as AnchorLinkItemProps[],
},
direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'),
onChange: Function as PropType<(currentActiveLink: string) => void>,
onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>,
});
@ -93,6 +101,24 @@ export default defineComponent({
props: anchorProps(),
setup(props, { emit, attrs, slots, expose }) {
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
const anchorDirection = computed(() => props.direction ?? 'vertical');
if (process.env.NODE_ENV !== 'production') {
devWarning(
typeof slots.default !== 'function',
'Anchor',
'`Anchor children` is deprecated. Please use `items` instead.',
);
}
if (process.env.NODE_ENV !== 'production') {
devWarning(
!(anchorDirection.value === 'horizontal' && props.items?.some(n => 'children' in n)),
'Anchor',
'`Anchor items#children` is not supported when `Anchor` direction is horizontal.',
);
}
const spanLinkNode = ref<HTMLSpanElement>(null);
const anchorRef = ref();
const state = reactive<AnchorState>({
@ -184,12 +210,21 @@ export default defineComponent({
};
const updateInk = () => {
const linkNode = anchorRef.value.getElementsByClassName(
`${prefixCls.value}-link-title-active`,
)[0];
const linkNode = anchorRef.value.querySelector(`.${prefixCls.value}-link-title-active`);
if (linkNode && spanLinkNode.value) {
spanLinkNode.value.style.top = `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
spanLinkNode.value.style.height = `${linkNode.clientHeight}px`;
const horizontalAnchor = anchorDirection.value === 'horizontal';
spanLinkNode.value.style.top = horizontalAnchor
? ''
: `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
spanLinkNode.value.style.height = horizontalAnchor ? '' : `${linkNode.clientHeight}px`;
spanLinkNode.value.style.left = horizontalAnchor ? `${linkNode.offsetLeft}px` : '';
spanLinkNode.value.style.width = horizontalAnchor ? `${linkNode.clientWidth}px` : '';
if (horizontalAnchor) {
scrollIntoView(linkNode, {
scrollMode: 'if-needed',
block: 'nearest',
});
}
}
};
@ -210,6 +245,7 @@ export default defineComponent({
handleClick: (e, info) => {
emit('click', e, info);
},
direction: anchorDirection,
});
onMounted(() => {
@ -237,23 +273,31 @@ export default defineComponent({
}
updateInk();
});
watch([anchorDirection, getCurrentAnchor, state.links, activeLink], () => {
updateInk();
});
const createNestedLink = (options?: AnchorLinkItemProps[]) =>
Array.isArray(options)
? options.map(item => (
<AnchorLink {...item} key={item.key}>
{createNestedLink(item.children)}
{anchorDirection.value === 'vertical' ? createNestedLink(item.children) : null}
</AnchorLink>
))
: null;
const [wrapSSR, hashId] = useStyle(prefixCls);
return () => {
const { offsetTop, affix, showInkInFixed } = props;
const pre = prefixCls.value;
const inkClass = classNames(`${pre}-ink-ball`, {
[`${pre}-ink-ball-visible`]: activeLink.value,
const inkClass = classNames(`${pre}-ink`, {
[`${pre}-ink-visible`]: activeLink.value,
});
const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, {
[`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal',
[`${pre}-rtl`]: direction.value === 'rtl',
});
@ -268,9 +312,7 @@ export default defineComponent({
const anchorContent = (
<div class={wrapperClass} style={wrapperStyle} ref={anchorRef}>
<div class={anchorClass}>
<div class={`${pre}-ink`}>
<span class={inkClass} ref={spanLinkNode} />
</div>
<span class={inkClass} ref={spanLinkNode} />
{Array.isArray(props.items) ? createNestedLink(props.items) : slots.default?.()}
</div>
</div>

View File

@ -2,27 +2,19 @@
exports[`renders ./components/anchor/demo/basic.vue correctly 1`] = `
<div>
<div class="">
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
<div class="css-dev-only-do-not-override-1tii49m">
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#part-1" title="Part 1">Part 1</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 class="ant-anchor-link"><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="#components-anchor-demo-basic" title="Basic demo with Target" target="_blank">Basic demo with Target</a>
<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="#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>
</div>
@ -30,9 +22,8 @@ exports[`renders ./components/anchor/demo/basic.vue correctly 1`] = `
`;
exports[`renders ./components/anchor/demo/customizeHighlight.vue correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
</div>
@ -51,10 +42,41 @@ exports[`renders ./components/anchor/demo/customizeHighlight.vue correctly 1`] =
</div>
`;
exports[`renders ./components/anchor/demo/horizontal.vue correctly 1`] = `
<div>
<div>
<!---->
<div class="css-dev-only-do-not-override-1tii49m">
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper ant-anchor-wrapper-horizontal" style="max-height: 100vh;">
<div class="ant-anchor"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-1" title="Part 1">Part 1</a>
<!---->
</div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-2" title="Part 2">Part 2</a>
<!---->
</div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-3" title="Part 3">Part 3</a>
<!---->
</div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-4" title="Part 4">Part 4</a>
<!---->
</div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-5" title="Part 5">Part 5</a>
<!---->
</div>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#horizontally-part-6" title="Part 6">Part 6</a>
<!---->
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/anchor/demo/onChange.vue correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
</div>
@ -74,9 +96,8 @@ exports[`renders ./components/anchor/demo/onChange.vue correctly 1`] = `
`;
exports[`renders ./components/anchor/demo/onClick.vue correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
</div>
@ -96,9 +117,8 @@ exports[`renders ./components/anchor/demo/onClick.vue correctly 1`] = `
`;
exports[`renders ./components/anchor/demo/static.vue correctly 1`] = `
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor ant-anchor-fixed"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
</div>
@ -119,10 +139,10 @@ exports[`renders ./components/anchor/demo/static.vue correctly 1`] = `
exports[`renders ./components/anchor/demo/targetOffset.vue correctly 1`] = `
<div>
<div class="">
<div class="ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor">
<div class="ant-anchor-ink"><span class="ant-anchor-ink-ball"></span></div>
<!---->
<div class="css-dev-only-do-not-override-1tii49m">
<div class="css-dev-only-do-not-override-1tii49m ant-anchor-wrapper" style="max-height: 100vh;">
<div class="ant-anchor"><span class="ant-anchor-ink"></span>
<div class="ant-anchor-link"><a class="ant-anchor-link-title" href="#components-anchor-demo-basic" title="Basic demo">Basic demo</a>
<!---->
</div>

View File

@ -1,4 +1,5 @@
import type { Ref, InjectionKey } from 'vue';
import type { Ref, InjectionKey, ComputedRef } from 'vue';
import type { AnchorDirection } from './Anchor';
import { computed, inject, provide } from 'vue';
export interface AnchorContext {
@ -7,6 +8,7 @@ export interface AnchorContext {
activeLink: Ref<string>;
scrollTo: (link: string) => void;
handleClick: (e: Event, info: { title: any; href: string }) => void;
direction: ComputedRef<AnchorDirection>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -25,6 +27,7 @@ const useInjectAnchor = () => {
scrollTo: noop,
activeLink: computed(() => ''),
handleClick: noop,
direction: computed(() => 'vertical'),
} as AnchorContext);
};

View File

@ -16,17 +16,23 @@ The simplest usage.
</docs>
<template>
<a-anchor>
<a-anchor-link href="#components-anchor-demo-basic" title="Basic demo" />
<a-anchor-link href="#components-anchor-demo-static" title="Static demo" />
<a-anchor-link
href="#components-anchor-demo-basic"
title="Basic demo with Target"
target="_blank"
/>
<a-anchor-link href="#API" title="API">
<a-anchor-link href="#Anchor-Props" title="Anchor Props" />
<a-anchor-link href="#Link-Props" title="Link Props" />
</a-anchor-link>
</a-anchor>
<a-anchor
: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',
},
]"
/>
</template>

View File

@ -0,0 +1,64 @@
<docs>
---
order: 1
title:
zh-CN: 横向 Anchor
en-US: Horizontally aligned anchors
---
## zh-CN
横向 Anchor
## en-US
Horizontally aligned anchors
</docs>
<template>
<div
style="
{
padding: '20px';
}
"
>
<a-anchor
direction="horizontal"
:items="[
{
key: 'horizontally-part-1',
href: '#horizontally-part-1',
title: 'Part 1',
},
{
key: 'horizontally-part-2',
href: '#horizontally-part-2',
title: 'Part 2',
},
{
key: 'horizontally-part-3',
href: '#horizontally-part-3',
title: 'Part 3',
},
{
key: 'horizontally-part-4',
href: '#horizontally-part-4',
title: 'Part 4',
},
{
key: 'horizontally-part-5',
href: '#horizontally-part-5',
title: 'Part 5',
},
{
key: 'horizontally-part-6',
href: '#horizontally-part-6',
title: 'Part 6',
},
]"
/>
</div>
</template>

View File

@ -1,6 +1,7 @@
<template>
<demo-sort>
<basic />
<horizontal />
<static />
<on-click />
<customize-highlight />
@ -16,6 +17,7 @@ import OnClick from './onClick.vue';
import CustomizeHighlight from './customizeHighlight.vue';
import TargetOffset from './targetOffset.vue';
import OnChange from './onChange.vue';
import Horizontal from './horizontal.vue';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
import { defineComponent } from 'vue';
@ -29,6 +31,7 @@ export default defineComponent({
OnClick,
CustomizeHighlight,
TargetOffset,
Horizontal,
OnChange,
},
setup() {

View File

@ -1,6 +1,6 @@
---
category: Components
type: Other
type: Navigation
cols: 2
title: Anchor
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original
@ -29,7 +29,18 @@ For displaying anchor hyperlinks on page and jumping between them.
| targetOffset | Anchor scroll offset, default as `offsetTop`, [example](#components-anchor-demo-targetOffset) | number | `offsetTop` | 1.5.0 |
| wrapperClass | The class name of the container | string | - | |
| wrapperStyle | The style of the container | object | - | |
| items | Data configuration option content, support nesting through children | { href, title, target, children, key }\[] | - | 4.0 |
| items | Data configuration option content, support nesting through children | { key, href, title, target, children }\[] [see](#anchoritem) | - | 4.0 |
| direction | Set Anchor direction | `vertical` \| `horizontal` | `vertical` | 4.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 | VueNode \| (item: AnchorItem) => VueNode | | |
| children | Nested Anchor Link, `Attention: This attribute does not support horizontal orientation` | [AnchorItem](#anchoritem)\[] | - | |
### Events

View File

@ -2,7 +2,7 @@
category: Components
subtitle: 锚点
cols: 2
type: 其他
type: 导航
title: Anchor
cover: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*TBTSR4PyVmkAAAAAAAAAAAAADrJ8AQ/original
coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*JGb3RIzyOCkAAAAAAAAAAAAADrJ8AQ/original
@ -30,7 +30,18 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*JGb3RIzyOCkAAA
| targetOffset | 锚点滚动偏移量,默认与 offsetTop 相同,[例子](#components-anchor-demo-targetOffset) | number | `offsetTop` | 1.5.0 |
| wrapperClass | 容器的类名 | string | - | |
| wrapperStyle | 容器样式 | object | - | |
| items | 数据化配置选项内容,支持通过 children 嵌套 | { href, title, target, children, key }\[] | - | 4.0 |
| items | 数据化配置选项内容,支持通过 children 嵌套 | { key, href, title, target, children }\[] [具体见](#anchoritem) | - | 4.0 |
| direction | 设置导航方向 | `vertical` \| `horizontal` | `vertical` | 4.0 |
### AnchorItem
| 成员 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| key | 唯一标志 | string \| number | - | |
| href | 锚点链接 | string | - | |
| target | 该属性指定在何处显示链接的资源 | string | - | |
| title | 文字内容 | VueNode \| (item: AnchorItem) => VueNode | - | |
| children | 嵌套的 Anchor Link`注意:水平方向该属性不支持` | [AnchorItem](#anchoritem)\[] | - | |
### 事件

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(${componentCls}-wrapper-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)];
});