@mozaic-ds/vue
Version:
Mozaic-Vue is the Vue.js implementation of ADEO Design system
136 lines (108 loc) • 3.52 kB
text/typescript
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,
};
};