mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-18 10:59:10 +08:00
feat(hooks): [floating] add use-floating (#6822)
* feat(hooks): [floating] add use-floating - Implement floating-ui vue with composition API - Add test for the hook. * Update coordinate type
This commit is contained in:
parent
2b40ea8145
commit
da992a97b2
87
packages/hooks/__tests__/use-floating.tsx
Normal file
87
packages/hooks/__tests__/use-floating.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { rAF } from '@element-plus/test-utils/tick'
|
||||
import { useFloating, arrowMiddleware } from '../use-floating'
|
||||
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { Placement, Strategy, Middleware } from '@floating-ui/dom'
|
||||
|
||||
describe('useFloating', () => {
|
||||
const createComponent = (arrow = false) => {
|
||||
return mount({
|
||||
setup() {
|
||||
const placement = ref<Placement>('bottom')
|
||||
const strategy = ref<Strategy>('fixed')
|
||||
const arrowRef = ref<HTMLElement>()
|
||||
const middleware = ref<Array<Middleware>>(
|
||||
arrow
|
||||
? [
|
||||
arrowMiddleware({
|
||||
arrowRef,
|
||||
}),
|
||||
]
|
||||
: []
|
||||
)
|
||||
|
||||
const { referenceRef, contentRef, x, y, update, middlewareData } =
|
||||
useFloating({
|
||||
placement,
|
||||
strategy,
|
||||
middleware,
|
||||
})
|
||||
|
||||
const contentStyle = computed<CSSProperties>(() => {
|
||||
return reactive({
|
||||
position: strategy,
|
||||
x,
|
||||
y,
|
||||
})
|
||||
})
|
||||
|
||||
watch(arrowRef, () => update())
|
||||
|
||||
return {
|
||||
arrowRef,
|
||||
contentRef,
|
||||
contentStyle,
|
||||
referenceRef,
|
||||
middlewareData,
|
||||
}
|
||||
},
|
||||
render() {
|
||||
const { contentStyle } = this
|
||||
return (
|
||||
<div>
|
||||
<button ref="referenceRef">My button</button>
|
||||
<div ref="contentRef" style={contentStyle}>
|
||||
My tooltip
|
||||
<div ref="arrowRef" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let wrapper: ReturnType<typeof createComponent>
|
||||
|
||||
it('should render without arrow correctly', async () => {
|
||||
wrapper = createComponent()
|
||||
await rAF()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.referenceRef).toBeInstanceOf(Element)
|
||||
expect(wrapper.vm.contentRef).toBeInstanceOf(Element)
|
||||
expect(wrapper.vm.middlewareData.arrow).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should render with arrow correctly', async () => {
|
||||
wrapper = createComponent(true)
|
||||
await rAF()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.referenceRef).toBeInstanceOf(Element)
|
||||
expect(wrapper.vm.contentRef).toBeInstanceOf(Element)
|
||||
expect(wrapper.vm.middlewareData.arrow).toBeDefined()
|
||||
})
|
||||
})
|
@ -26,3 +26,4 @@ export * from './use-delayed-toggle'
|
||||
export * from './use-forward-ref'
|
||||
export * from './use-namespace'
|
||||
export * from './use-z-index'
|
||||
export * from './use-floating'
|
||||
|
128
packages/hooks/use-floating/index.ts
Normal file
128
packages/hooks/use-floating/index.ts
Normal file
@ -0,0 +1,128 @@
|
||||
import { ref, onMounted, watch, unref } from 'vue'
|
||||
import { isClient, unrefElement } from '@vueuse/core'
|
||||
import { isNil } from 'lodash-unified'
|
||||
import { computePosition, arrow as arrowCore } from '@floating-ui/dom'
|
||||
|
||||
import { buildProps } from '@element-plus/utils'
|
||||
|
||||
import type { ToRefs, Ref } from 'vue'
|
||||
import type {
|
||||
ComputePositionReturn,
|
||||
Placement,
|
||||
Strategy,
|
||||
Middleware,
|
||||
SideObject,
|
||||
VirtualElement,
|
||||
} from '@floating-ui/dom'
|
||||
|
||||
export const useFloatingProps = buildProps({} as const)
|
||||
|
||||
export type UseFloatingProps = ToRefs<{
|
||||
middleware: Array<Middleware>
|
||||
placement: Placement
|
||||
strategy: Strategy
|
||||
}>
|
||||
|
||||
type ElementRef = Parameters<typeof unrefElement>['0']
|
||||
|
||||
const unrefReference = (
|
||||
elRef: ElementRef | Ref<VirtualElement | undefined>
|
||||
) => {
|
||||
if (!isClient) return
|
||||
if (!elRef) return elRef
|
||||
const unrefEl = unrefElement(elRef as ElementRef)
|
||||
if (unrefEl) return unrefEl
|
||||
return elRef as VirtualElement
|
||||
}
|
||||
|
||||
export const getPositionDataWithUnit = <T extends Record<string, number>>(
|
||||
record: T | undefined,
|
||||
key: keyof T
|
||||
) => {
|
||||
const value = record?.[key]
|
||||
return isNil(value) ? '' : `${value}px`
|
||||
}
|
||||
|
||||
export const useFloating = ({
|
||||
middleware,
|
||||
placement,
|
||||
strategy,
|
||||
}: UseFloatingProps) => {
|
||||
const referenceRef = ref<HTMLElement | VirtualElement>()
|
||||
const contentRef = ref<HTMLElement>()
|
||||
const x = ref<string>()
|
||||
const y = ref<string>()
|
||||
const middlewareData = ref<ComputePositionReturn['middlewareData']>({})
|
||||
|
||||
const states = {
|
||||
x,
|
||||
y,
|
||||
placement,
|
||||
strategy,
|
||||
middlewareData,
|
||||
} as const
|
||||
|
||||
const update = async () => {
|
||||
if (!isClient) return
|
||||
|
||||
const referenceEl = unrefReference(referenceRef)
|
||||
const contentEl = unrefElement(contentRef)
|
||||
|
||||
if (!referenceEl || !contentEl) return
|
||||
|
||||
const data = await computePosition(referenceEl, contentEl, {
|
||||
placement: unref(placement),
|
||||
strategy: unref(strategy),
|
||||
middleware: unref(middleware),
|
||||
})
|
||||
|
||||
;['x', 'y'].forEach(
|
||||
(key) => (data[key] = getPositionDataWithUnit(data as any, key))
|
||||
)
|
||||
|
||||
Object.keys(states).forEach((key) => {
|
||||
states[key].value = data[key]
|
||||
})
|
||||
}
|
||||
|
||||
watch(referenceRef, () => update())
|
||||
|
||||
watch(contentRef, () => update())
|
||||
|
||||
onMounted(() => update())
|
||||
|
||||
return {
|
||||
...states,
|
||||
update,
|
||||
referenceRef,
|
||||
contentRef,
|
||||
}
|
||||
}
|
||||
|
||||
export type ArrowMiddlewareProps = {
|
||||
arrowRef: Ref<HTMLElement | undefined>
|
||||
padding?: number | SideObject
|
||||
}
|
||||
|
||||
export const arrowMiddleware = ({
|
||||
arrowRef,
|
||||
padding,
|
||||
}: ArrowMiddlewareProps): Middleware => {
|
||||
return {
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: arrowRef,
|
||||
padding,
|
||||
},
|
||||
|
||||
fn(args) {
|
||||
const arrowEl = unref(arrowRef)
|
||||
if (!arrowEl) return {}
|
||||
|
||||
return arrowCore({
|
||||
element: arrowEl,
|
||||
padding,
|
||||
}).fn(args)
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user