Skip to content

Commit

Permalink
feat(floating): new component
Browse files Browse the repository at this point in the history
  • Loading branch information
jd-solanki committed Feb 9, 2023
1 parent 93372da commit 5eba736
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 167 deletions.
131 changes: 131 additions & 0 deletions packages/anu-vue/src/components/floating/AFloating.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<script lang="ts" setup>
import type { Middleware } from '@floating-ui/vue'
import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
import { onClickOutside, useEventListener, useMounted } from '@vueuse/core'
import { ref } from 'vue'
import { sameWidth as sameWidthMiddleware } from './middlewares'
import { floatingProps } from './props'
import { useTeleport } from '@/composables/useTeleport'
import { useInternalBooleanState } from '@/composables/useInternalBooleanState'
const props = defineProps(floatingProps)
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
defineOptions({
name: 'AFloating',
})
const { teleportTarget } = useTeleport()
const isMounted = useMounted()
const { internalState: isFloatingElVisible, toggle: toggleFloatingElVisibility } = useInternalBooleanState(toRef(props, 'modelValue'), emit, 'update:modelValue', false)
// Template refs
// const props.referenceEl = ref()
const refFloating = ref()
/*
ℹ️ We need to construct the internal middleware variable
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
Here, we will use user's middleware if passed via props or we will use our defaults
*/
const _middleware = props.middleware === undefined
? [
// ℹ️ For this we need need bridge to handle keep menu content open
// offset(6),
sameWidthMiddleware(refFloating),
flip(),
shift({ padding: 10 }),
] as Middleware[]
: props.middleware(props.referenceEl, refFloating)
// Calculate position of floating element
const { x, y, strategy } = useFloating(toRef(props, 'referenceEl'), refFloating, {
strategy: toRef(props, 'strategy'),
placement: toRef(props, 'placement'),
middleware: _middleware,
whileElementsMounted: autoUpdate,
})
// onMounted(() => {
// const vm = getCurrentInstance()
// console.log('vm?.proxy?.$el :>> ', vm?.proxy?.$parent)
// console.log('vm?.proxy?.$parent.$el :>> ', vm?.proxy?.$parent.$el)
// if (vm?.proxy?.$parent)
// props.referenceEl.value = vm?.proxy?.$parent.$el
// })
// 👉 Event listeners
/*
If moduleValue is provided don't attach any event to modify the visibility of menu
props.modelValue === undefined => modelValue isn't provided
*/
if (props.modelValue === undefined) {
// If trigger is hover
if (props.trigger === 'hover') {
// Reference
useEventListener(toRef(props, 'referenceEl'), 'mouseenter', () => {
if (isFloatingElVisible.value === false)
toggleFloatingElVisibility()
})
useEventListener(toRef(props, 'referenceEl'), 'mouseleave', () => {
if (isFloatingElVisible.value === true)
toggleFloatingElVisibility()
})
// Floating
useEventListener(refFloating, 'mouseenter', () => {
if (isFloatingElVisible.value === false)
toggleFloatingElVisibility()
})
useEventListener(refFloating, 'mouseleave', () => {
if (isFloatingElVisible.value === true)
toggleFloatingElVisibility()
})
}
else {
useEventListener(toRef(props, 'referenceEl'), 'click', toggleFloatingElVisibility)
if (props.persist !== true) {
onClickOutside(
toRef(props, 'referenceEl'),
_event => {
if (isFloatingElVisible.value)
toggleFloatingElVisibility()
},
{
ignore: props.persist === 'content' ? [refFloating] : [],
},
)
}
}
}
</script>

<template>
<Teleport
v-if="isMounted"
:to="teleportTarget"
>
<!-- ℹ️ Transition component don't accept null as value of name prop so we need `props.transition || undefined` -->
<Transition :name="props.transition || undefined">
<div
v-show="props.modelValue ?? isFloatingElVisible"
ref="refFloating"
class="a-floating"
:style="{
top: `${y ?? 0}px`,
left: `${x ?? 0}px`,
}"
:class="strategy"
>
<slot />
</div>
</Transition>
</Teleport>
</template>
2 changes: 2 additions & 0 deletions packages/anu-vue/src/components/floating/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as AFloating } from './AFloating.vue'
export * from './props'
14 changes: 14 additions & 0 deletions packages/anu-vue/src/components/floating/middlewares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ElementRects } from '@floating-ui/vue'
import type { Ref } from 'vue'

export const sameWidth = (floatingEl: Ref<HTMLElement>) => {
return {
name: 'sameWidth',
fn: ({ rects, x, y }: { rects: ElementRects; x: number; y: number }) => {
// Set width of reference to floating
floatingEl.value.style.minWidth = `${rects.reference.width}px`

return { x, y }
},
}
}
68 changes: 68 additions & 0 deletions packages/anu-vue/src/components/floating/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Middleware, Placement, Strategy } from '@floating-ui/vue'
import type { ExtractPropTypes, PropType, Ref } from 'vue'

export const floatingProps = {
referenceEl: {
type: Object as PropType<HTMLElement | any>,
},

/**
* Show/Hide floating element base on v-model value
*/
modelValue: {
type: Boolean,
default: undefined,
},

/**
* Persistence of floating element when clicked outside of reference element
*/
persist: {
type: [Boolean, String] as PropType<boolean | 'content'>,
default: false,
},

/**
* Trigger event to open the floating element
*/
trigger: {
type: String as PropType<'click' | 'hover'>,
default: 'click',
},

/**
* Transition to add while showing/hiding floating element
*/
transition: {
type: [String, null] as PropType<string | null>,
default: 'slide-up',
},

// -- Floating UI based Props

// https://floating-ui.com/docs/computePosition#placement
/**
* Placement option from Floating UI
*/
placement: {
type: String as PropType<Placement>,
default: 'bottom-start',
},

// https://floating-ui.com/docs/computePosition#strategy
/**
* Strategy option from Floating UI
*/
strategy: {
type: String as PropType<Strategy>,
default: 'absolute',
},

// https://floating-ui.com/docs/tutorial#middleware
/**
* Middleware option from Floating UI
*/
middleware: Function as PropType<(referenceEl: Ref<HTMLElement>, floatingEl: Ref<HTMLElement>) => Middleware[]>,
}

export type FloatingProps = ExtractPropTypes<typeof floatingProps>
Loading

0 comments on commit 5eba736

Please sign in to comment.