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; return obj !== null && obj !== undefined && obj === obj.window;
} }
export default function getScroll( const getScroll = (target: HTMLElement | Window | Document | null): number => {
target: HTMLElement | Window | Document | null,
top: boolean,
): number {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return 0; return 0;
} }
const method = top ? 'scrollTop' : 'scrollLeft';
let result = 0; let result = 0;
if (isWindow(target)) { if (isWindow(target)) {
result = target[top ? 'scrollY' : 'scrollX']; result = target.pageYOffset;
} else if (target instanceof Document) { } else if (target instanceof Document) {
result = target.documentElement[method]; result = target.documentElement.scrollTop;
} else if (target instanceof HTMLElement) { } else if (target instanceof HTMLElement) {
result = target[method]; result = target.scrollTop;
} else if (target) { } else if (target) {
// According to the type inference, the `target` is `never` type. // 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:: // Since we configured the loose mode type checking, and supports mocking the target with such shape below::
// `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`, // `{ documentElement: { scrollLeft: 200, scrollTop: 400 } }`,
// the program may falls into this branch. // the program may falls into this branch.
// Check the corresponding tests for details. Don't sure what is the real scenario this happens. // 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') { 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; return result;
} };
export default getScroll;

View File

@ -14,7 +14,7 @@ interface ScrollToOptions {
export default function scrollTo(y: number, options: ScrollToOptions = {}) { export default function scrollTo(y: number, options: ScrollToOptions = {}) {
const { getContainer = () => window, callback, duration = 450 } = options; const { getContainer = () => window, callback, duration = 450 } = options;
const container = getContainer(); const container = getContainer();
const scrollTop = getScroll(container, true); const scrollTop = getScroll(container);
const startTime = Date.now(); const startTime = Date.now();
const frameFunc = () => { const frameFunc = () => {

View File

@ -23,6 +23,7 @@ import AnchorLink from './AnchorLink';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import devWarning from '../vc-util/devWarning'; import devWarning from '../vc-util/devWarning';
import { arrayType } from '../_util/type'; import { arrayType } from '../_util/type';
import useCSSVarCls from '../config-provider/hooks/useCssVarCls';
export type AnchorDirection = 'vertical' | 'horizontal'; export type AnchorDirection = 'vertical' | 'horizontal';
@ -39,8 +40,7 @@ function getOffsetTop(element: HTMLElement, container: AnchorContainer): number
if (rect.width || rect.height) { if (rect.width || rect.height) {
if (container === window) { if (container === window) {
container = element.ownerDocument!.documentElement!; return rect.top - element.ownerDocument!.documentElement!.clientTop;
return rect.top - container.clientTop;
} }
return rect.top - (container as HTMLElement).getBoundingClientRect().top; return rect.top - (container as HTMLElement).getBoundingClientRect().top;
} }
@ -70,6 +70,7 @@ export const anchorProps = () => ({
targetOffset: Number, targetOffset: Number,
items: arrayType<AnchorLinkItemProps[]>(), items: arrayType<AnchorLinkItemProps[]>(),
direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'), direction: PropTypes.oneOf(['vertical', 'horizontal'] as AnchorDirection[]).def('vertical'),
replace: Boolean,
onChange: Function as PropType<(currentActiveLink: string) => void>, onChange: Function as PropType<(currentActiveLink: string) => void>,
onClick: Function as PropType<(e: MouseEvent, link: { title: any; href: 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 }) { setup(props, { emit, attrs, slots, expose }) {
const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props); const { prefixCls, getTargetContainer, direction } = useConfigInject('anchor', props);
const anchorDirection = computed(() => props.direction ?? 'vertical'); const anchorDirection = computed(() => props.direction ?? 'vertical');
const rootCls = useCSSVarCls(prefixCls);
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
devWarning( devWarning(
props.items && typeof slots.default !== 'function', props.items && typeof slots.default !== 'function',
@ -133,7 +134,7 @@ export default defineComponent({
const target = document.getElementById(sharpLinkMatch[1]); const target = document.getElementById(sharpLinkMatch[1]);
if (target) { if (target) {
const top = getOffsetTop(target, container); const top = getOffsetTop(target, container);
if (top < offsetTop + bounds) { if (top <= offsetTop + bounds) {
linkSections.push({ linkSections.push({
link, link,
top, top,
@ -170,7 +171,7 @@ export default defineComponent({
} }
const container = getContainer.value(); const container = getContainer.value();
const scrollTop = getScroll(container, true); const scrollTop = getScroll(container);
const eleOffsetTop = getOffsetTop(targetElement, container); const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop; let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0; y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
@ -277,6 +278,7 @@ export default defineComponent({
title={title} title={title}
customTitleProps={option} customTitleProps={option}
v-slots={{ customTitle: slots.customTitle }} v-slots={{ customTitle: slots.customTitle }}
replace={props.replace}
> >
{anchorDirection.value === 'vertical' ? createNestedLink(children) : null} {anchorDirection.value === 'vertical' ? createNestedLink(children) : null}
</AnchorLink> </AnchorLink>
@ -284,7 +286,7 @@ export default defineComponent({
}) })
: null; : null;
const [wrapSSR, hashId] = useStyle(prefixCls); const [wrapSSR, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
return () => { return () => {
const { offsetTop, affix, showInkInFixed } = props; const { offsetTop, affix, showInkInFixed } = props;
@ -296,6 +298,8 @@ export default defineComponent({
const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, { const wrapperClass = classNames(hashId.value, props.wrapperClass, `${pre}-wrapper`, {
[`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal', [`${pre}-wrapper-horizontal`]: anchorDirection.value === 'horizontal',
[`${pre}-rtl`]: direction.value === 'rtl', [`${pre}-rtl`]: direction.value === 'rtl',
[rootCls.value]: true,
[cssVarCls.value]: true,
}); });
const anchorClass = classNames(pre, { const anchorClass = classNames(pre, {

View File

@ -13,6 +13,7 @@ export const anchorLinkProps = () => ({
href: String, href: String,
title: anyType<VueNode | ((item: any) => VueNode)>(), title: anyType<VueNode | ((item: any) => VueNode)>(),
target: String, target: String,
replace: Boolean,
/* private use */ /* private use */
customTitleProps: objectType<AnchorLinkItemProps>(), customTitleProps: objectType<AnchorLinkItemProps>(),
}); });
@ -53,6 +54,10 @@ export default defineComponent({
const { href } = props; const { href } = props;
contextHandleClick(e, { title: mergedTitle, href }); contextHandleClick(e, { title: mergedTitle, href });
scrollTo(href); scrollTo(href);
if (props.replace) {
e.preventDefault();
window.location.replace(href);
}
}; };
watch( watch(

View File

@ -1,21 +1,55 @@
import type { CSSObject } from '../../_util/cssinjs'; import { unit } from '../../_util/cssinjs';
import type { FullToken, GenerateStyle } from '../../theme/internal'; import {
import { genComponentStyleHook, mergeToken } from '../../theme/internal'; FullToken,
GenerateStyle,
genStyleHooks,
GetDefaultToken,
mergeToken,
} from '../../theme/internal';
import { resetComponent, textEllipsis } from '../../style'; 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'> { interface AnchorToken extends FullToken<'Anchor'> {
/**
* @desc
* @descEN Holder block offset
*/
holderOffsetBlock: number; holderOffsetBlock: number;
anchorPaddingBlock: number; /**
anchorPaddingBlockSecondary: number; * @desc
anchorPaddingInline: number; * @descEN Secondary anchor block padding
anchorBallSize: number; */
anchorTitleBlock: number; anchorPaddingBlockSecondary: number | string;
/**
* @desc
* @descEN Anchor ball size
*/
anchorBallSize: number | string;
/**
* @desc
* @descEN Anchor title block
*/
anchorTitleBlock: number | string;
} }
// ============================== Shared ============================== // ============================== Shared ==============================
const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => { const genSharedAnchorStyle: GenerateStyle<AnchorToken> = token => {
const { const {
componentCls, componentCls,
holderOffsetBlock, holderOffsetBlock,
@ -24,26 +58,25 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
colorPrimary, colorPrimary,
lineType, lineType,
colorSplit, colorSplit,
calc,
} = token; } = token;
return { return {
[`${componentCls}-wrapper`]: { [`${componentCls}-wrapper`]: {
marginBlockStart: -holderOffsetBlock, marginBlockStart: calc(holderOffsetBlock).mul(-1).equal(),
paddingBlockStart: holderOffsetBlock, paddingBlockStart: holderOffsetBlock,
// delete overflow: auto // delete overflow: auto
// overflow: 'auto', // overflow: 'auto',
backgroundColor: 'transparent',
[componentCls]: { [componentCls]: {
...resetComponent(token), ...resetComponent(token),
position: 'relative', position: 'relative',
paddingInlineStart: lineWidthBold, paddingInlineStart: lineWidthBold,
[`${componentCls}-link`]: { [`${componentCls}-link`]: {
paddingBlock: token.anchorPaddingBlock, paddingBlock: token.linkPaddingBlock,
paddingInline: `${token.anchorPaddingInline}px 0`, paddingInline: `${unit(token.linkPaddingInlineStart)} 0`,
'&-title': { '&-title': {
...textEllipsis, ...textEllipsis,
@ -73,28 +106,21 @@ const genSharedAnchorStyle: GenerateStyle<AnchorToken> = (token): CSSObject => {
[componentCls]: { [componentCls]: {
'&::before': { '&::before': {
position: 'absolute', position: 'absolute',
left: { insetInlineStart: 0,
_skip_check_: true,
value: 0,
},
top: 0, top: 0,
height: '100%', height: '100%',
borderInlineStart: `${lineWidthBold}px ${lineType} ${colorSplit}`, borderInlineStart: `${unit(lineWidthBold)} ${lineType} ${colorSplit}`,
content: '" "', content: '" "',
}, },
[`${componentCls}-ink`]: { [`${componentCls}-ink`]: {
position: 'absolute', position: 'absolute',
left: { insetInlineStart: 0,
_skip_check_: true,
value: 0,
},
display: 'none', display: 'none',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
transition: `top ${motionDurationSlow} ease-in-out`, transition: `top ${motionDurationSlow} ease-in-out`,
width: lineWidthBold, width: lineWidthBold,
backgroundColor: colorPrimary, backgroundColor: colorPrimary,
[`&${componentCls}-ink-visible`]: { [`&${componentCls}-ink-visible`]: {
display: 'inline-block', 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; const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token;
return { return {
@ -127,7 +153,7 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
value: 0, value: 0,
}, },
bottom: 0, bottom: 0,
borderBottom: `1px ${token.lineType} ${token.colorSplit}`, borderBottom: `${unit(token.lineWidth)} ${token.lineType} ${token.colorSplit}`,
content: '" "', content: '" "',
}, },
@ -157,17 +183,23 @@ const genSharedAnchorHorizontalStyle: GenerateStyle<AnchorToken> = (token): CSSO
}; };
}; };
// ============================== Export ============================== export const prepareComponentToken: GetDefaultToken<'Anchor'> = token => ({
export default genComponentStyleHook('Anchor', token => { linkPaddingBlock: token.paddingXXS,
const { fontSize, fontSizeLG, padding, paddingXXS } = token; linkPaddingInlineStart: token.padding,
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 ==============================
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 handleScroll = throttleByAnimationFrame((e: Event | { target: any }) => {
const { visibilityHeight } = props; const { visibilityHeight } = props;
const scrollTop = getScroll(e.target, true); const scrollTop = getScroll(e.target);
state.visible = scrollTop >= visibilityHeight; state.visible = scrollTop >= visibilityHeight;
}); });