@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
164 lines (163 loc) • 5.87 kB
JavaScript
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