ooxml-ui 0.6.0 → 0.8.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
@@ -58,24 +58,29 @@ with an incompatible build of this package.
58
58
 
59
59
  ## Features: elements
60
60
 
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` | `options` `[{ value, label, disabled? }]`, `value`, `selectedIndex`, `disabled` | `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?, disabled?, title? }]`, `placeholder`, `label`, `limit` (Office's "Tell me what you want to do") | choice: `office-command` `{ command }` |
76
- | `office-ui-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
77
- | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
78
- | `office-ui-icon` | `name`, `label` | none |
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` | `options` `[{ value, label, disabled? }]`, `value`, `selectedIndex`, `disabled` | `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-presence` | `participants`, `max`, `label` | `office-presence-select` `{ id }` |
77
+ | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
78
+ | `office-ui-icon` | `name`, `label` | none |
79
+
80
+ `attachKeyTips(scope)` (from `ooxml-ui/controls`) adds Office KeyTips to any element or shadow
81
+ root: press and release Alt to show badges on controls marked `data-keytip="H"`, type a badge to
82
+ activate it, and use `data-keytip-panel` / `data-keytip-level` for a tab's second level. Escape
83
+ steps back; disabled controls are dimmed and never run.
79
84
 
80
85
  Setting a property never emits an event; only user activation does. Controls honour
81
86
  `prefers-color-scheme`, `forced-colors: active` (system colours and a visible focus ring) and
@@ -452,7 +452,10 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
452
452
  const query = this.input.value.trim().toLowerCase();
453
453
  if (!query) return this.hide();
454
454
  const limit = Math.max(1, Number(this.getAttribute("limit")) || 8);
455
- this.found = this.items.filter((item) => `${item.label} ${item.keywords ?? ""}`.toLowerCase().includes(query)).slice(0, limit);
455
+ const rank = (item) => (item.disabled ? 2 : 0) + (item.label.toLowerCase().startsWith(query) ? 0 : 1);
456
+ this.found = this.items.map((item, order) => ({ item, order })).filter(
457
+ ({ item }) => `${item.label} ${item.keywords ?? ""} ${item.description ?? ""}`.toLowerCase().includes(query)
458
+ ).sort((a, b) => rank(a.item) - rank(b.item) || a.order - b.order).map(({ item }) => item).slice(0, limit);
456
459
  this.active = this.found.findIndex((item) => !item.disabled);
457
460
  this.render();
458
461
  this.show();
@@ -480,11 +483,12 @@ var defineCommandSearch = definer("office-ui-command-search", () => {
480
483
  const label = doc.createElement("span");
481
484
  label.textContent = item.label;
482
485
  option.append(label);
483
- if (item.disabled) {
484
- option.setAttribute("aria-disabled", "true");
485
- const reason = doc.createElement("small");
486
- reason.textContent = item.title ?? "Not available";
487
- option.append(reason);
486
+ if (item.disabled) option.setAttribute("aria-disabled", "true");
487
+ const detail = item.disabled ? item.title ?? "Not available" : item.description;
488
+ if (detail) {
489
+ const line = doc.createElement("small");
490
+ line.textContent = detail;
491
+ option.append(line);
488
492
  }
489
493
  return option;
490
494
  })
@@ -1728,6 +1732,140 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
1728
1732
  return OfficeUiZoomSlider;
1729
1733
  });
1730
1734
 
1735
+ // src/keytips.ts
1736
+ var BADGE_CSS = "position:fixed;inset:auto;margin:0;padding:0;border:0;background:transparent;overflow:visible;pointer-events:none";
1737
+ var TIP_CSS = 'position:fixed;transform:translate(-50%,-30%);min-width:14px;padding:1px 4px;border:1px solid #8a8886;border-radius:2px;background:#fff;color:#1f1f1f;font:600 11px/14px "Segoe UI",system-ui,sans-serif;text-align:center;box-shadow:0 1px 3px rgb(0 0 0 / 25%)';
1738
+ var isDisabled = (el) => el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
1739
+ var visible = (el) => {
1740
+ const box = el.getBoundingClientRect();
1741
+ return box.width > 0 && box.height > 0;
1742
+ };
1743
+ function activate(el) {
1744
+ const inner = el.shadowRoot?.querySelector(".main, button");
1745
+ (inner ?? el).click();
1746
+ }
1747
+ function attachKeyTips(scope) {
1748
+ const doc = (scope instanceof ShadowRoot ? scope.host : scope).ownerDocument;
1749
+ const view = doc.defaultView;
1750
+ const layer = doc.createElement("div");
1751
+ layer.setAttribute("aria-hidden", "true");
1752
+ layer.style.cssText = BADGE_CSS;
1753
+ const topLayer = typeof layer.showPopover === "function";
1754
+ if (topLayer) layer.setAttribute("popover", "manual");
1755
+ else layer.hidden = true;
1756
+ scope.append(layer);
1757
+ let levels = [];
1758
+ let typed = "";
1759
+ let altAlone = false;
1760
+ const tipsAt = (container) => [...(container ?? scope).querySelectorAll("[data-keytip]")].filter((el) => {
1761
+ const owner = el.parentElement?.closest("[data-keytip-level]") ?? null;
1762
+ return owner === container && visible(el);
1763
+ });
1764
+ const current = () => tipsAt(levels.at(-1) ?? null);
1765
+ const render = () => {
1766
+ if (!handle.active) return;
1767
+ layer.replaceChildren(
1768
+ ...current().map((el) => {
1769
+ const box = el.getBoundingClientRect();
1770
+ const tip = doc.createElement("span");
1771
+ tip.textContent = el.dataset.keytip;
1772
+ tip.style.cssText = `${TIP_CSS};left:${Math.round(box.left + box.width / 2)}px;top:${Math.round(box.bottom - 4)}px;opacity:${isDisabled(el) || typed && !el.dataset.keytip.startsWith(typed) ? "0.45" : "1"}`;
1773
+ return tip;
1774
+ })
1775
+ );
1776
+ };
1777
+ const show = () => {
1778
+ if (topLayer && !layer.matches(":popover-open")) layer.showPopover?.();
1779
+ else layer.hidden = false;
1780
+ render();
1781
+ };
1782
+ const stop = () => {
1783
+ levels = [];
1784
+ typed = "";
1785
+ if (topLayer) {
1786
+ try {
1787
+ layer.hidePopover?.();
1788
+ } catch {
1789
+ }
1790
+ } else layer.hidden = true;
1791
+ layer.replaceChildren();
1792
+ handle.active = false;
1793
+ };
1794
+ const start = () => {
1795
+ levels = [];
1796
+ typed = "";
1797
+ handle.active = true;
1798
+ show();
1799
+ };
1800
+ const choose = (el) => {
1801
+ typed = "";
1802
+ if (isDisabled(el)) return render();
1803
+ const panel = el.dataset.keytipPanel;
1804
+ activate(el);
1805
+ const next = panel ? [...scope.querySelectorAll("[data-keytip-level]")].find((el2) => el2.id === panel) ?? null : null;
1806
+ if (next) {
1807
+ levels.push(next);
1808
+ view?.requestAnimationFrame ? view.requestAnimationFrame(render) : render();
1809
+ } else stop();
1810
+ };
1811
+ const onKeyDown = (event) => {
1812
+ const key = event;
1813
+ if (key.key === "Alt") {
1814
+ altAlone = !key.ctrlKey && !key.metaKey && !key.shiftKey;
1815
+ return;
1816
+ }
1817
+ altAlone = false;
1818
+ if (!handle.active) return;
1819
+ if (key.key === "Escape") {
1820
+ key.preventDefault();
1821
+ key.stopPropagation();
1822
+ typed = "";
1823
+ if (levels.length) {
1824
+ levels.pop();
1825
+ render();
1826
+ } else stop();
1827
+ return;
1828
+ }
1829
+ if (key.key.length !== 1 || key.ctrlKey || key.metaKey) return stop();
1830
+ key.preventDefault();
1831
+ key.stopPropagation();
1832
+ typed += key.key.toUpperCase();
1833
+ const candidates = current().filter((el) => el.dataset.keytip.toUpperCase().startsWith(typed));
1834
+ const exact = candidates.find((el) => el.dataset.keytip.toUpperCase() === typed);
1835
+ if (exact) choose(exact);
1836
+ else if (!candidates.length) typed = "";
1837
+ render();
1838
+ };
1839
+ const onKeyUp = (event) => {
1840
+ const key = event;
1841
+ if (key.key !== "Alt" || !altAlone) return;
1842
+ altAlone = false;
1843
+ key.preventDefault();
1844
+ if (handle.active) stop();
1845
+ else start();
1846
+ };
1847
+ const onPointer = () => {
1848
+ if (handle.active) stop();
1849
+ };
1850
+ const target = scope;
1851
+ target.addEventListener("keydown", onKeyDown, true);
1852
+ target.addEventListener("keyup", onKeyUp, true);
1853
+ target.addEventListener("pointerdown", onPointer, true);
1854
+ view?.addEventListener("blur", onPointer);
1855
+ const handle = {
1856
+ active: false,
1857
+ dispose() {
1858
+ stop();
1859
+ target.removeEventListener("keydown", onKeyDown, true);
1860
+ target.removeEventListener("keyup", onKeyUp, true);
1861
+ target.removeEventListener("pointerdown", onPointer, true);
1862
+ view?.removeEventListener("blur", onPointer);
1863
+ layer.remove();
1864
+ }
1865
+ };
1866
+ return handle;
1867
+ }
1868
+
1731
1869
  // src/controls.ts
1732
1870
  var CONTROL_DEFINERS = [
1733
1871
  defineIcon,
@@ -1753,4 +1891,4 @@ function registerControls(registry) {
1753
1891
  for (const define of CONTROL_DEFINERS) define(registry);
1754
1892
  }
1755
1893
 
1756
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls };
1894
+ export { CONTROL_DEFINERS, attachKeyTips, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls };
@@ -3,6 +3,8 @@ export interface OfficeSearchCommand {
3
3
  id: string;
4
4
  label: string;
5
5
  keywords?: string;
6
+ /** Where the command lives, shown under the label (for example "Home › Paragraph"). */
7
+ description?: string;
6
8
  disabled?: boolean;
7
9
  title?: string;
8
10
  }
@@ -17,6 +17,7 @@ export type { OfficeSelectOption } from './select.js';
17
17
  export type { OfficeStatusActivateEvent } from './status-bar.js';
18
18
  export type { OfficeTab, OfficeTabSelectEvent } from './tab-strip.js';
19
19
  export type { OfficeSearchCommand } from './command-search.js';
20
+ export { attachKeyTips, type KeyTipsHandle } from './keytips.js';
20
21
  /** Every control of this entry. */
21
22
  export declare const CONTROL_DEFINERS: readonly [import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer, import("./registry.js").Definer];
22
23
  /** Idempotent, SSR-safe (no-op without a DOM). */
package/dist/controls.js CHANGED
@@ -1,3 +1,3 @@
1
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RT6XKAM4.js';
1
+ export { CONTROL_DEFINERS, attachKeyTips, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-L5FYB6M7.js';
2
2
  export { defineIcon } from './chunk-PCSUQPMC.js';
3
3
  import './chunk-EY266IK3.js';
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { installOfficeUiTheme } from './chunk-H4KFVIGT.js';
2
2
  export { THEME_CSS, THEME_STYLE_ID, installOfficeUiTheme } from './chunk-H4KFVIGT.js';
3
- import { registerControls } from './chunk-RT6XKAM4.js';
4
- export { CONTROL_DEFINERS, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-RT6XKAM4.js';
3
+ import { registerControls } from './chunk-L5FYB6M7.js';
4
+ export { CONTROL_DEFINERS, attachKeyTips, defineButton, defineCheckbox, defineCommandSearch, defineContextMenu, defineDialog, defineMenuButton, defineMenuItem, defineMenuSeparator, defineRibbonGroup, defineRibbonStack, defineSelect, defineStatusBar, defineStatusItem, defineSwitch, defineTabStrip, defineToolbar, defineZoomSlider, registerControls } from './chunk-L5FYB6M7.js';
5
5
  export { ICON_CSS, createIconSvg, defineIcon, getIcon, listIcons, paintIcon, registerIcon } from './chunk-PCSUQPMC.js';
6
6
  import { definePresence } from './chunk-RLPL7Q6J.js';
7
7
  export { definePresence, initialsOf, participantsFromAwareness } from './chunk-RLPL7Q6J.js';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Office KeyTips: press and release Alt to show letter badges on ribbon controls, then type a
3
+ * badge to activate it. Products annotate controls only:
4
+ * - `data-keytip="H"` marks a control (one or two characters);
5
+ * - `data-keytip-panel="home-panel"` on a control (a ribbon tab) makes the controls inside the
6
+ * element with that id the next level after it is activated.
7
+ * First-level tips are those not inside any `[data-keytip-level]` container; a panel element
8
+ * carries `data-keytip-level` so its tips only appear after its tab. Escape steps back a level;
9
+ * Alt, a click or focus leaving the scope exits. Disabled controls show dimmed and never run.
10
+ * Badges render in a `popover` top layer, so containment and overflow never misplace them.
11
+ */
12
+ export interface KeyTipsHandle {
13
+ /** Whether badges are currently shown. */
14
+ readonly active: boolean;
15
+ dispose(): void;
16
+ }
17
+ type Root = ShadowRoot | HTMLElement;
18
+ export declare function attachKeyTips(scope: Root): KeyTipsHandle;
19
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ooxml-ui",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",