ooxml-ui 0.7.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
@@ -77,6 +77,11 @@ with an incompatible build of this package.
77
77
  | `office-ui-smartart` | `drawing` (core `DiagramDrawing`), `schemeColors`, `label` | `office-smartart-render` (render report) |
78
78
  | `office-ui-icon` | `name`, `label` | none |
79
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.
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
82
87
  coarse pointers (44px targets through `--office-target-size`). Tokens are the `--office-*`
@@ -1732,6 +1732,140 @@ var defineZoomSlider = definer("office-ui-zoom-slider", () => {
1732
1732
  return OfficeUiZoomSlider;
1733
1733
  });
1734
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
+
1735
1869
  // src/controls.ts
1736
1870
  var CONTROL_DEFINERS = [
1737
1871
  defineIcon,
@@ -1757,4 +1891,4 @@ function registerControls(registry) {
1757
1891
  for (const define of CONTROL_DEFINERS) define(registry);
1758
1892
  }
1759
1893
 
1760
- 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 };
@@ -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-LCHYMVBZ.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-LCHYMVBZ.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-LCHYMVBZ.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.7.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",