// SPDX-License-Identifier: FSL-1.1-MIT import { defaultWindow } from '@vueuse/core' let keyboard = false defaultWindow?.addEventListener('keydown', () => (keyboard = true), { capture: true, passive: true }) defaultWindow?.addEventListener('pointerdown', () => (keyboard = false), { capture: true, passive: true }) /** Whether the last input was a key rather than a pointer, tracked from load so it holds the input that opened an overlay. */ export function usingKeyboard(): boolean { return keyboard } type OutsideEvent = CustomEvent<{ originalEvent: PointerEvent | FocusEvent }> /** Whether a pointer press is a right click, or a control-click that opens the same menu on a Mac. */ export function isRightClick(event: PointerEvent | FocusEvent): boolean { return 'button' in event && (event.button === 2 || (event.button === 0 && event.ctrlKey)) } // Focusing the element that already has focus changes nothing, its ring included. function focusWithoutRing(target: HTMLElement | undefined) { if (target && target !== target.ownerDocument.activeElement) target.focus({ preventScroll: true, focusVisible: false }) } /** * Gives focus back to an overlay's trigger where Reka would, but after a pointer without a focus ring: Chrome and Safari ring a script's focus until something on the page took focus from a pointer, * which is so for an overlay open on load and for a select, whose trigger a click never focuses. After a key, Reka's return stays, ring and all. * `counts` names the outside interactions after which Reka leaves focus where it went, `when` whether Reka gives focus back to the trigger at all, `defer` keeps Reka's return a task later. */ export function useReturnFocus(trigger: () => HTMLElement | undefined, options: { counts?: (event: OutsideEvent) => boolean; when?: () => boolean; defer?: boolean } = {}) { let interacted = false const returns = () => { const back = !interacted && (options.when?.() ?? true) interacted = false return back } return { onInteractOutside(event: OutsideEvent) { if (!event.defaultPrevented && options.counts?.(event)) interacted = true }, /** For an overlay whose trigger Reka focuses as it closes, outside any event that could be cancelled: focuses it first, so Reka's focus changes nothing. */ focusAhead() { if (returns() && !keyboard) focusWithoutRing(trigger()) }, onCloseAutoFocus(event: Event) { const back = returns() if (event.defaultPrevented || keyboard || !back) return event.preventDefault() const target = trigger() if (options.defer) setTimeout(() => focusWithoutRing(target), 0) else focusWithoutRing(target) }, } }