feat: anchor support cssvar
parent
3375bd4695
commit
d6ec6bf26e
|
@ -2,32 +2,31 @@ export function isWindow(obj: any): obj is Window {
|
|||
return obj !== null && obj !== undefined && obj === obj.window;
|
||||
}
|
||||
|
||||
export default function getScroll(
|
||||
target: HTMLElement | Window | Document | null,
|
||||
top: boolean,
|
||||
): number {
|
||||
const getScroll = (target: HTMLElement | Window | Document | null): number => {
|
||||
if (typeof window === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
const method = top ? 'scrollTop' : 'scrollLeft';
|
||||
let result = 0;
|
||||
if (isWindow(target)) {
|
||||
result = target[top ? 'scrollY' : 'scrollX'];
|
||||
result = target.pageYOffset;
|
||||
} else if (target instanceof Document) {
|
||||
result = target.documentElement[method];
|
||||
result = target.documentElement.scrollTop;
|
||||
} else if (target instanceof HTMLElement) {
|
||||
result = target[method];
|
||||
result = target.scrollTop;
|
||||
} else if (target) {
|
||||
// According to the type inference, the `target` is `never` type.
|
||||
// Since we configured the loose mode type checking, and supports mocking the target with such shape below::
|
||||
// `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`,
|
||||
// the program may falls into this branch.
|
||||
// Check the corresponding tests for details. Don't sure what is the real scenario this happens.
|
||||
result = target[method];
|
||||
/* biome-ignore lint/complexity/useLiteralKeys: target is a never type */ /* eslint-disable-next-line dot-notation */
|
||||
result = target['scrollTop'];
|
||||
}
|
||||
|
||||
if (target && !isWindow(target) && typeof result !== 'number') {
|
||||
result = ((target.ownerDocument ?? target) as any).documentElement?.[method];
|
||||
result = (target.ownerDocument ?? (target as Document)).documentElement?.scrollTop;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
export default getScroll;
|
||||
|
|
|
@ -14,7 +14,7 @@ interface ScrollToOptions {
|
|||
export default function scrollTo(y: number, options: ScrollToOptions = {}) {
|
||||
const { getContainer = () => window, callback, duration = 450 } = options;
|
||||
const container = getContainer();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const scrollTop = getScroll(container);
|
||||
const startTime = Date.now();
|
||||
|
||||
const frameFunc = () => {
|
||||
|
|
|
@ -23,6 +23,7 @@ import AnchorLink from './AnchorLink';
|
|||
import PropTypes from '../_util/vue-types';
|
||||
import devWarning from '../vc-util/devWarning';
|
||||
import { arrayType } from '../_util/type';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCssVarCls';
|
||||
|
||||
export type AnchorDirection = 'vertical' | 'horizontal';
|
||||
|
||||
|
@ -39,8 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
|
|||
|
||||
if (rect.width || rect.height) {
|
||||
if (container === window) {
|
||||
container = element.ownerDocument!.documentElement!;
|
||||
return rect.top - container.clientTop;
|
||||
return rect.top - element.ownerDocument!.documentElement!.clientTop;
|
||||
}
|
||||
return rect.top - (container as HTMLElement).getBoundingClientRect().top;
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ export const anchorProps = () => ({
|
|||
targetOffset: Number,
|
||||
items: arrayType<AnchorLinkItemProps[]>(),
|
||||
direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'),
|
||||
replace: Boolean,
|
||||
onChange: Function as PropType<(currentActiveLink: string) => void>,
|
||||
onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: string }) => void>,
|
||||
});
|
||||
|
@ -91,7 +92,7 @@ export default defineComponent({
|
|||
setup(props, { emit, attrs, slots, expose }) {
|
||||
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
|
||||
const anchorDirection = computed(() => props.direction ?? 'vertical');
|
||||
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
devWarning(
|
||||
props.items && typeof slots.default !== 'function',
|
||||
|
@ -133,7 +134,7 @@ export default defineComponent({
|
|||
const target = document.getElementById(sharpLinkMatch[1]);
|
||||
if (target) {
|
||||
const top = getOffsetTop(target, container);
|
||||
if (top < offsetTop + bounds) {
|
||||
if (top <= offsetTop + bounds) {
|
||||
linkSections.push({
|
||||
link,
|
||||
top,
|
||||
|
@ -170,7 +171,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
const container = getContainer.value();
|
||||
const scrollTop = getScroll(container, true);
|
||||
const scrollTop = getScroll(container);
|
||||
const eleOffsetTop = getOffsetTop(targetElement, container);
|
||||
let y = scrollTop + eleOffsetTop;
|
||||
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
|
||||
|
@ -277,6 +278,7 @@ export default defineComponent({
|
|||
title={title}
|
||||
customTitleProps={option}
|
||||
v-slots={{ customTitle: slots.customTitle }}
|
||||
replace={props.replace}
|
||||
>
|
||||
{anchorDirection.value === 'vertical' ? createNestedLink(children) : null}
|
||||
</AnchorLink>
|
||||
|
@ -284,7 +286,7 @@ export default defineComponent({
|
|||
})
|
||||
: null;
|
||||
|
||||
const [wrapSSR, hashId] = useStyle(prefixCls);
|
||||
const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
|
||||
return () => {
|
||||
const { offsetTop, affix, showInkInFixed } = props;
|
||||
|
@ -296,6 +298,8 @@ export default defineComponent({
|
|||
const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, {
|
||||
[`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal',
|
||||
[`${pre}-rtl`]: direction.value === 'rtl',
|
||||
[rootCls.value]: true,
|
||||
[cssVarCls.value]: true,
|
||||
});
|
||||
|
||||
const anchorClass = classNames(pre, {
|
||||
|
|
|
@ -13,6 +13,7 @@ export const anchorLinkProps = () => ({
|
|||
href: String,
|
||||
title: anyType<VueNode | ((item: any) => VueNode)>(),
|
||||
target: String,
|
||||
replace: Boolean,
|
||||
/* private use */
|
||||
customTitleProps: objectType<AnchorLinkItemProps>(),
|
||||
});
|
||||
|
@ -53,6 +54,10 @@ export default defineComponent({
|
|||
const { href } = props;
|
||||
contextHandleClick(e, { title: mergedTitle, href });
|
||||
scrollTo(href);
|
||||
if (props.replace) {
|
||||
e.preventDefault();
|
||||
window.location.replace(href);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
|
|
|
@ -1,21 +1,55 @@
|
|||
import type { CSSObject } from '../../_util/cssinjs';
|
||||
import type { FullToken, GenerateStyle } from '../../theme/internal';
|
||||
import { genComponentStyleHook, mergeToken } from '../../theme/internal';
|
||||
import { unit } from '../../_util/cssinjs';
|
||||
import {
|
||||
FullToken,
|
||||
GenerateStyle,
|
||||
genStyleHooks,
|
||||
GetDefaultToken,
|
||||
mergeToken,
|
||||
} from '../../theme/internal';
|
||||
import { resetComponent, textEllipsis } from '../../style';
|
||||
|
||||
export interface ComponentToken {}
|
||||
export interface ComponentToken {
|
||||
/**
|
||||
* @desc 链接纵向内间距
|
||||
* @descEN Vertical padding of link
|
||||
*/
|
||||
linkPaddingBlock: number;
|
||||
/**
|
||||
* @desc 链接横向内间距
|
||||
* @descEN Horizontal padding of link
|
||||
*/
|
||||
linkPaddingInlineStart: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @desc Anchor 组件的 Token
|
||||
* @descEN Token for Anchor component
|
||||
*/
|
||||
interface AnchorToken extends FullToken<'Anchor'> {
|
||||
/**
|
||||
* @desc 容器块偏移量
|
||||
* @descEN Holder block offset
|
||||
*/
|
||||
holderOffsetBlock: number;
|
||||
anchorPaddingBlock: number;
|
||||
anchorPaddingBlockSecondary: number;
|
||||
anchorPaddingInline: number;
|
||||
anchorBallSize: number;
|
||||
anchorTitleBlock: number;
|
||||
/**
|
||||
* @desc 次级锚点块内间距
|
||||
* @descEN Secondary anchor block padding
|
||||
*/
|
||||
anchorPaddingBlockSecondary: number | string;
|
||||
/**
|
||||
* @desc 锚点球大小
|
||||
* @descEN Anchor ball size
|
||||
*/
|
||||
anchorBallSize: number | string;
|
||||
/**
|
||||
* @desc 锚点标题块
|
||||
* @descEN Anchor title block
|
||||
*/
|
||||
anchorTitleBlock: number | string;
|
||||
}
|
||||
|
||||
// ============================== Shared ==============================
|
||||
const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
||||
const genSharedAnchorStyle: GenerateStyle<AnchorToken> = token => {
|
||||
const {
|
||||
componentCls,
|
||||
holderOffsetBlock,
|
||||
|
@ -24,26 +58,25 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
|||
colorPrimary,
|
||||
lineType,
|
||||
colorSplit,
|
||||
calc,
|
||||
} = token;
|
||||
|
||||
return {
|
||||
[`${componentCls}-wrapper`]: {
|
||||
marginBlockStart: -holderOffsetBlock,
|
||||
marginBlockStart: calc(holderOffsetBlock).mul(-1).equal(),
|
||||
paddingBlockStart: holderOffsetBlock,
|
||||
|
||||
// delete overflow: auto
|
||||
// overflow: 'auto',
|
||||
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
[componentCls]: {
|
||||
...resetComponent(token),
|
||||
position: 'relative',
|
||||
paddingInlineStart: lineWidthBold,
|
||||
|
||||
[`${componentCls}-link`]: {
|
||||
paddingBlock: token.anchorPaddingBlock,
|
||||
paddingInline: `${token.anchorPaddingInline}px 0`,
|
||||
paddingBlock: token.linkPaddingBlock,
|
||||
paddingInline: `${unit(token.linkPaddingInlineStart)} 0`,
|
||||
|
||||
'&-title': {
|
||||
...textEllipsis,
|
||||
|
@ -73,28 +106,21 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
|||
[componentCls]: {
|
||||
'&::before': {
|
||||
position: 'absolute',
|
||||
left: {
|
||||
_skip_check_: true,
|
||||
value: 0,
|
||||
},
|
||||
insetInlineStart: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
borderInlineStart: `${lineWidthBold}px ${lineType} ${colorSplit}`,
|
||||
borderInlineStart: `${unit(lineWidthBold)} ${lineType} ${colorSplit}`,
|
||||
content: '" "',
|
||||
},
|
||||
|
||||
[`${componentCls}-ink`]: {
|
||||
position: 'absolute',
|
||||
left: {
|
||||
_skip_check_: true,
|
||||
value: 0,
|
||||
},
|
||||
insetInlineStart: 0,
|
||||
display: 'none',
|
||||
transform: 'translateY(-50%)',
|
||||
transition: `top ${motionDurationSlow} ease-in-out`,
|
||||
width: lineWidthBold,
|
||||
backgroundColor: colorPrimary,
|
||||
|
||||
[`&${componentCls}-ink-visible`]: {
|
||||
display: 'inline-block',
|
||||
},
|
||||
|
@ -109,7 +135,7 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
|||
};
|
||||
};
|
||||
|
||||
const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
|
||||
const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = token => {
|
||||
const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token;
|
||||
|
||||
return {
|
||||
|
@ -127,7 +153,7 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
|
|||
value: 0,
|
||||
},
|
||||
bottom: 0,
|
||||
borderBottom: `1px ${token.lineType} ${token.colorSplit}`,
|
||||
borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`,
|
||||
content: '" "',
|
||||
},
|
||||
|
||||
|
@ -157,17 +183,23 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
|
|||
};
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genComponentStyleHook('Anchor', token => {
|
||||
const { fontSize, fontSizeLG, padding, paddingXXS } = token;
|
||||
|
||||
const anchorToken = mergeToken<AnchorToken>(token, {
|
||||
holderOffsetBlock: paddingXXS,
|
||||
anchorPaddingBlock: paddingXXS,
|
||||
anchorPaddingBlockSecondary: paddingXXS / 2,
|
||||
anchorPaddingInline: padding,
|
||||
anchorTitleBlock: (fontSize / 14) * 3,
|
||||
anchorBallSize: fontSizeLG / 2,
|
||||
});
|
||||
return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)];
|
||||
export const prepareComponentToken: GetDefaultToken<'Anchor'> = token => ({
|
||||
linkPaddingBlock: token.paddingXXS,
|
||||
linkPaddingInlineStart: token.padding,
|
||||
});
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genStyleHooks(
|
||||
'Anchor',
|
||||
token => {
|
||||
const { fontSize, fontSizeLG, paddingXXS, calc } = token;
|
||||
const anchorToken = mergeToken<AnchorToken>(token, {
|
||||
holderOffsetBlock: paddingXXS,
|
||||
anchorPaddingBlockSecondary: calc(paddingXXS).div(2).equal(),
|
||||
anchorTitleBlock: calc(fontSize).div(14).mul(3).equal(),
|
||||
anchorBallSize: calc(fontSizeLG).div(2).equal(),
|
||||
});
|
||||
return [genSharedAnchorStyle(anchorToken), genSharedAnchorHorizontalStyle(anchorToken)];
|
||||
},
|
||||
prepareComponentToken,
|
||||
);
|
||||
|
|
|
@ -60,7 +60,7 @@ const BackTop = defineComponent({
|
|||
|
||||
const handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
|
||||
const { visibilityHeight } = props;
|
||||
const scrollTop = getScroll(e.target, true);
|
||||
const scrollTop = getScroll(e.target);
|
||||
state.visible = scrollTop >= visibilityHeight;
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue