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 <cszhjh@gmail.com>

* Update packages/components/affix/src/affix.vue

Co-authored-by: rzzf <cszhjh@gmail.com>

* Update packages/components/affix/src/affix.vue

Co-authored-by: rzzf <cszhjh@gmail.com>

* 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 <cszhjh@gmail.com>
Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
micaiguai
2025-12-15 21:13:54 +08:00
committed by GitHub
parent 5862e866ab
commit b171632d4e
4 changed files with 69 additions and 9 deletions

View File

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

View File

@@ -192,4 +192,41 @@ describe('Affix.vue', () => {
mockAffixRect.mockRestore()
mockDocumentRect.mockRestore()
})
test('should render append-to props', async () => {
const wrapper = _mount(() => (
<>
<div class="teleport-target"></div>
<Affix teleported appendTo=".teleport-target">
{AXIOM}
</Affix>
</>
))
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()
})
})

View File

@@ -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<typeof affixProps>
export type AffixPropsPublic = __ExtractPublicPropTypes<typeof affixProps>

View File

@@ -1,8 +1,10 @@
<template>
<div ref="root" :class="ns.b()" :style="rootStyle">
<div :class="{ [ns.m('fixed')]: fixed }" :style="affixStyle">
<slot />
</div>
<el-teleport :disabled="teleportDisabled" :to="appendTo">
<div :class="{ [ns.m('fixed')]: fixed }" :style="affixStyle">
<slot />
</div>
</el-teleport>
</div>
</template>
@@ -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<CSSProperties>(() => {
return {
height: fixed.value ? `${rootHeight.value}px` : '',
@@ -70,6 +78,7 @@ const affixStyle = computed<CSSProperties>(() => {
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,
}