Skip to content

Commit 5eba736

Browse files
committed
feat(floating): new component
1 parent 93372da commit 5eba736

File tree

6 files changed

+231
-167
lines changed

6 files changed

+231
-167
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<script lang="ts" setup>
2+
import type { Middleware } from '@floating-ui/vue'
3+
import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
4+
import { onClickOutside, useEventListener, useMounted } from '@vueuse/core'
5+
import { ref } from 'vue'
6+
import { sameWidth as sameWidthMiddleware } from './middlewares'
7+
import { floatingProps } from './props'
8+
import { useTeleport } from '@/composables/useTeleport'
9+
import { useInternalBooleanState } from '@/composables/useInternalBooleanState'
10+
11+
const props = defineProps(floatingProps)
12+
13+
const emit = defineEmits<{
14+
(e: 'update:modelValue', value: boolean): void
15+
}>()
16+
17+
defineOptions({
18+
name: 'AFloating',
19+
})
20+
21+
const { teleportTarget } = useTeleport()
22+
const isMounted = useMounted()
23+
const { internalState: isFloatingElVisible, toggle: toggleFloatingElVisibility } = useInternalBooleanState(toRef(props, 'modelValue'), emit, 'update:modelValue', false)
24+
25+
// Template refs
26+
// const props.referenceEl = ref()
27+
const refFloating = ref()
28+
29+
/*
30+
ℹ️ We need to construct the internal middleware variable
31+
32+
If user don't pass the middleware prop then prop value will be `undefined` which will easy to tackle with simple if condition as shown below
33+
34+
Here, we will use user's middleware if passed via props or we will use our defaults
35+
*/
36+
const _middleware = props.middleware === undefined
37+
? [
38+
// ℹ️ For this we need need bridge to handle keep menu content open
39+
// offset(6),
40+
41+
sameWidthMiddleware(refFloating),
42+
flip(),
43+
shift({ padding: 10 }),
44+
] as Middleware[]
45+
: props.middleware(props.referenceEl, refFloating)
46+
47+
// Calculate position of floating element
48+
const { x, y, strategy } = useFloating(toRef(props, 'referenceEl'), refFloating, {
49+
strategy: toRef(props, 'strategy'),
50+
placement: toRef(props, 'placement'),
51+
middleware: _middleware,
52+
whileElementsMounted: autoUpdate,
53+
})
54+
55+
// onMounted(() => {
56+
// const vm = getCurrentInstance()
57+
// console.log('vm?.proxy?.$el :>> ', vm?.proxy?.$parent)
58+
// console.log('vm?.proxy?.$parent.$el :>> ', vm?.proxy?.$parent.$el)
59+
// if (vm?.proxy?.$parent)
60+
// props.referenceEl.value = vm?.proxy?.$parent.$el
61+
// })
62+
63+
// 👉 Event listeners
64+
/*
65+
If moduleValue is provided don't attach any event to modify the visibility of menu
66+
props.modelValue === undefined => modelValue isn't provided
67+
*/
68+
if (props.modelValue === undefined) {
69+
// If trigger is hover
70+
if (props.trigger === 'hover') {
71+
// Reference
72+
useEventListener(toRef(props, 'referenceEl'), 'mouseenter', () => {
73+
if (isFloatingElVisible.value === false)
74+
toggleFloatingElVisibility()
75+
})
76+
useEventListener(toRef(props, 'referenceEl'), 'mouseleave', () => {
77+
if (isFloatingElVisible.value === true)
78+
toggleFloatingElVisibility()
79+
})
80+
81+
// Floating
82+
useEventListener(refFloating, 'mouseenter', () => {
83+
if (isFloatingElVisible.value === false)
84+
toggleFloatingElVisibility()
85+
})
86+
useEventListener(refFloating, 'mouseleave', () => {
87+
if (isFloatingElVisible.value === true)
88+
toggleFloatingElVisibility()
89+
})
90+
}
91+
else {
92+
useEventListener(toRef(props, 'referenceEl'), 'click', toggleFloatingElVisibility)
93+
94+
if (props.persist !== true) {
95+
onClickOutside(
96+
toRef(props, 'referenceEl'),
97+
_event => {
98+
if (isFloatingElVisible.value)
99+
toggleFloatingElVisibility()
100+
},
101+
{
102+
ignore: props.persist === 'content' ? [refFloating] : [],
103+
},
104+
)
105+
}
106+
}
107+
}
108+
</script>
109+
110+
<template>
111+
<Teleport
112+
v-if="isMounted"
113+
:to="teleportTarget"
114+
>
115+
<!-- ℹ️ Transition component don't accept null as value of name prop so we need `props.transition || undefined` -->
116+
<Transition :name="props.transition || undefined">
117+
<div
118+
v-show="props.modelValue ?? isFloatingElVisible"
119+
ref="refFloating"
120+
class="a-floating"
121+
:style="{
122+
top: `${y ?? 0}px`,
123+
left: `${x ?? 0}px`,
124+
}"
125+
:class="strategy"
126+
>
127+
<slot />
128+
</div>
129+
</Transition>
130+
</Teleport>
131+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as AFloating } from './AFloating.vue'
2+
export * from './props'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { ElementRects } from '@floating-ui/vue'
2+
import type { Ref } from 'vue'
3+
4+
export const sameWidth = (floatingEl: Ref<HTMLElement>) => {
5+
return {
6+
name: 'sameWidth',
7+
fn: ({ rects, x, y }: { rects: ElementRects; x: number; y: number }) => {
8+
// Set width of reference to floating
9+
floatingEl.value.style.minWidth = `${rects.reference.width}px`
10+
11+
return { x, y }
12+
},
13+
}
14+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Middleware, Placement, Strategy } from '@floating-ui/vue'
2+
import type { ExtractPropTypes, PropType, Ref } from 'vue'
3+
4+
export const floatingProps = {
5+
referenceEl: {
6+
type: Object as PropType<HTMLElement | any>,
7+
},
8+
9+
/**
10+
* Show/Hide floating element base on v-model value
11+
*/
12+
modelValue: {
13+
type: Boolean,
14+
default: undefined,
15+
},
16+
17+
/**
18+
* Persistence of floating element when clicked outside of reference element
19+
*/
20+
persist: {
21+
type: [Boolean, String] as PropType<boolean | 'content'>,
22+
default: false,
23+
},
24+
25+
/**
26+
* Trigger event to open the floating element
27+
*/
28+
trigger: {
29+
type: String as PropType<'click' | 'hover'>,
30+
default: 'click',
31+
},
32+
33+
/**
34+
* Transition to add while showing/hiding floating element
35+
*/
36+
transition: {
37+
type: [String, null] as PropType<string | null>,
38+
default: 'slide-up',
39+
},
40+
41+
// -- Floating UI based Props
42+
43+
// https://floating-ui.com/docs/computePosition#placement
44+
/**
45+
* Placement option from Floating UI
46+
*/
47+
placement: {
48+
type: String as PropType<Placement>,
49+
default: 'bottom-start',
50+
},
51+
52+
// https://floating-ui.com/docs/computePosition#strategy
53+
/**
54+
* Strategy option from Floating UI
55+
*/
56+
strategy: {
57+
type: String as PropType<Strategy>,
58+
default: 'absolute',
59+
},
60+
61+
// https://floating-ui.com/docs/tutorial#middleware
62+
/**
63+
* Middleware option from Floating UI
64+
*/
65+
middleware: Function as PropType<(referenceEl: Ref<HTMLElement>, floatingEl: Ref<HTMLElement>) => Middleware[]>,
66+
}
67+
68+
export type FloatingProps = ExtractPropTypes<typeof floatingProps>

0 commit comments

Comments
 (0)