ooxml-ui 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,44 +52,46 @@ document.body.append(button);
52
52
  | `ooxml-ui/presence` | `<office-ui-presence>` avatar stack for collaboration awareness |
53
53
  | `ooxml-ui/smartart` | `<office-ui-smartart>`: draws a core `DiagramDrawing` as SVG |
54
54
 
55
+ Every entry declares an `import` and a `default` export condition, so CommonJS consumers on Node 20.19+ or 22.12+ load it through `require(esm)`.
56
+
55
57
  Importing an entry never touches the DOM (SSR-safe). Elements are defined by the `define*` /
56
58
  `register*` functions, which are idempotent, no-ops without a `window`, and refuse to coexist
57
59
  with an incompatible build of this package.
58
60
 
59
61
  ## Features: elements
60
62
 
61
- | Tag | Attributes / properties | Events (all bubble, composed) |
62
- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
63
- | `office-ui-button` | `label`, `icon`, `command`, `disabled`, `pressed`, `expanded`, `icon-only`, `variant="stacked"`, `keyshortcuts` | `office-command` `{ command }` |
64
- | `office-ui-checkbox`, `office-ui-switch` | `checked`, `disabled`, `value`, `aria-label`; form-associated | `input`, `change` |
65
- | `office-ui-select` | `<option>` / `<optgroup>` children (`data-display-label`, `data-description`, font previews) or the `options` property `[{ value, label, disabled? }]`; `value`, `selectedIndex`, `disabled`, `aria-label`, `variant="ribbon-font" | "ribbon-icon"`, `data-font-picker`; `icon`and`custom` slots; arrows, Home, End, PageUp/PageDown, typeahead | `input`, `change` |
66
- | `office-ui-ribbon-group` | `label`, `launcher` (dialog launcher command), `launcher-disabled`; default slot | launcher: `office-command` `{ command }` |
67
- | `office-ui-ribbon-stack` | `orientation="horizontal"`; default slot (three-row column of small commands by default) | none |
68
- | `office-ui-menu-button` / `-menu-item` | button: `label`, `icon`, `command` (split button), `variant`, `disabled`, `keyshortcuts`; item: `command`, `label`, `icon`, `disabled`, `checked` | item choice: `office-command` `{ command }` |
69
- | `office-ui-toolbar` | `aria-label`, `aria-orientation`; arrow-key focus movement | none |
70
- | `office-ui-dialog` | `open`, `heading`, `dismissible="false"`, `show()`, `close()`; slots default, `footer` | `office-dialog-close` `{ reason }` (cancelable) |
71
- | `office-ui-status-bar` / `-status-item` | `label`, `value`, `interactive`, `id` | `office-status-activate` `{ id }` |
72
- | `office-ui-zoom-slider` | `value` (percent), `min`, `max`, `step`, `disabled`, `fit` (fit button name), `label` | `input`, `change`; fit: `office-command` `{ command: 'zoom-fit' }` |
73
- | `office-ui-tab-strip` | `tabs` `[{ id, label, title? }]`, `selected`, `label`, `previous-label`, `next-label`, `add-label`, `add-disabled`, `add-title`, `disabled` | `office-tab-select` `{ id }` (cancelable); add: `office-command` `{ command: 'tab-add' }` |
74
- | `office-ui-context-menu` / `office-ui-menu-separator` | `label`; `openAt(x, y)`, `close()`, `open`; `office-ui-menu-item` and separator children | item choice: `office-command` `{ command }` |
75
- | `office-ui-command-search` | `commands` `[{ id, label, keywords?, description?, disabled?, title? }]`, `placeholder`, `label`, `limit` (Office's "Tell me what you want to do") | choice: `office-command` `{ command }` |
76
- | `office-ui-options-dialog` | `categories` `[{ id, label, description?, disabled?, sections: [{ id, title, controls }] }]` (controls: `toggle`, `select`, `number`, `text`, each optionally `disabled` with a reason), `values`, `category`, `open`, `heading`, `show()`, `close()` | OK: `office-options-change` `{ values, changed }` |
77
- | `office-ui-account` | `profile` `{ displayName, avatarColor, initial? }`; slots `sign-in` and default (product sections) | `office-profile-change` `{ profile }` |
78
- | `office-ui-ribbon` | Tab row over tab panels. Panels are children with `data-ribbon-tab`, `data-label`, optional `data-tab-keytip`; slots `quick-access`, `search`, `end`; `selected`, `label`, `file-label`, `no-file`, `file-expanded`, `file-keytip`; `focusFile()`, `focusTab()` | `office-ribbon-select` `{ tab }` (cancelable), `office-ribbon-file` |
79
- | `office-ui-backstage` | `items` `[{ id, label, group?: 'footer', disabled?, title? }]`, `open`, `selected`, `label`, `back-label`, `show(page?)`, `close()`; pages are children with `data-backstage-page` | `office-backstage-select` `{ id }` (cancelable), `office-backstage-close` `{ reason }` (cancelable) |
80
- | `office-ui-find-bar` | `open`, `label`, `input-label`, `placeholder`, `maxlength`, `disabled`, `navigation-disabled`, `previous-label`, `next-label`, `close-label`; `value`, `status`, `statusTitle`, `show()`, `close()` | `office-find-input` `{ query }`, `office-find-step` `{ direction }`, `office-find-close` |
81
- | `office-ui-ruler` | `orientation="vertical"`, `origin` (px offset of zero), `scale` (px per unit, default 96), `direction="reverse"`; decorative | none |
82
- | `office-ui-print-preview` | `pages` (rendered nodes; an inert clone of the current one is shown), `index`, `label`, `empty-label` | `office-print-preview-page` `{ index }` |
83
- | `office-ui-radio` | `checked`, `disabled`, `value`, `name`; radios of one tag sharing a `name` form a roving-focus group; form-associated | `input`, `change` |
84
- | `office-ui-search` | `value`, `placeholder`, `disabled`, `aria-label`, `variant="titlebar"`; `focus()`, `select()` | `input`, `change` |
85
- | `office-ui-ribbon-toggle` | `label`, `checked`, `disabled`, `title`, `command` (a labelled ribbon checkbox row) | `office-toggle` `{ command, checked }` |
86
- | `office-ui-dialog-footer` | `state` `{ actions: [{ id, label, variant?, icon?, disabled?, busy?, title?, align?, testId? }] }`, `focusAction(id)` | `office-dialog-footer-request` `{ id }` |
87
- | `office-ui-toasts` | `state` `{ toasts: [{ id, code?, severity, message }], overflowCount?, labels? }`; hidden when empty, host positions it | `office-toasts-request` `{ id: 'dismiss', toastId }` or `{ id: 'dismissAll' }` |
88
- | `office-ui-read-only-banner` | `state` `{ kind, message, passwordPromptOpen?, passwordError?, checkingPassword?, labels? }` | `office-read-only-request` (Edit anyway, Dismiss, password submit or cancel) |
89
- | `office-ui-paste-options` | `state` `{ left, top, options: [{ id, label }], label? }`; fixed 4px past the pasted object | `office-paste-options-request` `{ format }`, `office-paste-options-dismiss` |
90
- | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
91
- | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
92
- | `office-ui-icon` | `name`, `label` | none |
63
+ | Tag | Attributes / properties | Events (all bubble, composed) |
64
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `office-ui-button` | `label`, `icon`, `command`, `disabled`, `pressed`, `expanded`, `icon-only`, `variant="stacked"`, `keyshortcuts` | `office-command` `{ command }` |
66
+ | `office-ui-checkbox`, `office-ui-switch` | `checked`, `disabled`, `value`, `aria-label`; form-associated | `input`, `change` |
67
+ | `office-ui-select` | `<option>` / `<optgroup>` children (`data-display-label`, `data-description`, font previews) or the `options` property `[{ value, label, disabled? }]`; `value`, `selectedIndex`, `disabled`, `aria-label`, `variant="ribbon-font" | "ribbon-icon"`, `data-font-picker`; `icon`and`custom` slots; arrows, Home, End, PageUp/PageDown, typeahead | `input`, `change` |
68
+ | `office-ui-ribbon-group` | `label`, `launcher` (dialog launcher command), `launcher-disabled`; default slot | launcher: `office-command` `{ command }` |
69
+ | `office-ui-ribbon-stack` | `orientation="horizontal"`; default slot (three-row column of small commands by default) | none |
70
+ | `office-ui-menu-button` / `-menu-item` | button: `label`, `icon`, `command` (split button), `variant`, `disabled`, `keyshortcuts`; item: `command`, `label`, `icon`, `disabled`, `checked` | item choice: `office-command` `{ command }` |
71
+ | `office-ui-toolbar` | `aria-label`, `aria-orientation`; arrow-key focus movement | none |
72
+ | `office-ui-dialog` | `open`, `heading`, `dismissible="false"`, `show()`, `close()`; slots default, `footer` | `office-dialog-close` `{ reason }` (cancelable) |
73
+ | `office-ui-status-bar` / `-status-item` | `label`, `value`, `interactive`, `id` | `office-status-activate` `{ id }` |
74
+ | `office-ui-zoom-slider` | `value` (percent), `min`, `max`, `step`, `disabled`, `fit` (fit button name), `label` | `input`, `change`; fit: `office-command` `{ command: 'zoom-fit' }` |
75
+ | `office-ui-tab-strip` | `tabs` `[{ id, label, title? }]`, `selected`, `label`, `previous-label`, `next-label`, `add-label`, `add-disabled`, `add-title`, `disabled` | `office-tab-select` `{ id }` (cancelable); add: `office-command` `{ command: 'tab-add' }` |
76
+ | `office-ui-context-menu` / `office-ui-menu-separator` | Children mode: `label`, `openAt(x, y)`, `close()`, `open` over `office-ui-menu-item` and separator children. Controlled mode: `state` `{ x, y, label, items: [{ id, label, separatorBefore?, heading?, danger?, disabled?, checked?, icon? }], markers?, zIndex?, autoFocus? }`; arrows, Home, End and type-ahead in both | children: item `office-command` `{ command }`; controlled: `office-menu-request` `{ id }`, `office-menu-close` `{ reason }` (never closes itself) |
77
+ | `office-ui-command-search` | `commands` `[{ id, label, keywords?, description?, disabled?, title? }]`, `placeholder`, `label`, `limit` (Office's "Tell me what you want to do") | choice: `office-command` `{ command }` |
78
+ | `office-ui-options-dialog` | `categories` `[{ id, label, description?, disabled?, sections: [{ id, title, controls }] }]` (controls: `toggle`, `select`, `number`, `text`, each optionally `disabled` with a reason), `values`, `category`, `open`, `heading`, `show()`, `close()` | OK: `office-options-change` `{ values, changed }` |
79
+ | `office-ui-account` | `profile` `{ displayName, avatarColor, initial? }`; slots `sign-in` and default (product sections) | `office-profile-change` `{ profile }` |
80
+ | `office-ui-ribbon` | Tab row over tab panels. Panels are children with `data-ribbon-tab`, `data-label`, optional `data-tab-keytip`; slots `quick-access`, `search`, `end`; `selected`, `label`, `file-label`, `no-file`, `file-expanded`, `file-keytip`; `focusFile()`, `focusTab()` | `office-ribbon-select` `{ tab }` (cancelable), `office-ribbon-file` |
81
+ | `office-ui-backstage` | `items` `[{ id, label, group?: 'footer', disabled?, title? }]`, `open`, `selected`, `label`, `back-label`, `show(page?)`, `close()`; pages are children with `data-backstage-page` | `office-backstage-select` `{ id }` (cancelable), `office-backstage-close` `{ reason }` (cancelable) |
82
+ | `office-ui-find-bar` | `open`, `label`, `input-label`, `placeholder`, `maxlength`, `disabled`, `navigation-disabled`, `previous-label`, `next-label`, `close-label`; `value`, `status`, `statusTitle`, `show()`, `close()` | `office-find-input` `{ query }`, `office-find-step` `{ direction }`, `office-find-close` |
83
+ | `office-ui-ruler` | `orientation="vertical"`, `origin` (px offset of zero), `scale` (px per unit, default 96), `direction="reverse"`; decorative | none |
84
+ | `office-ui-print-preview` | `pages` (rendered nodes; an inert clone of the current one is shown), `index`, `label`, `empty-label` | `office-print-preview-page` `{ index }` |
85
+ | `office-ui-radio` | `checked`, `disabled`, `value`, `name`; radios of one tag sharing a `name` form a roving-focus group; form-associated | `input`, `change` |
86
+ | `office-ui-search` | `value`, `placeholder`, `disabled`, `aria-label`, `variant="titlebar"`; `focus()`, `select()` | `input`, `change` |
87
+ | `office-ui-ribbon-toggle` | `label`, `checked`, `disabled`, `title`, `command` (a labelled ribbon checkbox row; static `checkboxTag` picks the inner checkbox) | `office-toggle` `{ command, checked }` |
88
+ | `office-ui-dialog-footer` | `state` `{ actions: [{ id, label, variant?, icon?, disabled?, busy?, title?, align?, testId? }] }`, `focusAction(id)` | `office-dialog-footer-request` `{ id }` |
89
+ | `office-ui-toasts` | `state` `{ toasts: [{ id, code?, severity, message }], overflowCount?, labels? }`; hidden when empty, host positions it | `office-toasts-request` `{ id: 'dismiss', toastId }` or `{ id: 'dismissAll' }` |
90
+ | `office-ui-read-only-banner` | `state` `{ kind, message, passwordPromptOpen?, passwordError?, checkingPassword?, labels? }` | `office-read-only-request` (Edit anyway, Dismiss, password submit or cancel) |
91
+ | `office-ui-paste-options` | `state` `{ left, top, options: [{ id, label }], label? }`; fixed 4px past the pasted object | `office-paste-options-request` `{ format }`, `office-paste-options-dismiss` |
92
+ | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
93
+ | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
94
+ | `office-ui-icon` | `name`, `label` | none |
93
95
 
94
96
  The local profile behind `office-ui-account` is stored on the device only, under one suite-wide
95
97
  key (`readOfficeProfile`, `writeOfficeProfile`, `clearOfficeProfile`), so every Office viewer on
@@ -702,6 +702,8 @@ var defineRibbonToggle = definer("office-ui-ribbon-toggle", () => {
702
702
  static requestEvent = "office-toggle";
703
703
  static idAttribute = "command";
704
704
  static detailKey = "command";
705
+ /** The checkbox drawn inside; a product subclass may use its own aliased tag. */
706
+ static checkboxTag = "office-ui-checkbox";
705
707
  static get observedAttributes() {
706
708
  return ["label", "checked", "disabled", "title"];
707
709
  }
@@ -713,7 +715,9 @@ var defineRibbonToggle = definer("office-ui-ribbon-toggle", () => {
713
715
  const root = this.attachShadow({ mode: "open" });
714
716
  attachStyles(root, controlCss(CSS2));
715
717
  const label = doc.createElement("label");
716
- this.checkbox = doc.createElement("office-ui-checkbox");
718
+ this.checkbox = doc.createElement(
719
+ this.constructor.checkboxTag
720
+ );
717
721
  this.text = doc.createTextNode("");
718
722
  label.append(this.checkbox, this.text);
719
723
  label.addEventListener("click", (event) => {
@@ -1841,8 +1845,8 @@ var defineButton = definer("office-ui-button", () => {
1841
1845
  button;
1842
1846
  svg;
1843
1847
  text;
1844
- caret;
1845
- badge;
1848
+ #caret;
1849
+ #badge;
1846
1850
  constructor() {
1847
1851
  super();
1848
1852
  const doc = this.ownerDocument;
@@ -1855,13 +1859,13 @@ var defineButton = definer("office-ui-button", () => {
1855
1859
  this.svg.classList.add("glyph");
1856
1860
  this.text = doc.createElement("span");
1857
1861
  this.text.className = "label";
1858
- this.caret = createIconSvg(doc);
1859
- this.caret.classList.add("caret");
1860
- paintIcon(this.caret, "chevronDown");
1861
- this.badge = doc.createElement("span");
1862
- this.badge.className = "badge";
1863
- this.badge.setAttribute("aria-hidden", "true");
1864
- this.button.append(this.svg, this.text, this.badge);
1862
+ this.#caret = createIconSvg(doc);
1863
+ this.#caret.classList.add("caret");
1864
+ paintIcon(this.#caret, "chevronDown");
1865
+ this.#badge = doc.createElement("span");
1866
+ this.#badge.className = "badge";
1867
+ this.#badge.setAttribute("aria-hidden", "true");
1868
+ this.button.append(this.svg, this.text, this.#badge);
1865
1869
  root.append(this.button);
1866
1870
  this.button.addEventListener("keydown", (event) => {
1867
1871
  if ((event.key === " " || event.key === "Enter") && !event.ctrlKey && !event.metaKey && !event.altKey)
@@ -1904,6 +1908,10 @@ var defineButton = definer("office-ui-button", () => {
1904
1908
  iconName() {
1905
1909
  return this.getAttribute("icon");
1906
1910
  }
1911
+ /** The tooltip: `title`, else the label of an icon-only button; a product may always show it. */
1912
+ tooltip(label, iconOnly) {
1913
+ return this.getAttribute("title") ?? (iconOnly ? label : "");
1914
+ }
1907
1915
  /** Whether the trailing menu chevron shows. */
1908
1916
  showsCaret() {
1909
1917
  const caret = this.getAttribute("caret");
@@ -1913,20 +1921,20 @@ var defineButton = definer("office-ui-button", () => {
1913
1921
  const label = this.getAttribute("label") ?? "";
1914
1922
  const iconOnly = this.hasAttribute("icon-only");
1915
1923
  this.text.textContent = label;
1916
- this.text.append(this.caret);
1917
- this.caret.toggleAttribute("hidden", !this.showsCaret());
1924
+ this.text.append(this.#caret);
1925
+ this.#caret.toggleAttribute("hidden", !this.showsCaret());
1918
1926
  this.text.hidden = iconOnly;
1919
1927
  if (iconOnly && label) this.button.setAttribute("aria-label", label);
1920
1928
  else this.button.removeAttribute("aria-label");
1921
- this.button.title = this.getAttribute("title") ?? (iconOnly ? label : "");
1929
+ this.button.title = this.tooltip(label, iconOnly);
1922
1930
  this.button.disabled = this.disabled;
1923
1931
  const shortcuts = this.getAttribute("keyshortcuts");
1924
1932
  if (shortcuts) this.button.setAttribute("aria-keyshortcuts", shortcuts);
1925
1933
  else this.button.removeAttribute("aria-keyshortcuts");
1926
1934
  if (paintIcon(this.svg, this.iconName())) this.svg.setAttribute("data-painted", "");
1927
1935
  else this.svg.removeAttribute("data-painted");
1928
- this.badge.textContent = this.getAttribute("badge") ?? "";
1929
- this.badge.hidden = !this.badge.textContent;
1936
+ this.#badge.textContent = this.getAttribute("badge") ?? "";
1937
+ this.#badge.hidden = !this.#badge.textContent;
1930
1938
  for (const attr of ["pressed", "expanded"]) {
1931
1939
  const value = this.getAttribute(attr);
1932
1940
  if (value === null) this.button.removeAttribute(`aria-${attr}`);
@@ -2091,7 +2099,7 @@ var defineDialog = definer("office-ui-dialog", () => {
2091
2099
  class OfficeUiDialog extends HTMLElement {
2092
2100
  static observedAttributes = ["open", "heading"];
2093
2101
  box;
2094
- heading;
2102
+ #heading;
2095
2103
  closeButton;
2096
2104
  footer;
2097
2105
  returnFocus = null;
@@ -2108,8 +2116,8 @@ var defineDialog = definer("office-ui-dialog", () => {
2108
2116
  this.box.setAttribute("aria-modal", "true");
2109
2117
  this.box.tabIndex = -1;
2110
2118
  const header = doc.createElement("header");
2111
- this.heading = doc.createElement("h2");
2112
- this.heading.id = "title";
2119
+ this.#heading = doc.createElement("h2");
2120
+ this.#heading.id = "title";
2113
2121
  this.box.setAttribute("aria-labelledby", "title");
2114
2122
  this.closeButton = doc.createElement("button");
2115
2123
  this.closeButton.type = "button";
@@ -2118,7 +2126,7 @@ var defineDialog = definer("office-ui-dialog", () => {
2118
2126
  const icon2 = createIconSvg(doc);
2119
2127
  paintIcon(icon2, "close");
2120
2128
  this.closeButton.append(icon2);
2121
- header.append(this.heading, this.closeButton);
2129
+ header.append(this.#heading, this.closeButton);
2122
2130
  const body = doc.createElement("div");
2123
2131
  body.className = "body";
2124
2132
  body.append(doc.createElement("slot"));
@@ -2133,14 +2141,14 @@ var defineDialog = definer("office-ui-dialog", () => {
2133
2141
  this.addEventListener("keydown", (event) => this.onKey(event));
2134
2142
  }
2135
2143
  attributeChangedCallback(name, old, value) {
2136
- if (name === "heading") this.heading.textContent = value ?? "";
2144
+ if (name === "heading") this.#heading.textContent = value ?? "";
2137
2145
  if (name === "open" && old === null !== (value === null)) {
2138
2146
  if (value !== null) this.onOpen();
2139
2147
  else this.onClose();
2140
2148
  }
2141
2149
  }
2142
2150
  connectedCallback() {
2143
- this.heading.textContent = this.getAttribute("heading") ?? "";
2151
+ this.#heading.textContent = this.getAttribute("heading") ?? "";
2144
2152
  if (this.open) this.onOpen();
2145
2153
  }
2146
2154
  get open() {
@@ -2398,21 +2406,85 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
2398
2406
  return OfficeUiCommandSearch;
2399
2407
  });
2400
2408
 
2409
+ // src/menu-model.ts
2410
+ var EMPTY_MENU_STATE = { x: 0, y: 0, label: "", items: [] };
2411
+ function nextEnabledIndex(items, from, step) {
2412
+ const count = items.length;
2413
+ for (let offset = 1; offset <= count; offset += 1) {
2414
+ const index = ((from + step * offset) % count + count) % count;
2415
+ if (!items[index]?.disabled) return index;
2416
+ }
2417
+ return -1;
2418
+ }
2419
+ function typeAheadIndex(items, from, query) {
2420
+ const needle = query.toLowerCase();
2421
+ const cycling = [...needle].every((char) => char === needle[0]);
2422
+ const prefix = cycling ? needle[0] : needle;
2423
+ for (let offset = cycling ? 1 : 0; offset <= items.length; offset += 1) {
2424
+ const index = (from + offset + items.length) % items.length;
2425
+ const item = items[index];
2426
+ if (item && !item.disabled && item.label.trim().toLowerCase().startsWith(prefix ?? ""))
2427
+ return index;
2428
+ }
2429
+ return -1;
2430
+ }
2431
+ function clampFlyoutPosition(input) {
2432
+ const margin = input.margin ?? 8;
2433
+ const maxLeft = input.viewportWidth - input.width - margin;
2434
+ const maxTop = input.viewportHeight - input.height - margin;
2435
+ return {
2436
+ left: Math.max(margin, input.width > 0 ? Math.min(input.x, maxLeft) : input.x),
2437
+ top: Math.max(margin, input.height > 0 ? Math.min(input.y, maxTop) : input.y)
2438
+ };
2439
+ }
2440
+
2401
2441
  // src/context-menu.ts
2402
2442
  var CSS13 = `
2403
2443
  :host { display: contents; }
2444
+ :host([data-controlled]) { display: block; position: fixed; z-index: ${tok("--office-z-popover")};
2445
+ color: ${tok("--office-popover-foreground")}; font: ${tok("--office-font-size-sm")}/${tok("--office-line-height")} ${tok("--office-font")}; }
2446
+ :host([data-controlled][hidden]) { display: none; }
2404
2447
  .panel { position: fixed; inset: auto; margin: 0; min-width: ${tok("--office-context-menu-min-width")}; padding: ${tok("--office-space-1")}; box-sizing: border-box;
2405
2448
  background: ${tok("--office-background")}; color: ${tok("--office-foreground")};
2406
2449
  border: ${tok("--office-border-width")} solid ${tok("--office-border")}; border-radius: ${tok("--office-radius")};
2407
2450
  box-shadow: ${tok("--office-shadow")}; font-family: ${tok("--office-font")}; }
2408
2451
  .panel[hidden] { display: none; }
2409
- @media (forced-colors: active) { .panel { border-color: CanvasText; background: Canvas; color: CanvasText; } }
2452
+ /* Controlled mode: the host is the fixed box; rows are drawn here from \`state\`. */
2453
+ .menu { box-sizing: border-box; display: flex; flex-direction: column; min-width: ${tok("--office-menu-min-width")};
2454
+ max-height: calc(100vh - 2 * ${tok("--office-space-2")}); overflow-y: auto; padding: ${tok("--office-space-1-5")} 0;
2455
+ border: ${tok("--office-border-width")} solid ${tok("--office-border")}; border-radius: ${tok("--office-radius-md")};
2456
+ background: ${tok("--office-popover")}; color: inherit; box-shadow: ${tok("--office-shadow-lg")}; user-select: none; }
2457
+ .menu:focus { outline: none; }
2458
+ .item { box-sizing: border-box; display: flex; align-items: center; gap: ${tok("--office-space-1-5")}; width: 100%;
2459
+ min-height: ${tok("--office-field-height")}; padding: ${tok("--office-space-1-5")} ${tok("--office-space-3")}; border: 0;
2460
+ background: transparent; color: inherit; font: inherit; text-align: start; white-space: nowrap; cursor: pointer; }
2461
+ .item:hover:not(:disabled), .item:focus:not(:disabled) { background: ${tok("--office-selected")}; outline: none; }
2462
+ .item:focus-visible { outline: ${tok("--office-focus-width")} solid ${tok("--office-ring")}; outline-offset: calc(-1 * ${tok("--office-focus-width")}); }
2463
+ .item:disabled { opacity: .45; cursor: default; }
2464
+ .item.danger { color: ${tok("--office-danger")}; }
2465
+ .item svg { width: ${tok("--office-icon-size-md")}; height: ${tok("--office-icon-size-md")}; flex: none; fill: none; stroke: currentColor;
2466
+ stroke-width: ${tok("--office-icon-stroke")}; stroke-linecap: round; stroke-linejoin: round; }
2467
+ .check { flex: none; width: ${tok("--office-icon-size")}; text-align: center; }
2468
+ .separator { height: ${tok("--office-border-width")}; margin: ${tok("--office-space-1")} 0; background: ${tok("--office-border")}; }
2469
+ .heading { padding: ${tok("--office-space-1-5")} ${tok("--office-space-3")} ${tok("--office-space-0")}; color: ${tok("--office-muted-foreground")};
2470
+ font-size: ${tok("--office-font-size-2xs")}; font-weight: ${tok("--office-font-weight-bold")}; letter-spacing: .06em; text-transform: uppercase; }
2471
+ @media (pointer: coarse) { .item { min-height: ${tok("--office-target-size-touch")}; } }
2472
+ @media (forced-colors: active) {
2473
+ .panel, .menu { border-color: CanvasText; background: Canvas; color: CanvasText; box-shadow: none; }
2474
+ .item, .item.danger { color: CanvasText; }
2475
+ .item:disabled { color: GrayText; opacity: 1; }
2476
+ .item:hover:not(:disabled), .item:focus:not(:disabled) { background: Highlight; color: HighlightText; }
2477
+ .separator { background: CanvasText; }
2478
+ .heading { color: CanvasText; }
2479
+ }
2410
2480
  `;
2411
2481
  var SEPARATOR_CSS = `
2412
2482
  :host { display: block; height: ${tok("--office-border-width")}; margin: ${tok("--office-space-1")} ${tok("--office-space-1-5")}; background: ${tok("--office-border")}; }
2413
2483
  @media (forced-colors: active) { :host { background: CanvasText; } }
2414
2484
  `;
2415
2485
  var ITEMS = "office-ui-menu-item:not([disabled]):not([hidden])";
2486
+ var TYPE_AHEAD_MS = 700;
2487
+ var openMenus = /* @__PURE__ */ new WeakMap();
2416
2488
  var defineMenuSeparator = definer("office-ui-menu-separator", () => {
2417
2489
  class OfficeUiMenuSeparator extends HTMLElement {
2418
2490
  constructor() {
@@ -2427,10 +2499,25 @@ var defineMenuSeparator = definer("office-ui-menu-separator", () => {
2427
2499
  });
2428
2500
  var defineContextMenu = definer("office-ui-context-menu", () => {
2429
2501
  class OfficeUiContextMenu extends HTMLElement {
2502
+ static requestEvent = "office-menu-request";
2503
+ static closeEvent = "office-menu-close";
2504
+ static markerPattern = /^data-[a-z0-9-]+$/;
2430
2505
  static observedAttributes = ["label"];
2431
2506
  panel;
2507
+ menu;
2432
2508
  returnFocus = null;
2433
2509
  outside;
2510
+ model = EMPTY_MENU_STATE;
2511
+ controlled = false;
2512
+ rows = [];
2513
+ active = -1;
2514
+ focused = false;
2515
+ query = "";
2516
+ queryTimer;
2517
+ markers = [];
2518
+ listening = false;
2519
+ signature = "";
2520
+ opener = null;
2434
2521
  constructor() {
2435
2522
  super();
2436
2523
  const doc = this.ownerDocument;
@@ -2443,22 +2530,49 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2443
2530
  this.panel.setAttribute("popover", "auto");
2444
2531
  else this.panel.hidden = true;
2445
2532
  this.panel.append(doc.createElement("slot"));
2446
- root.append(this.panel);
2533
+ this.menu = doc.createElement("div");
2534
+ this.menu.className = "menu";
2535
+ this.menu.setAttribute("role", "menu");
2536
+ this.menu.setAttribute("aria-orientation", "vertical");
2537
+ this.menu.tabIndex = -1;
2538
+ this.menu.hidden = true;
2539
+ root.append(this.panel, this.menu);
2447
2540
  this.panel.addEventListener("toggle", (event) => {
2448
2541
  if (event.newState === "closed") this.closed(false);
2449
2542
  });
2450
- this.panel.addEventListener("keydown", (event) => this.onKey(event));
2543
+ this.panel.addEventListener("keydown", (event) => this.onChildKey(event));
2544
+ this.menu.addEventListener("keydown", (event) => this.onRowKey(event));
2545
+ this.menu.addEventListener("pointerover", (event) => {
2546
+ const row = event.target?.closest("button");
2547
+ const index = row ? this.rows.indexOf(row) : -1;
2548
+ if (index >= 0 && !row?.disabled && index !== this.active) this.focusRow(index);
2549
+ });
2550
+ this.menu.addEventListener("contextmenu", (event) => event.preventDefault());
2451
2551
  this.addEventListener("office-command", () => this.close());
2452
2552
  }
2453
2553
  connectedCallback() {
2454
2554
  this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
2555
+ if (this.controlled) {
2556
+ this.opener = this.deepActive();
2557
+ this.render();
2558
+ }
2455
2559
  }
2456
2560
  attributeChangedCallback() {
2457
2561
  this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
2458
2562
  }
2459
2563
  disconnectedCallback() {
2460
2564
  this.close();
2565
+ if (!this.controlled) return;
2566
+ this.stopListening();
2567
+ clearTimeout(this.queryTimer);
2568
+ this.focused = false;
2569
+ const opener = this.opener;
2570
+ this.opener = null;
2571
+ const active = this.ownerDocument.activeElement;
2572
+ if (opener?.isConnected && (!active || active === this.ownerDocument.body))
2573
+ opener.focus({ preventScroll: true });
2461
2574
  }
2575
+ // ---- Children mode ------------------------------------------------------------------
2462
2576
  get open() {
2463
2577
  return this.hasAttribute("data-open");
2464
2578
  }
@@ -2481,12 +2595,17 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2481
2595
  }
2482
2596
  this.setAttribute("data-open", "");
2483
2597
  const view = doc.defaultView;
2484
- const width = this.panel.offsetWidth;
2485
- const height = this.panel.offsetHeight;
2486
- const left = view && width ? Math.min(x, view.innerWidth - width - 4) : x;
2487
- const top = view && height ? Math.min(y, view.innerHeight - height - 4) : y;
2488
- this.panel.style.left = `${Math.max(4, Math.round(left))}px`;
2489
- this.panel.style.top = `${Math.max(4, Math.round(top))}px`;
2598
+ const { left, top } = clampFlyoutPosition({
2599
+ x,
2600
+ y,
2601
+ width: this.panel.offsetWidth,
2602
+ height: this.panel.offsetHeight,
2603
+ viewportWidth: view?.innerWidth ?? x,
2604
+ viewportHeight: view?.innerHeight ?? y,
2605
+ margin: 4
2606
+ });
2607
+ this.panel.style.left = `${Math.round(left)}px`;
2608
+ this.panel.style.top = `${Math.round(top)}px`;
2490
2609
  this.items()[0]?.focus();
2491
2610
  }
2492
2611
  close() {
@@ -2507,7 +2626,7 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2507
2626
  if (restoreFocus) this.returnFocus?.focus?.();
2508
2627
  this.returnFocus = null;
2509
2628
  }
2510
- onKey(event) {
2629
+ onChildKey(event) {
2511
2630
  const items = this.items();
2512
2631
  const from = items.findIndex(
2513
2632
  (item) => item === event.target || item.contains(event.target)
@@ -2523,8 +2642,217 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2523
2642
  else if (event.key === "Escape" || event.key === "Tab") {
2524
2643
  if (event.key === "Escape") event.preventDefault();
2525
2644
  this.close();
2645
+ } else if (event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey) {
2646
+ const labels = items.map((item) => ({
2647
+ label: item.getAttribute("label") ?? item.textContent ?? ""
2648
+ }));
2649
+ const target = this.typeAhead(labels, from, event.key);
2650
+ if (target >= 0) move(target);
2651
+ }
2652
+ }
2653
+ // ---- Controlled mode ----------------------------------------------------------------
2654
+ get state() {
2655
+ return this.model;
2656
+ }
2657
+ set state(value) {
2658
+ const signature = JSON.stringify(value);
2659
+ this.model = value;
2660
+ if (!this.controlled) {
2661
+ this.controlled = true;
2662
+ this.toggleAttribute("data-controlled", true);
2663
+ this.panel.remove();
2664
+ if (this.isConnected) this.opener = this.deepActive();
2665
+ }
2666
+ if (signature !== this.signature) {
2667
+ this.signature = signature;
2668
+ this.render();
2526
2669
  }
2527
2670
  }
2671
+ config() {
2672
+ return this.constructor;
2673
+ }
2674
+ deepActive() {
2675
+ let node = this.ownerDocument.activeElement;
2676
+ while (node?.shadowRoot?.activeElement) node = node.shadowRoot.activeElement;
2677
+ return node instanceof HTMLElement && node !== this.ownerDocument.body ? node : null;
2678
+ }
2679
+ render() {
2680
+ const state = this.model;
2681
+ const items = state.items;
2682
+ this.hidden = items.length === 0;
2683
+ this.menu.hidden = items.length === 0;
2684
+ this.applyMarkers(state.markers ?? []);
2685
+ if (state.zIndex !== void 0) this.style.zIndex = String(state.zIndex);
2686
+ else this.style.removeProperty("z-index");
2687
+ const same = items[this.active];
2688
+ this.active = same && !same.disabled ? this.active : nextEnabledIndex(items, -1, 1);
2689
+ this.renderRows();
2690
+ if (items.length === 0) {
2691
+ this.stopListening();
2692
+ this.focused = false;
2693
+ }
2694
+ if (!this.isConnected || items.length === 0) return;
2695
+ this.place();
2696
+ this.listen();
2697
+ if (!this.focused && state.autoFocus !== false) {
2698
+ this.focused = true;
2699
+ this.focusRow(nextEnabledIndex(items, -1, 1));
2700
+ } else if (this.focused && this.active >= 0)
2701
+ this.rows[this.active]?.focus({ preventScroll: true });
2702
+ }
2703
+ renderRows() {
2704
+ const doc = this.ownerDocument;
2705
+ const state = this.model;
2706
+ this.menu.setAttribute("aria-label", state.label);
2707
+ this.rows = [];
2708
+ let container = this.menu;
2709
+ const nodes = [];
2710
+ const place = (node) => container === this.menu ? nodes.push(node) : container.append(node);
2711
+ state.items.forEach((item, index) => {
2712
+ if (item.separatorBefore || item.heading) container = this.menu;
2713
+ if (item.separatorBefore) {
2714
+ const rule = doc.createElement("div");
2715
+ rule.className = "separator";
2716
+ rule.setAttribute("role", "separator");
2717
+ place(rule);
2718
+ }
2719
+ if (item.heading) {
2720
+ const group = doc.createElement("div");
2721
+ group.setAttribute("role", "group");
2722
+ group.setAttribute("aria-label", item.heading);
2723
+ const title = doc.createElement("div");
2724
+ title.className = "heading";
2725
+ title.setAttribute("aria-hidden", "true");
2726
+ title.textContent = item.heading;
2727
+ group.append(title);
2728
+ place(group);
2729
+ container = group;
2730
+ }
2731
+ const row = doc.createElement("button");
2732
+ row.type = "button";
2733
+ row.className = item.danger ? "item danger" : "item";
2734
+ row.dataset.itemId = item.id;
2735
+ row.setAttribute("role", item.checked === void 0 ? "menuitem" : "menuitemcheckbox");
2736
+ if (item.checked !== void 0) {
2737
+ row.setAttribute("aria-checked", String(item.checked));
2738
+ const check = doc.createElement("span");
2739
+ check.className = "check";
2740
+ check.setAttribute("aria-hidden", "true");
2741
+ check.textContent = item.checked ? "\u2713" : "";
2742
+ row.append(check);
2743
+ }
2744
+ if (item.icon) {
2745
+ const glyph = createIconSvg(doc);
2746
+ paintIcon(glyph, item.icon);
2747
+ row.append(glyph);
2748
+ }
2749
+ row.append(doc.createTextNode(item.label));
2750
+ row.tabIndex = index === this.active ? 0 : -1;
2751
+ if (item.disabled) {
2752
+ row.disabled = true;
2753
+ row.setAttribute("aria-disabled", "true");
2754
+ }
2755
+ row.addEventListener(
2756
+ "click",
2757
+ () => this.dispatchEvent(
2758
+ new CustomEvent(this.config().requestEvent, {
2759
+ detail: { id: item.id },
2760
+ bubbles: true,
2761
+ composed: true
2762
+ })
2763
+ )
2764
+ );
2765
+ this.rows.push(row);
2766
+ place(row);
2767
+ });
2768
+ this.menu.replaceChildren(...nodes);
2769
+ }
2770
+ applyMarkers(next) {
2771
+ for (const name of this.markers) this.removeAttribute(name);
2772
+ const pattern = this.config().markerPattern;
2773
+ this.markers = next.filter((name) => pattern.test(name));
2774
+ for (const name of this.markers) this.setAttribute(name, "true");
2775
+ }
2776
+ place() {
2777
+ const view = this.ownerDocument.defaultView;
2778
+ this.style.left = `${this.model.x}px`;
2779
+ this.style.top = `${this.model.y}px`;
2780
+ const box = this.getBoundingClientRect();
2781
+ const { left, top } = clampFlyoutPosition({
2782
+ x: this.model.x,
2783
+ y: this.model.y,
2784
+ width: box.width,
2785
+ height: box.height,
2786
+ viewportWidth: view?.innerWidth ?? box.right,
2787
+ viewportHeight: view?.innerHeight ?? box.bottom
2788
+ });
2789
+ this.style.left = `${left}px`;
2790
+ this.style.top = `${top}px`;
2791
+ }
2792
+ focusRow(index) {
2793
+ if (index < 0) return;
2794
+ this.active = index;
2795
+ this.rows.forEach((row, at) => row.tabIndex = at === index ? 0 : -1);
2796
+ this.rows[index]?.focus({ preventScroll: true });
2797
+ }
2798
+ typeAhead(items, from, key) {
2799
+ this.query += key;
2800
+ clearTimeout(this.queryTimer);
2801
+ this.queryTimer = setTimeout(() => this.query = "", TYPE_AHEAD_MS);
2802
+ return typeAheadIndex(items, from, this.query);
2803
+ }
2804
+ onRowKey(event) {
2805
+ const items = this.model.items;
2806
+ let target = -2;
2807
+ if (event.key === "ArrowDown") target = nextEnabledIndex(items, this.active, 1);
2808
+ else if (event.key === "ArrowUp") target = nextEnabledIndex(items, this.active, -1);
2809
+ else if (event.key === "Home") target = nextEnabledIndex(items, -1, 1);
2810
+ else if (event.key === "End") target = nextEnabledIndex(items, items.length, -1);
2811
+ else if (event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey)
2812
+ target = this.typeAhead(items, this.active, event.key);
2813
+ if (target === -2) return;
2814
+ event.preventDefault();
2815
+ this.focusRow(target);
2816
+ }
2817
+ dismiss(reason) {
2818
+ this.dispatchEvent(
2819
+ new CustomEvent(this.config().closeEvent, {
2820
+ detail: { reason },
2821
+ bubbles: true,
2822
+ composed: true
2823
+ })
2824
+ );
2825
+ }
2826
+ onDocumentKey = (event) => {
2827
+ if (openMenus.get(this.ownerDocument)?.at(-1) !== this) return;
2828
+ if (event.key === "Escape") {
2829
+ event.preventDefault();
2830
+ event.stopPropagation();
2831
+ this.dismiss("escape");
2832
+ } else if (event.key === "Tab") this.dismiss("tab");
2833
+ };
2834
+ onDocumentPointer = (event) => {
2835
+ if (!event.composedPath().includes(this)) this.dismiss("outside");
2836
+ };
2837
+ listen() {
2838
+ if (this.listening) return;
2839
+ this.listening = true;
2840
+ const doc = this.ownerDocument;
2841
+ openMenus.set(doc, [...openMenus.get(doc) ?? [], this]);
2842
+ doc.addEventListener("keydown", this.onDocumentKey, true);
2843
+ doc.addEventListener("pointerdown", this.onDocumentPointer, true);
2844
+ }
2845
+ stopListening() {
2846
+ if (!this.listening) return;
2847
+ this.listening = false;
2848
+ const doc = this.ownerDocument;
2849
+ openMenus.set(
2850
+ doc,
2851
+ (openMenus.get(doc) ?? []).filter((menu) => menu !== this)
2852
+ );
2853
+ doc.removeEventListener("keydown", this.onDocumentKey, true);
2854
+ doc.removeEventListener("pointerdown", this.onDocumentPointer, true);
2855
+ }
2528
2856
  }
2529
2857
  return OfficeUiContextMenu;
2530
2858
  });
@@ -3172,13 +3500,13 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
3172
3500
  "icon",
3173
3501
  "data-open"
3174
3502
  ];
3175
- caption;
3176
- launcher;
3177
- face;
3178
- faceIcon;
3179
- faceLabel;
3180
- slotEl;
3181
- observer;
3503
+ #caption;
3504
+ #launcher;
3505
+ #face;
3506
+ #faceIcon;
3507
+ #faceLabel;
3508
+ #slotEl;
3509
+ #observer;
3182
3510
  constructor() {
3183
3511
  super();
3184
3512
  const doc = this.ownerDocument;
@@ -3189,19 +3517,19 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
3189
3517
  group.className = "group";
3190
3518
  const row = doc.createElement("div");
3191
3519
  row.className = "row";
3192
- this.slotEl = doc.createElement("slot");
3193
- this.slotEl.addEventListener("slotchange", () => this.observe());
3194
- row.append(this.slotEl);
3195
- this.caption = doc.createElement("span");
3196
- this.caption.className = "caption";
3197
- this.launcher = doc.createElement("button");
3198
- this.launcher.type = "button";
3199
- this.launcher.className = "launcher";
3520
+ this.#slotEl = doc.createElement("slot");
3521
+ this.#slotEl.addEventListener("slotchange", () => this.#observe());
3522
+ row.append(this.#slotEl);
3523
+ this.#caption = doc.createElement("span");
3524
+ this.#caption.className = "caption";
3525
+ this.#launcher = doc.createElement("button");
3526
+ this.#launcher.type = "button";
3527
+ this.#launcher.className = "launcher";
3200
3528
  const glyph = createIconSvg(doc);
3201
3529
  paintIcon(glyph, "launcher");
3202
- this.launcher.append(glyph);
3203
- this.launcher.addEventListener("click", () => {
3204
- if (this.launcher.disabled) return;
3530
+ this.#launcher.append(glyph);
3531
+ this.#launcher.addEventListener("click", () => {
3532
+ if (this.#launcher.disabled) return;
3205
3533
  const detail = this.launcherDetail();
3206
3534
  if (detail)
3207
3535
  this.dispatchEvent(
@@ -3210,36 +3538,36 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
3210
3538
  });
3211
3539
  const foot = doc.createElement("div");
3212
3540
  foot.className = "foot";
3213
- foot.append(this.caption, this.launcher);
3214
- this.face = doc.createElement("button");
3215
- this.face.type = "button";
3216
- this.face.className = "face";
3217
- this.face.setAttribute("aria-haspopup", "true");
3218
- this.faceIcon = createIconSvg(doc);
3219
- this.faceLabel = doc.createElement("span");
3541
+ foot.append(this.#caption, this.#launcher);
3542
+ this.#face = doc.createElement("button");
3543
+ this.#face.type = "button";
3544
+ this.#face.className = "face";
3545
+ this.#face.setAttribute("aria-haspopup", "true");
3546
+ this.#faceIcon = createIconSvg(doc);
3547
+ this.#faceLabel = doc.createElement("span");
3220
3548
  const chevron = createIconSvg(doc);
3221
3549
  chevron.classList.add("chev");
3222
3550
  paintIcon(chevron, "chevronDown");
3223
- this.face.append(this.faceIcon, this.faceLabel, chevron);
3224
- this.face.addEventListener(
3551
+ this.#face.append(this.#faceIcon, this.#faceLabel, chevron);
3552
+ this.#face.addEventListener(
3225
3553
  "click",
3226
3554
  () => this.dispatchEvent(
3227
3555
  new CustomEvent(config3().collapseEvent, { bubbles: true, composed: true })
3228
3556
  )
3229
3557
  );
3230
- group.append(this.face, row, foot);
3558
+ group.append(this.#face, row, foot);
3231
3559
  root.append(group);
3232
3560
  }
3233
3561
  connectedCallback() {
3234
3562
  this.setAttribute("role", "group");
3235
- this.sync();
3236
- this.observe();
3563
+ this.#sync();
3564
+ this.#observe();
3237
3565
  }
3238
3566
  disconnectedCallback() {
3239
- this.observer?.disconnect();
3567
+ this.#observer?.disconnect();
3240
3568
  }
3241
3569
  attributeChangedCallback() {
3242
- this.sync();
3570
+ this.#sync();
3243
3571
  }
3244
3572
  /** The launcher's event detail; null emits nothing. */
3245
3573
  launcherDetail() {
@@ -3250,15 +3578,15 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
3250
3578
  iconName() {
3251
3579
  return this.getAttribute("icon") ?? "grid";
3252
3580
  }
3253
- observe() {
3254
- this.observer?.disconnect();
3581
+ #observe() {
3582
+ this.#observer?.disconnect();
3255
3583
  if (!this.isConnected) return;
3256
3584
  const Observer = this.ownerDocument.defaultView?.ResizeObserver;
3257
- if (Observer) this.observer ??= new Observer(() => this.measure());
3258
- for (const el of this.slotEl.assignedElements()) this.observer?.observe(el);
3259
- this.measure();
3585
+ if (Observer) this.#observer ??= new Observer(() => this.#measure());
3586
+ for (const el of this.#slotEl.assignedElements()) this.#observer?.observe(el);
3587
+ this.#measure();
3260
3588
  }
3261
- measure() {
3589
+ #measure() {
3262
3590
  const children = [...this.children];
3263
3591
  const stackable = children.filter((el) => {
3264
3592
  const gallery = el.matches('[mode="dropdown"]') ? el : el.querySelector('[mode="dropdown"]');
@@ -3275,20 +3603,20 @@ var defineRibbonGroup = definer("office-ui-ribbon-group", () => {
3275
3603
  tallest <= tokenPx(this, "--office-ribbon-compact-row-max", 36)
3276
3604
  );
3277
3605
  }
3278
- sync() {
3606
+ #sync() {
3279
3607
  const label = this.getAttribute("label") ?? "";
3280
- this.caption.textContent = label;
3608
+ this.#caption.textContent = label;
3281
3609
  this.setAttribute("aria-label", label);
3282
3610
  const launcherLabel = this.getAttribute("launcher-label") ?? `${label} options`;
3283
- this.launcher.hidden = !this.hasAttribute("launcher");
3284
- this.launcher.disabled = this.hasAttribute("launcher-disabled");
3285
- this.launcher.setAttribute("aria-label", launcherLabel);
3286
- this.launcher.title = launcherLabel;
3287
- this.faceLabel.textContent = label;
3288
- this.face.title = label;
3289
- this.face.setAttribute("aria-label", label);
3290
- this.face.setAttribute("aria-expanded", String(this.hasAttribute("data-open")));
3291
- paintIcon(this.faceIcon, this.iconName());
3611
+ this.#launcher.hidden = !this.hasAttribute("launcher");
3612
+ this.#launcher.disabled = this.hasAttribute("launcher-disabled");
3613
+ this.#launcher.setAttribute("aria-label", launcherLabel);
3614
+ this.#launcher.title = launcherLabel;
3615
+ this.#faceLabel.textContent = label;
3616
+ this.#face.title = label;
3617
+ this.#face.setAttribute("aria-label", label);
3618
+ this.#face.setAttribute("aria-expanded", String(this.hasAttribute("data-open")));
3619
+ paintIcon(this.#faceIcon, this.iconName());
3292
3620
  }
3293
3621
  }
3294
3622
  return OfficeUiRibbonGroup;
@@ -4157,7 +4485,7 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
4157
4485
  in;
4158
4486
  range;
4159
4487
  output;
4160
- fit;
4488
+ #fit;
4161
4489
  constructor() {
4162
4490
  super();
4163
4491
  const doc = this.ownerDocument;
@@ -4177,12 +4505,12 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
4177
4505
  this.range.type = "range";
4178
4506
  this.range.setAttribute("aria-label", "Zoom");
4179
4507
  this.output = doc.createElement("output");
4180
- this.fit = button("Fit", "");
4181
- this.fit.className = "fit";
4508
+ this.#fit = button("Fit", "");
4509
+ this.#fit.className = "fit";
4182
4510
  const svg = createIconSvg(doc);
4183
4511
  paintIcon(svg, "fitPage");
4184
- this.fit.append(svg);
4185
- root.append(this.out, this.range, this.in, this.output, this.fit);
4512
+ this.#fit.append(svg);
4513
+ root.append(this.out, this.range, this.in, this.output, this.#fit);
4186
4514
  this.out.addEventListener("click", () => this.stepBy(-1));
4187
4515
  this.in.addEventListener("click", () => this.stepBy(1));
4188
4516
  this.range.addEventListener("input", () => this.commit(this.range.valueAsNumber, "input"));
@@ -4190,7 +4518,7 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
4190
4518
  "change",
4191
4519
  () => this.dispatchEvent(new Event("change", { bubbles: true, composed: true }))
4192
4520
  );
4193
- this.fit.addEventListener(
4521
+ this.#fit.addEventListener(
4194
4522
  "click",
4195
4523
  () => emit(this, "office-command", { command: "zoom-fit" })
4196
4524
  );
@@ -4248,9 +4576,9 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
4248
4576
  this.in.disabled = disabled || value >= max;
4249
4577
  this.range.disabled = disabled;
4250
4578
  const fit = this.getAttribute("fit");
4251
- this.fit.disabled = disabled;
4252
- this.fit.setAttribute("aria-label", fit || "Fit");
4253
- this.fit.title = fit || "Fit";
4579
+ this.#fit.disabled = disabled;
4580
+ this.#fit.setAttribute("aria-label", fit || "Fit");
4581
+ this.#fit.title = fit || "Fit";
4254
4582
  }
4255
4583
  }
4256
4584
  return OfficeUiZoomSlider;
@@ -4442,4 +4770,4 @@ function registerControls(registry) {
4442
4770
  for (const define of CONTROL_DEFINERS) define(registry);
4443
4771
  }
4444
4772
 
4445
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile };
4773
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, nextEnabledIndex, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile };
@@ -1,10 +1,20 @@
1
1
  /** A `role="separator"` rule between groups of menu items. */
2
2
  export declare const defineMenuSeparator: import("./registry.js").Definer;
3
3
  /**
4
- * Right-click menu of `office-ui-menu-item` and `office-ui-menu-separator` children (Office's
5
- * shape and text context menus). `openAt(x, y)` shows it at client coordinates in the popover
6
- * top layer, kept inside the viewport; `close()` hides it. Arrow keys, Home and End move between
7
- * enabled items; Escape and Tab close and return focus to the element that was focused. Choosing
8
- * an item closes the menu and its `office-command` bubbles to the host. `label` names the menu.
4
+ * Office's right-click menu, in two modes sharing keyboard, type-ahead and dismissal:
5
+ *
6
+ * - **Children** (self-managed): `office-ui-menu-item` and `office-ui-menu-separator` children.
7
+ * `openAt(x, y)` shows it in the popover top layer, kept inside the viewport; `close()` hides it.
8
+ * Choosing an item closes the menu and its `office-command` bubbles to the host.
9
+ * - **Controlled** (moved from pptx-viewer `pptx-ui-context-menu`): set `state`
10
+ * (`{ x, y, label, items: [{ id, label, separatorBefore?, heading?, danger?, disabled?,
11
+ * checked?, icon? }], markers?, zIndex?, autoFocus? }`). The host is the fixed box at the
12
+ * pointer, clamped into the window. It emits `office-menu-request` `{ id }` when a row is chosen
13
+ * and `office-menu-close` `{ reason: 'escape' | 'outside' | 'tab' }` when dismissed, and never
14
+ * closes itself: the host removes it or clears its items. While open it takes focus on the first
15
+ * enabled row and returns focus to the opener when it goes away, unless something else took it.
16
+ *
17
+ * Arrow keys, Home, End and type-ahead (repeating a letter cycles) move between enabled rows.
18
+ * Static `requestEvent`, `closeEvent` and `markerPattern` let a product keep its published names.
9
19
  */
10
20
  export declare const defineContextMenu: import("./registry.js").Definer;
@@ -35,6 +35,7 @@ export type { OfficeFindInputEvent, OfficeFindStepEvent } from './find-bar.js';
35
35
  export type { OfficePrintPreviewPageEvent } from './print-preview.js';
36
36
  export type { OfficeRibbonSelectEvent } from './ribbon-tabs.js';
37
37
  export { rulerDivisions } from './ruler.js';
38
+ export { clampFlyoutPosition, EMPTY_MENU_STATE, nextEnabledIndex, typeAheadIndex, type FlyoutPositionInput, type OfficeMenuCloseReason, type OfficeMenuItem, type OfficeMenuState, } from './menu-model.js';
38
39
  export { OFFICE_TOAST_VISIBLE_LIMIT, type OfficeDialogFooterAction, type OfficeDialogFooterState, type OfficeDialogFooterVariant, type OfficeToast, type OfficeToastsState, } from './chrome-controls.js';
39
40
  export type { OfficePasteOption, OfficePasteOptionsState, OfficeReadOnlyBannerIntent, OfficeReadOnlyBannerState, } from './document-notices.js';
40
41
  export { clearOfficeProfile, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, profileInitials, readOfficeProfile, sanitizeOfficeProfile, writeOfficeProfile, type OfficeProfile, type OfficeProfileChangeEvent, } from './account.js';
package/dist/controls.js CHANGED
@@ -1,4 +1,4 @@
1
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-ZBFAPBNU.js';
1
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, nextEnabledIndex, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-2XJDHLPW.js';
2
2
  export { defineIcon } from './chunk-VUXNXH4Q.js';
3
3
  import './chunk-VYF7LGXZ.js';
4
4
  import './chunk-NDEB6EBU.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { installOfficeUiTheme } from './chunk-F4TM7U4P.js';
2
2
  export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme } from './chunk-F4TM7U4P.js';
3
- import { registerControls } from './chunk-ZBFAPBNU.js';
4
- export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, writeOfficeProfile } from './chunk-ZBFAPBNU.js';
3
+ import { registerControls } from './chunk-2XJDHLPW.js';
4
+ export { CONTROL_DEFINERS, DEFAULT_OFFICE_PROFILE, EMPTY_MENU_STATE, OFFICE_AVATAR_SWATCHES, OFFICE_PROFILE_STORAGE_KEY, OFFICE_TOAST_VISIBLE_LIMIT, attachKeyTips, clampFlyoutPosition, clampOptionNumber, clearOfficeProfile, defineAccount, defineBackstage, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineDialogFooter, defineFindBar, defineMenuButton, defineMenuItem, defineMenuSeparator, defineOptionsDialog, definePasteOptions, definePrintPreview, defineRadio, defineReadOnlyBanner, defineRibbon, defineRibbonGroup, defineRibbonStack, defineRibbonToggle, defineRuler, defineSearchField, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToasts, defineToolbar, defineZoomSlider, nextEnabledIndex, profileInitials, readOfficeProfile, registerControls, rulerDivisions, sanitizeOfficeProfile, typeAheadIndex, writeOfficeProfile } from './chunk-2XJDHLPW.js';
5
5
  export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-VUXNXH4Q.js';
6
6
  import { definePresence } from './chunk-VYF7LGXZ.js';
7
7
  export { definePresence, initialsOf, participantsFromAwareness } from './chunk-VYF7LGXZ.js';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Menu data and the pure helpers both menu modes share. Moved from pptx-viewer
3
+ * (`web-components/context-menu-model.ts`, `render/flyout-position.ts`).
4
+ */
5
+ /** One row of a controlled menu. Hosts decide `id`, wording, gating and what activation does. */
6
+ export interface OfficeMenuItem {
7
+ /** Stable command id, echoed by the request event. */
8
+ id: string;
9
+ /** Translated visible label. */
10
+ label: string;
11
+ /** Draw a rule before this row. */
12
+ separatorBefore?: boolean;
13
+ /** Group heading shown before this row; the row and the ones after it join that group. */
14
+ heading?: string;
15
+ /** Destructive command (Delete): tinted. */
16
+ danger?: boolean;
17
+ /** Offered but unavailable: announced and drawn dim, skipped by the arrow keys. */
18
+ disabled?: boolean;
19
+ /** A toggle (Grid, Ruler) in this state; omitted for one-shot commands. */
20
+ checked?: boolean;
21
+ /** A registered icon drawn before the label. */
22
+ icon?: string;
23
+ }
24
+ /** Everything a controlled menu needs to draw. */
25
+ export interface OfficeMenuState {
26
+ /** Pointer position, in viewport pixels; the menu clamps itself into the window. */
27
+ x: number;
28
+ y: number;
29
+ /** Accessible name. */
30
+ label: string;
31
+ items: readonly OfficeMenuItem[];
32
+ /** Test-hook attributes copied onto the host (names matching the element's `markerPattern`). */
33
+ markers?: readonly string[];
34
+ /** Stacking order; the `--office-z-popover` token applies when omitted. */
35
+ zIndex?: number;
36
+ /** Move focus onto the first enabled command on open. Default true. */
37
+ autoFocus?: boolean;
38
+ }
39
+ export type OfficeMenuCloseReason = 'escape' | 'outside' | 'tab';
40
+ export declare const EMPTY_MENU_STATE: OfficeMenuState;
41
+ /** Index of the next enabled row from `from` in `step` direction, wrapping; -1 if none. */
42
+ export declare function nextEnabledIndex(items: readonly {
43
+ disabled?: boolean;
44
+ }[], from: number, step: 1 | -1): number;
45
+ /**
46
+ * Type-ahead target: the next enabled row after `from` whose label starts with `query`.
47
+ * Repeating one character cycles through the rows that start with it.
48
+ */
49
+ export declare function typeAheadIndex(items: readonly {
50
+ label: string;
51
+ disabled?: boolean;
52
+ }[], from: number, query: string): number;
53
+ /** Where a flyout wants to sit, how big it is, and what it must fit inside. */
54
+ export interface FlyoutPositionInput {
55
+ /** Anchor point in viewport coordinates (a pointer event's clientX/Y). */
56
+ x: number;
57
+ y: number;
58
+ /** Measured size; zero means "not measured yet". */
59
+ width: number;
60
+ height: number;
61
+ /** The box it must stay inside, normally the window's inner size. */
62
+ viewportWidth: number;
63
+ viewportHeight: number;
64
+ /** Gap kept from every edge, in px. Default 8. */
65
+ margin?: number;
66
+ }
67
+ /**
68
+ * The top-left corner keeping a pointer-anchored flyout fully on screen. Flips inwards at the
69
+ * right and bottom edges, as desktop menus do, and never returns a negative coordinate, so a
70
+ * flyout larger than the viewport stays reachable from the top.
71
+ */
72
+ export declare function clampFlyoutPosition(input: FlyoutPositionInput): {
73
+ left: number;
74
+ top: number;
75
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml-ui",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Shared, format-neutral web components and styles for the Office viewers (Word, PowerPoint, Excel)",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -35,27 +35,33 @@
35
35
  "exports": {
36
36
  ".": {
37
37
  "types": "./dist/index.d.ts",
38
- "import": "./dist/index.js"
38
+ "import": "./dist/index.js",
39
+ "default": "./dist/index.js"
39
40
  },
40
41
  "./theme": {
41
42
  "types": "./dist/theme.d.ts",
42
- "import": "./dist/theme.js"
43
+ "import": "./dist/theme.js",
44
+ "default": "./dist/theme.js"
43
45
  },
44
46
  "./icons": {
45
47
  "types": "./dist/icons.d.ts",
46
- "import": "./dist/icons.js"
48
+ "import": "./dist/icons.js",
49
+ "default": "./dist/icons.js"
47
50
  },
48
51
  "./controls": {
49
52
  "types": "./dist/controls.d.ts",
50
- "import": "./dist/controls.js"
53
+ "import": "./dist/controls.js",
54
+ "default": "./dist/controls.js"
51
55
  },
52
56
  "./presence": {
53
57
  "types": "./dist/presence.d.ts",
54
- "import": "./dist/presence.js"
58
+ "import": "./dist/presence.js",
59
+ "default": "./dist/presence.js"
55
60
  },
56
61
  "./smartart": {
57
62
  "types": "./dist/smartart.d.ts",
58
- "import": "./dist/smartart.js"
63
+ "import": "./dist/smartart.js",
64
+ "default": "./dist/smartart.js"
59
65
  },
60
66
  "./package.json": "./package.json"
61
67
  },