@api-client/ui 0.5.32 → 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.
Files changed (41) hide show
  1. package/build/src/elements/navigation/internals/Navigation.d.ts +68 -0
  2. package/build/src/elements/navigation/internals/Navigation.d.ts.map +1 -0
  3. package/build/src/elements/navigation/internals/Navigation.js +205 -0
  4. package/build/src/elements/navigation/internals/Navigation.js.map +1 -0
  5. package/build/src/elements/navigation/internals/Navigation.styles.d.ts +3 -0
  6. package/build/src/elements/navigation/internals/Navigation.styles.d.ts.map +1 -0
  7. package/build/src/elements/navigation/internals/Navigation.styles.js +24 -0
  8. package/build/src/elements/navigation/internals/Navigation.styles.js.map +1 -0
  9. package/build/src/elements/navigation/internals/NavigationItem.d.ts +57 -2
  10. package/build/src/elements/navigation/internals/NavigationItem.d.ts.map +1 -1
  11. package/build/src/elements/navigation/internals/NavigationItem.js +73 -18
  12. package/build/src/elements/navigation/internals/NavigationItem.js.map +1 -1
  13. package/build/src/elements/navigation/ui-navigation.d.ts +11 -0
  14. package/build/src/elements/navigation/ui-navigation.d.ts.map +1 -0
  15. package/build/src/elements/navigation/ui-navigation.js +27 -0
  16. package/build/src/elements/navigation/ui-navigation.js.map +1 -0
  17. package/build/src/md/input/Input.d.ts +2 -0
  18. package/build/src/md/input/Input.d.ts.map +1 -1
  19. package/build/src/types/aria.d.ts +28 -0
  20. package/build/src/types/aria.d.ts.map +1 -0
  21. package/build/src/types/aria.js +2 -0
  22. package/build/src/types/aria.js.map +1 -0
  23. package/build/src/types/role.d.ts +1 -16
  24. package/build/src/types/role.d.ts.map +1 -1
  25. package/build/src/types/role.js.map +1 -1
  26. package/build/test/elements/navigation/Navigation.test.d.ts +3 -0
  27. package/build/test/elements/navigation/Navigation.test.d.ts.map +1 -0
  28. package/build/test/elements/navigation/Navigation.test.js +113 -0
  29. package/build/test/elements/navigation/Navigation.test.js.map +1 -0
  30. package/demo/elements/index.html +2 -0
  31. package/demo/elements/navigation/navigation.html +20 -0
  32. package/demo/elements/navigation/navigation.ts +45 -0
  33. package/package.json +1 -1
  34. package/src/elements/navigation/internals/Navigation.styles.ts +24 -0
  35. package/src/elements/navigation/internals/Navigation.ts +181 -0
  36. package/src/elements/navigation/internals/NavigationItem.ts +74 -5
  37. package/src/elements/navigation/ui-navigation.ts +15 -0
  38. package/src/types/aria.ts +141 -0
  39. package/src/types/role.ts +1 -129
  40. package/test/elements/navigation/Navigation.test.ts +120 -0
  41. package/tsconfig.json +1 -1
@@ -1,19 +1,84 @@
1
- import { html, TemplateResult } from 'lit'
1
+ import { html, TemplateResult, nothing } from 'lit'
2
2
  import { property, state } from 'lit/decorators.js'
3
3
  import { classMap } from 'lit/directives/class-map.js'
4
+ import { when } from 'lit/directives/when.js'
4
5
  import { UiElement } from '../../../md/UiElement.js'
6
+ import { isDisabled, setDisabled } from '../../../lib/disabled.js'
5
7
 
8
+ /**
9
+ * NavigationItem
10
+ *
11
+ * An element to be placed inside a navigation bar. This component is designed for use with navigation bars and supports
12
+ * icon-only, selected, and disabled states. It is built with accessibility and composability in mind.
13
+ *
14
+ * ## Accessibility
15
+ *
16
+ * - Uses a native `<button>` element for proper semantics and keyboard accessibility.
17
+ * - The icon is marked with `role="presentation"` to indicate it is decorative.
18
+ * - Focus ring and ripple effects are included for visual feedback.
19
+ *
20
+ * ## Usage Example
21
+ *
22
+ * ### Accessible Navigation Example
23
+ *
24
+ * ```html
25
+ * <nav aria-label="Main navigation">
26
+ * <ul style="display: flex; gap: 8px; list-style: none; padding: 0; margin: 0;">
27
+ * <li>
28
+ * <navigation-item selected aria-current="page">
29
+ * <span slot="icon" aria-hidden="true">🏠</span>
30
+ * Home
31
+ * </navigation-item>
32
+ * </li>
33
+ * <li>
34
+ * <navigation-item>
35
+ * <span slot="icon" aria-hidden="true">🔍</span>
36
+ * Search
37
+ * </navigation-item>
38
+ * </li>
39
+ * <li>
40
+ * <navigation-item>
41
+ * <span slot="icon" aria-hidden="true">📁</span>
42
+ * Files
43
+ * </navigation-item>
44
+ * </li>
45
+ * <li>
46
+ * <navigation-item iconOnly disabled aria-disabled="true" tabindex="-1">
47
+ * <span slot="icon" aria-hidden="true">⚙️</span>
48
+ * </navigation-item>
49
+ * </li>
50
+ * </ul>
51
+ * </nav>
52
+ * ```
53
+ *
54
+ * This example demonstrates a horizontal navigation bar using semantic HTML. Each `navigation-item`
55
+ * is placed inside a list item for proper structure. The `aria-current="page"` attribute marks the current
56
+ * page, and `aria-disabled="true"` with `tabindex="-1"` ensures the disabled item is not focusable.
57
+ * Icons use `aria-hidden="true"` for accessibility.
58
+ *
59
+ * @slot icon - A slot for the icon element
60
+ * @slot - The default slot for the label
61
+ */
6
62
  export default class NavigationItem extends UiElement {
7
63
  static override shadowRootOptions: ShadowRootInit = {
8
64
  mode: 'open',
9
65
  delegatesFocus: true,
10
66
  }
11
67
 
68
+ get disabled(): boolean {
69
+ return isDisabled(this)
70
+ }
71
+
12
72
  /**
13
- * When set, the button is a disabled state.
73
+ * When set, the navigation item is in a disabled state.
14
74
  * @attribute
15
75
  */
16
- @property({ reflect: true, type: Boolean }) accessor disabled = false
76
+ @property({ reflect: true, type: Boolean })
77
+ set disabled(value: boolean) {
78
+ const old = isDisabled(this)
79
+ setDisabled(this, value)
80
+ this.requestUpdate('disabled', old)
81
+ }
17
82
 
18
83
  /**
19
84
  * Whether the navigation item is selected.
@@ -49,10 +114,14 @@ export default class NavigationItem extends UiElement {
49
114
  'icon-only': this.iconOnly,
50
115
  })
51
116
  return html`
52
- <button class="${containerClasses}" id="button">
117
+ <button class="${containerClasses}" id="button" ?disabled="${this.disabled}" aria-disabled="${this.disabled}">
53
118
  <md-focus-ring part="focus-ring" for="button"></md-focus-ring>
54
119
  <md-ripple></md-ripple>
55
- ${this.renderIcon()}${this.iconOnly ? '' : html`<slot></slot>`}
120
+ ${this.renderIcon()}${when(
121
+ this.iconOnly,
122
+ () => nothing,
123
+ () => html`<slot></slot>`
124
+ )}
56
125
  </button>
57
126
  `
58
127
  }
@@ -0,0 +1,15 @@
1
+ import type { CSSResultOrNative } from 'lit'
2
+ import { customElement } from 'lit/decorators.js'
3
+ import Element from './internals/Navigation.js'
4
+ import styles from './internals/Navigation.styles.js'
5
+
6
+ @customElement('ui-navigation')
7
+ export class NavigationListElement extends Element {
8
+ static override styles: CSSResultOrNative[] = [styles]
9
+ }
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'ui-navigation': NavigationListElement
14
+ }
15
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * - `page` - Represents the current page within a set of pages.
3
+ * - `step` - Represents the current step within a process.
4
+ * - `location` - Represents the current location, for example the current page in a breadcrumbs hierarchy.
5
+ * - `date` - Represents the current date within a collection of dates.
6
+ * - `time` - Represents the current time within a set of times.
7
+ * - `true` - Represents the current item within a set.
8
+ * - `false` - Does not represent the current item within a set.
9
+ * - `''` - No current state.
10
+ */
11
+ export type AriaCurrent = 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | ''
12
+
13
+ /**
14
+ * Valid values for `aria-expanded`.
15
+ */
16
+ export type ARIAAutoComplete = 'none' | 'inline' | 'list' | 'both'
17
+
18
+ /**
19
+ * Valid values for `aria-expanded`.
20
+ */
21
+ export type ARIAExpanded = 'true' | 'false'
22
+
23
+ /**
24
+ * Valid values for `aria-haspopup`.
25
+ */
26
+ export type ARIAHasPopup = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
27
+
28
+ /**
29
+ * Valid values for `role`.
30
+ */
31
+ export type ARIARole =
32
+ | 'alert'
33
+ | 'alertdialog'
34
+ | 'button'
35
+ | 'checkbox'
36
+ | 'dialog'
37
+ | 'gridcell'
38
+ | 'link'
39
+ | 'log'
40
+ | 'marquee'
41
+ | 'menuitem'
42
+ | 'menuitemcheckbox'
43
+ | 'menuitemradio'
44
+ | 'option'
45
+ | 'progressbar'
46
+ | 'radio'
47
+ | 'scrollbar'
48
+ | 'searchbox'
49
+ | 'slider'
50
+ | 'spinbutton'
51
+ | 'status'
52
+ | 'switch'
53
+ | 'tab'
54
+ | 'tabpanel'
55
+ | 'textbox'
56
+ | 'timer'
57
+ | 'tooltip'
58
+ | 'treeitem'
59
+ | 'combobox'
60
+ | 'grid'
61
+ | 'listbox'
62
+ | 'menu'
63
+ | 'menubar'
64
+ | 'radiogroup'
65
+ | 'tablist'
66
+ | 'tree'
67
+ | 'treegrid'
68
+ | 'application'
69
+ | 'article'
70
+ | 'cell'
71
+ | 'columnheader'
72
+ | 'definition'
73
+ | 'directory'
74
+ | 'document'
75
+ | 'feed'
76
+ | 'figure'
77
+ | 'group'
78
+ | 'heading'
79
+ | 'img'
80
+ | 'list'
81
+ | 'listitem'
82
+ | 'math'
83
+ | 'none'
84
+ | 'note'
85
+ | 'presentation'
86
+ | 'region'
87
+ | 'row'
88
+ | 'rowgroup'
89
+ | 'rowheader'
90
+ | 'separator'
91
+ | 'table'
92
+ | 'term'
93
+ | 'text'
94
+ | 'toolbar'
95
+ | 'banner'
96
+ | 'complementary'
97
+ | 'contentinfo'
98
+ | 'form'
99
+ | 'main'
100
+ | 'navigation'
101
+ | 'region'
102
+ | 'search'
103
+ | 'doc-abstract'
104
+ | 'doc-acknowledgments'
105
+ | 'doc-afterword'
106
+ | 'doc-appendix'
107
+ | 'doc-backlink'
108
+ | 'doc-biblioentry'
109
+ | 'doc-bibliography'
110
+ | 'doc-biblioref'
111
+ | 'doc-chapter'
112
+ | 'doc-colophon'
113
+ | 'doc-conclusion'
114
+ | 'doc-cover'
115
+ | 'doc-credit'
116
+ | 'doc-credits'
117
+ | 'doc-dedication'
118
+ | 'doc-endnote'
119
+ | 'doc-endnotes'
120
+ | 'doc-epigraph'
121
+ | 'doc-epilogue'
122
+ | 'doc-errata'
123
+ | 'doc-example'
124
+ | 'doc-footnote'
125
+ | 'doc-foreword'
126
+ | 'doc-glossary'
127
+ | 'doc-glossref'
128
+ | 'doc-index'
129
+ | 'doc-introduction'
130
+ | 'doc-noteref'
131
+ | 'doc-notice'
132
+ | 'doc-pagebreak'
133
+ | 'doc-pagelist'
134
+ | 'doc-part'
135
+ | 'doc-preface'
136
+ | 'doc-prologue'
137
+ | 'doc-pullquote'
138
+ | 'doc-qna'
139
+ | 'doc-subtitle'
140
+ | 'doc-tip'
141
+ | 'doc-toc'
package/src/types/role.ts CHANGED
@@ -1,129 +1 @@
1
- /**
2
- * Valid values for `aria-expanded`.
3
- */
4
- export type ARIAAutoComplete = 'none' | 'inline' | 'list' | 'both'
5
-
6
- /**
7
- * Valid values for `aria-expanded`.
8
- */
9
- export type ARIAExpanded = 'true' | 'false'
10
-
11
- /**
12
- * Valid values for `aria-haspopup`.
13
- */
14
- export type ARIAHasPopup = 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'
15
-
16
- /**
17
- * Valid values for `role`.
18
- */
19
- export type ARIARole =
20
- | 'alert'
21
- | 'alertdialog'
22
- | 'button'
23
- | 'checkbox'
24
- | 'dialog'
25
- | 'gridcell'
26
- | 'link'
27
- | 'log'
28
- | 'marquee'
29
- | 'menuitem'
30
- | 'menuitemcheckbox'
31
- | 'menuitemradio'
32
- | 'option'
33
- | 'progressbar'
34
- | 'radio'
35
- | 'scrollbar'
36
- | 'searchbox'
37
- | 'slider'
38
- | 'spinbutton'
39
- | 'status'
40
- | 'switch'
41
- | 'tab'
42
- | 'tabpanel'
43
- | 'textbox'
44
- | 'timer'
45
- | 'tooltip'
46
- | 'treeitem'
47
- | 'combobox'
48
- | 'grid'
49
- | 'listbox'
50
- | 'menu'
51
- | 'menubar'
52
- | 'radiogroup'
53
- | 'tablist'
54
- | 'tree'
55
- | 'treegrid'
56
- | 'application'
57
- | 'article'
58
- | 'cell'
59
- | 'columnheader'
60
- | 'definition'
61
- | 'directory'
62
- | 'document'
63
- | 'feed'
64
- | 'figure'
65
- | 'group'
66
- | 'heading'
67
- | 'img'
68
- | 'list'
69
- | 'listitem'
70
- | 'math'
71
- | 'none'
72
- | 'note'
73
- | 'presentation'
74
- | 'region'
75
- | 'row'
76
- | 'rowgroup'
77
- | 'rowheader'
78
- | 'separator'
79
- | 'table'
80
- | 'term'
81
- | 'text'
82
- | 'toolbar'
83
- | 'banner'
84
- | 'complementary'
85
- | 'contentinfo'
86
- | 'form'
87
- | 'main'
88
- | 'navigation'
89
- | 'region'
90
- | 'search'
91
- | 'doc-abstract'
92
- | 'doc-acknowledgments'
93
- | 'doc-afterword'
94
- | 'doc-appendix'
95
- | 'doc-backlink'
96
- | 'doc-biblioentry'
97
- | 'doc-bibliography'
98
- | 'doc-biblioref'
99
- | 'doc-chapter'
100
- | 'doc-colophon'
101
- | 'doc-conclusion'
102
- | 'doc-cover'
103
- | 'doc-credit'
104
- | 'doc-credits'
105
- | 'doc-dedication'
106
- | 'doc-endnote'
107
- | 'doc-endnotes'
108
- | 'doc-epigraph'
109
- | 'doc-epilogue'
110
- | 'doc-errata'
111
- | 'doc-example'
112
- | 'doc-footnote'
113
- | 'doc-foreword'
114
- | 'doc-glossary'
115
- | 'doc-glossref'
116
- | 'doc-index'
117
- | 'doc-introduction'
118
- | 'doc-noteref'
119
- | 'doc-notice'
120
- | 'doc-pagebreak'
121
- | 'doc-pagelist'
122
- | 'doc-part'
123
- | 'doc-preface'
124
- | 'doc-prologue'
125
- | 'doc-pullquote'
126
- | 'doc-qna'
127
- | 'doc-subtitle'
128
- | 'doc-tip'
129
- | 'doc-toc'
1
+ export { ARIAAutoComplete, ARIAExpanded, ARIAHasPopup, ARIARole } from './aria.js'
@@ -0,0 +1,120 @@
1
+ import { html, fixture, assert, oneEvent, aTimeout } from '@open-wc/testing'
2
+ import type Navigation from '../../../src/elements/navigation/internals/Navigation.js'
3
+ import '../../../src/elements/navigation/ui-navigation.js'
4
+ import '../../../src/elements/navigation/ui-navigation-item.js'
5
+
6
+ describe('Navigation', () => {
7
+ it('renders with slot content', async () => {
8
+ const el = await fixture<Navigation>(html`
9
+ <ui-navigation aria-label="Main navigation">
10
+ <ui-navigation-item selected aria-current="page">Home</ui-navigation-item>
11
+ <ui-navigation-item>Search</ui-navigation-item>
12
+ <ui-navigation-item>Files</ui-navigation-item>
13
+ </ui-navigation>
14
+ `)
15
+ const nav = el.shadowRoot!.querySelector('nav')
16
+ assert.ok(nav, 'nav element is rendered')
17
+ assert.equal(nav?.getAttribute('aria-label'), 'Main navigation')
18
+ const items = el._items
19
+ assert.equal(items.length, 3)
20
+ assert.isTrue(items[0].selected)
21
+ assert.equal(items[0].getAttribute('aria-current'), 'page')
22
+ })
23
+
24
+ it('sets correct tabindex for items', async () => {
25
+ const el = await fixture<Navigation>(html`
26
+ <ui-navigation>
27
+ <ui-navigation-item selected>Home</ui-navigation-item>
28
+ <ui-navigation-item>Search</ui-navigation-item>
29
+ </ui-navigation>
30
+ `)
31
+ const items = el._items
32
+ assert.equal(items[0].getAttribute('tabindex'), '0')
33
+ assert.equal(items[1].getAttribute('tabindex'), '-1')
34
+ })
35
+
36
+ it('selects item on click and fires select event', async () => {
37
+ const el = await fixture<Navigation>(html`
38
+ <ui-navigation>
39
+ <ui-navigation-item>Home</ui-navigation-item>
40
+ <ui-navigation-item>Search</ui-navigation-item>
41
+ </ui-navigation>
42
+ `)
43
+ const items = el._items
44
+ setTimeout(() => items[1].click())
45
+ const ev = await oneEvent(el, 'select')
46
+ assert.equal(ev.detail.item, items[1])
47
+ assert.isTrue(items[1].selected)
48
+ assert.equal(items[1].getAttribute('tabindex'), '0')
49
+ assert.isFalse(items[0].selected)
50
+ assert.equal(items[0].getAttribute('tabindex'), '-1')
51
+ })
52
+
53
+ it('handles keyboard navigation (vertical)', async () => {
54
+ const el = await fixture<Navigation>(html`
55
+ <ui-navigation>
56
+ <ui-navigation-item>Home</ui-navigation-item>
57
+ <ui-navigation-item>Search</ui-navigation-item>
58
+ <ui-navigation-item>Files</ui-navigation-item>
59
+ </ui-navigation>
60
+ `)
61
+ const items = el._items
62
+ items[0].focus()
63
+ // ArrowDown moves to next
64
+ items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
65
+ await aTimeout(0)
66
+ assert.dom.equal(document.activeElement, items[1])
67
+ // ArrowUp wraps to last
68
+ items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }))
69
+ await aTimeout(0)
70
+ assert.dom.equal(document.activeElement, items[0])
71
+ })
72
+
73
+ it('handles keyboard navigation (horizontal)', async () => {
74
+ const el = await fixture<Navigation>(html`
75
+ <ui-navigation orientation="horizontal">
76
+ <ui-navigation-item>Home</ui-navigation-item>
77
+ <ui-navigation-item>Search</ui-navigation-item>
78
+ </ui-navigation>
79
+ `)
80
+ const items = el._items
81
+ items[0].focus()
82
+ items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
83
+ await aTimeout(0)
84
+ assert.dom.equal(document.activeElement, items[1])
85
+ items[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
86
+ await aTimeout(0)
87
+ assert.dom.equal(document.activeElement, items[0])
88
+ })
89
+
90
+ it('skips disabled items in navigation', async () => {
91
+ const el = await fixture<Navigation>(html`
92
+ <ui-navigation>
93
+ <ui-navigation-item>Home</ui-navigation-item>
94
+ <ui-navigation-item disabled>Search</ui-navigation-item>
95
+ <ui-navigation-item>Files</ui-navigation-item>
96
+ </ui-navigation>
97
+ `)
98
+ const items = el._items
99
+ items[0].focus()
100
+ items[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
101
+ await aTimeout(0)
102
+ // Should skip disabled and go to Files
103
+ assert.dom.equal(document.activeElement, items[2])
104
+ })
105
+
106
+ it('sets aria-current on selected item if current is set', async () => {
107
+ const el = await fixture<Navigation>(html`
108
+ <ui-navigation current="page">
109
+ <ui-navigation-item>Home</ui-navigation-item>
110
+ <ui-navigation-item>Search</ui-navigation-item>
111
+ </ui-navigation>
112
+ `)
113
+ const items = el._items
114
+ // Simulate user selection by clicking the item (public API)
115
+ items[1].click()
116
+ await aTimeout(0)
117
+ assert.equal(items[1].getAttribute('aria-current'), 'page')
118
+ assert.isFalse(items[0].hasAttribute('aria-current'))
119
+ })
120
+ })
package/tsconfig.json CHANGED
@@ -36,6 +36,6 @@
36
36
  }
37
37
  ]
38
38
  },
39
- "include": ["src/**/*.ts"],
39
+ "include": ["src/**/*.ts", "test/elements/navigation/Navigation.test.ts"],
40
40
  "exclude": ["demo/apis/*", "demo/models/*", "node_modules", ".wireit", ".tmp", ".rollup.cache"]
41
41
  }