diff design_system/src/components/overlays.js @ 254:2b6e732087ff

[ui] Add complete native component catalog Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 15:12:09 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/design_system/src/components/overlays.js	Tue Aug 04 15:12:09 2026 -0700
@@ -0,0 +1,865 @@
+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);
+}