ooxml-ui 0.15.0 → 0.16.0

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) => {
@@ -2398,21 +2402,85 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
2398
2402
  return OfficeUiCommandSearch;
2399
2403
  });
2400
2404
 
2405
+ // src/menu-model.ts
2406
+ var EMPTY_MENU_STATE = { x: 0, y: 0, label: "", items: [] };
2407
+ function nextEnabledIndex(items, from, step) {
2408
+ const count = items.length;
2409
+ for (let offset = 1; offset <= count; offset += 1) {
2410
+ const index = ((from + step * offset) % count + count) % count;
2411
+ if (!items[index]?.disabled) return index;
2412
+ }
2413
+ return -1;
2414
+ }
2415
+ function typeAheadIndex(items, from, query) {
2416
+ const needle = query.toLowerCase();
2417
+ const cycling = [...needle].every((char) => char === needle[0]);
2418
+ const prefix = cycling ? needle[0] : needle;
2419
+ for (let offset = cycling ? 1 : 0; offset <= items.length; offset += 1) {
2420
+ const index = (from + offset + items.length) % items.length;
2421
+ const item = items[index];
2422
+ if (item && !item.disabled && item.label.trim().toLowerCase().startsWith(prefix ?? ""))
2423
+ return index;
2424
+ }
2425
+ return -1;
2426
+ }
2427
+ function clampFlyoutPosition(input) {
2428
+ const margin = input.margin ?? 8;
2429
+ const maxLeft = input.viewportWidth - input.width - margin;
2430
+ const maxTop = input.viewportHeight - input.height - margin;
2431
+ return {
2432
+ left: Math.max(margin, input.width > 0 ? Math.min(input.x, maxLeft) : input.x),
2433
+ top: Math.max(margin, input.height > 0 ? Math.min(input.y, maxTop) : input.y)
2434
+ };
2435
+ }
2436
+
2401
2437
  // src/context-menu.ts
2402
2438
  var CSS13 = `
2403
2439
  :host { display: contents; }
2440
+ :host([data-controlled]) { display: block; position: fixed; z-index: ${tok("--office-z-popover")};
2441
+ color: ${tok("--office-popover-foreground")}; font: ${tok("--office-font-size-sm")}/${tok("--office-line-height")} ${tok("--office-font")}; }
2442
+ :host([data-controlled][hidden]) { display: none; }
2404
2443
  .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
2444
  background: ${tok("--office-background")}; color: ${tok("--office-foreground")};
2406
2445
  border: ${tok("--office-border-width")} solid ${tok("--office-border")}; border-radius: ${tok("--office-radius")};
2407
2446
  box-shadow: ${tok("--office-shadow")}; font-family: ${tok("--office-font")}; }
2408
2447
  .panel[hidden] { display: none; }
2409
- @media (forced-colors: active) { .panel { border-color: CanvasText; background: Canvas; color: CanvasText; } }
2448
+ /* Controlled mode: the host is the fixed box; rows are drawn here from \`state\`. */
2449
+ .menu { box-sizing: border-box; display: flex; flex-direction: column; min-width: ${tok("--office-menu-min-width")};
2450
+ max-height: calc(100vh - 2 * ${tok("--office-space-2")}); overflow-y: auto; padding: ${tok("--office-space-1-5")} 0;
2451
+ border: ${tok("--office-border-width")} solid ${tok("--office-border")}; border-radius: ${tok("--office-radius-md")};
2452
+ background: ${tok("--office-popover")}; color: inherit; box-shadow: ${tok("--office-shadow-lg")}; user-select: none; }
2453
+ .menu:focus { outline: none; }
2454
+ .item { box-sizing: border-box; display: flex; align-items: center; gap: ${tok("--office-space-1-5")}; width: 100%;
2455
+ min-height: ${tok("--office-field-height")}; padding: ${tok("--office-space-1-5")} ${tok("--office-space-3")}; border: 0;
2456
+ background: transparent; color: inherit; font: inherit; text-align: start; white-space: nowrap; cursor: pointer; }
2457
+ .item:hover:not(:disabled), .item:focus:not(:disabled) { background: ${tok("--office-selected")}; outline: none; }
2458
+ .item:focus-visible { outline: ${tok("--office-focus-width")} solid ${tok("--office-ring")}; outline-offset: calc(-1 * ${tok("--office-focus-width")}); }
2459
+ .item:disabled { opacity: .45; cursor: default; }
2460
+ .item.danger { color: ${tok("--office-danger")}; }
2461
+ .item svg { width: ${tok("--office-icon-size-md")}; height: ${tok("--office-icon-size-md")}; flex: none; fill: none; stroke: currentColor;
2462
+ stroke-width: ${tok("--office-icon-stroke")}; stroke-linecap: round; stroke-linejoin: round; }
2463
+ .check { flex: none; width: ${tok("--office-icon-size")}; text-align: center; }
2464
+ .separator { height: ${tok("--office-border-width")}; margin: ${tok("--office-space-1")} 0; background: ${tok("--office-border")}; }
2465
+ .heading { padding: ${tok("--office-space-1-5")} ${tok("--office-space-3")} ${tok("--office-space-0")}; color: ${tok("--office-muted-foreground")};
2466
+ font-size: ${tok("--office-font-size-2xs")}; font-weight: ${tok("--office-font-weight-bold")}; letter-spacing: .06em; text-transform: uppercase; }
2467
+ @media (pointer: coarse) { .item { min-height: ${tok("--office-target-size-touch")}; } }
2468
+ @media (forced-colors: active) {
2469
+ .panel, .menu { border-color: CanvasText; background: Canvas; color: CanvasText; box-shadow: none; }
2470
+ .item, .item.danger { color: CanvasText; }
2471
+ .item:disabled { color: GrayText; opacity: 1; }
2472
+ .item:hover:not(:disabled), .item:focus:not(:disabled) { background: Highlight; color: HighlightText; }
2473
+ .separator { background: CanvasText; }
2474
+ .heading { color: CanvasText; }
2475
+ }
2410
2476
  `;
2411
2477
  var SEPARATOR_CSS = `
2412
2478
  :host { display: block; height: ${tok("--office-border-width")}; margin: ${tok("--office-space-1")} ${tok("--office-space-1-5")}; background: ${tok("--office-border")}; }
2413
2479
  @media (forced-colors: active) { :host { background: CanvasText; } }
2414
2480
  `;
2415
2481
  var ITEMS = "office-ui-menu-item:not([disabled]):not([hidden])";
2482
+ var TYPE_AHEAD_MS = 700;
2483
+ var openMenus = /* @__PURE__ */ new WeakMap();
2416
2484
  var defineMenuSeparator = definer("office-ui-menu-separator", () => {
2417
2485
  class OfficeUiMenuSeparator extends HTMLElement {
2418
2486
  constructor() {
@@ -2427,10 +2495,25 @@ var defineMenuSeparator = definer("office-ui-menu-separator", () => {
2427
2495
  });
2428
2496
  var defineContextMenu = definer("office-ui-context-menu", () => {
2429
2497
  class OfficeUiContextMenu extends HTMLElement {
2498
+ static requestEvent = "office-menu-request";
2499
+ static closeEvent = "office-menu-close";
2500
+ static markerPattern = /^data-[a-z0-9-]+$/;
2430
2501
  static observedAttributes = ["label"];
2431
2502
  panel;
2503
+ menu;
2432
2504
  returnFocus = null;
2433
2505
  outside;
2506
+ model = EMPTY_MENU_STATE;
2507
+ controlled = false;
2508
+ rows = [];
2509
+ active = -1;
2510
+ focused = false;
2511
+ query = "";
2512
+ queryTimer;
2513
+ markers = [];
2514
+ listening = false;
2515
+ signature = "";
2516
+ opener = null;
2434
2517
  constructor() {
2435
2518
  super();
2436
2519
  const doc = this.ownerDocument;
@@ -2443,22 +2526,49 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2443
2526
  this.panel.setAttribute("popover", "auto");
2444
2527
  else this.panel.hidden = true;
2445
2528
  this.panel.append(doc.createElement("slot"));
2446
- root.append(this.panel);
2529
+ this.menu = doc.createElement("div");
2530
+ this.menu.className = "menu";
2531
+ this.menu.setAttribute("role", "menu");
2532
+ this.menu.setAttribute("aria-orientation", "vertical");
2533
+ this.menu.tabIndex = -1;
2534
+ this.menu.hidden = true;
2535
+ root.append(this.panel, this.menu);
2447
2536
  this.panel.addEventListener("toggle", (event) => {
2448
2537
  if (event.newState === "closed") this.closed(false);
2449
2538
  });
2450
- this.panel.addEventListener("keydown", (event) => this.onKey(event));
2539
+ this.panel.addEventListener("keydown", (event) => this.onChildKey(event));
2540
+ this.menu.addEventListener("keydown", (event) => this.onRowKey(event));
2541
+ this.menu.addEventListener("pointerover", (event) => {
2542
+ const row = event.target?.closest("button");
2543
+ const index = row ? this.rows.indexOf(row) : -1;
2544
+ if (index >= 0 && !row?.disabled && index !== this.active) this.focusRow(index);
2545
+ });
2546
+ this.menu.addEventListener("contextmenu", (event) => event.preventDefault());
2451
2547
  this.addEventListener("office-command", () => this.close());
2452
2548
  }
2453
2549
  connectedCallback() {
2454
2550
  this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
2551
+ if (this.controlled) {
2552
+ this.opener = this.deepActive();
2553
+ this.render();
2554
+ }
2455
2555
  }
2456
2556
  attributeChangedCallback() {
2457
2557
  this.panel.setAttribute("aria-label", this.getAttribute("label") ?? "Context menu");
2458
2558
  }
2459
2559
  disconnectedCallback() {
2460
2560
  this.close();
2561
+ if (!this.controlled) return;
2562
+ this.stopListening();
2563
+ clearTimeout(this.queryTimer);
2564
+ this.focused = false;
2565
+ const opener = this.opener;
2566
+ this.opener = null;
2567
+ const active = this.ownerDocument.activeElement;
2568
+ if (opener?.isConnected && (!active || active === this.ownerDocument.body))
2569
+ opener.focus({ preventScroll: true });
2461
2570
  }
2571
+ // ---- Children mode ------------------------------------------------------------------
2462
2572
  get open() {
2463
2573
  return this.hasAttribute("data-open");
2464
2574
  }
@@ -2481,12 +2591,17 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2481
2591
  }
2482
2592
  this.setAttribute("data-open", "");
2483
2593
  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`;
2594
+ const { left, top } = clampFlyoutPosition({
2595
+ x,
2596
+ y,
2597
+ width: this.panel.offsetWidth,
2598
+ height: this.panel.offsetHeight,
2599
+ viewportWidth: view?.innerWidth ?? x,
2600
+ viewportHeight: view?.innerHeight ?? y,
2601
+ margin: 4
2602
+ });
2603
+ this.panel.style.left = `${Math.round(left)}px`;
2604
+ this.panel.style.top = `${Math.round(top)}px`;
2490
2605
  this.items()[0]?.focus();
2491
2606
  }
2492
2607
  close() {
@@ -2507,7 +2622,7 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2507
2622
  if (restoreFocus) this.returnFocus?.focus?.();
2508
2623
  this.returnFocus = null;
2509
2624
  }
2510
- onKey(event) {
2625
+ onChildKey(event) {
2511
2626
  const items = this.items();
2512
2627
  const from = items.findIndex(
2513
2628
  (item) => item === event.target || item.contains(event.target)
@@ -2523,8 +2638,217 @@ var defineContextMenu = definer("office-ui-context-menu", () => {
2523
2638
  else if (event.key === "Escape" || event.key === "Tab") {
2524
2639
  if (event.key === "Escape") event.preventDefault();
2525
2640
  this.close();
2641
+ } else if (event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey) {
2642
+ const labels = items.map((item) => ({
2643
+ label: item.getAttribute("label") ?? item.textContent ?? ""
2644
+ }));
2645
+ const target = this.typeAhead(labels, from, event.key);
2646
+ if (target >= 0) move(target);
2526
2647
  }
2527
2648
  }
2649
+ // ---- Controlled mode ----------------------------------------------------------------
2650
+ get state() {
2651
+ return this.model;
2652
+ }
2653
+ set state(value) {
2654
+ const signature = JSON.stringify(value);
2655
+ this.model = value;
2656
+ if (!this.controlled) {
2657
+ this.controlled = true;
2658
+ this.toggleAttribute("data-controlled", true);
2659
+ this.panel.remove();
2660
+ if (this.isConnected) this.opener = this.deepActive();
2661
+ }
2662
+ if (signature !== this.signature) {
2663
+ this.signature = signature;
2664
+ this.render();
2665
+ }
2666
+ }
2667
+ config() {
2668
+ return this.constructor;
2669
+ }
2670
+ deepActive() {
2671
+ let node = this.ownerDocument.activeElement;
2672
+ while (node?.shadowRoot?.activeElement) node = node.shadowRoot.activeElement;
2673
+ return node instanceof HTMLElement && node !== this.ownerDocument.body ? node : null;
2674
+ }
2675
+ render() {
2676
+ const state = this.model;
2677
+ const items = state.items;
2678
+ this.hidden = items.length === 0;
2679
+ this.menu.hidden = items.length === 0;
2680
+ this.applyMarkers(state.markers ?? []);
2681
+ if (state.zIndex !== void 0) this.style.zIndex = String(state.zIndex);
2682
+ else this.style.removeProperty("z-index");
2683
+ const same = items[this.active];
2684
+ this.active = same && !same.disabled ? this.active : nextEnabledIndex(items, -1, 1);
2685
+ this.renderRows();
2686
+ if (items.length === 0) {
2687
+ this.stopListening();
2688
+ this.focused = false;
2689
+ }
2690
+ if (!this.isConnected || items.length === 0) return;
2691
+ this.place();
2692
+ this.listen();
2693
+ if (!this.focused && state.autoFocus !== false) {
2694
+ this.focused = true;
2695
+ this.focusRow(nextEnabledIndex(items, -1, 1));
2696
+ } else if (this.focused && this.active >= 0)
2697
+ this.rows[this.active]?.focus({ preventScroll: true });
2698
+ }
2699
+ renderRows() {
2700
+ const doc = this.ownerDocument;
2701
+ const state = this.model;
2702
+ this.menu.setAttribute("aria-label", state.label);
2703
+ this.rows = [];
2704
+ let container = this.menu;
2705
+ const nodes = [];
2706
+ const place = (node) => container === this.menu ? nodes.push(node) : container.append(node);
2707
+ state.items.forEach((item, index) => {
2708
+ if (item.separatorBefore || item.heading) container = this.menu;
2709
+ if (item.separatorBefore) {
2710
+ const rule = doc.createElement("div");
2711
+ rule.className = "separator";
2712
+ rule.setAttribute("role", "separator");
2713
+ place(rule);
2714
+ }
2715
+ if (item.heading) {
2716
+ const group = doc.createElement("div");
2717
+ group.setAttribute("role", "group");
2718
+ group.setAttribute("aria-label", item.heading);
2719
+ const title = doc.createElement("div");
2720
+ title.className = "heading";
2721
+ title.setAttribute("aria-hidden", "true");
2722
+ title.textContent = item.heading;
2723
+ group.append(title);
2724
+ place(group);
2725
+ container = group;
2726
+ }
2727
+ const row = doc.createElement("button");
2728
+ row.type = "button";
2729
+ row.className = item.danger ? "item danger" : "item";
2730
+ row.dataset.itemId = item.id;
2731
+ row.setAttribute("role", item.checked === void 0 ? "menuitem" : "menuitemcheckbox");
2732
+ if (item.checked !== void 0) {
2733
+ row.setAttribute("aria-checked", String(item.checked));
2734
+ const check = doc.createElement("span");
2735
+ check.className = "check";
2736
+ check.setAttribute("aria-hidden", "true");
2737
+ check.textContent = item.checked ? "\u2713" : "";
2738
+ row.append(check);
2739
+ }
2740
+ if (item.icon) {
2741
+ const glyph = createIconSvg(doc);
2742
+ paintIcon(glyph, item.icon);
2743
+ row.append(glyph);
2744
+ }
2745
+ row.append(doc.createTextNode(item.label));
2746
+ row.tabIndex = index === this.active ? 0 : -1;
2747
+ if (item.disabled) {
2748
+ row.disabled = true;
2749
+ row.setAttribute("aria-disabled", "true");
2750
+ }
2751
+ row.addEventListener(
2752
+ "click",
2753
+ () => this.dispatchEvent(
2754
+ new CustomEvent(this.config().requestEvent, {
2755
+ detail: { id: item.id },
2756
+ bubbles: true,
2757
+ composed: true
2758
+ })
2759
+ )
2760
+ );
2761
+ this.rows.push(row);
2762
+ place(row);
2763
+ });
2764
+ this.menu.replaceChildren(...nodes);
2765
+ }
2766
+ applyMarkers(next) {
2767
+ for (const name of this.markers) this.removeAttribute(name);
2768
+ const pattern = this.config().markerPattern;
2769
+ this.markers = next.filter((name) => pattern.test(name));
2770
+ for (const name of this.markers) this.setAttribute(name, "true");
2771
+ }
2772
+ place() {
2773
+ const view = this.ownerDocument.defaultView;
2774
+ this.style.left = `${this.model.x}px`;
2775
+ this.style.top = `${this.model.y}px`;
2776
+ const box = this.getBoundingClientRect();
2777
+ const { left, top } = clampFlyoutPosition({
2778
+ x: this.model.x,
2779
+ y: this.model.y,
2780
+ width: box.width,
2781
+ height: box.height,
2782
+ viewportWidth: view?.innerWidth ?? box.right,
2783
+ viewportHeight: view?.innerHeight ?? box.bottom
2784
+ });
2785
+ this.style.left = `${left}px`;
2786
+ this.style.top = `${top}px`;
2787
+ }
2788
+ focusRow(index) {
2789
+ if (index < 0) return;
2790
+ this.active = index;
2791
+ this.rows.forEach((row, at) => row.tabIndex = at === index ? 0 : -1);
2792
+ this.rows[index]?.focus({ preventScroll: true });
2793
+ }
2794
+ typeAhead(items, from, key) {
2795
+ this.query += key;
2796
+ clearTimeout(this.queryTimer);
2797
+ this.queryTimer = setTimeout(() => this.query = "", TYPE_AHEAD_MS);
2798
+ return typeAheadIndex(items, from, this.query);
2799
+ }
2800
+ onRowKey(event) {
2801
+ const items = this.model.items;
2802
+ let target = -2;
2803
+ if (event.key === "ArrowDown") target = nextEnabledIndex(items, this.active, 1);
2804
+ else if (event.key === "ArrowUp") target = nextEnabledIndex(items, this.active, -1);
2805
+ else if (event.key === "Home") target = nextEnabledIndex(items, -1, 1);
2806
+ else if (event.key === "End") target = nextEnabledIndex(items, items.length, -1);
2807
+ else if (event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey)
2808
+ target = this.typeAhead(items, this.active, event.key);
2809
+ if (target === -2) return;
2810
+ event.preventDefault();
2811
+ this.focusRow(target);
2812
+ }
2813
+ dismiss(reason) {
2814
+ this.dispatchEvent(
2815
+ new CustomEvent(this.config().closeEvent, {
2816
+ detail: { reason },
2817
+ bubbles: true,
2818
+ composed: true
2819
+ })
2820
+ );
2821
+ }
2822
+ onDocumentKey = (event) => {
2823
+ if (openMenus.get(this.ownerDocument)?.at(-1) !== this) return;
2824
+ if (event.key === "Escape") {
2825
+ event.preventDefault();
2826
+ event.stopPropagation();
2827
+ this.dismiss("escape");
2828
+ } else if (event.key === "Tab") this.dismiss("tab");
2829
+ };
2830
+ onDocumentPointer = (event) => {
2831
+ if (!event.composedPath().includes(this)) this.dismiss("outside");
2832
+ };
2833
+ listen() {
2834
+ if (this.listening) return;
2835
+ this.listening = true;
2836
+ const doc = this.ownerDocument;
2837
+ openMenus.set(doc, [...openMenus.get(doc) ?? [], this]);
2838
+ doc.addEventListener("keydown", this.onDocumentKey, true);
2839
+ doc.addEventListener("pointerdown", this.onDocumentPointer, true);
2840
+ }
2841
+ stopListening() {
2842
+ if (!this.listening) return;
2843
+ this.listening = false;
2844
+ const doc = this.ownerDocument;
2845
+ openMenus.set(
2846
+ doc,
2847
+ (openMenus.get(doc) ?? []).filter((menu) => menu !== this)
2848
+ );
2849
+ doc.removeEventListener("keydown", this.onDocumentKey, true);
2850
+ doc.removeEventListener("pointerdown", this.onDocumentPointer, true);
2851
+ }
2528
2852
  }
2529
2853
  return OfficeUiContextMenu;
2530
2854
  });
@@ -4442,4 +4766,4 @@ function registerControls(registry) {
4442
4766
  for (const define of CONTROL_DEFINERS) define(registry);
4443
4767
  }
4444
4768
 
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 };
4769
+ 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-G2ZIAXOV.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-G2ZIAXOV.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-G2ZIAXOV.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.0",
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
  },