// 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
});
}