@api-client/ui 0.5.31 → 0.5.33
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/build/src/core/renderer/ApplicationRenderer.d.ts +3 -2
- package/build/src/core/renderer/ApplicationRenderer.d.ts.map +1 -1
- package/build/src/core/renderer/ApplicationRenderer.js +24 -33
- package/build/src/core/renderer/ApplicationRenderer.js.map +1 -1
- package/build/src/core/renderer/FragmentRenderer.d.ts +2 -2
- package/build/src/core/renderer/FragmentRenderer.d.ts.map +1 -1
- package/build/src/core/renderer/FragmentRenderer.js +6 -16
- package/build/src/core/renderer/FragmentRenderer.js.map +1 -1
- package/build/src/core/renderer/Renderer.d.ts +38 -9
- package/build/src/core/renderer/Renderer.d.ts.map +1 -1
- package/build/src/core/renderer/Renderer.js +51 -8
- package/build/src/core/renderer/Renderer.js.map +1 -1
- package/build/src/elements/navigation/internals/Navigation.d.ts +68 -0
- package/build/src/elements/navigation/internals/Navigation.d.ts.map +1 -0
- package/build/src/elements/navigation/internals/Navigation.js +205 -0
- package/build/src/elements/navigation/internals/Navigation.js.map +1 -0
- package/build/src/elements/navigation/internals/Navigation.styles.d.ts +3 -0
- package/build/src/elements/navigation/internals/Navigation.styles.d.ts.map +1 -0
- package/build/src/elements/navigation/internals/Navigation.styles.js +24 -0
- package/build/src/elements/navigation/internals/Navigation.styles.js.map +1 -0
- package/build/src/elements/navigation/internals/NavigationItem.d.ts +57 -2
- package/build/src/elements/navigation/internals/NavigationItem.d.ts.map +1 -1
- package/build/src/elements/navigation/internals/NavigationItem.js +73 -18
- package/build/src/elements/navigation/internals/NavigationItem.js.map +1 -1
- package/build/src/elements/navigation/ui-navigation.d.ts +11 -0
- package/build/src/elements/navigation/ui-navigation.d.ts.map +1 -0
- package/build/src/elements/navigation/ui-navigation.js +27 -0
- package/build/src/elements/navigation/ui-navigation.js.map +1 -0
- package/build/src/md/input/Input.d.ts +2 -0
- package/build/src/md/input/Input.d.ts.map +1 -1
- package/build/src/types/aria.d.ts +28 -0
- package/build/src/types/aria.d.ts.map +1 -0
- package/build/src/types/aria.js +2 -0
- package/build/src/types/aria.js.map +1 -0
- package/build/src/types/role.d.ts +1 -16
- package/build/src/types/role.d.ts.map +1 -1
- package/build/src/types/role.js.map +1 -1
- package/build/test/elements/navigation/Navigation.test.d.ts +3 -0
- package/build/test/elements/navigation/Navigation.test.d.ts.map +1 -0
- package/build/test/elements/navigation/Navigation.test.js +113 -0
- package/build/test/elements/navigation/Navigation.test.js.map +1 -0
- package/demo/elements/index.html +2 -0
- package/demo/elements/navigation/navigation.html +20 -0
- package/demo/elements/navigation/navigation.ts +45 -0
- package/package.json +1 -1
- package/src/core/renderer/ApplicationRenderer.ts +28 -32
- package/src/core/renderer/FragmentRenderer.ts +6 -15
- package/src/core/renderer/Renderer.ts +60 -12
- package/src/elements/navigation/internals/Navigation.styles.ts +24 -0
- package/src/elements/navigation/internals/Navigation.ts +181 -0
- package/src/elements/navigation/internals/NavigationItem.ts +74 -5
- package/src/elements/navigation/ui-navigation.ts +15 -0
- package/src/types/aria.ts +141 -0
- package/src/types/role.ts +1 -129
- package/test/core/renderer.spec.ts +113 -0
- package/test/elements/navigation/Navigation.test.ts +120 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria.d.ts","sourceRoot":"","sources":["../../../src/types/aria.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,EAAE,CAAA;AAEhG;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAElE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,CAAA;AAE3C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE7F;;GAEG;AACH,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,aAAa,GACb,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,UAAU,GACV,MAAM,GACN,KAAK,GACL,SAAS,GACT,UAAU,GACV,kBAAkB,GAClB,eAAe,GACf,QAAQ,GACR,aAAa,GACb,OAAO,GACP,WAAW,GACX,WAAW,GACX,QAAQ,GACR,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,KAAK,GACL,UAAU,GACV,SAAS,GACT,OAAO,GACP,SAAS,GACT,UAAU,GACV,UAAU,GACV,MAAM,GACN,SAAS,GACT,MAAM,GACN,SAAS,GACT,YAAY,GACZ,SAAS,GACT,MAAM,GACN,UAAU,GACV,aAAa,GACb,SAAS,GACT,MAAM,GACN,cAAc,GACd,YAAY,GACZ,WAAW,GACX,UAAU,GACV,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,KAAK,GACL,MAAM,GACN,UAAU,GACV,MAAM,GACN,MAAM,GACN,MAAM,GACN,cAAc,GACd,QAAQ,GACR,KAAK,GACL,UAAU,GACV,WAAW,GACX,WAAW,GACX,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,GACT,QAAQ,GACR,eAAe,GACf,aAAa,GACb,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,qBAAqB,GACrB,eAAe,GACf,cAAc,GACd,cAAc,GACd,iBAAiB,GACjB,kBAAkB,GAClB,eAAe,GACf,aAAa,GACb,cAAc,GACd,gBAAgB,GAChB,WAAW,GACX,YAAY,GACZ,aAAa,GACb,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,cAAc,GACd,cAAc,GACd,YAAY,GACZ,aAAa,GACb,cAAc,GACd,cAAc,GACd,cAAc,GACd,cAAc,GACd,WAAW,GACX,kBAAkB,GAClB,aAAa,GACb,YAAY,GACZ,eAAe,GACf,cAAc,GACd,UAAU,GACV,aAAa,GACb,cAAc,GACd,eAAe,GACf,SAAS,GACT,cAAc,GACd,SAAS,GACT,SAAS,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria.js","sourceRoot":"","sources":["../../../src/types/aria.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * - `page` - Represents the current page within a set of pages.\n * - `step` - Represents the current step within a process.\n * - `location` - Represents the current location, for example the current page in a breadcrumbs hierarchy.\n * - `date` - Represents the current date within a collection of dates.\n * - `time` - Represents the current time within a set of times.\n * - `true` - Represents the current item within a set.\n * - `false` - Does not represent the current item within a set.\n * - `''` - No current state.\n */\nexport type AriaCurrent = 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | ''\n\n/**\n * Valid values for `aria-expanded`.\n */\nexport type ARIAAutoComplete = 'none' | 'inline' | 'list' | 'both'\n\n/**\n * Valid values for `aria-expanded`.\n */\nexport type ARIAExpanded = 'true' | 'false'\n\n/**\n * Valid values for `aria-haspopup`.\n */\nexport type ARIAHasPopup = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'\n\n/**\n * Valid values for `role`.\n */\nexport type ARIARole =\n | 'alert'\n | 'alertdialog'\n | 'button'\n | 'checkbox'\n | 'dialog'\n | 'gridcell'\n | 'link'\n | 'log'\n | 'marquee'\n | 'menuitem'\n | 'menuitemcheckbox'\n | 'menuitemradio'\n | 'option'\n | 'progressbar'\n | 'radio'\n | 'scrollbar'\n | 'searchbox'\n | 'slider'\n | 'spinbutton'\n | 'status'\n | 'switch'\n | 'tab'\n | 'tabpanel'\n | 'textbox'\n | 'timer'\n | 'tooltip'\n | 'treeitem'\n | 'combobox'\n | 'grid'\n | 'listbox'\n | 'menu'\n | 'menubar'\n | 'radiogroup'\n | 'tablist'\n | 'tree'\n | 'treegrid'\n | 'application'\n | 'article'\n | 'cell'\n | 'columnheader'\n | 'definition'\n | 'directory'\n | 'document'\n | 'feed'\n | 'figure'\n | 'group'\n | 'heading'\n | 'img'\n | 'list'\n | 'listitem'\n | 'math'\n | 'none'\n | 'note'\n | 'presentation'\n | 'region'\n | 'row'\n | 'rowgroup'\n | 'rowheader'\n | 'separator'\n | 'table'\n | 'term'\n | 'text'\n | 'toolbar'\n | 'banner'\n | 'complementary'\n | 'contentinfo'\n | 'form'\n | 'main'\n | 'navigation'\n | 'region'\n | 'search'\n | 'doc-abstract'\n | 'doc-acknowledgments'\n | 'doc-afterword'\n | 'doc-appendix'\n | 'doc-backlink'\n | 'doc-biblioentry'\n | 'doc-bibliography'\n | 'doc-biblioref'\n | 'doc-chapter'\n | 'doc-colophon'\n | 'doc-conclusion'\n | 'doc-cover'\n | 'doc-credit'\n | 'doc-credits'\n | 'doc-dedication'\n | 'doc-endnote'\n | 'doc-endnotes'\n | 'doc-epigraph'\n | 'doc-epilogue'\n | 'doc-errata'\n | 'doc-example'\n | 'doc-footnote'\n | 'doc-foreword'\n | 'doc-glossary'\n | 'doc-glossref'\n | 'doc-index'\n | 'doc-introduction'\n | 'doc-noteref'\n | 'doc-notice'\n | 'doc-pagebreak'\n | 'doc-pagelist'\n | 'doc-part'\n | 'doc-preface'\n | 'doc-prologue'\n | 'doc-pullquote'\n | 'doc-qna'\n | 'doc-subtitle'\n | 'doc-tip'\n | 'doc-toc'\n"]}
|
|
@@ -1,17 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Valid values for `aria-expanded`.
|
|
3
|
-
*/
|
|
4
|
-
export type ARIAAutoComplete = 'none' | 'inline' | 'list' | 'both';
|
|
5
|
-
/**
|
|
6
|
-
* Valid values for `aria-expanded`.
|
|
7
|
-
*/
|
|
8
|
-
export type ARIAExpanded = 'true' | 'false';
|
|
9
|
-
/**
|
|
10
|
-
* Valid values for `aria-haspopup`.
|
|
11
|
-
*/
|
|
12
|
-
export type ARIAHasPopup = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
|
|
13
|
-
/**
|
|
14
|
-
* Valid values for `role`.
|
|
15
|
-
*/
|
|
16
|
-
export type ARIARole = 'alert' | 'alertdialog' | 'button' | 'checkbox' | 'dialog' | 'gridcell' | 'link' | 'log' | 'marquee' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'option' | 'progressbar' | 'radio' | 'scrollbar' | 'searchbox' | 'slider' | 'spinbutton' | 'status' | 'switch' | 'tab' | 'tabpanel' | 'textbox' | 'timer' | 'tooltip' | 'treeitem' | 'combobox' | 'grid' | 'listbox' | 'menu' | 'menubar' | 'radiogroup' | 'tablist' | 'tree' | 'treegrid' | 'application' | 'article' | 'cell' | 'columnheader' | 'definition' | 'directory' | 'document' | 'feed' | 'figure' | 'group' | 'heading' | 'img' | 'list' | 'listitem' | 'math' | 'none' | 'note' | 'presentation' | 'region' | 'row' | 'rowgroup' | 'rowheader' | 'separator' | 'table' | 'term' | 'text' | 'toolbar' | 'banner' | 'complementary' | 'contentinfo' | 'form' | 'main' | 'navigation' | 'region' | 'search' | 'doc-abstract' | 'doc-acknowledgments' | 'doc-afterword' | 'doc-appendix' | 'doc-backlink' | 'doc-biblioentry' | 'doc-bibliography' | 'doc-biblioref' | 'doc-chapter' | 'doc-colophon' | 'doc-conclusion' | 'doc-cover' | 'doc-credit' | 'doc-credits' | 'doc-dedication' | 'doc-endnote' | 'doc-endnotes' | 'doc-epigraph' | 'doc-epilogue' | 'doc-errata' | 'doc-example' | 'doc-footnote' | 'doc-foreword' | 'doc-glossary' | 'doc-glossref' | 'doc-index' | 'doc-introduction' | 'doc-noteref' | 'doc-notice' | 'doc-pagebreak' | 'doc-pagelist' | 'doc-part' | 'doc-preface' | 'doc-prologue' | 'doc-pullquote' | 'doc-qna' | 'doc-subtitle' | 'doc-tip' | 'doc-toc';
|
|
1
|
+
export { ARIAAutoComplete, ARIAExpanded, ARIAHasPopup, ARIARole } from './aria.js';
|
|
17
2
|
//# sourceMappingURL=role.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../../src/types/role.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../../src/types/role.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.js","sourceRoot":"","sources":["../../../src/types/role.ts"],"names":[],"mappings":"","sourcesContent":["
|
|
1
|
+
{"version":3,"file":"role.js","sourceRoot":"","sources":["../../../src/types/role.ts"],"names":[],"mappings":"","sourcesContent":["export { ARIAAutoComplete, ARIAExpanded, ARIAHasPopup, ARIARole } from './aria.js'\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Navigation.test.d.ts","sourceRoot":"","sources":["../../../../test/elements/navigation/Navigation.test.ts"],"names":[],"mappings":"AAEA,OAAO,mDAAmD,CAAA;AAC1D,OAAO,wDAAwD,CAAA"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { html, fixture, assert, oneEvent, aTimeout } from '@open-wc/testing';
|
|
2
|
+
import '../../../src/elements/navigation/ui-navigation.js';
|
|
3
|
+
import '../../../src/elements/navigation/ui-navigation-item.js';
|
|
4
|
+
describe('Navigation', () => {
|
|
5
|
+
it('renders with slot content', async () => {
|
|
6
|
+
const el = await fixture(html `
|
|
7
|
+
<ui-navigation aria-label="Main navigation">
|
|
8
|
+
<ui-navigation-item selected aria-current="page">Home</ui-navigation-item>
|
|
9
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
10
|
+
<ui-navigation-item>Files</ui-navigation-item>
|
|
11
|
+
</ui-navigation>
|
|
12
|
+
`);
|
|
13
|
+
const nav = el.shadowRoot.querySelector('nav');
|
|
14
|
+
assert.ok(nav, 'nav element is rendered');
|
|
15
|
+
assert.equal(nav?.getAttribute('aria-label'), 'Main navigation');
|
|
16
|
+
const items = el._items;
|
|
17
|
+
assert.equal(items.length, 3);
|
|
18
|
+
assert.isTrue(items[0].selected);
|
|
19
|
+
assert.equal(items[0].getAttribute('aria-current'), 'page');
|
|
20
|
+
});
|
|
21
|
+
it('sets correct tabindex for items', async () => {
|
|
22
|
+
const el = await fixture(html `
|
|
23
|
+
<ui-navigation>
|
|
24
|
+
<ui-navigation-item selected>Home</ui-navigation-item>
|
|
25
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
26
|
+
</ui-navigation>
|
|
27
|
+
`);
|
|
28
|
+
const items = el._items;
|
|
29
|
+
assert.equal(items[0].getAttribute('tabindex'), '0');
|
|
30
|
+
assert.equal(items[1].getAttribute('tabindex'), '-1');
|
|
31
|
+
});
|
|
32
|
+
it('selects item on click and fires select event', async () => {
|
|
33
|
+
const el = await fixture(html `
|
|
34
|
+
<ui-navigation>
|
|
35
|
+
<ui-navigation-item>Home</ui-navigation-item>
|
|
36
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
37
|
+
</ui-navigation>
|
|
38
|
+
`);
|
|
39
|
+
const items = el._items;
|
|
40
|
+
setTimeout(() => items[1].click());
|
|
41
|
+
const ev = await oneEvent(el, 'select');
|
|
42
|
+
assert.equal(ev.detail.item, items[1]);
|
|
43
|
+
assert.isTrue(items[1].selected);
|
|
44
|
+
assert.equal(items[1].getAttribute('tabindex'), '0');
|
|
45
|
+
assert.isFalse(items[0].selected);
|
|
46
|
+
assert.equal(items[0].getAttribute('tabindex'), '-1');
|
|
47
|
+
});
|
|
48
|
+
it('handles keyboard navigation (vertical)', async () => {
|
|
49
|
+
const el = await fixture(html `
|
|
50
|
+
<ui-navigation>
|
|
51
|
+
<ui-navigation-item>Home</ui-navigation-item>
|
|
52
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
53
|
+
<ui-navigation-item>Files</ui-navigation-item>
|
|
54
|
+
</ui-navigation>
|
|
55
|
+
`);
|
|
56
|
+
const items = el._items;
|
|
57
|
+
items[0].focus();
|
|
58
|
+
// ArrowDown moves to next
|
|
59
|
+
items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
60
|
+
await aTimeout(0);
|
|
61
|
+
assert.dom.equal(document.activeElement, items[1]);
|
|
62
|
+
// ArrowUp wraps to last
|
|
63
|
+
items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
|
|
64
|
+
await aTimeout(0);
|
|
65
|
+
assert.dom.equal(document.activeElement, items[0]);
|
|
66
|
+
});
|
|
67
|
+
it('handles keyboard navigation (horizontal)', async () => {
|
|
68
|
+
const el = await fixture(html `
|
|
69
|
+
<ui-navigation orientation="horizontal">
|
|
70
|
+
<ui-navigation-item>Home</ui-navigation-item>
|
|
71
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
72
|
+
</ui-navigation>
|
|
73
|
+
`);
|
|
74
|
+
const items = el._items;
|
|
75
|
+
items[0].focus();
|
|
76
|
+
items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
|
77
|
+
await aTimeout(0);
|
|
78
|
+
assert.dom.equal(document.activeElement, items[1]);
|
|
79
|
+
items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
80
|
+
await aTimeout(0);
|
|
81
|
+
assert.dom.equal(document.activeElement, items[0]);
|
|
82
|
+
});
|
|
83
|
+
it('skips disabled items in navigation', async () => {
|
|
84
|
+
const el = await fixture(html `
|
|
85
|
+
<ui-navigation>
|
|
86
|
+
<ui-navigation-item>Home</ui-navigation-item>
|
|
87
|
+
<ui-navigation-item disabled>Search</ui-navigation-item>
|
|
88
|
+
<ui-navigation-item>Files</ui-navigation-item>
|
|
89
|
+
</ui-navigation>
|
|
90
|
+
`);
|
|
91
|
+
const items = el._items;
|
|
92
|
+
items[0].focus();
|
|
93
|
+
items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
|
|
94
|
+
await aTimeout(0);
|
|
95
|
+
// Should skip disabled and go to Files
|
|
96
|
+
assert.dom.equal(document.activeElement, items[2]);
|
|
97
|
+
});
|
|
98
|
+
it('sets aria-current on selected item if current is set', async () => {
|
|
99
|
+
const el = await fixture(html `
|
|
100
|
+
<ui-navigation current="page">
|
|
101
|
+
<ui-navigation-item>Home</ui-navigation-item>
|
|
102
|
+
<ui-navigation-item>Search</ui-navigation-item>
|
|
103
|
+
</ui-navigation>
|
|
104
|
+
`);
|
|
105
|
+
const items = el._items;
|
|
106
|
+
// Simulate user selection by clicking the item (public API)
|
|
107
|
+
items[1].click();
|
|
108
|
+
await aTimeout(0);
|
|
109
|
+
assert.equal(items[1].getAttribute('aria-current'), 'page');
|
|
110
|
+
assert.isFalse(items[0].hasAttribute('aria-current'));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=Navigation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Navigation.test.js","sourceRoot":"","sources":["../../../../test/elements/navigation/Navigation.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, fixture, assert, oneEvent, aTimeout } from '@open-wc/testing'\nimport type Navigation from '../../../src/elements/navigation/internals/Navigation.js'\nimport '../../../src/elements/navigation/ui-navigation.js'\nimport '../../../src/elements/navigation/ui-navigation-item.js'\n\ndescribe('Navigation', () => {\n it('renders with slot content', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation aria-label=\"Main navigation\">\n <ui-navigation-item selected aria-current=\"page\">Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n <ui-navigation-item>Files</ui-navigation-item>\n </ui-navigation>\n `)\n const nav = el.shadowRoot!.querySelector('nav')\n assert.ok(nav, 'nav element is rendered')\n assert.equal(nav?.getAttribute('aria-label'), 'Main navigation')\n const items = el._items\n assert.equal(items.length, 3)\n assert.isTrue(items[0].selected)\n assert.equal(items[0].getAttribute('aria-current'), 'page')\n })\n\n it('sets correct tabindex for items', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation>\n <ui-navigation-item selected>Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n assert.equal(items[0].getAttribute('tabindex'), '0')\n assert.equal(items[1].getAttribute('tabindex'), '-1')\n })\n\n it('selects item on click and fires select event', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation>\n <ui-navigation-item>Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n setTimeout(() => items[1].click())\n const ev = await oneEvent(el, 'select')\n assert.equal(ev.detail.item, items[1])\n assert.isTrue(items[1].selected)\n assert.equal(items[1].getAttribute('tabindex'), '0')\n assert.isFalse(items[0].selected)\n assert.equal(items[0].getAttribute('tabindex'), '-1')\n })\n\n it('handles keyboard navigation (vertical)', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation>\n <ui-navigation-item>Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n <ui-navigation-item>Files</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n items[0].focus()\n // ArrowDown moves to next\n items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))\n await aTimeout(0)\n assert.dom.equal(document.activeElement, items[1])\n // ArrowUp wraps to last\n items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))\n await aTimeout(0)\n assert.dom.equal(document.activeElement, items[0])\n })\n\n it('handles keyboard navigation (horizontal)', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation orientation=\"horizontal\">\n <ui-navigation-item>Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n items[0].focus()\n items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))\n await aTimeout(0)\n assert.dom.equal(document.activeElement, items[1])\n items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))\n await aTimeout(0)\n assert.dom.equal(document.activeElement, items[0])\n })\n\n it('skips disabled items in navigation', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation>\n <ui-navigation-item>Home</ui-navigation-item>\n <ui-navigation-item disabled>Search</ui-navigation-item>\n <ui-navigation-item>Files</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n items[0].focus()\n items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))\n await aTimeout(0)\n // Should skip disabled and go to Files\n assert.dom.equal(document.activeElement, items[2])\n })\n\n it('sets aria-current on selected item if current is set', async () => {\n const el = await fixture<Navigation>(html`\n <ui-navigation current=\"page\">\n <ui-navigation-item>Home</ui-navigation-item>\n <ui-navigation-item>Search</ui-navigation-item>\n </ui-navigation>\n `)\n const items = el._items\n // Simulate user selection by clicking the item (public API)\n items[1].click()\n await aTimeout(0)\n assert.equal(items[1].getAttribute('aria-current'), 'page')\n assert.isFalse(items[0].hasAttribute('aria-current'))\n })\n})\n"]}
|
package/demo/elements/index.html
CHANGED
|
@@ -52,6 +52,8 @@
|
|
|
52
52
|
<dt><a href="mention-textarea/index.html">Mention Textarea</a></dt>
|
|
53
53
|
<dd>The mention-textarea component.</dd>
|
|
54
54
|
|
|
55
|
+
<dt><a href="navigation/navigation.html">Navigation Menu</a></dt>
|
|
56
|
+
<dd>The navigation component.</dd>
|
|
55
57
|
<dt><a href="navigation/navigation-item.html">Navigation Item</a></dt>
|
|
56
58
|
<dd>The navigation-item component.</dd>
|
|
57
59
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Navigation item Demo</title>
|
|
7
|
+
<link
|
|
8
|
+
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
|
9
|
+
rel="stylesheet"
|
|
10
|
+
/>
|
|
11
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" rel="stylesheet" />
|
|
12
|
+
<link href="../../../src/styles/m3/tokens.css" rel="stylesheet" type="text/css" />
|
|
13
|
+
<link href="../../../src/styles/m3/theme.css" rel="stylesheet" type="text/css" />
|
|
14
|
+
<link href="../../page.css" rel="stylesheet" type="text/css" />
|
|
15
|
+
</head>
|
|
16
|
+
<body data-gr-ext-disabled="next">
|
|
17
|
+
<div id="app"></div>
|
|
18
|
+
<script type="module" src="/.tmp/demo/elements/navigation/navigation.js"></script>
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { html, TemplateResult } from 'lit'
|
|
2
|
+
import { DemoPage } from '../../../src/demo/DemoPage.js'
|
|
3
|
+
import { reactive } from '../../../src/decorators/index.js'
|
|
4
|
+
|
|
5
|
+
import '../../../src/elements/navigation/ui-navigation-item.js'
|
|
6
|
+
import '../../../src/elements/navigation/ui-navigation.js'
|
|
7
|
+
import '../../../src/md/icons/ui-icon.js'
|
|
8
|
+
import '../../../src/md/checkbox/ui-checkbox.js'
|
|
9
|
+
|
|
10
|
+
class ComponentDemoPage extends DemoPage {
|
|
11
|
+
@reactive()
|
|
12
|
+
protected accessor selected = false
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super()
|
|
16
|
+
this.componentName = '<ui-navigation>'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override contentTemplate(): TemplateResult {
|
|
20
|
+
return html`
|
|
21
|
+
<section class="demo-section">
|
|
22
|
+
<ui-navigation aria-label="Main navigation" current="page">
|
|
23
|
+
<ui-navigation-item selected aria-current="page">
|
|
24
|
+
<span slot="icon" aria-hidden="true">🏠</span>
|
|
25
|
+
Home
|
|
26
|
+
</ui-navigation-item>
|
|
27
|
+
<ui-navigation-item>
|
|
28
|
+
<span slot="icon" aria-hidden="true">🔍</span>
|
|
29
|
+
Search
|
|
30
|
+
</ui-navigation-item>
|
|
31
|
+
<ui-navigation-item>
|
|
32
|
+
<span slot="icon" aria-hidden="true">📁</span>
|
|
33
|
+
Files
|
|
34
|
+
</ui-navigation-item>
|
|
35
|
+
<ui-navigation-item iconOnly disabled aria-disabled="true" tabindex="-1" aria-label="Settings">
|
|
36
|
+
<span slot="icon" aria-hidden="true">⚙️</span>
|
|
37
|
+
</ui-navigation-item>
|
|
38
|
+
</ui-navigation>
|
|
39
|
+
</section>
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const instance = new ComponentDemoPage()
|
|
45
|
+
instance.render()
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { nothing,
|
|
1
|
+
import { nothing, type TemplateResult } from 'lit'
|
|
2
2
|
import type { Application } from '../Application.js'
|
|
3
3
|
import type { Activity } from '../Activity.js'
|
|
4
|
-
import { Renderer
|
|
4
|
+
import { Renderer } from './Renderer.js'
|
|
5
5
|
import { ModalActivity } from '../ModalActivity.js'
|
|
6
6
|
|
|
7
7
|
export class ApplicationRenderer extends Renderer {
|
|
@@ -12,46 +12,42 @@ export class ApplicationRenderer extends Renderer {
|
|
|
12
12
|
this.renderedFirst = new WeakMap()
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const activity = app.manager.getTopActivity()
|
|
21
|
-
if (activity) {
|
|
22
|
-
host = activity
|
|
23
|
-
if (activity.renderRoot) {
|
|
24
|
-
root = activity.renderRoot
|
|
25
|
-
}
|
|
26
|
-
if (activity instanceof ModalActivity) {
|
|
27
|
-
content = activity.renderDialog()
|
|
28
|
-
} else {
|
|
29
|
-
content = activity.render()
|
|
15
|
+
private _handleFirstRender(target: Application | Activity, clearRoot: boolean, root: HTMLElement): void {
|
|
16
|
+
if (!this.renderedFirst.has(target)) {
|
|
17
|
+
this.renderedFirst.set(target, true)
|
|
18
|
+
if (clearRoot) {
|
|
19
|
+
this._clearRoot(root)
|
|
30
20
|
}
|
|
21
|
+
queueMicrotask(() => target.onFirstRender())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected renderer(): void {
|
|
26
|
+
const { app, renderRoot: defaultRenderRoot } = this
|
|
27
|
+
const activity = app.manager.getTopActivity()
|
|
28
|
+
|
|
29
|
+
const host: Application | Activity = activity || app
|
|
30
|
+
const root: HTMLElement | null = activity?.renderRoot || defaultRenderRoot
|
|
31
|
+
|
|
32
|
+
let content: TemplateResult | typeof nothing
|
|
33
|
+
if (activity instanceof ModalActivity) {
|
|
34
|
+
content = activity.renderDialog()
|
|
31
35
|
} else {
|
|
32
|
-
|
|
33
|
-
content = app.render()
|
|
36
|
+
content = host.render()
|
|
34
37
|
}
|
|
35
38
|
if (!root) {
|
|
36
39
|
// eslint-disable-next-line no-console
|
|
37
40
|
console.warn(`The "renderRoot" is not set up.`)
|
|
38
41
|
return
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
setTimeout(() => app.onFirstRender())
|
|
44
|
-
}
|
|
45
|
-
// host might !== app
|
|
46
|
-
if (!this.renderedFirst.has(host)) {
|
|
47
|
-
setTimeout(() => host.onFirstRender())
|
|
43
|
+
this._handleFirstRender(app, true, root)
|
|
44
|
+
if (!this.firstRendered) {
|
|
45
|
+
this.firstRenderedFlag = true
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
litPart._$litPart$.options.host = host
|
|
47
|
+
if (host !== app) {
|
|
48
|
+
this._handleFirstRender(host, false, root)
|
|
52
49
|
}
|
|
53
|
-
|
|
54
|
-
this.resolveUpdatePromise()
|
|
50
|
+
this._render(content, root, host)
|
|
55
51
|
this.app.onRendered()
|
|
56
52
|
}
|
|
57
53
|
}
|
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import { render, RenderOptions, RootPart } from 'lit'
|
|
2
1
|
import type { Fragment } from '../Fragment.js'
|
|
3
|
-
import { Renderer
|
|
2
|
+
import { Renderer } from './Renderer.js'
|
|
4
3
|
|
|
5
4
|
export class FragmentRenderer extends Renderer {
|
|
6
5
|
constructor(protected fragment: Fragment) {
|
|
7
6
|
super()
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
protected renderer(): void {
|
|
11
10
|
const root = this.renderRoot
|
|
12
11
|
if (!root) {
|
|
13
12
|
// return quietly. Fragments can be rendered directly.
|
|
14
13
|
return
|
|
15
14
|
}
|
|
16
|
-
const
|
|
17
|
-
const host = fragment.getSingleVisibleFragment() || fragment
|
|
15
|
+
const host = this.fragment.getSingleVisibleFragment() ?? this.fragment
|
|
18
16
|
if (!this.firstRendered) {
|
|
19
17
|
this.firstRenderedFlag = true
|
|
20
18
|
// cleanup any pre-existing content.
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
this._clearRoot(root)
|
|
20
|
+
queueMicrotask(() => host.onFirstRender())
|
|
23
21
|
}
|
|
24
|
-
|
|
25
|
-
if (litPart._$litPart$) {
|
|
26
|
-
;(litPart._$litPart$.options as RenderOptions).host = host
|
|
27
|
-
}
|
|
28
|
-
// console.log(host, root)
|
|
29
|
-
render(host.render(), root, { host })
|
|
30
|
-
this.resolveUpdatePromise()
|
|
31
|
-
// this.app.onRendered()
|
|
22
|
+
this._render(host.render(), root, host)
|
|
32
23
|
}
|
|
33
24
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { render, type RootPart, type TemplateResult, type nothing } from 'lit'
|
|
2
|
+
|
|
1
3
|
export enum RenderingState {
|
|
2
4
|
Idle,
|
|
3
5
|
Rendering,
|
|
4
6
|
}
|
|
5
7
|
|
|
6
|
-
export const renderFn = Symbol('renderFn')
|
|
7
|
-
|
|
8
8
|
/**
|
|
9
9
|
* A class that manages rendering of the application screen.
|
|
10
10
|
*/
|
|
@@ -22,8 +22,10 @@ export abstract class Renderer {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Sets the root element where the application will be rendered.
|
|
26
|
-
*
|
|
25
|
+
* Sets the root element where the application or activity will be rendered.
|
|
26
|
+
*
|
|
27
|
+
* @param renderRoot - The root element or a CSS selector string for the element.
|
|
28
|
+
* If a string is provided, it will be queried from the document.
|
|
27
29
|
*/
|
|
28
30
|
setRenderRoot(renderRoot: HTMLElement | string): void {
|
|
29
31
|
if (typeof renderRoot === 'string') {
|
|
@@ -34,8 +36,10 @@ export abstract class Renderer {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
* Requests an update of the
|
|
38
|
-
*
|
|
39
|
+
* Requests an update of the UI. Schedules a render if one is not already pending.
|
|
40
|
+
*
|
|
41
|
+
* This will call the `renderer()` method and update the DOM in the next animation frame.
|
|
42
|
+
* Ensures only one render is scheduled at a time.
|
|
39
43
|
*/
|
|
40
44
|
requestUpdate(): void {
|
|
41
45
|
if (this.#renderingState !== RenderingState.Idle) {
|
|
@@ -47,7 +51,7 @@ export abstract class Renderer {
|
|
|
47
51
|
this.#setUpdatePromise()
|
|
48
52
|
}
|
|
49
53
|
requestAnimationFrame(() => {
|
|
50
|
-
this
|
|
54
|
+
this.renderer()
|
|
51
55
|
this.#renderingState = RenderingState.Idle
|
|
52
56
|
})
|
|
53
57
|
}
|
|
@@ -55,8 +59,7 @@ export abstract class Renderer {
|
|
|
55
59
|
protected firstRenderedFlag = false
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
59
|
-
* function was called.
|
|
62
|
+
* Indicates whether the initial render has occurred and `onFirstRender()` was called.
|
|
60
63
|
*/
|
|
61
64
|
get firstRendered(): boolean {
|
|
62
65
|
return this.firstRenderedFlag
|
|
@@ -67,6 +70,15 @@ export abstract class Renderer {
|
|
|
67
70
|
*/
|
|
68
71
|
#renderingState = RenderingState.Idle
|
|
69
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Returns the current rendering state.
|
|
75
|
+
*
|
|
76
|
+
* @returns The current rendering state.
|
|
77
|
+
*/
|
|
78
|
+
get state(): RenderingState {
|
|
79
|
+
return this.#renderingState
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
/**
|
|
71
83
|
* A flag that helps to determine whether the `updateComplete` is setup.
|
|
72
84
|
*/
|
|
@@ -80,13 +92,19 @@ export abstract class Renderer {
|
|
|
80
92
|
/**
|
|
81
93
|
* The resolver to call when the update completes.
|
|
82
94
|
*/
|
|
83
|
-
#updateResolver?: () => void
|
|
95
|
+
#updateResolver?: () => void
|
|
84
96
|
|
|
85
97
|
/**
|
|
86
|
-
*
|
|
98
|
+
* Abstract method that must be implemented by subclasses to perform the actual rendering logic.
|
|
99
|
+
*
|
|
100
|
+
* Called by `requestUpdate()` when a render is scheduled.
|
|
87
101
|
*/
|
|
88
|
-
abstract
|
|
102
|
+
protected abstract renderer(): void
|
|
89
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Sets up the promise that will be resolved when the next render completes.
|
|
106
|
+
* Used for tracking asynchronous rendering.
|
|
107
|
+
*/
|
|
90
108
|
#setUpdatePromise(): void {
|
|
91
109
|
this.#updateComplete = new Promise<void>((resolve) => {
|
|
92
110
|
this.#updateResolver = resolve
|
|
@@ -94,6 +112,9 @@ export abstract class Renderer {
|
|
|
94
112
|
})
|
|
95
113
|
}
|
|
96
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Resolves the update promise if one is pending, indicating that rendering has finished.
|
|
117
|
+
*/
|
|
97
118
|
protected resolveUpdatePromise(): void {
|
|
98
119
|
if (!this.#hasPendingUpdatePromise) {
|
|
99
120
|
return
|
|
@@ -104,4 +125,31 @@ export abstract class Renderer {
|
|
|
104
125
|
resolver()
|
|
105
126
|
}
|
|
106
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Renders the given Lit template into the specified root element, setting the host context.
|
|
131
|
+
*
|
|
132
|
+
* @param content - The Lit template or `nothing` to render.
|
|
133
|
+
* @param root - The root HTMLElement to render into.
|
|
134
|
+
* @param host - The host object for Lit's context (usually the Application or Activity).
|
|
135
|
+
*/
|
|
136
|
+
protected _render(content: TemplateResult | typeof nothing, root: HTMLElement, host: object): void {
|
|
137
|
+
const litPart = root as HTMLElement & { _$litPart$?: RootPart }
|
|
138
|
+
if (litPart._$litPart$ && litPart._$litPart$.options) {
|
|
139
|
+
litPart._$litPart$.options.host = host
|
|
140
|
+
}
|
|
141
|
+
render(content, root, { host })
|
|
142
|
+
this.resolveUpdatePromise()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Removes all child nodes from the given root element.
|
|
147
|
+
*
|
|
148
|
+
* Uses `replaceChildren()` for efficient DOM cleanup.
|
|
149
|
+
* @param root - The root HTMLElement to clear.
|
|
150
|
+
*/
|
|
151
|
+
protected _clearRoot(root: HTMLElement): void {
|
|
152
|
+
// Using replaceChildren() is a modern and clean way to clear a node's children.
|
|
153
|
+
root.replaceChildren()
|
|
154
|
+
}
|
|
107
155
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { css } from 'lit'
|
|
2
|
+
|
|
3
|
+
export default css`
|
|
4
|
+
:host {
|
|
5
|
+
display: block;
|
|
6
|
+
outline: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
nav {
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
list-style: none;
|
|
13
|
+
padding: 0;
|
|
14
|
+
margin: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:host([orientation='horizontal']) nav {
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:host([orientation='vertical']) nav {
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
}
|
|
24
|
+
`
|