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