83 lines
2.5 KiB
JavaScript
83 lines
2.5 KiB
JavaScript
import { computed, unref, watch } from 'vue';
|
|
import { useElementBounding, useWindowSize } from '@vueuse/core';
|
|
|
|
const FALLBACK_SIZE = 200;
|
|
const RTL_DIRECTION = 'rtl';
|
|
|
|
/**
|
|
* Auto-position dropdown based on available space within container
|
|
* Returns reactive position object containing class and style
|
|
*
|
|
* @param {Ref} triggerRef - Trigger element ref
|
|
* @param {Ref} dropdownRef - Dropdown element ref
|
|
* @param {Ref} enabled - Whether to calculate position
|
|
* @param {Object} options - { container: Ref, margin: Number }
|
|
*/
|
|
export function useDropdownPosition(
|
|
triggerRef,
|
|
dropdownRef,
|
|
enabled,
|
|
{ container = null, margin = 16 } = {}
|
|
) {
|
|
const trigger = useElementBounding(triggerRef);
|
|
const dropdown = useElementBounding(dropdownRef);
|
|
const bounds = useElementBounding(container);
|
|
const { width: winWidth, height: winHeight } = useWindowSize();
|
|
|
|
const isRTL = computed(
|
|
() =>
|
|
document.querySelector('#app[dir]')?.getAttribute('dir') === RTL_DIRECTION
|
|
);
|
|
|
|
const position = computed(() => {
|
|
// Default fallback if not enabled or refs aren't ready
|
|
if (!unref(enabled)) {
|
|
return { class: 'top-full mt-2', style: {} };
|
|
}
|
|
|
|
const dropdownHeight = dropdown.height.value || FALLBACK_SIZE;
|
|
const dropdownWidth = dropdown.width.value || FALLBACK_SIZE;
|
|
|
|
/* ---------- Vertical (Tailwind Classes) ---------- */
|
|
const spaceBelow = winHeight.value - trigger.bottom.value;
|
|
const className =
|
|
spaceBelow >= dropdownHeight + margin
|
|
? 'top-full mt-2'
|
|
: 'bottom-full mb-2';
|
|
|
|
/* ---------- Horizontal (Inline Styles for precise alignment) ---------- */
|
|
const leftBound = container ? bounds.left.value : 0;
|
|
const rightBound = container ? bounds.right.value : winWidth.value;
|
|
|
|
const style = {};
|
|
|
|
if (isRTL.value) {
|
|
const availableLeft = trigger.right.value - leftBound;
|
|
const overflow = dropdownWidth - availableLeft;
|
|
style.right = overflow > 0 ? `-${overflow}px` : '0px';
|
|
} else {
|
|
const availableRight = rightBound - trigger.left.value;
|
|
const overflow = dropdownWidth - availableRight;
|
|
style.left = overflow > 0 ? `-${overflow}px` : '0px';
|
|
}
|
|
|
|
return { class: className, style };
|
|
});
|
|
|
|
const updatePosition = () => {
|
|
trigger.update();
|
|
dropdown.update();
|
|
if (container) bounds.update();
|
|
};
|
|
|
|
// Update position when dropdown opens to ensure RTL state is current
|
|
watch(
|
|
() => unref(enabled),
|
|
isEnabled => {
|
|
if (isEnabled) updatePosition();
|
|
}
|
|
);
|
|
|
|
return { position, updatePosition };
|
|
}
|