feat: anchor support cssvar

pull/7940/head
tangjinzhou 2024-11-11 11:57:58 +08:00
parent 3375bd4695
commit d6ec6bf26e
6 changed files with 99 additions and 59 deletions

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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, {

View File

@ -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(

View File

@ -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,
);

View File

@ -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;
});