From b171632d4ebc5244f59d2c90ea7a14407c55482a Mon Sep 17 00:00:00 2001 From: micaiguai <1399880823@qq.com> Date: Mon, 15 Dec 2025 21:13:54 +0800 Subject: [PATCH] feat(components): [affix] support append-to and teleported (#23053) * feat(components): [affix] implement append-to-body * feat: change append-to-body to teleported and append-to * test: optimize affix teleport case * Update packages/components/affix/src/affix.ts Co-authored-by: rzzf * Update packages/components/affix/src/affix.vue Co-authored-by: rzzf * Update packages/components/affix/src/affix.vue Co-authored-by: rzzf * Update packages/components/affix/src/affix.vue Co-authored-by: btea <2356281422@qq.com> * Update docs/en-US/component/affix.md Co-authored-by: btea <2356281422@qq.com> * Update docs/en-US/component/affix.md Co-authored-by: btea <2356281422@qq.com> * chore: update the affix.md format --------- Co-authored-by: rzzf Co-authored-by: btea <2356281422@qq.com> --- docs/en-US/component/affix.md | 14 ++++--- .../components/affix/__tests__/affix.test.tsx | 37 +++++++++++++++++++ packages/components/affix/src/affix.ts | 12 ++++++ packages/components/affix/src/affix.vue | 15 ++++++-- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/docs/en-US/component/affix.md b/docs/en-US/component/affix.md index 63373e0988..7627170331 100644 --- a/docs/en-US/component/affix.md +++ b/docs/en-US/component/affix.md @@ -41,12 +41,14 @@ affix/fixed ### Attributes -| Name | Description | Type | Default | -| -------- | ------------------------------- | -------------------------- | ------- | -| offset | offset distance | ^[number] | 0 | -| position | position of affix | ^[enum]`'top' \| 'bottom'` | top | -| target | target container (CSS selector) | ^[string] | — | -| z-index | `z-index` of affix | ^[number] | 100 | +| Name | Description | Type | Default | +| -------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------- | ------- | +| offset | offset distance | ^[number] | 0 | +| position | position of affix | ^[enum]`'top' \| 'bottom'` | top | +| target | target container (CSS selector) | ^[string] | — | +| z-index | `z-index` of affix | ^[number] | 100 | +| teleported ^(2.13.0) | whether affix element is teleported, if `true` it will be teleported to where `append-to` sets | ^[boolean] | false | +| append-to ^(2.13.0) | which element the affix element appends to | ^[CSSSelector] / ^[HTMLElement] | body | ### Events diff --git a/packages/components/affix/__tests__/affix.test.tsx b/packages/components/affix/__tests__/affix.test.tsx index 09213f5ba0..e36299ec4f 100644 --- a/packages/components/affix/__tests__/affix.test.tsx +++ b/packages/components/affix/__tests__/affix.test.tsx @@ -192,4 +192,41 @@ describe('Affix.vue', () => { mockAffixRect.mockRestore() mockDocumentRect.mockRestore() }) + + test('should render append-to props', async () => { + const wrapper = _mount(() => ( + <> +
+ + {AXIOM} + + + )) + const teleportTarget = wrapper.find('.teleport-target') + await nextTick() + + expect(wrapper.text()).toEqual(AXIOM) + const mockAffixRect = vi + .spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect') + .mockReturnValue({ + height: 40, + width: 1000, + top: -100, + bottom: -80, + } as DOMRect) + const mockDocumentRect = vi + .spyOn(document.documentElement, 'getBoundingClientRect') + .mockReturnValue({ + height: 200, + width: 1000, + top: 0, + bottom: 200, + } as DOMRect) + expect(wrapper.find('.el-affix--fixed').exists()).toBe(false) + expect(teleportTarget.find('.el-affix--fixed').exists()).toBe(false) + await makeScroll(document.documentElement, 'scrollTop', 200) + expect(teleportTarget.find('.el-affix--fixed').exists()).toBe(true) + mockAffixRect.mockRestore() + mockDocumentRect.mockRestore() + }) }) diff --git a/packages/components/affix/src/affix.ts b/packages/components/affix/src/affix.ts index 7ed9d08b6d..85ebb5e966 100644 --- a/packages/components/affix/src/affix.ts +++ b/packages/components/affix/src/affix.ts @@ -5,6 +5,7 @@ import { isNumber, } from '@element-plus/utils' import { CHANGE_EVENT } from '@element-plus/constants' +import { teleportProps } from '@element-plus/components/teleport' import type { ExtractPropTypes, __ExtractPublicPropTypes } from 'vue' import type { ZIndexProperty } from 'csstype' @@ -40,6 +41,17 @@ export const affixProps = buildProps({ values: ['top', 'bottom'], default: 'top', }, + /** + * @description whether affix element is teleported, if `true` it will be teleported to where `append-to` sets + * */ + teleported: Boolean, + /** + * @description which element the affix element appends to + * */ + appendTo: { + type: teleportProps.to.type, + default: 'body', + }, } as const) export type AffixProps = ExtractPropTypes export type AffixPropsPublic = __ExtractPublicPropTypes diff --git a/packages/components/affix/src/affix.vue b/packages/components/affix/src/affix.vue index 6a01cf39be..eaf58bc406 100644 --- a/packages/components/affix/src/affix.vue +++ b/packages/components/affix/src/affix.vue @@ -1,8 +1,10 @@ @@ -21,6 +23,7 @@ import { useEventListener, useWindowSize, } from '@vueuse/core' +import ElTeleport from '@element-plus/components/teleport' import { addUnit, getScrollContainer, throwError } from '@element-plus/utils' import { useNamespace } from '@element-plus/hooks' import { CHANGE_EVENT } from '@element-plus/constants' @@ -46,6 +49,7 @@ const { width: rootWidth, top: rootTop, bottom: rootBottom, + left: rootLeft, update: updateRoot, } = useElementBounding(root, { windowScroll: false }) const targetRect = useElementBounding(target) @@ -54,6 +58,10 @@ const fixed = ref(false) const scrollTop = ref(0) const transform = ref(0) +const teleportDisabled = computed(() => { + return !props.teleported || !fixed.value +}) + const rootStyle = computed(() => { return { height: fixed.value ? `${rootHeight.value}px` : '', @@ -70,6 +78,7 @@ const affixStyle = computed(() => { width: `${rootWidth.value}px`, top: props.position === 'top' ? offset : '', bottom: props.position === 'bottom' ? offset : '', + left: props.teleported ? `${rootLeft.value}px` : '', transform: transform.value ? `translateY(${transform.value}px)` : '', zIndex: props.zIndex, }