UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

164 lines (163 loc) • 5.87 kB
import { globalFocusRingCSS } from "./style.mjs"; //#region src/GlobalFocusRing/index.ts const installations = /* @__PURE__ */ new WeakMap(); /** Install once per document. The returned cleanup is safe for nested providers and Strict Mode. */ const installGlobalFocusRing = (doc = document) => { const existing = installations.get(doc); if (existing) { existing.users += 1; let released = false; return () => { if (released) return; released = true; if (--existing.users === 0) existing.dispose(); }; } const win = doc.defaultView; if (!win || !doc.body || !win.CSS?.supports("top", "anchor(top)") || !win.CSS.supports("position-visibility", "anchors-visible") || !("showPopover" in doc.createElement("div"))) return () => {}; const sheet = doc.createElement("style"); sheet.textContent = globalFocusRingCSS; const ring = doc.createElement("div"); ring.setAttribute("data-lobe-global-focus-ring", ""); ring.setAttribute("aria-hidden", "true"); ring.setAttribute("popover", "manual"); doc.head.append(sheet); doc.body.append(ring); let target; let restoreTarget; let frame = 0; let disposed = false; const clear = () => { if (ring.matches(":popover-open")) ring.hidePopover(); restoreTarget?.(); restoreTarget = void 0; target = void 0; }; const sync = () => { if (disposed) return; const next = doc.activeElement; if (next === target && next?.matches(":focus-visible") && next.isConnected && ring.matches(":popover-open")) return; clear(); if (!next || next === doc.body || next.namespaceURI !== "http://www.w3.org/1999/xhtml" || next.shadowRoot || next.closest("[data-lobe-focus-ring=\"off\"]") || next.matches("textarea") || next.isContentEditable || next.matches("input") && ![ "button", "submit", "reset", "image", "checkbox", "radio", "range", "color", "file" ].includes(next.type) || !next.matches(":focus-visible")) return; const element = next; let scope = element.parentElement; while (scope && win.getComputedStyle(scope).contain === "none") scope = scope.parentElement; const ringParent = scope || doc.body; const moved = ring.parentElement !== ringParent; if (moved) ringParent.append(ring); const computed = win.getComputedStyle(element); const borderRadius = computed.borderRadius; const color = computed.getPropertyValue("--lobe-focus-ring-color").trim() || computed.getPropertyValue("--ant-color-info").trim() || "#1677ff"; const styleChanged = ring.style.borderRadius !== borderRadius || ring.style.getPropertyValue("--lobe-focus-ring-color") !== color; if (styleChanged) { ring.style.borderRadius = borderRadius; ring.style.setProperty("--lobe-focus-ring-color", color); } if (moved || styleChanged) { schedule(); return; } const saved = ["anchor-name", "outline"].map((property) => ({ priority: element.style.getPropertyPriority(property), property, value: element.style.getPropertyValue(property) })); const oldMarker = element.getAttribute("data-lobe-focus-ring"); const anchor = computed.getPropertyValue("anchor-name").trim(); const assignedAnchor = anchor && anchor !== "none" ? `${anchor}, --lobe-global-focus` : "--lobe-global-focus"; element.style.setProperty("anchor-name", assignedAnchor, "important"); restoreTarget = () => { for (const { property, value, priority } of saved) { const assigned = property === "anchor-name" ? assignedAnchor : "none"; if (element.style.getPropertyValue(property) !== assigned) continue; if (value) element.style.setProperty(property, value, priority); else element.style.removeProperty(property); } if (element.getAttribute("data-lobe-focus-ring") === "managed") { if (oldMarker === null) element.removeAttribute("data-lobe-focus-ring"); else element.setAttribute("data-lobe-focus-ring", oldMarker); } }; const rect = element.getBoundingClientRect(); if (element.getClientRects().length !== 1 || !rect.width || !rect.height || computed.visibility !== "visible") { clear(); return; } try { ring.showPopover(); const bounds = ring.getBoundingClientRect(); if (Math.max(...[ "top", "right", "bottom", "left" ].map((edge) => Math.abs(bounds[edge] - rect[edge]))) > 1) { clear(); return; } element.style.setProperty("outline", "none", "important"); element.setAttribute("data-lobe-focus-ring", "managed"); target = element; } catch { clear(); } }; const schedule = () => { if (!frame) frame = win.requestAnimationFrame(() => { frame = 0; sync(); }); }; const focus = () => win.queueMicrotask(sync); const blur = () => { win.cancelAnimationFrame(frame); frame = 0; if (ring.matches(":popover-open")) ring.hidePopover(); }; const observer = new MutationObserver(() => { if (target && !target.isConnected) clear(); }); observer.observe(doc.body, { childList: true, subtree: true }); doc.addEventListener("focusin", focus, true); doc.addEventListener("focusout", schedule, true); doc.addEventListener("keydown", schedule, true); doc.addEventListener("pointerdown", schedule, true); win.addEventListener("blur", blur); win.addEventListener("focus", schedule); installations.set(doc, { dispose: () => { disposed = true; win.cancelAnimationFrame(frame); observer.disconnect(); doc.removeEventListener("focusin", focus, true); doc.removeEventListener("focusout", schedule, true); doc.removeEventListener("keydown", schedule, true); doc.removeEventListener("pointerdown", schedule, true); win.removeEventListener("blur", blur); win.removeEventListener("focus", schedule); clear(); ring.remove(); sheet.remove(); installations.delete(doc); }, users: 0 }); sync(); return installGlobalFocusRing(doc); }; //#endregion export { installGlobalFocusRing }; //# sourceMappingURL=index.mjs.map