UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

136 lines (108 loc) 3.52 kB
import { useIsMobile } from '@/utils/use-is-mobile.composable'; import { ref, type ShallowRef } from 'vue'; type Options = { allowItemHover?: boolean; position?: 'bottom' | 'top'; }; const defaultOptions: Options = { allowItemHover: true, position: 'bottom' }; export const useFloatingItem = ( trigger: ShallowRef<HTMLElement | null>, floatingItem: ShallowRef<HTMLElement | null>, options: Options = defaultOptions, ) => { const normalizedOptions = { ...defaultOptions, ...options, }; const { isMobile } = useIsMobile(); const floatingItemIsDisplayed = ref(false); function getListboxPosition() { const triggerRect = trigger.value?.getBoundingClientRect(); const floatingItemRect = floatingItem.value?.getBoundingClientRect(); if (!triggerRect || !floatingItemRect) return {}; if (isMobile.value) { return { top: `-${floatingItemRect.height + 24}px`, left: `-${floatingItemRect.width - triggerRect.width}px`, }; } return { top: options.position === 'top' ? `-${floatingItemRect.height - triggerRect.height}px` : floatingItem.value?.style.left || '0px', }; } function hideFloatingItem(event?: MouseEvent | FocusEvent) { if (!floatingItem.value || !trigger.value) return; const target = event?.relatedTarget as Node | null; // Prevent floating item from being hidden if mouse goes from trigger to listbox, unless specified if ( normalizedOptions.allowItemHover && target && (floatingItem.value.contains(target) || trigger.value.contains(target)) ) { return; } floatingItem.value.classList.add('mc-sidebar__floating-item--hidden'); floatingItemIsDisplayed.value = false; document.removeEventListener('mousedown', handleClickOutside); } function handleClickOutside(event: MouseEvent) { const target = event.target as Node; if ( !floatingItem.value?.contains(target) && !trigger.value?.contains(target) ) { hideFloatingItem(event); } } function showFloatingItem() { floatingItem.value?.classList.remove('mc-sidebar__floating-item--hidden'); floatingItemIsDisplayed.value = !floatingItem.value?.classList.contains( 'mc-sidebar__floating-item--hidden', ); if (!floatingItemIsDisplayed.value) return; const { left, top } = getListboxPosition(); if (!floatingItem.value) return; if (top) { floatingItem.value.style.top = top; } if (left) { floatingItem.value.style.left = left; } floatingItemIsDisplayed.value = true; document.addEventListener('mousedown', handleClickOutside); } function onTriggerKeydown(event: KeyboardEvent) { if (!floatingItem.value) return; if (['ArrowDown', 'Enter', ' '].includes(event.key)) { event.preventDefault(); showFloatingItem(); focusFirstListboxItem(); } if (event.key === 'Tab') { hideFloatingItem(); } } function focusFirstListboxItem() { const firstItem = floatingItem.value?.querySelector<HTMLElement>( 'button, [href], [tabindex]:not([tabindex="-1"])', ); firstItem?.focus(); } function onListboxKeydown(event: KeyboardEvent) { if (event.key === 'Escape') { event.preventDefault(); hideFloatingItem(); trigger.value?.focus(); } } return { floatingItemIsDisplayed, hideFloatingItem, showFloatingItem, onTriggerKeydown, onListboxKeydown, }; };