Skip to content

Commit

Permalink
feat(cdk:popper): migrate popperjs to floating-ui (#1191)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: modifiers is now changed to middlewares
BREAKING CHANGE: forceUpdate is now removed
BREAKING CHANGE: onFirstUpdate is now removed
  • Loading branch information
sallerli1 authored and danranVm committed Oct 21, 2022
1 parent 3e7083c commit d4c582e
Show file tree
Hide file tree
Showing 27 changed files with 516 additions and 322 deletions.
2 changes: 1 addition & 1 deletion packages/cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"jsdelivr": "./index.full.js",
"scripts": {},
"dependencies": {
"@popperjs/core": "^2.9.0",
"@floating-ui/dom": "^1.0.2",
"lodash-es": "^4.17.0"
},
"devDependencies": {
Expand Down
28 changes: 14 additions & 14 deletions packages/cdk/popper/__tests__/__snapshots__/popper.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,70 +12,70 @@ exports[`usePopper > options > autoAdjust work 2`] = `
exports[`usePopper > options > offset work 1`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; bottom: 0px; transform: translate(0px, -4px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"top\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 4px; top: -4px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"top\\">Popper</div>"
`;
exports[`usePopper > options > offset work 2`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; bottom: 0px; transform: translate(0px, -8px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"top\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 8px; top: -8px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"top\\">Popper</div>"
`;
exports[`usePopper > options > placement work 1`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 2`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 3`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 4`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 5`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 6`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 7`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 8`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 9`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 10`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 11`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
exports[`usePopper > options > placement work 12`] = `
"<div id=\\"trigger\\">Trigger</div>
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px; margin: 0px; right: 0px; bottom: 0px; transform: translate(0px, 0px);\\" data-popper-reference-hidden=\\"\\" data-popper-escaped=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
<div id=\\"overlay\\" style=\\"position: absolute; left: 0px; top: 0px;\\" data-popper-reference-hidden=\\"\\" data-popper-placement=\\"left-end\\">Popper</div>"
`;
6 changes: 2 additions & 4 deletions packages/cdk/popper/docs/Api.zh.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## API

`@idux/cdk/popper` 基于 `@popperjs/core` 对浮层的创建进行了封装.
`@idux/cdk/popper` 基于 `@floating-ui/dom` 对浮层的创建进行了封装.

### usePopper

Expand All @@ -20,8 +20,7 @@ export function usePopper(options?: PopperOptions): PopperInstance
| `trigger` | 浮层的触发方式 | `PopperTrigger` | `hover` | - | - |
| `visible` | 是否显示浮层 | `boolean` | `false` | - | - |
| `strategy` | 浮层的定位策略 | `'absolute' \| 'fixed'` | `absolute` | - | - |
| `modifiers` | 自定义浮层的 `modifier` | `Partial<Modifier>[]` | `[]` | - | 参见[popper.js](https://popper.js.org/docs/v2/modifiers/) |
| `onFirstUpdate` | 浮层创建后的回调 | `(state: Partial<State>) => void` | - | - | 参见[popper.js](https://popper.js.org/docs/v2/lifecycle/#hook-into-the-lifecycle) |
| `middleware` | 自定义浮层的 `middleware` | `Middleware[]` | `[]` | - | 参见[floating-ui](https://floating-ui.com/docs/middleware) |

```ts
export declare type PopperPlacement = 'topStart' | 'top' | 'topEnd' | 'rightStart' | 'right' | 'rightEnd' | 'bottomStart' | 'bottom' | 'bottomEnd' | 'leftStart' | 'left' | 'leftEnd'
Expand All @@ -37,7 +36,6 @@ export type PopperTrigger = 'click' | 'hover' | 'focus' | 'contextmenu' | 'manua
| `show` | 显示浮层 | `(delay?: number): void` | - | - | `delay` 是延迟显示的时间 |
| `hide` | 隐藏浮层 | `(delay?: number): void` | - | - | `delay` 是延迟隐藏的时间 |
| `update` | 更新浮层 | `(options: Partial<PopperOptions>): void` | - | - | - |
| `forceUpdate` | 强制更新浮层 | `(): void` | - | - | - |
| `destroy` | 销毁浮层 | `(): void` | - | - | - |
| `visibility` | 浮层显示状态 | `ComputedRef<boolean>` | -| - | - |
| `placement` | 浮层位置 | `ComputedRef<PopperPlacement>` | - | - | - |
Expand Down
24 changes: 24 additions & 0 deletions packages/cdk/popper/src/composables/useDelay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { PopperOptions } from '../types'

import { type ComputedRef, computed } from 'vue'

import { defaultDelay } from './useOptions'

export function useDelay(options: Required<PopperOptions>): ComputedRef<{ show: number; hide: number }> {
const convertDelay = (delay: number | [number | null, number | null]) => {
if (Array.isArray(delay)) {
const [show, hide] = delay
return { show: show ?? defaultDelay, hide: hide ?? defaultDelay }
}
return { show: delay, hide: delay }
}

return computed(() => convertDelay(options.delay))
}
85 changes: 85 additions & 0 deletions packages/cdk/popper/src/composables/useInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { PopperElement } from '../types'

import { type Ref, watch } from 'vue'

import { type ComputePositionConfig, type ComputePositionReturn, autoUpdate, computePosition } from '@floating-ui/dom'

import { convertElement } from '@idux/cdk/utils'

export interface Instance {
update: () => Promise<void>
destroy: () => void
}

export function useInstance(
triggerRef: Ref<PopperElement | undefined>,
popperRef: Ref<PopperElement | undefined>,
options: Ref<ComputePositionConfig>,
): Instance {
const updatePopperPosition = (state: ComputePositionReturn) => {
const popperEl = convertElement(popperRef.value)
const { x, y, strategy } = state

if (popperEl) {
Object.assign(popperEl.style, {
position: strategy,
left: `${x}px`,
top: `${y}px`,
})
}
}

const update = async () => {
const triggerEl = convertElement(triggerRef.value)
const popperEl = convertElement(popperRef.value)

if (!triggerEl || !popperEl) {
return
}

const state = await computePosition(triggerEl, popperEl, options.value)
state && updatePopperPosition(state)
}

let cleanUpHandler: (() => void) | null = null
const initialize = () => {
const triggerEl = convertElement(triggerRef.value)
const popperEl = convertElement(popperRef.value)
if (!triggerEl || !popperEl) {
return
}

cleanUpHandler?.()

Object.assign(popperEl.style, {
position: 'absolute',
left: 0,
top: 0,
})

cleanUpHandler = autoUpdate(triggerEl, popperEl, () => {
update()
})
}

const watchStopHandlers = [
watch([triggerRef, popperRef], initialize, { immediate: true }),
watch(options, update, { immediate: true }),
]
const destroy = () => {
watchStopHandlers.forEach(handler => handler())
cleanUpHandler?.()
}

return {
update,
destroy,
}
}
62 changes: 62 additions & 0 deletions packages/cdk/popper/src/composables/useOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { PopperOptions } from '../types'

import { type ComputedRef, computed, reactive, watch } from 'vue'

import { isEqual } from 'lodash-es'

export const defaultDelay = 0

export function usePopperOptions(options: PopperOptions): {
popperOptions: Required<PopperOptions>
updateOptions: (options: PopperOptions) => void
} {
const popperOptions = reactive<Required<PopperOptions>>({
allowEnter: options.allowEnter ?? true,
autoAdjust: options.autoAdjust ?? true,
delay: options.delay ?? defaultDelay,
disabled: options.disabled ?? false,
offset: options.offset ?? [0, 0],
placement: options.placement ?? 'top',
trigger: options.trigger ?? 'hover',
visible: options.visible ?? false,
strategy: options.strategy ?? 'absolute',
middlewares: options.middlewares ?? [],
})
const updateOptions = (options: PopperOptions) => {
Object.entries(options).forEach(([key, value]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (value !== undefined && !isEqual(value, (popperOptions as any)[key])) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(popperOptions as any)[key] = value
}
})
}

watch(options, _options => {
updateOptions(_options)
})

return {
popperOptions,
updateOptions,
}
}

export type BaseOptions = Pick<
Required<PopperOptions>,
'placement' | 'strategy' | 'middlewares' | 'offset' | 'autoAdjust'
>

export function useBaseOptions(options: Required<PopperOptions>): ComputedRef<BaseOptions> {
return computed(() => {
const { placement, strategy, middlewares, offset, autoAdjust } = options
return { placement, strategy, middlewares, offset, autoAdjust }
})
}
27 changes: 27 additions & 0 deletions packages/cdk/popper/src/composables/usePlacement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { PopperOptions, PopperPlacement } from '../types'

import { type ComputedRef, computed, ref, watch } from 'vue'

export function usePlacement(options: Required<PopperOptions>): {
placement: ComputedRef<PopperPlacement>
updatePlacement: (value: PopperPlacement) => void
} {
const _placement = ref(options.placement)

const updatePlacement = (value: PopperPlacement) => {
_placement.value = value
}

watch(() => options.placement, updatePlacement)

const placement = computed(() => _placement.value)

return { placement, updatePlacement }
}
32 changes: 32 additions & 0 deletions packages/cdk/popper/src/composables/usePopperEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import type { PopperEvents, PopperOptions } from '../types'

import { type ComputedRef, computed } from 'vue'

import { NoopObject } from '@idux/cdk/utils'

export function usePopperEvents(
baseOptions: Required<PopperOptions>,
eventOptions: { show(): void; hide(): void },
): ComputedRef<PopperEvents> {
const { show, hide } = eventOptions

const onMouseenter = () => show()
const onMouseleave = () => hide()

const eventsMap = {
click: NoopObject,
focus: NoopObject,
hover: { onMouseenter, onMouseleave },
contextmenu: NoopObject,
manual: NoopObject,
}

return computed(() => (baseOptions.allowEnter ? eventsMap[baseOptions.trigger] : NoopObject))
}
26 changes: 26 additions & 0 deletions packages/cdk/popper/src/composables/useTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* @license
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

export function useTimer(): { setTimer: (action: () => void, delay: number) => void; clearTimer: () => void } {
let timer: number | null = null

const setTimer = (action: () => void, delay: number) => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(action, delay)
}

const clearTimer = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}

return { setTimer, clearTimer }
}

0 comments on commit d4c582e

Please sign in to comment.