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:
JeremyWuuuuu 2022-03-25 15:43:54 +08:00 committed by GitHub
parent 2b40ea8145
commit da992a97b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 216 additions and 0 deletions

View 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()
})
})

View File

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

View 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)
},
}
}