import { isElement, isHTMLElement } from "@floating-ui/utils/dom"; import { activeElement } from "../../dom/active-element"; import { contains } from "../../dom/contains"; import { getTarget } from "../../dom/get-target"; import { isTestEnv } from "../detect-browser"; import type { PopupTriggerMap } from "../popup-trigger-map"; import { FOCUSABLE_ATTRIBUTE, TYPEABLE_SELECTOR } from "./constants"; export { activeElement, contains, getTarget }; export function isTargetInsideEnabledTrigger( target: EventTarget | null, triggerElements: PopupTriggerMap, ): boolean { if (!isElement(target)) { return false; } const targetElement = target as Element; if (triggerElements.hasElement(targetElement)) { return !targetElement.hasAttribute("data-trigger-disabled"); } for (const [, trigger] of triggerElements.entries()) { if (contains(trigger, targetElement)) { return !trigger.hasAttribute("data-trigger-disabled"); } } return false; } export function isEventTargetWithin( event: Event, node: Node | null | undefined, ): boolean { if (node == null) { return false; } if ("composedPath" in event) { return event.composedPath().includes(node); } // TS narrows `event` to `never` because it assumes every browser has // `composedPath`, but shadow-DOM-less environments don't. const eventAgain = event as Event; return eventAgain.target != null && node.contains(eventAgain.target as Node); } export function isRootElement(element: Element): boolean { return element.matches("html,body"); } export function isTypeableElement(element: unknown): boolean { return isHTMLElement(element) && element.matches(TYPEABLE_SELECTOR); } export function isInteractiveElement(element: Element | null): boolean { return ( element?.closest( `button,a[href],[role="button"],select,[tabindex]:not([tabindex="-1"]),${TYPEABLE_SELECTOR}`, ) != null ); } export function isTypeableCombobox(element: Element | null): boolean { if (!element) { return false; } return ( element.getAttribute("role") === "combobox" && isTypeableElement(element) ); } /** * `:focus-visible` is unreliable in happy-dom / jsdom (the test DOMs don't * distinguish keyboard vs mouse focus), so we short-circuit to `true` there. * Matches the React source's `isJSDOM` gate but broadened to cover happy-dom * too — see `detect-browser.ts`. */ export function matchesFocusVisible(element: Element | null): boolean { if (!element || isTestEnv) { return true; } try { return element.matches(":focus-visible"); } catch (_e) { return true; } } export function getFloatingFocusElement( floatingElement: HTMLElement | null | undefined, ): HTMLElement | null { if (!floatingElement) { return null; } // Try to find the element that has `{...getFloatingProps()}` spread on // it. This indicates the floating element is acting as a positioning // wrapper, so focus should be managed on the inner child with the event // handlers and aria props. return floatingElement.hasAttribute(FOCUSABLE_ATTRIBUTE) ? floatingElement : floatingElement.querySelector(`[${FOCUSABLE_ATTRIBUTE}]`) || floatingElement; }