Mercurial
view design_system/src/components/overlays.js @ 267:a78f5986011f
discover bundled production runtimes
Locate Bazel-packaged Copilot and Python binaries by stable suffix instead of hardcoding an external repository prefix.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 12:58:55 -0700 |
| parents | 2b6e732087ff |
| children |
line wrap: on
line source
let nextOverlayId = 1; function uniqueId(prefix) { let id; do id = `${prefix}-${nextOverlayId++}`; while (document.getElementById(id)); return id; } function rememberAttribute(state, element, name) { let attributes = state.get(element); if (!attributes) { attributes = new Map(); state.set(element, attributes); } if (!attributes.has(name)) { attributes.set(name, element.getAttribute(name)); } } function setManagedAttribute(state, element, name, value) { rememberAttribute(state, element, name); if (value === null) element.removeAttribute(name); else element.setAttribute(name, value); } function restoreAttributes(state) { for (const [element, attributes] of state) { for (const [name, value] of attributes) { if (value === null) element.removeAttribute(name); else element.setAttribute(name, value); } } state.clear(); } function resetManagedAttribute(state, element, name) { const attributes = state.get(element); if (!attributes?.has(name)) return; const value = attributes.get(name); if (value === null) element.removeAttribute(name); else element.setAttribute(name, value); } function ensureId(state, element, prefix) { const existing = element.id && document.getElementById(element.id); if (!element.id || (existing && existing !== element)) { setManagedAttribute(state, element, "id", uniqueId(prefix)); } return element.id; } function eventElement(event) { return event.target instanceof Element ? event.target : null; } const OVERLAY_COMPONENTS = [ "zen-dialog", "zen-alert-dialog", "zen-sheet", "zen-drawer", "zen-popover", "zen-hover-card", "zen-tooltip", ].join(","); function ownedElement(host, selector) { return [...host.querySelectorAll(selector)].find( element => element.closest(OVERLAY_COMPONENTS) === host, ) || null; } /** * Wires a native modal dialog to explicit trigger and close controls. * * @extends {HTMLElement} */ export class ZenDialog extends HTMLElement { static dialogRole = null; constructor() { super(); this._managed = new Map(); this._open = false; this._openedByComponent = false; this._onTriggerClick = event => { event.preventDefault(); this.open(); }; this._onDialogClick = event => { const control = eventElement(event)?.closest("[data-zen-close]"); if (!control || !this._dialog?.contains(control)) return; const value = control.hasAttribute("value") ? control.getAttribute("value") : undefined; this.close(value); }; this._onNativeClose = () => this.syncOpen(false); } connectedCallback() { this._observer ||= new MutationObserver(() => this.sync()); this._observer.observe(this, { childList: true, subtree: true }); this.sync(); } sync() { const trigger = ownedElement(this, "button[data-zen-trigger]"); const dialog = ownedElement(this, "dialog"); if (trigger === this._trigger && dialog === this._dialog) return; if (this._openedByComponent) this.close(); this.unbind(); this._trigger = trigger; this._dialog = dialog; if (!this._trigger || !this._dialog) return; const id = ensureId(this._managed, this._dialog, this.localName); setManagedAttribute( this._managed, this._trigger, "aria-haspopup", "dialog", ); setManagedAttribute(this._managed, this._trigger, "aria-controls", id); if (this.constructor.dialogRole) { setManagedAttribute( this._managed, this._dialog, "role", this.constructor.dialogRole, ); } this._trigger.addEventListener("click", this._onTriggerClick); this._dialog.addEventListener("click", this._onDialogClick); this._dialog.addEventListener("close", this._onNativeClose); this.syncOpen(this._dialog.open); } unbind() { this._trigger?.removeEventListener("click", this._onTriggerClick); this._dialog?.removeEventListener("click", this._onDialogClick); this._dialog?.removeEventListener("close", this._onNativeClose); restoreAttributes(this._managed); this._trigger = null; this._dialog = null; this._open = false; this._openedByComponent = false; } disconnectedCallback() { this._observer?.disconnect(); if (this._openedByComponent) this.close(); this.unbind(); } open() { if (!this._dialog || this._dialog.open || this._open) return; try { if (typeof this._dialog.showModal === "function") { this._dialog.showModal(); } else { setManagedAttribute(this._managed, this._dialog, "open", ""); } this._openedByComponent = true; this.syncOpen(true); } catch { this.syncOpen(Boolean(this._dialog.open)); } } close(returnValue) { if (!this._dialog || (!this._dialog.open && !this._open)) return; if (typeof this._dialog.close === "function") { if (returnValue === undefined) this._dialog.close(); else this._dialog.close(returnValue); } else { setManagedAttribute(this._managed, this._dialog, "open", null); } this.syncOpen(false); } syncOpen(open) { this._open = open; if (!open) this._openedByComponent = false; if (this._trigger) { setManagedAttribute( this._managed, this._trigger, "aria-expanded", String(open), ); } } } /** * Presents a native modal with alert-dialog semantics. * * @extends {ZenDialog} */ export class ZenAlertDialog extends ZenDialog { static dialogRole = "alertdialog"; } /** * Presents a native dialog as an edge-aligned sheet. * * @extends {ZenDialog} */ export class ZenSheet extends ZenDialog {} /** * Presents a native dialog as a drawer. * * @extends {ZenDialog} */ export class ZenDrawer extends ZenDialog {} function isPopoverOpen(element) { try { return element.matches(":popover-open"); } catch { return false; } } /** * Connects a trigger to a native popover with an accessible fallback. * * @extends {HTMLElement} */ export class ZenPopover extends HTMLElement { constructor() { super(); this._managed = new Map(); this._open = false; this._onClick = event => { const target = eventElement(event); if (!target) return; if (this._trigger?.contains(target)) { event.preventDefault(); this.toggle(); } else if (this._content?.contains(target) && target.closest("[data-zen-close]")) { this.hide(); } }; this._onToggle = event => { this.syncOpen(event.newState ? event.newState === "open" : isPopoverOpen(this._content)); }; this._onDocumentPointerDown = event => { if (!this._native && this._open && !this.contains(event.target)) { this.hide(); } }; this._onDocumentKeyDown = event => { if (!this._native && this._open && event.key === "Escape") { this.hide(); this._trigger?.focus(); } }; } connectedCallback() { this.addEventListener("click", this._onClick); document.addEventListener("pointerdown", this._onDocumentPointerDown); document.addEventListener("keydown", this._onDocumentKeyDown); this._observer ||= new MutationObserver(() => this.sync()); this._observer.observe(this, { childList: true, subtree: true }); this.sync(); } sync() { const trigger = ownedElement(this, "[data-zen-trigger]"); const content = ownedElement( this, "[data-zen-content], [popover]", ); if (trigger === this._trigger && content === this._content) return; if (this._open) this.hide(); this.unbindElements(); this._trigger = trigger; this._content = content; if (!this._trigger || !this._content) return; const id = ensureId(this._managed, this._content, "zen-popover"); setManagedAttribute(this._managed, this._trigger, "aria-controls", id); this._native = typeof this._content.showPopover === "function" && typeof this._content.hidePopover === "function"; if (this._native && !this._content.hasAttribute("popover")) { setManagedAttribute(this._managed, this._content, "popover", "auto"); } if (this._native && "popoverTargetElement" in this._trigger) { this._originalPopoverTarget = this._trigger.popoverTargetElement; this._trigger.popoverTargetElement = this._content; } if (this._native) { setManagedAttribute(this._managed, this._content, "hidden", null); } else { setManagedAttribute(this._managed, this._content, "hidden", ""); } this._content.addEventListener("toggle", this._onToggle); this.syncOpen(this._native && isPopoverOpen(this._content)); } unbindElements() { this._content?.removeEventListener("toggle", this._onToggle); if (this._trigger && "popoverTargetElement" in this._trigger) { this._trigger.popoverTargetElement = this._originalPopoverTarget || null; } restoreAttributes(this._managed); this._trigger = null; this._content = null; this._native = false; this._open = false; this._originalPopoverTarget = null; } disconnectedCallback() { this._observer?.disconnect(); this.removeEventListener("click", this._onClick); document.removeEventListener("pointerdown", this._onDocumentPointerDown); document.removeEventListener("keydown", this._onDocumentKeyDown); if (this._open) this.hide(); this.unbindElements(); } toggle() { if (this._open || isPopoverOpen(this._content)) this.hide(); else this.show(); } show() { if (!this._content || this._open) return; if (this._native) { try { this._content.showPopover(); } catch { this._native = false; setManagedAttribute(this._managed, this._content, "hidden", null); } } else { setManagedAttribute(this._managed, this._content, "hidden", null); } this.syncOpen(true); } hide() { if (!this._content || !this._open) return; if (this._native) { try { this._content.hidePopover(); } catch { this._native = false; setManagedAttribute(this._managed, this._content, "hidden", ""); } } else { setManagedAttribute(this._managed, this._content, "hidden", ""); } this.syncOpen(false); } syncOpen(open) { this._open = open; if (this._trigger) { setManagedAttribute( this._managed, this._trigger, "aria-expanded", String(open), ); } if (!this._native && this._content) { setManagedAttribute( this._managed, this._content, "hidden", open ? null : "", ); } } } class ZenTimedOverlay extends HTMLElement { static delay = 150; static tooltip = false; constructor() { super(); this._managed = new Map(); this._timer = null; this._open = false; this._pointerWithin = false; this._focusWithin = false; this._onPointerEnter = () => { this._pointerWithin = true; this.schedule(true); }; this._onPointerLeave = event => { if (event.relatedTarget instanceof Node && this.contains(event.relatedTarget)) return; this._pointerWithin = false; this.schedule(this._focusWithin); }; this._onFocusIn = () => { this._focusWithin = true; this.schedule(true); }; this._onFocusOut = event => { if (event.relatedTarget instanceof Node && this.contains(event.relatedTarget)) return; this._focusWithin = false; this.schedule(this._pointerWithin); }; this._onKeyDown = event => { if (event.key !== "Escape" || !this._open) return; clearTimeout(this._timer); this._timer = null; this.setOpen(false); }; } connectedCallback() { this.addEventListener("keydown", this._onKeyDown); this._observer ||= new MutationObserver(() => this.sync()); this._observer.observe(this, { childList: true, subtree: true }); this.sync(); } sync() { const trigger = ownedElement(this, "[data-zen-trigger]"); const content = ownedElement(this, "[data-zen-content]"); if (trigger === this._trigger && content === this._content) return; this.unbindElements(); this._trigger = trigger; this._content = content; if (!this._trigger || !this._content) return; const id = ensureId(this._managed, this._content, this.localName); setManagedAttribute(this._managed, this._content, "hidden", ""); if (this.constructor.tooltip) { setManagedAttribute(this._managed, this._content, "role", "tooltip"); const ids = new Set( (this._trigger.getAttribute("aria-describedby") || "") .split(/\s+/) .filter(Boolean), ); ids.add(id); setManagedAttribute( this._managed, this._trigger, "aria-describedby", [...ids].join(" "), ); } else { setManagedAttribute(this._managed, this._trigger, "aria-controls", id); setManagedAttribute( this._managed, this._trigger, "aria-expanded", "false", ); } for (const element of [this._trigger, this._content]) { element.addEventListener("pointerenter", this._onPointerEnter); element.addEventListener("pointerleave", this._onPointerLeave); element.addEventListener("focusin", this._onFocusIn); element.addEventListener("focusout", this._onFocusOut); } } unbindElements() { clearTimeout(this._timer); this._timer = null; for (const element of [this._trigger, this._content]) { element?.removeEventListener("pointerenter", this._onPointerEnter); element?.removeEventListener("pointerleave", this._onPointerLeave); element?.removeEventListener("focusin", this._onFocusIn); element?.removeEventListener("focusout", this._onFocusOut); } restoreAttributes(this._managed); this._trigger = null; this._content = null; this._open = false; this._pointerWithin = false; this._focusWithin = false; } disconnectedCallback() { this._observer?.disconnect(); this.removeEventListener("keydown", this._onKeyDown); this.unbindElements(); } schedule(open) { clearTimeout(this._timer); this._timer = setTimeout( () => this.setOpen(open), this.constructor.delay, ); } setOpen(open) { if (!this._content || open === this._open) return; this._open = open; setManagedAttribute( this._managed, this._content, "hidden", open ? null : "", ); if (!this.constructor.tooltip) { setManagedAttribute( this._managed, this._trigger, "aria-expanded", String(open), ); } } } /** * Shows supplemental interactive content on hover or focus. * * @extends {ZenTimedOverlay} */ export class ZenHoverCard extends ZenTimedOverlay { static delay = 150; } /** * Shows a delayed non-interactive description on hover or focus. * * @extends {ZenTimedOverlay} */ export class ZenTooltip extends ZenTimedOverlay { static delay = 350; static tooltip = true; } const MENU_COMPONENTS = [ "zen-dropdown-menu", "zen-context-menu", "zen-menubar", "zen-navigation-menu", ].join(","); const MENU_SELECTOR = "[data-zen-menu], [role='menu'], [data-zen-content]"; const ITEM_SELECTOR = [ "[role='menuitem']", "[role='menuitemcheckbox']", "[role='menuitemradio']", "[data-zen-menu-item]", "a[href]", "button", "input[type='button']", "input[type='submit']", ].join(","); class ZenMenu extends HTMLElement { static contextMenu = false; static menubar = false; static multiple = false; constructor() { super(); this._managed = new Map(); this._pairs = []; this._openPair = null; this._onClick = event => { const target = eventElement(event); if (!target) return; const pair = this._pairs.find(({ trigger }) => trigger.contains(target)); if (pair) { event.preventDefault(); if (this._openPair === pair) this.close(false); else this.open(pair, 0, this.constructor.contextMenu ? event : null); return; } const item = target.closest(ITEM_SELECTOR); if (this._openPair?.menu.contains(target) && item && !item.hasAttribute("disabled") && item.getAttribute("aria-disabled") !== "true") { this.close(true); } }; this._onContextMenu = event => { if (!this.constructor.contextMenu || this._pairs.some(({ menu }) => menu.contains(event.target))) return; event.preventDefault(); this.open(this._pairs[0], 0, event); }; this._onKeyDown = event => this.handleKeyDown(event); this._onDocumentPointerDown = event => { if (!this._openPair) return; const { trigger, menu } = this._openPair; if (!trigger.contains(event.target) && !menu.contains(event.target)) { this.close(false); } }; } connectedCallback() { this.setupPairs(); this.addEventListener("click", this._onClick); this.addEventListener("contextmenu", this._onContextMenu); this.addEventListener("keydown", this._onKeyDown); document.addEventListener("pointerdown", this._onDocumentPointerDown); this._observer ||= new MutationObserver(() => this.refreshPairs()); this._observer.observe(this, { childList: true, subtree: true }); } disconnectedCallback() { this._observer?.disconnect(); this.removeEventListener("click", this._onClick); this.removeEventListener("contextmenu", this._onContextMenu); this.removeEventListener("keydown", this._onKeyDown); document.removeEventListener("pointerdown", this._onDocumentPointerDown); this._openPair = null; restoreAttributes(this._managed); this._pairs = []; } refreshPairs() { if (this._openPair) this.close(false); restoreAttributes(this._managed); this._pairs = []; this.setupPairs(); } ownedElements(selector) { return [...this.querySelectorAll(selector)].filter(element => element.closest(MENU_COMPONENTS) === this); } setupPairs() { let triggers = this.ownedElements("[data-zen-trigger]"); const menus = this.ownedElements(MENU_SELECTOR); triggers = triggers.filter(trigger => !menus.some(menu => menu.contains(trigger))); if (!this.constructor.multiple) triggers = triggers.slice(0, 1); const unused = new Set(menus); for (const trigger of triggers) { const controlled = trigger.getAttribute("aria-controls"); let menu = controlled ? menus.find(candidate => candidate.id === controlled) : null; if (!menu) menu = [...unused][0]; if (!menu) continue; unused.delete(menu); const triggerId = ensureId(this._managed, trigger, `${this.localName}-trigger`); const menuId = ensureId(this._managed, menu, `${this.localName}-menu`); setManagedAttribute(this._managed, trigger, "aria-haspopup", "menu"); setManagedAttribute(this._managed, trigger, "aria-controls", menuId); setManagedAttribute(this._managed, trigger, "aria-expanded", "false"); setManagedAttribute(this._managed, menu, "role", "menu"); setManagedAttribute(this._managed, menu, "aria-labelledby", triggerId); setManagedAttribute(this._managed, menu, "hidden", ""); if (this.constructor.menubar) { setManagedAttribute(this._managed, trigger, "role", "menuitem"); } const pair = { trigger, menu }; this._pairs.push(pair); this.prepareItems(pair); } if (this.constructor.menubar) { setManagedAttribute(this._managed, this, "role", "menubar"); this.setMenubarTabStop(this._pairs[0]); } } prepareItems(pair) { const items = [...pair.menu.querySelectorAll(ITEM_SELECTOR)].filter(item => item.closest(MENU_SELECTOR) === pair.menu && !item.hidden && !item.hasAttribute("disabled") && item.getAttribute("aria-disabled") !== "true"); for (const item of items) { if (!item.hasAttribute("role")) { setManagedAttribute(this._managed, item, "role", "menuitem"); } setManagedAttribute(this._managed, item, "tabindex", "-1"); } return items; } open(pair, itemIndex = 0, pointerEvent = null) { if (!pair) return; if (this._openPair && this._openPair !== pair) this.close(false); this._openPair = pair; if (this.constructor.menubar) this.setMenubarTabStop(pair); if (pointerEvent) { rememberAttribute(this._managed, pair.menu, "style"); pair.menu.style.position = "fixed"; pair.menu.style.left = `${Number(pointerEvent.clientX) || 0}px`; pair.menu.style.top = `${Number(pointerEvent.clientY) || 0}px`; } else { resetManagedAttribute(this._managed, pair.menu, "style"); } setManagedAttribute(this._managed, pair.menu, "hidden", null); setManagedAttribute(this._managed, pair.trigger, "aria-expanded", "true"); const items = this.prepareItems(pair); if (items.length) { const index = itemIndex < 0 ? items.length - 1 : itemIndex; setManagedAttribute(this._managed, items[index], "tabindex", "0"); items[index].focus(); } else { setManagedAttribute(this._managed, pair.menu, "tabindex", "-1"); pair.menu.focus(); } } close(restoreFocus) { if (!this._openPair) return; const pair = this._openPair; this._openPair = null; setManagedAttribute(this._managed, pair.menu, "hidden", ""); setManagedAttribute(this._managed, pair.trigger, "aria-expanded", "false"); resetManagedAttribute(this._managed, pair.menu, "style"); if (restoreFocus) pair.trigger.focus(); } moveItem(pair, offset, edge = null) { const active = document.activeElement; const items = this.prepareItems(pair); if (!items.length) return; let index = items.indexOf(active); if (edge === "first") index = 0; else if (edge === "last") index = items.length - 1; else index = (Math.max(index, 0) + offset + items.length) % items.length; setManagedAttribute(this._managed, items[index], "tabindex", "0"); items[index].focus(); } moveMenubarTrigger(pair, offset, openMenu) { const index = this._pairs.indexOf(pair); const next = this._pairs[ (index + offset + this._pairs.length) % this._pairs.length ]; this.setMenubarTabStop(next); if (openMenu) this.open(next); else next.trigger.focus(); } setMenubarTabStop(activePair) { for (const pair of this._pairs) { setManagedAttribute( this._managed, pair.trigger, "tabindex", pair === activePair ? "0" : "-1", ); } } handleKeyDown(event) { const target = eventElement(event); if (!target) return; const triggerPair = this._pairs.find(({ trigger }) => trigger.contains(target)); if (triggerPair) { if (this.constructor.menubar && (event.key === "ArrowRight" || event.key === "ArrowLeft")) { event.preventDefault(); this.moveMenubarTrigger( triggerPair, event.key === "ArrowRight" ? 1 : -1, false, ); } else if (["Enter", " ", "ArrowDown", "ArrowUp"].includes(event.key)) { event.preventDefault(); this.open(triggerPair, event.key === "ArrowUp" ? -1 : 0); } else if (event.key === "Escape" && this._openPair) { event.preventDefault(); this.close(true); } return; } const pair = this._pairs.find(({ menu }) => menu.contains(target)); if (!pair || this._openPair !== pair) return; if (event.key === "ArrowDown") { event.preventDefault(); this.moveItem(pair, 1); } else if (event.key === "ArrowUp") { event.preventDefault(); this.moveItem(pair, -1); } else if (event.key === "Home" || event.key === "End") { event.preventDefault(); this.moveItem(pair, 0, event.key === "Home" ? "first" : "last"); } else if (event.key === "Escape") { event.preventDefault(); this.close(true); } else if (event.key === "Tab") { this.close(false); } else if (this.constructor.menubar && (event.key === "ArrowRight" || event.key === "ArrowLeft")) { event.preventDefault(); this.moveMenubarTrigger( pair, event.key === "ArrowRight" ? 1 : -1, true, ); } } } /** * Provides an accessible action menu opened from a native trigger. * * @extends {ZenMenu} */ export class ZenDropdownMenu extends ZenMenu {} /** * Opens an accessible action menu at the pointer location. * * @extends {ZenMenu} */ export class ZenContextMenu extends ZenMenu { static contextMenu = true; } /** * Coordinates multiple keyboard-accessible application menus. * * @extends {ZenMenu} */ export class ZenMenubar extends ZenMenu { static menubar = true; static multiple = true; } /** * Provides keyboard-accessible navigation flyouts. * * @extends {ZenMenu} */ export class ZenNavigationMenu extends ZenMenu {} const definitions = { "zen-dialog": ZenDialog, "zen-alert-dialog": ZenAlertDialog, "zen-sheet": ZenSheet, "zen-drawer": ZenDrawer, "zen-popover": ZenPopover, "zen-hover-card": ZenHoverCard, "zen-tooltip": ZenTooltip, "zen-dropdown-menu": ZenDropdownMenu, "zen-context-menu": ZenContextMenu, "zen-menubar": ZenMenubar, "zen-navigation-menu": ZenNavigationMenu, }; for (const [name, constructor] of Object.entries(definitions)) { if (!customElements.get(name)) customElements.define(name, constructor); }