UNPKG

@ariakit/react-core

Version:

Ariakit React core

313 lines (310 loc) 9.55 kB
"use client"; import { FocusableContext } from "./SWN3JYXT.js"; import { createElement, createHook, forwardRef } from "./L4OUMOCQ.js"; import { useEvent, useMergeRefs, useMetadataProps, useTagName } from "./W2TDKEPX.js"; // src/focusable/focusable.tsx import { addGlobalEventListener, isFocusEventOutside, isSelfTarget, queueBeforeEvent } from "@ariakit/core/utils/events"; import { hasFocus, isFocusable } from "@ariakit/core/utils/focus"; import { disabledFromProps, removeUndefinedValues } from "@ariakit/core/utils/misc"; import { isSafari } from "@ariakit/core/utils/platform"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; var TagName = "div"; var accessibleWhenDisabledSymbol = /* @__PURE__ */ Symbol("accessibleWhenDisabled"); var isSafariBrowser = isSafari(); var alwaysFocusVisibleInputTypes = [ "text", "search", "url", "tel", "email", "password", "number", "date", "month", "week", "time", "datetime", "datetime-local" ]; function isAlwaysFocusVisible(element) { const { tagName, readOnly, type } = element; if (tagName === "TEXTAREA" && !readOnly) return true; if (tagName === "SELECT" && !readOnly) return true; if (tagName === "INPUT" && !readOnly) { return alwaysFocusVisibleInputTypes.includes(type); } if (element.isContentEditable) return true; const role = element.getAttribute("role"); if (role === "combobox" && element.dataset.name) { return true; } return false; } function isNativeTabbable(tagName) { if (!tagName) return true; return tagName === "button" || tagName === "summary" || tagName === "input" || tagName === "select" || tagName === "textarea" || tagName === "a"; } function supportsDisabledAttribute(tagName) { if (!tagName) return true; return tagName === "button" || tagName === "input" || tagName === "select" || tagName === "textarea"; } var buttonInputTypes = [ "button", "color", "file", "image", "reset", "submit" ]; function needsSafariTabIndex(tagName, inputType) { if (tagName === "button") return true; if (tagName === "input" && inputType) { if (inputType === "checkbox" || inputType === "radio") return true; return buttonInputTypes.includes(inputType); } return false; } function getTabIndex({ focusable, trulyDisabled, nativeTabbable, supportsDisabled, safariTabIndex, tabIndexProp }) { if (!focusable) return tabIndexProp; if (trulyDisabled) { if (nativeTabbable && !supportsDisabled) { return -1; } return; } if (nativeTabbable) { if (safariTabIndex && tabIndexProp == null) { return 0; } return tabIndexProp; } return tabIndexProp != null ? tabIndexProp : 0; } function useDisableEvent(onEvent, disabled) { return useEvent((event) => { onEvent == null ? void 0 : onEvent(event); if (event.defaultPrevented) return; if (disabled) { event.stopPropagation(); event.preventDefault(); } }); } var hasInstalledGlobalEventListeners = false; var isKeyboardModality = true; function onGlobalMouseDown(event) { const target = event.target; if (target && "hasAttribute" in target) { if (!target.hasAttribute("data-focus-visible")) { isKeyboardModality = false; } } } function onGlobalKeyDown(event) { if (event.metaKey) return; if (event.ctrlKey) return; if (event.altKey) return; isKeyboardModality = true; } var useFocusable = createHook( function useFocusable2({ focusable = true, accessibleWhenDisabled, autoFocus, onFocusVisible, ...props }) { const ref = useRef(null); const [parentAccessibleWhenDisabled, metadataProps] = useMetadataProps( props, accessibleWhenDisabledSymbol, accessibleWhenDisabled ); accessibleWhenDisabled != null ? accessibleWhenDisabled : accessibleWhenDisabled = parentAccessibleWhenDisabled; useEffect(() => { if (!focusable) return; if (hasInstalledGlobalEventListeners) return; addGlobalEventListener("mousedown", onGlobalMouseDown, true); addGlobalEventListener("keydown", onGlobalKeyDown, true); hasInstalledGlobalEventListeners = true; }, [focusable]); const disabled = focusable && disabledFromProps(props); const trulyDisabled = disabled && !accessibleWhenDisabled; const [focusVisible, setFocusVisible] = useState(false); useEffect(() => { if (!focusable) return; if (trulyDisabled && focusVisible) { setFocusVisible(false); } }, [focusable, trulyDisabled, focusVisible]); useEffect(() => { if (!focusable) return; if (!focusVisible) return; const element = ref.current; if (!element) return; if (typeof IntersectionObserver === "undefined") return; const observer = new IntersectionObserver(() => { if (!isFocusable(element)) { setFocusVisible(false); } }); observer.observe(element); return () => observer.disconnect(); }, [focusable, focusVisible]); const onKeyPressCapture = useDisableEvent( props.onKeyPressCapture, disabled ); const onMouseDownCapture = useDisableEvent( props.onMouseDownCapture, disabled ); const onClickCapture = useDisableEvent(props.onClickCapture, disabled); const handleFocusVisible = (event, currentTarget) => { if (currentTarget) { event.currentTarget = currentTarget; } if (!focusable) return; const element = event.currentTarget; if (!element) return; if (!hasFocus(element)) return; onFocusVisible == null ? void 0 : onFocusVisible(event); if (event.defaultPrevented) return; element.dataset.focusVisible = "true"; setFocusVisible(true); }; const onKeyDownCaptureProp = props.onKeyDownCapture; const onKeyDownCapture = useEvent((event) => { onKeyDownCaptureProp == null ? void 0 : onKeyDownCaptureProp(event); if (event.defaultPrevented) return; if (!focusable) return; if (focusVisible) return; if (event.metaKey) return; if (event.altKey) return; if (event.ctrlKey) return; if (!isSelfTarget(event)) return; const element = event.currentTarget; const applyFocusVisible = () => handleFocusVisible(event, element); queueBeforeEvent(element, "focusout", applyFocusVisible); }); const onFocusCaptureProp = props.onFocusCapture; const onFocusCapture = useEvent((event) => { onFocusCaptureProp == null ? void 0 : onFocusCaptureProp(event); if (event.defaultPrevented) return; if (!focusable) return; if (!isSelfTarget(event)) { setFocusVisible(false); return; } const element = event.currentTarget; const applyFocusVisible = () => handleFocusVisible(event, element); if (isKeyboardModality || isAlwaysFocusVisible(event.target)) { queueBeforeEvent(event.target, "focusout", applyFocusVisible); } else { setFocusVisible(false); } }); const onBlurProp = props.onBlur; const onBlur = useEvent((event) => { onBlurProp == null ? void 0 : onBlurProp(event); if (!focusable) return; if (!isFocusEventOutside(event)) return; event.currentTarget.removeAttribute("data-focus-visible"); setFocusVisible(false); }); const autoFocusOnShow = useContext(FocusableContext); const autoFocusRef = useEvent((element) => { if (!focusable) return; if (!autoFocus) return; if (!element) return; if (!autoFocusOnShow) return; queueMicrotask(() => { if (hasFocus(element)) return; if (!isFocusable(element)) return; element.focus(); }); }); const tagName = useTagName(ref); const nativeTabbable = focusable && isNativeTabbable(tagName); const supportsDisabled = focusable && supportsDisabledAttribute(tagName); const [safariTabIndex, setSafariTabIndex] = useState(false); if (isSafariBrowser) { useEffect(() => { if (!focusable) return; const element = ref.current; if (!element) return; const tag = element.tagName.toLowerCase(); const type = element.type; setSafariTabIndex(needsSafariTabIndex(tag, type)); }, [focusable]); } const styleProp = props.style; const style = useMemo(() => { if (trulyDisabled) { return { pointerEvents: "none", ...styleProp }; } return styleProp; }, [trulyDisabled, styleProp]); props = { "data-focus-visible": focusable && focusVisible || void 0, "data-autofocus": autoFocus || void 0, "aria-disabled": disabled || void 0, ...props, ...metadataProps, ref: useMergeRefs(ref, autoFocusRef, props.ref), style, tabIndex: getTabIndex({ focusable, trulyDisabled, nativeTabbable, supportsDisabled, safariTabIndex, tabIndexProp: props.tabIndex }), disabled: supportsDisabled && trulyDisabled ? true : void 0, // TODO: Test Focusable contentEditable. contentEditable: disabled ? void 0 : props.contentEditable, onKeyPressCapture, onClickCapture, onMouseDownCapture, onKeyDownCapture, onFocusCapture, onBlur }; return removeUndefinedValues(props); } ); var Focusable = forwardRef(function Focusable2(props) { const htmlProps = useFocusable(props); return createElement(TagName, htmlProps); }); export { useFocusable, Focusable };