diff --git a/components/_util/getScroll.ts b/components/_util/getScroll.ts index ca0b10005..f3e42f1bc 100644 --- a/components/_util/getScroll.ts +++ b/components/_util/getScroll.ts @@ -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; diff --git a/components/_util/scrollTo.ts b/components/_util/scrollTo.ts index 992d6a930..59bb1436a 100644 --- a/components/_util/scrollTo.ts +++ b/components/_util/scrollTo.ts @@ -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 = () => { diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx index 60f9e243d..d727e3d40 100644 --- a/components/anchor/Anchor.tsx +++ b/components/anchor/Anchor.tsx @@ -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(), 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} @@ -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, { diff --git a/components/anchor/AnchorLink.tsx b/components/anchor/AnchorLink.tsx index 5c5afa873..c45fc7ad5 100644 --- a/components/anchor/AnchorLink.tsx +++ b/components/anchor/AnchorLink.tsx @@ -13,6 +13,7 @@ export const anchorLinkProps = () => ({ href: String, title: anyType VueNode)>(), target: String, + replace: Boolean, /* private use */ customTitleProps: objectType(), }); @@ -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( diff --git a/components/anchor/style/index.ts b/components/anchor/style/index.ts index 119055aac..387bf2636 100644 --- a/components/anchor/style/index.ts +++ b/components/anchor/style/index.ts @@ -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 = (token): CSSObject => { +const genSharedAnchorStyle: GenerateStyle = token => { const { componentCls, holderOffsetBlock, @@ -24,26 +58,25 @@ const genSharedAnchorStyle: GenerateStyle = (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 = (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 = (token): CSSObject => { }; }; -const genSharedAnchorHorizontalStyle: GenerateStyle = (token): CSSObject => { +const genSharedAnchorHorizontalStyle: GenerateStyle = token => { const { componentCls, motionDurationSlow, lineWidthBold, colorPrimary } = token; return { @@ -127,7 +153,7 @@ const genSharedAnchorHorizontalStyle: GenerateStyle = (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 = (token): CSSO }; }; -// ============================== Export ============================== -export default genComponentStyleHook('Anchor', token => { - const { fontSize, fontSizeLG, padding, paddingXXS } = token; - - const anchorToken = mergeToken(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(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, +); diff --git a/components/float-button/BackTop.tsx b/components/float-button/BackTop.tsx index 722905975..38dbee0e7 100644 --- a/components/float-button/BackTop.tsx +++ b/components/float-button/BackTop.tsx @@ -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; });