// Copyright (c) Meta Platforms, Inc. and affiliates. 'use client'; /** * @file useInputContainer.ts * @input Container ref, input ref * @output onClick and onMouseUp handlers for the input container wrapper * @position Hook for input wrapper containers that delegate focus to the inner input/textarea * * Uses useClickableContainer to safely handle nested interactive elements * (clear buttons, calendar toggles, etc.) while focusing the input when * clicking non-interactive areas (icons, padding, status indicators). */ import { useCallback, type RefObject } from 'react'; import { useClickableContainer } from "./useClickableContainer.js"; /** * Input types that should receive `.focus()` when the container is clicked. * Other input types (e.g. checkbox, radio, file) use `.click()` instead. */ const FOCUS_INPUT_TYPES = new Set(['text', 'password', 'email', 'number', 'search', 'tel', 'url', 'date', 'datetime-local', 'month', 'time', 'week']); /** * `aria-haspopup` values that advertise a control opens a popup on activation. * https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup */ const HASPOPUP_VALUES = new Set(['true', 'menu', 'listbox', 'tree', 'grid', 'dialog']); /** * Whether an element is a popup trigger — a combobox or any control that * advertises `aria-haspopup`. Such controls activate their popup on *click* * (e.g. DateInput's `` * opens its calendar via `onClick`, with no `onFocus` opener). Forwarding a * container click to `.focus()` would focus the control but leave the popup * closed — so we `.click()` these instead. Plain text inputs (no popup) are * unaffected and keep `.focus()`. */ function isPopupTrigger(el: HTMLElement): boolean { if (el.getAttribute('role') === 'combobox') { return true; } const haspopup = el.getAttribute('aria-haspopup'); return haspopup != null && HASPOPUP_VALUES.has(haspopup); } export interface UseInputContainerOptions { /** Ref to the outer container/wrapper element */ containerRef: RefObject; /** Ref to the inner input or textarea element */ inputRef: RefObject; /** Whether the input is disabled */ disabled?: boolean; } /** * Hook that makes an input container wrapper clickable, delegating focus * to the inner input/textarea when the user clicks non-interactive areas * (icons, padding, status indicators). * * Nested interactive elements (clear buttons, links) are handled safely * via useClickableContainer — clicking them does NOT steal focus. * * @compositionHint Use inside input wrapper components (TextInput, * NumberInput, TimeInput, TextArea, etc.). * * @example * ``` * const containerRef = useRef(null); * const inputRef = useRef(null); * const { onClick, onMouseUp } = useInputContainer({ * containerRef, * inputRef, * }); * return ( *
* * *
* ); * ``` */ export function useInputContainer({ containerRef, inputRef, disabled = false }: UseInputContainerOptions) { const onClick = useCallback(() => { const input = inputRef.current; if (input == null) { return; } // Popup triggers (combobox / aria-haspopup) activate their popup on // *click*, not focus — so check this BEFORE the type check, otherwise a // `type="text"` combobox (DateInput) would `.focus()` and never open. if (input instanceof HTMLElement && isPopupTrigger(input)) { input.click(); } else if (input instanceof HTMLInputElement && FOCUS_INPUT_TYPES.has(input.type)) { input.focus(); } else if (input instanceof HTMLTextAreaElement) { input.focus(); } else if (input instanceof HTMLElement) { input.click(); } else if ('focus' in input) { (input as unknown as { focus: () => void; }).focus(); } }, [inputRef]); return useClickableContainer({ containerRef, interactiveRef: inputRef, onClick, disabled }); }