@operato/layout 10.27.1 → 10.28.2

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.
@@ -0,0 +1,163 @@
1
+ import { __decorate } from "tslib";
2
+ import { css, html, LitElement } from 'lit';
3
+ import { customElement, property, state } from 'lit/decorators.js';
4
+ import { navigate } from '@operato/shell';
5
+ /**
6
+ * Tabs whose choice lives in the address (ADR-0060 decision 8 addendum, the third frame).
7
+ *
8
+ * plant's quality and pending lists and its equipment screen keep their tab in the address, each its
9
+ * own way; twin's business screen reads the tab from the address on entry and never writes it back, so
10
+ * a copied link opens on the first tab. Here reading and writing always go together: open the link,
11
+ * press back after opening a row, reload - the same tab.
12
+ *
13
+ * - `?tab=` (the `param` name) is where new screens keep it. The first tab is the default and is not
14
+ * written, so a screen's plain address stays plain. Other query values are kept as they are.
15
+ * The change replaces the address rather than adding a history step - back leaves the screen, it
16
+ * does not walk through the tabs.
17
+ * - `base` puts the key in a path segment instead (`<base>/<key>`), through the shell's navigate, the
18
+ * way plant's three screens do. **Transitional** (architect ruling 2026-09-24): it is here so those
19
+ * screens can take the frame without breaking the links people have; new screens use `?tab=`.
20
+ * - `tab-change` (detail `{ key }`) fires when the tab changes - by a press, or by back / forward.
21
+ * `selected` holds the current key, read from the address when the element connects.
22
+ *
23
+ * The look is the suite's tab strip (SurfaceStyles `.ui-tabs`): an underline under the chosen one.
24
+ */
25
+ let OxUrlTabs = class OxUrlTabs extends LitElement {
26
+ constructor() {
27
+ super(...arguments);
28
+ this.tabs = [];
29
+ /** The query name the key is kept under. */
30
+ this.param = 'tab';
31
+ this.selected = '';
32
+ this.onPop = () => this.read(true);
33
+ }
34
+ connectedCallback() {
35
+ super.connectedCallback();
36
+ window.addEventListener('popstate', this.onPop);
37
+ this.read(false);
38
+ }
39
+ disconnectedCallback() {
40
+ super.disconnectedCallback();
41
+ window.removeEventListener('popstate', this.onPop);
42
+ }
43
+ updated(changed) {
44
+ if (changed.has('tabs') || changed.has('base') || changed.has('param'))
45
+ this.read(false);
46
+ }
47
+ /** The key the address names, or the first tab when it names none of them. */
48
+ keyInAddress() {
49
+ var _a, _b;
50
+ const keys = this.tabs.map(tab => tab.key);
51
+ let named = null;
52
+ if (this.base) {
53
+ const segments = location.pathname.split('/').filter(Boolean);
54
+ const at = segments.lastIndexOf(this.base);
55
+ named = at >= 0 ? (_a = segments[at + 1]) !== null && _a !== void 0 ? _a : null : null;
56
+ }
57
+ else {
58
+ named = new URLSearchParams(location.search).get(this.param);
59
+ }
60
+ return named && keys.includes(named) ? named : (_b = keys[0]) !== null && _b !== void 0 ? _b : '';
61
+ }
62
+ read(announce) {
63
+ const key = this.keyInAddress();
64
+ if (key === this.selected)
65
+ return;
66
+ this.selected = key;
67
+ if (announce)
68
+ this.announce();
69
+ }
70
+ /** Choose a tab: the address follows, and the screen hears it. */
71
+ select(key) {
72
+ var _a;
73
+ if (key === this.selected || !this.tabs.some(tab => tab.key === key))
74
+ return;
75
+ const first = (_a = this.tabs[0]) === null || _a === void 0 ? void 0 : _a.key;
76
+ if (this.base) {
77
+ navigate(key === first ? this.base : `${this.base}/${key}`);
78
+ }
79
+ else {
80
+ const url = new URL(location.href);
81
+ if (key === first)
82
+ url.searchParams.delete(this.param);
83
+ else
84
+ url.searchParams.set(this.param, key);
85
+ history.replaceState(history.state, '', url);
86
+ }
87
+ this.selected = key;
88
+ this.announce();
89
+ }
90
+ announce() {
91
+ this.dispatchEvent(new CustomEvent('tab-change', { detail: { key: this.selected }, bubbles: true, composed: true }));
92
+ }
93
+ onKey(event) {
94
+ const keys = this.tabs.map(tab => tab.key);
95
+ const at = keys.indexOf(this.selected);
96
+ const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
97
+ if (!step || at < 0)
98
+ return;
99
+ event.preventDefault();
100
+ const next = keys[(at + step + keys.length) % keys.length];
101
+ this.select(next);
102
+ this.updateComplete.then(() => { var _a; return (_a = this.renderRoot.querySelector(`[data-key="${next}"]`)) === null || _a === void 0 ? void 0 : _a.focus(); });
103
+ }
104
+ render() {
105
+ return html `<div role="tablist" style="display: contents" @keydown=${this.onKey}>
106
+ ${this.tabs.map(tab => html `<button
107
+ role="tab"
108
+ data-key=${tab.key}
109
+ aria-selected=${String(tab.key === this.selected)}
110
+ tabindex=${tab.key === this.selected ? 0 : -1}
111
+ @click=${() => this.select(tab.key)}
112
+ >
113
+ ${tab.label}
114
+ </button>`)}
115
+ </div>`;
116
+ }
117
+ };
118
+ OxUrlTabs.styles = css `
119
+ :host {
120
+ display: flex;
121
+ gap: var(--spacing-large, 16px);
122
+ border-bottom: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
123
+ }
124
+ button {
125
+ border: none;
126
+ border-bottom: 2px solid transparent;
127
+ background: none;
128
+ padding: var(--spacing-medium, 8px) 2px calc(var(--spacing-medium, 8px) - 2px);
129
+ font: inherit;
130
+ font-size: 14px;
131
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
132
+ cursor: pointer;
133
+ }
134
+ button:hover:not([aria-selected='true']) {
135
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
136
+ }
137
+ button[aria-selected='true'] {
138
+ font-weight: 600;
139
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
140
+ border-bottom-color: var(--ui-accent, var(--md-sys-color-primary));
141
+ }
142
+ button:focus-visible {
143
+ outline: 2px solid var(--md-sys-color-primary);
144
+ outline-offset: 2px;
145
+ }
146
+ `;
147
+ __decorate([
148
+ property({ attribute: false })
149
+ ], OxUrlTabs.prototype, "tabs", void 0);
150
+ __decorate([
151
+ property()
152
+ ], OxUrlTabs.prototype, "param", void 0);
153
+ __decorate([
154
+ property()
155
+ ], OxUrlTabs.prototype, "base", void 0);
156
+ __decorate([
157
+ state()
158
+ ], OxUrlTabs.prototype, "selected", void 0);
159
+ OxUrlTabs = __decorate([
160
+ customElement('ox-url-tabs')
161
+ ], OxUrlTabs);
162
+ export { OxUrlTabs };
163
+ //# sourceMappingURL=ox-url-tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-url-tabs.js","sourceRoot":"","sources":["../../../src/components/ox-url-tabs.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { navigate } from '@operato/shell'\n\nexport interface OxUrlTab {\n key: string\n label: string\n}\n\n/**\n * Tabs whose choice lives in the address (ADR-0060 decision 8 addendum, the third frame).\n *\n * plant's quality and pending lists and its equipment screen keep their tab in the address, each its\n * own way; twin's business screen reads the tab from the address on entry and never writes it back, so\n * a copied link opens on the first tab. Here reading and writing always go together: open the link,\n * press back after opening a row, reload - the same tab.\n *\n * - `?tab=` (the `param` name) is where new screens keep it. The first tab is the default and is not\n * written, so a screen's plain address stays plain. Other query values are kept as they are.\n * The change replaces the address rather than adding a history step - back leaves the screen, it\n * does not walk through the tabs.\n * - `base` puts the key in a path segment instead (`<base>/<key>`), through the shell's navigate, the\n * way plant's three screens do. **Transitional** (architect ruling 2026-09-24): it is here so those\n * screens can take the frame without breaking the links people have; new screens use `?tab=`.\n * - `tab-change` (detail `{ key }`) fires when the tab changes - by a press, or by back / forward.\n * `selected` holds the current key, read from the address when the element connects.\n *\n * The look is the suite's tab strip (SurfaceStyles `.ui-tabs`): an underline under the chosen one.\n */\n@customElement('ox-url-tabs')\nexport class OxUrlTabs extends LitElement {\n static styles = css`\n :host {\n display: flex;\n gap: var(--spacing-large, 16px);\n border-bottom: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));\n }\n button {\n border: none;\n border-bottom: 2px solid transparent;\n background: none;\n padding: var(--spacing-medium, 8px) 2px calc(var(--spacing-medium, 8px) - 2px);\n font: inherit;\n font-size: 14px;\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n cursor: pointer;\n }\n button:hover:not([aria-selected='true']) {\n color: var(--ui-ink, var(--md-sys-color-on-surface));\n }\n button[aria-selected='true'] {\n font-weight: 600;\n color: var(--ui-ink, var(--md-sys-color-on-surface));\n border-bottom-color: var(--ui-accent, var(--md-sys-color-primary));\n }\n button:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 2px;\n }\n `\n\n @property({ attribute: false }) tabs: OxUrlTab[] = []\n /** The query name the key is kept under. */\n @property() param = 'tab'\n /** Transitional: keep the key in a path segment after this page instead of the query. */\n @property() base?: string\n\n @state() selected = ''\n\n private onPop = () => this.read(true)\n\n connectedCallback() {\n super.connectedCallback()\n window.addEventListener('popstate', this.onPop)\n this.read(false)\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n window.removeEventListener('popstate', this.onPop)\n }\n\n updated(changed: Map<string, unknown>) {\n if (changed.has('tabs') || changed.has('base') || changed.has('param')) this.read(false)\n }\n\n /** The key the address names, or the first tab when it names none of them. */\n keyInAddress(): string {\n const keys = this.tabs.map(tab => tab.key)\n let named: string | null = null\n\n if (this.base) {\n const segments = location.pathname.split('/').filter(Boolean)\n const at = segments.lastIndexOf(this.base)\n named = at >= 0 ? segments[at + 1] ?? null : null\n } else {\n named = new URLSearchParams(location.search).get(this.param)\n }\n\n return named && keys.includes(named) ? named : keys[0] ?? ''\n }\n\n private read(announce: boolean) {\n const key = this.keyInAddress()\n if (key === this.selected) return\n this.selected = key\n if (announce) this.announce()\n }\n\n /** Choose a tab: the address follows, and the screen hears it. */\n select(key: string) {\n if (key === this.selected || !this.tabs.some(tab => tab.key === key)) return\n const first = this.tabs[0]?.key\n\n if (this.base) {\n navigate(key === first ? this.base : `${this.base}/${key}`)\n } else {\n const url = new URL(location.href)\n if (key === first) url.searchParams.delete(this.param)\n else url.searchParams.set(this.param, key)\n history.replaceState(history.state, '', url)\n }\n\n this.selected = key\n this.announce()\n }\n\n private announce() {\n this.dispatchEvent(new CustomEvent('tab-change', { detail: { key: this.selected }, bubbles: true, composed: true }))\n }\n\n private onKey(event: KeyboardEvent) {\n const keys = this.tabs.map(tab => tab.key)\n const at = keys.indexOf(this.selected)\n const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0\n if (!step || at < 0) return\n event.preventDefault()\n const next = keys[(at + step + keys.length) % keys.length]\n this.select(next)\n this.updateComplete.then(() => this.renderRoot.querySelector<HTMLButtonElement>(`[data-key=\"${next}\"]`)?.focus())\n }\n\n render() {\n return html`<div role=\"tablist\" style=\"display: contents\" @keydown=${this.onKey}>\n ${this.tabs.map(\n tab => html`<button\n role=\"tab\"\n data-key=${tab.key}\n aria-selected=${String(tab.key === this.selected)}\n tabindex=${tab.key === this.selected ? 0 : -1}\n @click=${() => this.select(tab.key)}\n >\n ${tab.label}\n </button>`\n )}\n </div>`\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-url-tabs': OxUrlTabs\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import './initializer.js';
2
- export { PopupHandle, PopupOptions } from '@operato/popup';
2
+ export type { PopupHandle, PopupOptions } from '@operato/popup';
3
3
  export * from './layouts/ox-snack-bar.js';
4
4
  export * from './layouts/ox-header-bar.js';
5
5
  export * from './layouts/ox-nav-bar.js';
@@ -11,6 +11,9 @@ export * from './actions/layout.js';
11
11
  export * from './actions/snackbar.js';
12
12
  export * from './components/ox-split-pane.js';
13
13
  export * from './components/ox-entity360-dock.js';
14
+ export * from './components/ox-three-pane.js';
15
+ export * from './components/ox-canvas-bar.js';
16
+ export * from './components/ox-url-tabs.js';
14
17
  export * from './actions/entity360.js';
15
18
  export * from './actions/entity360-registry.js';
16
19
  export * from './actions/entity360-door.js';
package/dist/src/index.js CHANGED
@@ -10,6 +10,9 @@ export * from './actions/layout.js';
10
10
  export * from './actions/snackbar.js';
11
11
  export * from './components/ox-split-pane.js';
12
12
  export * from './components/ox-entity360-dock.js';
13
+ export * from './components/ox-three-pane.js';
14
+ export * from './components/ox-canvas-bar.js';
15
+ export * from './components/ox-url-tabs.js';
13
16
  export * from './actions/entity360.js';
14
17
  export * from './actions/entity360-registry.js';
15
18
  export * from './actions/entity360-door.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAA;AAIzB,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,iCAAiC,CAAA;AAE/C,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,mCAAmC,CAAA;AACjD,cAAc,wBAAwB,CAAA;AACtC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA","sourcesContent":["import './initializer.js'\n\nexport { PopupHandle, PopupOptions } from '@operato/popup'\n\nexport * from './layouts/ox-snack-bar.js'\nexport * from './layouts/ox-header-bar.js'\nexport * from './layouts/ox-nav-bar.js'\nexport * from './layouts/ox-aside-bar.js'\nexport * from './layouts/ox-footer-bar.js'\nexport * from './layouts/ox-page-header-bar.js'\nexport * from './layouts/ox-page-footer-bar.js'\n\nexport * from './actions/layout.js'\nexport * from './actions/snackbar.js'\n\nexport * from './components/ox-split-pane.js'\nexport * from './components/ox-entity360-dock.js'\nexport * from './actions/entity360.js'\nexport * from './actions/entity360-registry.js'\nexport * from './actions/entity360-door.js'\nexport * from './actions/entity360-name.js'\nexport * from './components/ox-entity360-section.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAA;AAIzB,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,iCAAiC,CAAA;AAC/C,cAAc,iCAAiC,CAAA;AAE/C,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AAErC,cAAc,+BAA+B,CAAA;AAC7C,cAAc,mCAAmC,CAAA;AACjD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,wBAAwB,CAAA;AACtC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,sCAAsC,CAAA","sourcesContent":["import './initializer.js'\n\nexport type { PopupHandle, PopupOptions } from '@operato/popup'\n\nexport * from './layouts/ox-snack-bar.js'\nexport * from './layouts/ox-header-bar.js'\nexport * from './layouts/ox-nav-bar.js'\nexport * from './layouts/ox-aside-bar.js'\nexport * from './layouts/ox-footer-bar.js'\nexport * from './layouts/ox-page-header-bar.js'\nexport * from './layouts/ox-page-footer-bar.js'\n\nexport * from './actions/layout.js'\nexport * from './actions/snackbar.js'\n\nexport * from './components/ox-split-pane.js'\nexport * from './components/ox-entity360-dock.js'\nexport * from './components/ox-three-pane.js'\nexport * from './components/ox-canvas-bar.js'\nexport * from './components/ox-url-tabs.js'\nexport * from './actions/entity360.js'\nexport * from './actions/entity360-registry.js'\nexport * from './actions/entity360-door.js'\nexport * from './actions/entity360-name.js'\nexport * from './components/ox-entity360-section.js'\n"]}
@@ -0,0 +1 @@
1
+ import '../src/index.js';
@@ -0,0 +1,64 @@
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import '../src/index.js';
4
+ /**
5
+ * The canvas bar (ADR-0060 decision 8 addendum): the common controls stand in the same places on
6
+ * every canvas, and the screen answers them.
7
+ */
8
+ const actions = (bar) => Array.from(bar.shadowRoot.querySelectorAll('button[data-action]')).map(b => b.dataset.action);
9
+ describe('ox-canvas-bar', () => {
10
+ it('★ draws only what the screen says it has', async () => {
11
+ const bare = await fixture(html `<ox-canvas-bar></ox-canvas-bar>`);
12
+ expect(actions(bare)).to.deep.equal([]);
13
+ const full = await fixture(html `<ox-canvas-bar history fit></ox-canvas-bar>`);
14
+ expect(actions(full)).to.deep.equal(['undo', 'redo', 'fit']);
15
+ });
16
+ it('★ with a pane: fold toggles at the two edges, the common controls between them', async () => {
17
+ const pane = await fixture(html `<ox-three-pane></ox-three-pane>`);
18
+ const bar = await fixture(html `<ox-canvas-bar history fit .pane=${pane}></ox-canvas-bar>`);
19
+ expect(actions(bar)).to.deep.equal(['fold-start', 'undo', 'redo', 'fit', 'fold-end']);
20
+ });
21
+ it('★ a fold toggle folds the pane, and follows a fold made elsewhere', async () => {
22
+ const pane = await fixture(html `<ox-three-pane></ox-three-pane>`);
23
+ const bar = await fixture(html `<ox-canvas-bar .pane=${pane}></ox-canvas-bar>`);
24
+ const toggle = (side) => bar.shadowRoot.querySelector(`[data-action="fold-${side}"]`);
25
+ toggle('start').click();
26
+ await pane.updateComplete;
27
+ expect(pane.hasAttribute('fold-start')).to.equal(true);
28
+ await bar.updateComplete;
29
+ expect(toggle('start').getAttribute('aria-pressed')).to.equal('false');
30
+ pane.foldEnd = true;
31
+ await pane.updateComplete;
32
+ await new Promise(resolve => setTimeout(resolve));
33
+ await bar.updateComplete;
34
+ expect(toggle('end').getAttribute('aria-pressed')).to.equal('false');
35
+ expect(toggle('end').textContent).to.contain('right_panel_open');
36
+ });
37
+ it('★ undo, redo and fit reach the screen as events; unavailable ones cannot be pressed', async () => {
38
+ const bar = await fixture(html `<ox-canvas-bar history fit can-undo></ox-canvas-bar>`);
39
+ const heard = [];
40
+ for (const name of ['canvas-undo', 'canvas-redo', 'canvas-fit'])
41
+ document.addEventListener(name, () => heard.push(name), { once: true });
42
+ const press = (action) => bar.shadowRoot.querySelector(`[data-action="${action}"]`).click();
43
+ press('undo');
44
+ press('redo');
45
+ press('fit');
46
+ expect(bar.shadowRoot.querySelector('[data-action="redo"]').disabled).to.equal(true);
47
+ expect(heard).to.deep.equal(['canvas-undo', 'canvas-fit']);
48
+ });
49
+ it('every drawn control has a name a screen reader says', async () => {
50
+ const pane = await fixture(html `<ox-three-pane></ox-three-pane>`);
51
+ const bar = await fixture(html `<ox-canvas-bar history fit .pane=${pane}></ox-canvas-bar>`);
52
+ for (const button of Array.from(bar.shadowRoot.querySelectorAll('button'))) {
53
+ expect(button.getAttribute('aria-label')).to.be.a('string').and.not.equal('');
54
+ }
55
+ });
56
+ it('★ the keys a screen gives are shown in the titles; the spoken name stays the name', async () => {
57
+ const bar = await fixture(html `<ox-canvas-bar history undo-shortcut="Ctrl+Z" redo-shortcut="Ctrl+Y"></ox-canvas-bar>`);
58
+ const undo = bar.shadowRoot.querySelector('[data-action="undo"]');
59
+ expect(undo.getAttribute('title')).to.match(/\(Ctrl\+Z\)$/);
60
+ expect(undo.getAttribute('aria-label')).to.not.contain('Ctrl');
61
+ expect(bar.shadowRoot.querySelector('[data-action="redo"]').getAttribute('title')).to.match(/\(Ctrl\+Y\)$/);
62
+ });
63
+ });
64
+ //# sourceMappingURL=canvas-bar.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-bar.test.js","sourceRoot":"","sources":["../../test/canvas-bar.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html } from 'lit'\nimport '../src/index.js'\nimport type { OxCanvasBar, OxThreePane } from '../src/index.js'\n\n/**\n * The canvas bar (ADR-0060 decision 8 addendum): the common controls stand in the same places on\n * every canvas, and the screen answers them.\n */\n\nconst actions = (bar: OxCanvasBar) =>\n Array.from(bar.shadowRoot!.querySelectorAll('button[data-action]')).map(b => (b as HTMLElement).dataset.action)\n\ndescribe('ox-canvas-bar', () => {\n it('★ draws only what the screen says it has', async () => {\n const bare = await fixture<OxCanvasBar>(html`<ox-canvas-bar></ox-canvas-bar>`)\n expect(actions(bare)).to.deep.equal([])\n\n const full = await fixture<OxCanvasBar>(html`<ox-canvas-bar history fit></ox-canvas-bar>`)\n expect(actions(full)).to.deep.equal(['undo', 'redo', 'fit'])\n })\n\n it('★ with a pane: fold toggles at the two edges, the common controls between them', async () => {\n const pane = await fixture<OxThreePane>(html`<ox-three-pane></ox-three-pane>`)\n const bar = await fixture<OxCanvasBar>(html`<ox-canvas-bar history fit .pane=${pane}></ox-canvas-bar>`)\n expect(actions(bar)).to.deep.equal(['fold-start', 'undo', 'redo', 'fit', 'fold-end'])\n })\n\n it('★ a fold toggle folds the pane, and follows a fold made elsewhere', async () => {\n const pane = await fixture<OxThreePane>(html`<ox-three-pane></ox-three-pane>`)\n const bar = await fixture<OxCanvasBar>(html`<ox-canvas-bar .pane=${pane}></ox-canvas-bar>`)\n const toggle = (side: string) => bar.shadowRoot!.querySelector(`[data-action=\"fold-${side}\"]`) as HTMLButtonElement\n\n toggle('start').click()\n await pane.updateComplete\n expect(pane.hasAttribute('fold-start')).to.equal(true)\n await bar.updateComplete\n expect(toggle('start').getAttribute('aria-pressed')).to.equal('false')\n\n pane.foldEnd = true\n await pane.updateComplete\n await new Promise(resolve => setTimeout(resolve))\n await bar.updateComplete\n expect(toggle('end').getAttribute('aria-pressed')).to.equal('false')\n expect(toggle('end').textContent).to.contain('right_panel_open')\n })\n\n it('★ undo, redo and fit reach the screen as events; unavailable ones cannot be pressed', async () => {\n const bar = await fixture<OxCanvasBar>(html`<ox-canvas-bar history fit can-undo></ox-canvas-bar>`)\n const heard: string[] = []\n for (const name of ['canvas-undo', 'canvas-redo', 'canvas-fit']) document.addEventListener(name, () => heard.push(name), { once: true })\n\n const press = (action: string) => (bar.shadowRoot!.querySelector(`[data-action=\"${action}\"]`) as HTMLButtonElement).click()\n press('undo')\n press('redo')\n press('fit')\n\n expect((bar.shadowRoot!.querySelector('[data-action=\"redo\"]') as HTMLButtonElement).disabled).to.equal(true)\n expect(heard).to.deep.equal(['canvas-undo', 'canvas-fit'])\n })\n\n it('every drawn control has a name a screen reader says', async () => {\n const pane = await fixture<OxThreePane>(html`<ox-three-pane></ox-three-pane>`)\n const bar = await fixture<OxCanvasBar>(html`<ox-canvas-bar history fit .pane=${pane}></ox-canvas-bar>`)\n for (const button of Array.from(bar.shadowRoot!.querySelectorAll('button'))) {\n expect(button.getAttribute('aria-label')).to.be.a('string').and.not.equal('')\n }\n })\n\n it('★ the keys a screen gives are shown in the titles; the spoken name stays the name', async () => {\n const bar = await fixture<OxCanvasBar>(html`<ox-canvas-bar history undo-shortcut=\"Ctrl+Z\" redo-shortcut=\"Ctrl+Y\"></ox-canvas-bar>`)\n const undo = bar.shadowRoot!.querySelector('[data-action=\"undo\"]')!\n expect(undo.getAttribute('title')).to.match(/\\(Ctrl\\+Z\\)$/)\n expect(undo.getAttribute('aria-label')).to.not.contain('Ctrl')\n expect(bar.shadowRoot!.querySelector('[data-action=\"redo\"]')!.getAttribute('title')).to.match(/\\(Ctrl\\+Y\\)$/)\n })\n})\n"]}
@@ -0,0 +1 @@
1
+ import '../src/index.js';
@@ -0,0 +1,57 @@
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import '../src/index.js';
4
+ /**
5
+ * The three-pane frame (ADR-0060 decision 8 addendum): measured, not read - the widths a person
6
+ * gets at each size of the frame, and what folding a side gives to the middle.
7
+ */
8
+ const settle = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
9
+ async function frame(width, attrs = '', style = '') {
10
+ const holder = await fixture(html `<div style="width: ${width}px; height: 600px"></div>`);
11
+ holder.innerHTML = `<ox-three-pane ${attrs} style="height: 100%; ${style}">
12
+ <div slot="start">list</div><div>middle</div><div slot="end">details</div>
13
+ </ox-three-pane>`;
14
+ const pane = holder.querySelector('ox-three-pane');
15
+ await pane.updateComplete;
16
+ await settle();
17
+ const box = (name) => pane.shadowRoot.querySelector(`.${name}`).getBoundingClientRect();
18
+ return { pane, start: box('start'), middle: box('middle'), end: box('end') };
19
+ }
20
+ describe('ox-three-pane', () => {
21
+ it('★ start | middle | end at 200 | the rest | 340', async () => {
22
+ const { start, middle, end } = await frame(1200);
23
+ expect(start.width).to.equal(200);
24
+ expect(end.width).to.equal(340);
25
+ expect(middle.width).to.equal(660);
26
+ expect(start.left < middle.left && middle.left < end.left).to.equal(true);
27
+ });
28
+ it('★ folding a side gives its room to the middle', async () => {
29
+ const left = await frame(1200, 'fold-start');
30
+ expect(left.start.width).to.equal(0);
31
+ expect(left.middle.width).to.equal(860);
32
+ const both = await frame(1200, 'fold-start fold-end');
33
+ expect(both.middle.width).to.equal(1200);
34
+ });
35
+ it('a screen sets its own side widths without writing the grid again', async () => {
36
+ const { start, end } = await frame(1200, '', '--three-pane-start-width: 220px');
37
+ expect(start.width).to.equal(220);
38
+ expect(end.width).to.equal(340);
39
+ });
40
+ it('★ narrower than 860, the sides give up some room', async () => {
41
+ const { start, end } = await frame(800);
42
+ expect(start.width).to.equal(160);
43
+ expect(end.width).to.equal(300);
44
+ });
45
+ it('★ narrower than 560, one column with the middle first', async () => {
46
+ const { start, middle, end } = await frame(500);
47
+ expect(middle.width).to.equal(500);
48
+ expect(middle.top < start.top && middle.top < end.top).to.equal(true);
49
+ });
50
+ it('the fold is an attribute a toolbar can set and a stylesheet can read', async () => {
51
+ const { pane } = await frame(1200);
52
+ pane.foldEnd = true;
53
+ await pane.updateComplete;
54
+ expect(pane.hasAttribute('fold-end')).to.equal(true);
55
+ });
56
+ });
57
+ //# sourceMappingURL=three-pane.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"three-pane.test.js","sourceRoot":"","sources":["../../test/three-pane.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html } from 'lit'\nimport '../src/index.js'\nimport type { OxThreePane } from '../src/index.js'\n\n/**\n * The three-pane frame (ADR-0060 decision 8 addendum): measured, not read - the widths a person\n * gets at each size of the frame, and what folding a side gives to the middle.\n */\n\nconst settle = () => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))\n\nasync function frame(width: number, attrs = '', style = '') {\n const holder = await fixture<HTMLDivElement>(html`<div style=\"width: ${width}px; height: 600px\"></div>`)\n holder.innerHTML = `<ox-three-pane ${attrs} style=\"height: 100%; ${style}\">\n <div slot=\"start\">list</div><div>middle</div><div slot=\"end\">details</div>\n </ox-three-pane>`\n const pane = holder.querySelector('ox-three-pane') as OxThreePane\n await pane.updateComplete\n await settle()\n const box = (name: string) => pane.shadowRoot!.querySelector(`.${name}`)!.getBoundingClientRect()\n return { pane, start: box('start'), middle: box('middle'), end: box('end') }\n}\n\ndescribe('ox-three-pane', () => {\n it('★ start | middle | end at 200 | the rest | 340', async () => {\n const { start, middle, end } = await frame(1200)\n expect(start.width).to.equal(200)\n expect(end.width).to.equal(340)\n expect(middle.width).to.equal(660)\n expect(start.left < middle.left && middle.left < end.left).to.equal(true)\n })\n\n it('★ folding a side gives its room to the middle', async () => {\n const left = await frame(1200, 'fold-start')\n expect(left.start.width).to.equal(0)\n expect(left.middle.width).to.equal(860)\n\n const both = await frame(1200, 'fold-start fold-end')\n expect(both.middle.width).to.equal(1200)\n })\n\n it('a screen sets its own side widths without writing the grid again', async () => {\n const { start, end } = await frame(1200, '', '--three-pane-start-width: 220px')\n expect(start.width).to.equal(220)\n expect(end.width).to.equal(340)\n })\n\n it('★ narrower than 860, the sides give up some room', async () => {\n const { start, end } = await frame(800)\n expect(start.width).to.equal(160)\n expect(end.width).to.equal(300)\n })\n\n it('★ narrower than 560, one column with the middle first', async () => {\n const { start, middle, end } = await frame(500)\n expect(middle.width).to.equal(500)\n expect(middle.top < start.top && middle.top < end.top).to.equal(true)\n })\n\n it('the fold is an attribute a toolbar can set and a stylesheet can read', async () => {\n const { pane } = await frame(1200)\n pane.foldEnd = true\n await pane.updateComplete\n expect(pane.hasAttribute('fold-end')).to.equal(true)\n })\n})\n"]}
@@ -0,0 +1 @@
1
+ import '../src/index.js';
@@ -0,0 +1,67 @@
1
+ import { expect, fixture } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import '../src/index.js';
4
+ /**
5
+ * Tabs whose choice lives in the address (ADR-0060 decision 8 addendum): what is read, what is
6
+ * written, and that the two always go together.
7
+ */
8
+ const TABS = [
9
+ { key: 'orders', label: 'orders' },
10
+ { key: 'ledger', label: 'ledger' },
11
+ { key: 'journal', label: 'journal' }
12
+ ];
13
+ const start = location.href;
14
+ const at = (search, path = location.pathname) => history.replaceState(null, '', path + search);
15
+ describe('ox-url-tabs', () => {
16
+ afterEach(() => history.replaceState(null, '', start));
17
+ it('★ opens on the tab the address names, and on the first when it names none', async () => {
18
+ at('?tab=ledger&q=x');
19
+ const named = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
20
+ expect(named.selected).to.equal('ledger');
21
+ at('?tab=nothing-like-it');
22
+ const unknown = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
23
+ expect(unknown.selected).to.equal('orders');
24
+ });
25
+ it('★ choosing a tab writes it into the address and keeps the rest of the query', async () => {
26
+ at('?q=x');
27
+ const tabs = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
28
+ const lengthBefore = history.length;
29
+ tabs.shadowRoot.querySelector('[data-key="journal"]').click();
30
+ const query = new URLSearchParams(location.search);
31
+ expect(query.get('tab')).to.equal('journal');
32
+ expect(query.get('q')).to.equal('x');
33
+ expect(history.length).to.equal(lengthBefore);
34
+ });
35
+ it('the first tab is the default and is not written', async () => {
36
+ at('?tab=ledger');
37
+ const tabs = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
38
+ tabs.select('orders');
39
+ expect(new URLSearchParams(location.search).has('tab')).to.equal(false);
40
+ });
41
+ it('★ back or forward to another address moves the tab, and the screen hears it', async () => {
42
+ at('?tab=ledger');
43
+ const tabs = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
44
+ let heard = '';
45
+ tabs.addEventListener('tab-change', event => (heard = event.detail.key));
46
+ at('?tab=journal');
47
+ window.dispatchEvent(new PopStateEvent('popstate'));
48
+ expect(tabs.selected).to.equal('journal');
49
+ expect(heard).to.equal('journal');
50
+ });
51
+ it('arrow keys move between tabs', async () => {
52
+ at('');
53
+ const tabs = await fixture(html `<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`);
54
+ const first = tabs.shadowRoot.querySelector('[data-key="orders"]');
55
+ first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true }));
56
+ expect(tabs.selected).to.equal('journal');
57
+ });
58
+ it('transitional path segment: the key after the page is read', async () => {
59
+ at('', '/domain/x/quality-list/disposition');
60
+ const tabs = await fixture(html `<ox-url-tabs base="quality-list" .tabs=${[
61
+ { key: 'inspection', label: 'i' },
62
+ { key: 'disposition', label: 'd' }
63
+ ]}></ox-url-tabs>`);
64
+ expect(tabs.selected).to.equal('disposition');
65
+ });
66
+ });
67
+ //# sourceMappingURL=url-tabs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-tabs.test.js","sourceRoot":"","sources":["../../test/url-tabs.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect, fixture } from '@open-wc/testing'\nimport { html } from 'lit'\nimport '../src/index.js'\nimport type { OxUrlTabs } from '../src/index.js'\n\n/**\n * Tabs whose choice lives in the address (ADR-0060 decision 8 addendum): what is read, what is\n * written, and that the two always go together.\n */\n\nconst TABS = [\n { key: 'orders', label: 'orders' },\n { key: 'ledger', label: 'ledger' },\n { key: 'journal', label: 'journal' }\n]\n\nconst start = location.href\nconst at = (search: string, path = location.pathname) => history.replaceState(null, '', path + search)\n\ndescribe('ox-url-tabs', () => {\n afterEach(() => history.replaceState(null, '', start))\n\n it('★ opens on the tab the address names, and on the first when it names none', async () => {\n at('?tab=ledger&q=x')\n const named = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n expect(named.selected).to.equal('ledger')\n\n at('?tab=nothing-like-it')\n const unknown = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n expect(unknown.selected).to.equal('orders')\n })\n\n it('★ choosing a tab writes it into the address and keeps the rest of the query', async () => {\n at('?q=x')\n const tabs = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n const lengthBefore = history.length\n ;(tabs.shadowRoot!.querySelector('[data-key=\"journal\"]') as HTMLButtonElement).click()\n\n const query = new URLSearchParams(location.search)\n expect(query.get('tab')).to.equal('journal')\n expect(query.get('q')).to.equal('x')\n expect(history.length).to.equal(lengthBefore)\n })\n\n it('the first tab is the default and is not written', async () => {\n at('?tab=ledger')\n const tabs = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n tabs.select('orders')\n expect(new URLSearchParams(location.search).has('tab')).to.equal(false)\n })\n\n it('★ back or forward to another address moves the tab, and the screen hears it', async () => {\n at('?tab=ledger')\n const tabs = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n let heard = ''\n tabs.addEventListener('tab-change', event => (heard = (event as CustomEvent).detail.key))\n\n at('?tab=journal')\n window.dispatchEvent(new PopStateEvent('popstate'))\n expect(tabs.selected).to.equal('journal')\n expect(heard).to.equal('journal')\n })\n\n it('arrow keys move between tabs', async () => {\n at('')\n const tabs = await fixture<OxUrlTabs>(html`<ox-url-tabs .tabs=${TABS}></ox-url-tabs>`)\n const first = tabs.shadowRoot!.querySelector('[data-key=\"orders\"]') as HTMLButtonElement\n first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true, composed: true }))\n expect(tabs.selected).to.equal('journal')\n })\n\n it('transitional path segment: the key after the page is read', async () => {\n at('', '/domain/x/quality-list/disposition')\n const tabs = await fixture<OxUrlTabs>(html`<ox-url-tabs base=\"quality-list\" .tabs=${[\n { key: 'inspection', label: 'i' },\n { key: 'disposition', label: 'd' }\n ]}></ox-url-tabs>`)\n expect(tabs.selected).to.equal('disposition')\n })\n})\n"]}