@zag-js/focus-visible
Advanced tools
Comparing version
type Modality = "keyboard" | "pointer" | "virtual"; | ||
type FocusVisibleCallback = (isFocusVisible: boolean) => void; | ||
declare function trackFocusVisible(fn: FocusVisibleCallback): () => void; | ||
declare function trackInteractionModality(fn: (value: Modality | null) => void): () => void; | ||
declare function setInteractionModality(value: Modality): void; | ||
type RootNode = Document | ShadowRoot | Node; | ||
interface GlobalListenerData { | ||
focus: VoidFunction; | ||
} | ||
declare let listenerMap: Map<Window, GlobalListenerData>; | ||
declare function getInteractionModality(): Modality | null; | ||
declare function setInteractionModality(modality: Modality): void; | ||
interface InteractionModalityChangeDetails { | ||
/** The modality of the interaction that caused the focus to be visible. */ | ||
modality: Modality | null; | ||
} | ||
interface InteractionModalityProps { | ||
/** The root element to track focus visibility for. */ | ||
root?: RootNode | undefined; | ||
/** Callback to be called when the interaction modality changes. */ | ||
onChange: (details: InteractionModalityChangeDetails) => void; | ||
} | ||
declare function trackInteractionModality(props: InteractionModalityProps): VoidFunction; | ||
declare function isFocusVisible(): boolean; | ||
interface FocusVisibleChangeDetails { | ||
/** Whether keyboard focus is visible globally. */ | ||
isFocusVisible: boolean; | ||
/** The modality of the interaction that caused the focus to be visible. */ | ||
modality: Modality | null; | ||
} | ||
interface FocusVisibleProps { | ||
/** The root element to track focus visibility for. */ | ||
root?: RootNode | undefined; | ||
/** Whether the element is a text input. */ | ||
isTextInput?: boolean | undefined; | ||
/** Whether the element will be auto focused. */ | ||
autoFocus?: boolean | undefined; | ||
/** Callback to be called when the focus visibility changes. */ | ||
onChange?: ((details: FocusVisibleChangeDetails) => void) | undefined; | ||
} | ||
declare function trackFocusVisible(props?: FocusVisibleProps): VoidFunction; | ||
export { getInteractionModality, setInteractionModality, trackFocusVisible, trackInteractionModality }; | ||
export { type FocusVisibleChangeDetails, type FocusVisibleProps, type InteractionModalityChangeDetails, type InteractionModalityProps, type Modality, getInteractionModality, isFocusVisible, listenerMap, setInteractionModality, trackFocusVisible, trackInteractionModality }; |
@@ -1,83 +0,62 @@ | ||
"use strict"; | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
var __export = (target, all) => { | ||
for (var name in all) | ||
__defProp(target, name, { get: all[name], enumerable: true }); | ||
}; | ||
var __copyProps = (to, from, except, desc) => { | ||
if (from && typeof from === "object" || typeof from === "function") { | ||
for (let key of __getOwnPropNames(from)) | ||
if (!__hasOwnProp.call(to, key) && key !== except) | ||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
} | ||
return to; | ||
}; | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
'use strict'; | ||
var domQuery = require('@zag-js/dom-query'); | ||
// src/index.ts | ||
var src_exports = {}; | ||
__export(src_exports, { | ||
getInteractionModality: () => getInteractionModality, | ||
setInteractionModality: () => setInteractionModality, | ||
trackFocusVisible: () => trackFocusVisible, | ||
trackInteractionModality: () => trackInteractionModality | ||
}); | ||
module.exports = __toCommonJS(src_exports); | ||
var import_dom_query = require("@zag-js/dom-query"); | ||
var hasSetup = false; | ||
var modality = null; | ||
function isVirtualClick(event) { | ||
if (event.mozInputSource === 0 && event.isTrusted) return true; | ||
return event.detail === 0 && !event.pointerType; | ||
} | ||
function isValidKey(e) { | ||
return !(e.metaKey || !domQuery.isMac() && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta"); | ||
} | ||
var nonTextInputTypes = /* @__PURE__ */ new Set(["checkbox", "radio", "range", "color", "file", "image", "button", "submit", "reset"]); | ||
function isKeyboardFocusEvent(isTextInput, modality, e) { | ||
const target = e ? domQuery.getEventTarget(e) : null; | ||
const win = domQuery.getWindow(target); | ||
isTextInput = isTextInput || target instanceof win.HTMLInputElement && !nonTextInputTypes.has(target?.type) || target instanceof win.HTMLTextAreaElement || target instanceof win.HTMLElement && target.isContentEditable; | ||
return !(isTextInput && modality === "keyboard" && e instanceof win.KeyboardEvent && !Reflect.has(FOCUS_VISIBLE_INPUT_KEYS, e.key)); | ||
} | ||
var currentModality = null; | ||
var changeHandlers = /* @__PURE__ */ new Set(); | ||
var listenerMap = /* @__PURE__ */ new Map(); | ||
var hasEventBeforeFocus = false; | ||
var hasBlurredWindowRecently = false; | ||
var handlers = /* @__PURE__ */ new Set(); | ||
function trigger(modality2, event) { | ||
handlers.forEach((handler) => handler(modality2, event)); | ||
var FOCUS_VISIBLE_INPUT_KEYS = { | ||
Tab: true, | ||
Escape: true | ||
}; | ||
function triggerChangeHandlers(modality, e) { | ||
for (let handler of changeHandlers) { | ||
handler(modality, e); | ||
} | ||
} | ||
var isMac = typeof window !== "undefined" && window.navigator != null ? /^Mac/.test(window.navigator.platform) : false; | ||
function isValidKey(e) { | ||
return !(e.metaKey || !isMac && e.altKey || e.ctrlKey || e.key === "Control" || e.key === "Shift" || e.key === "Meta"); | ||
} | ||
function onKeyboardEvent(event) { | ||
function handleKeyboardEvent(e) { | ||
hasEventBeforeFocus = true; | ||
if (isValidKey(event)) { | ||
modality = "keyboard"; | ||
trigger("keyboard", event); | ||
if (isValidKey(e)) { | ||
currentModality = "keyboard"; | ||
triggerChangeHandlers("keyboard", e); | ||
} | ||
} | ||
function onPointerEvent(event) { | ||
modality = "pointer"; | ||
if (event.type === "mousedown" || event.type === "pointerdown") { | ||
function handlePointerEvent(e) { | ||
currentModality = "pointer"; | ||
if (e.type === "mousedown" || e.type === "pointerdown") { | ||
hasEventBeforeFocus = true; | ||
const target = event.composedPath ? event.composedPath()[0] : event.target; | ||
let matches = false; | ||
try { | ||
matches = target.matches(":focus-visible"); | ||
} catch { | ||
} | ||
if (matches) return; | ||
trigger("pointer", event); | ||
triggerChangeHandlers("pointer", e); | ||
} | ||
} | ||
function isVirtualClick(event) { | ||
if (event.mozInputSource === 0 && event.isTrusted) return true; | ||
return event.detail === 0 && !event.pointerType; | ||
} | ||
function onClickEvent(e) { | ||
function handleClickEvent(e) { | ||
if (isVirtualClick(e)) { | ||
hasEventBeforeFocus = true; | ||
modality = "virtual"; | ||
currentModality = "virtual"; | ||
} | ||
} | ||
function onWindowFocus(event) { | ||
if (event.target === window || event.target === document) { | ||
function handleFocusEvent(e) { | ||
const target = domQuery.getEventTarget(e); | ||
if (target === domQuery.getWindow(target) || target === domQuery.getDocument(target)) { | ||
return; | ||
} | ||
if (event.target instanceof Element && event.target.hasAttribute("tabindex")) { | ||
return; | ||
} | ||
if (!hasEventBeforeFocus && !hasBlurredWindowRecently) { | ||
modality = "virtual"; | ||
trigger("virtual", event); | ||
currentModality = "virtual"; | ||
triggerChangeHandlers("virtual", e); | ||
} | ||
@@ -87,66 +66,104 @@ hasEventBeforeFocus = false; | ||
} | ||
function onWindowBlur() { | ||
function handleWindowBlur() { | ||
hasEventBeforeFocus = false; | ||
hasBlurredWindowRecently = true; | ||
} | ||
function isFocusVisible() { | ||
return modality !== "pointer"; | ||
} | ||
function setupGlobalFocusEvents() { | ||
if (!(0, import_dom_query.isDom)() || hasSetup) { | ||
function setupGlobalFocusEvents(root) { | ||
if (typeof window === "undefined" || listenerMap.get(domQuery.getWindow(root))) { | ||
return; | ||
} | ||
const { focus } = HTMLElement.prototype; | ||
HTMLElement.prototype.focus = function focusElement(...args) { | ||
const win = domQuery.getWindow(root); | ||
const doc = domQuery.getDocument(root); | ||
let focus = win.HTMLElement.prototype.focus; | ||
win.HTMLElement.prototype.focus = function() { | ||
currentModality = "virtual"; | ||
triggerChangeHandlers("virtual", null); | ||
hasEventBeforeFocus = true; | ||
focus.apply(this, args); | ||
focus.apply(this, arguments); | ||
}; | ||
document.addEventListener("keydown", onKeyboardEvent, true); | ||
document.addEventListener("keyup", onKeyboardEvent, true); | ||
document.addEventListener("click", onClickEvent, true); | ||
window.addEventListener("focus", onWindowFocus, true); | ||
window.addEventListener("blur", onWindowBlur, false); | ||
if (typeof PointerEvent !== "undefined") { | ||
document.addEventListener("pointerdown", onPointerEvent, true); | ||
document.addEventListener("pointermove", onPointerEvent, true); | ||
document.addEventListener("pointerup", onPointerEvent, true); | ||
doc.addEventListener("keydown", handleKeyboardEvent, true); | ||
doc.addEventListener("keyup", handleKeyboardEvent, true); | ||
doc.addEventListener("click", handleClickEvent, true); | ||
win.addEventListener("focus", handleFocusEvent, true); | ||
win.addEventListener("blur", handleWindowBlur, false); | ||
if (typeof win.PointerEvent !== "undefined") { | ||
doc.addEventListener("pointerdown", handlePointerEvent, true); | ||
doc.addEventListener("pointermove", handlePointerEvent, true); | ||
doc.addEventListener("pointerup", handlePointerEvent, true); | ||
} else { | ||
document.addEventListener("mousedown", onPointerEvent, true); | ||
document.addEventListener("mousemove", onPointerEvent, true); | ||
document.addEventListener("mouseup", onPointerEvent, true); | ||
doc.addEventListener("mousedown", handlePointerEvent, true); | ||
doc.addEventListener("mousemove", handlePointerEvent, true); | ||
doc.addEventListener("mouseup", handlePointerEvent, true); | ||
} | ||
hasSetup = true; | ||
win.addEventListener( | ||
"beforeunload", | ||
() => { | ||
tearDownWindowFocusTracking(root); | ||
}, | ||
{ once: true } | ||
); | ||
listenerMap.set(win, { focus }); | ||
} | ||
function trackFocusVisible(fn) { | ||
setupGlobalFocusEvents(); | ||
fn(isFocusVisible()); | ||
const handler = () => fn(isFocusVisible()); | ||
handlers.add(handler); | ||
var tearDownWindowFocusTracking = (root, loadListener) => { | ||
const win = domQuery.getWindow(root); | ||
const doc = domQuery.getDocument(root); | ||
if (!listenerMap.has(win)) { | ||
return; | ||
} | ||
win.HTMLElement.prototype.focus = listenerMap.get(win).focus; | ||
doc.removeEventListener("keydown", handleKeyboardEvent, true); | ||
doc.removeEventListener("keyup", handleKeyboardEvent, true); | ||
doc.removeEventListener("click", handleClickEvent, true); | ||
win.removeEventListener("focus", handleFocusEvent, true); | ||
win.removeEventListener("blur", handleWindowBlur, false); | ||
if (typeof win.PointerEvent !== "undefined") { | ||
doc.removeEventListener("pointerdown", handlePointerEvent, true); | ||
doc.removeEventListener("pointermove", handlePointerEvent, true); | ||
doc.removeEventListener("pointerup", handlePointerEvent, true); | ||
} else { | ||
doc.removeEventListener("mousedown", handlePointerEvent, true); | ||
doc.removeEventListener("mousemove", handlePointerEvent, true); | ||
doc.removeEventListener("mouseup", handlePointerEvent, true); | ||
} | ||
listenerMap.delete(win); | ||
}; | ||
function getInteractionModality() { | ||
return currentModality; | ||
} | ||
function setInteractionModality(modality) { | ||
currentModality = modality; | ||
triggerChangeHandlers(modality, null); | ||
} | ||
function trackInteractionModality(props) { | ||
const { onChange, root } = props; | ||
setupGlobalFocusEvents(root); | ||
onChange({ modality: currentModality }); | ||
const handler = () => onChange({ modality: currentModality }); | ||
changeHandlers.add(handler); | ||
return () => { | ||
handlers.delete(handler); | ||
changeHandlers.delete(handler); | ||
}; | ||
} | ||
function trackInteractionModality(fn) { | ||
setupGlobalFocusEvents(); | ||
fn(modality); | ||
const handler = () => fn(modality); | ||
handlers.add(handler); | ||
function isFocusVisible() { | ||
return currentModality === "keyboard"; | ||
} | ||
function trackFocusVisible(props = {}) { | ||
const { isTextInput, autoFocus, onChange, root } = props; | ||
setupGlobalFocusEvents(root); | ||
onChange?.({ isFocusVisible: autoFocus || isFocusVisible(), modality: currentModality }); | ||
const handler = (modality, e) => { | ||
if (!isKeyboardFocusEvent(!!isTextInput, modality, e)) return; | ||
onChange?.({ isFocusVisible: isFocusVisible(), modality }); | ||
}; | ||
changeHandlers.add(handler); | ||
return () => { | ||
handlers.delete(handler); | ||
changeHandlers.delete(handler); | ||
}; | ||
} | ||
function setInteractionModality(value) { | ||
modality = value; | ||
trigger(value, null); | ||
} | ||
function getInteractionModality() { | ||
return modality; | ||
} | ||
// Annotate the CommonJS export names for ESM import in node: | ||
0 && (module.exports = { | ||
getInteractionModality, | ||
setInteractionModality, | ||
trackFocusVisible, | ||
trackInteractionModality | ||
}); | ||
//# sourceMappingURL=index.js.map | ||
exports.getInteractionModality = getInteractionModality; | ||
exports.isFocusVisible = isFocusVisible; | ||
exports.listenerMap = listenerMap; | ||
exports.setInteractionModality = setInteractionModality; | ||
exports.trackFocusVisible = trackFocusVisible; | ||
exports.trackInteractionModality = trackInteractionModality; |
{ | ||
"name": "@zag-js/focus-visible", | ||
"version": "0.0.0-dev-20240723090825", | ||
"version": "0.0.0-v1-beta-20250220125322", | ||
"description": "Focus visible polyfill utility based on WICG", | ||
@@ -17,4 +17,3 @@ "keywords": [ | ||
"files": [ | ||
"dist", | ||
"src" | ||
"dist" | ||
], | ||
@@ -30,3 +29,3 @@ "publishConfig": { | ||
"dependencies": { | ||
"@zag-js/dom-query": "0.0.0-dev-20240723090825" | ||
"@zag-js/dom-query": "0.0.0-v1-beta-20250220125322" | ||
}, | ||
@@ -48,4 +47,4 @@ "devDependencies": { | ||
"build": "tsup", | ||
"test": "jest --config ../../../jest.config.js --rootDir tests", | ||
"lint": "eslint src --ext .ts,.tsx", | ||
"test": "vitest", | ||
"lint": "eslint src", | ||
"test-ci": "pnpm test --ci --runInBand -u", | ||
@@ -52,0 +51,0 @@ "test-watch": "pnpm test --watchAll" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
17
6.25%18728
-44.42%7
-30%362
-13.6%1
Infinity%+ Added
+ Added
+ Added
- Removed