import type { ComputedRef, HTMLAttributes, Ref } from 'vue'; import { onBeforeUnmount, onMounted, watch, shallowRef, computed } from 'vue'; import type { FocusEventHandler } from '../../_util/EventInterface'; import KeyCode from '../../_util/KeyCode'; import { addGlobalMousedownEvent, getTargetFromEvent } from '../utils/uiUtil'; import raf from '../../_util/raf'; export default function usePickerInput({ open, value, isClickOutside, triggerOpen, forwardKeydown, onKeydown, blurToCancel, onSubmit, onCancel, onFocus, onBlur, }: { open: Ref; value: Ref; isClickOutside: (clickElement: EventTarget | null) => boolean; triggerOpen: (open: boolean) => void; forwardKeydown: (e: KeyboardEvent) => boolean; onKeydown: (e: KeyboardEvent, preventDefault: () => void) => void; blurToCancel?: ComputedRef; onSubmit: () => void | boolean; onCancel: () => void; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; }): [ComputedRef, { focused: Ref; typing: Ref }] { const typing = shallowRef(false); const focused = shallowRef(false); /** * We will prevent blur to handle open event when user click outside, * since this will repeat trigger `onOpenChange` event. */ const preventBlurRef = shallowRef(false); const valueChangedRef = shallowRef(false); const preventDefaultRef = shallowRef(false); const inputProps = computed(() => ({ onMousedown: () => { typing.value = true; triggerOpen(true); }, onKeydown: e => { const preventDefault = (): void => { preventDefaultRef.value = true; }; onKeydown(e, preventDefault); if (preventDefaultRef.value) return; switch (e.which) { case KeyCode.ENTER: { if (!open.value) { triggerOpen(true); } else if (onSubmit() !== false) { typing.value = true; } e.preventDefault(); return; } case KeyCode.TAB: { if (typing.value && open.value && !e.shiftKey) { typing.value = false; e.preventDefault(); } else if (!typing.value && open.value) { if (!forwardKeydown(e) && e.shiftKey) { typing.value = true; e.preventDefault(); } } return; } case KeyCode.ESC: { typing.value = true; onCancel(); return; } } if (!open.value && ![KeyCode.SHIFT].includes(e.which)) { triggerOpen(true); } else if (!typing.value) { // Let popup panel handle keyboard forwardKeydown(e); } }, onFocus: e => { typing.value = true; focused.value = true; if (onFocus) { onFocus(e); } }, onBlur: e => { if (preventBlurRef.value || !isClickOutside(document.activeElement)) { preventBlurRef.value = false; return; } if (blurToCancel.value) { setTimeout(() => { let { activeElement } = document; while (activeElement && activeElement.shadowRoot) { activeElement = activeElement.shadowRoot.activeElement; } if (isClickOutside(activeElement)) { onCancel(); } }, 0); } else if (open.value) { triggerOpen(false); if (valueChangedRef.value) { onSubmit(); } } focused.value = false; if (onBlur) { onBlur(e); } }, })); // check if value changed watch(open, () => { valueChangedRef.value = false; }); watch(value, () => { valueChangedRef.value = true; }); const globalMousedownEvent = shallowRef(); // Global click handler onMounted(() => { globalMousedownEvent.value = addGlobalMousedownEvent((e: MouseEvent) => { const target = getTargetFromEvent(e); if (open.value) { const clickedOutside = isClickOutside(target); if (!clickedOutside) { preventBlurRef.value = true; // Always set back in case `onBlur` prevented by user raf(() => { preventBlurRef.value = false; }); } else if (!focused.value || clickedOutside) { triggerOpen(false); } } }); }); onBeforeUnmount(() => { globalMousedownEvent.value && globalMousedownEvent.value(); }); return [inputProps, { focused, typing }]; }