@api-client/ui 0.5.5 → 0.5.7

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 (114) hide show
  1. package/.cursor/rules/html-and-css-best-practices.mdc +63 -0
  2. package/.cursor/rules/lit-best-practices.mdc +78 -0
  3. package/.github/instructions/html-and-css-best-practices.instructions.md +70 -0
  4. package/.github/instructions/lit-best-practices.instructions.md +86 -0
  5. package/build/src/elements/currency/currency-picker.d.ts +10 -0
  6. package/build/src/elements/currency/currency-picker.d.ts.map +1 -0
  7. package/build/src/elements/currency/currency-picker.js +27 -0
  8. package/build/src/elements/currency/currency-picker.js.map +1 -0
  9. package/build/src/elements/currency/internals/Picker.d.ts +311 -0
  10. package/build/src/elements/currency/internals/Picker.d.ts.map +1 -0
  11. package/build/src/elements/currency/internals/Picker.js +857 -0
  12. package/build/src/elements/currency/internals/Picker.js.map +1 -0
  13. package/build/src/elements/currency/internals/Picker.styles.d.ts +3 -0
  14. package/build/src/elements/currency/internals/Picker.styles.d.ts.map +1 -0
  15. package/build/src/elements/currency/internals/Picker.styles.js +58 -0
  16. package/build/src/elements/currency/internals/Picker.styles.js.map +1 -0
  17. package/build/src/elements/highlight/MarkdownStyles.d.ts.map +1 -1
  18. package/build/src/elements/highlight/MarkdownStyles.js +0 -13
  19. package/build/src/elements/highlight/MarkdownStyles.js.map +1 -1
  20. package/build/src/elements/http/BodyEditor.d.ts +0 -13
  21. package/build/src/elements/http/BodyEditor.d.ts.map +1 -1
  22. package/build/src/elements/http/BodyEditor.js +0 -13
  23. package/build/src/elements/http/BodyEditor.js.map +1 -1
  24. package/build/src/elements/http/BodyTextEditor.d.ts +0 -13
  25. package/build/src/elements/http/BodyTextEditor.d.ts.map +1 -1
  26. package/build/src/elements/http/BodyTextEditor.js +0 -13
  27. package/build/src/elements/http/BodyTextEditor.js.map +1 -1
  28. package/build/src/elements/http/BodyUrlEncodedEditor.d.ts +0 -13
  29. package/build/src/elements/http/BodyUrlEncodedEditor.d.ts.map +1 -1
  30. package/build/src/elements/http/BodyUrlEncodedEditor.js +0 -13
  31. package/build/src/elements/http/BodyUrlEncodedEditor.js.map +1 -1
  32. package/build/src/elements/http/UrlInput.d.ts +0 -13
  33. package/build/src/elements/http/UrlInput.d.ts.map +1 -1
  34. package/build/src/elements/http/UrlInput.js +0 -13
  35. package/build/src/elements/http/UrlInput.js.map +1 -1
  36. package/build/src/index.d.ts +2 -0
  37. package/build/src/index.d.ts.map +1 -1
  38. package/build/src/index.js +2 -0
  39. package/build/src/index.js.map +1 -1
  40. package/build/src/md/button/internals/base.d.ts +1 -0
  41. package/build/src/md/button/internals/base.d.ts.map +1 -1
  42. package/build/src/md/button/internals/base.js +7 -0
  43. package/build/src/md/button/internals/base.js.map +1 -1
  44. package/build/src/md/button/internals/button.styles.js +1 -1
  45. package/build/src/md/button/internals/button.styles.js.map +1 -1
  46. package/build/src/md/date/internals/DateTime.d.ts +0 -13
  47. package/build/src/md/date/internals/DateTime.d.ts.map +1 -1
  48. package/build/src/md/date/internals/DateTime.js +0 -13
  49. package/build/src/md/date/internals/DateTime.js.map +1 -1
  50. package/build/src/md/date-picker/index.d.ts +13 -0
  51. package/build/src/md/date-picker/index.d.ts.map +1 -0
  52. package/build/src/md/date-picker/index.js +13 -0
  53. package/build/src/md/date-picker/index.js.map +1 -0
  54. package/build/src/md/date-picker/internals/DatePicker.styles.d.ts +4 -0
  55. package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -0
  56. package/build/src/md/date-picker/internals/DatePicker.styles.js +409 -0
  57. package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -0
  58. package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +272 -0
  59. package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -0
  60. package/build/src/md/date-picker/internals/DatePickerCalendar.js +1062 -0
  61. package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -0
  62. package/build/src/md/date-picker/internals/DatePickerUtils.d.ts +93 -0
  63. package/build/src/md/date-picker/internals/DatePickerUtils.d.ts.map +1 -0
  64. package/build/src/md/date-picker/internals/DatePickerUtils.js +221 -0
  65. package/build/src/md/date-picker/internals/DatePickerUtils.js.map +1 -0
  66. package/build/src/md/date-picker/ui-date-picker-input.d.ts +160 -0
  67. package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -0
  68. package/build/src/md/date-picker/ui-date-picker-input.js +464 -0
  69. package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -0
  70. package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +178 -0
  71. package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -0
  72. package/build/src/md/date-picker/ui-date-picker-modal-input.js +538 -0
  73. package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -0
  74. package/build/src/md/date-picker/ui-date-picker-modal.d.ts +156 -0
  75. package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -0
  76. package/build/src/md/date-picker/ui-date-picker-modal.js +423 -0
  77. package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -0
  78. package/build/src/md/dialog/internals/Dialog.styles.d.ts.map +1 -1
  79. package/build/src/md/dialog/internals/Dialog.styles.js +1 -0
  80. package/build/src/md/dialog/internals/Dialog.styles.js.map +1 -1
  81. package/demo/elements/currency/index.html +91 -0
  82. package/demo/elements/currency/index.ts +272 -0
  83. package/demo/elements/har/har2.json +1 -1
  84. package/demo/elements/index.html +3 -0
  85. package/demo/md/date-picker/date-picker.ts +336 -0
  86. package/demo/md/date-picker/index.html +171 -0
  87. package/demo/md/index.html +2 -0
  88. package/package.json +1 -1
  89. package/src/elements/currency/currency-picker.ts +14 -0
  90. package/src/elements/currency/internals/Picker.styles.ts +58 -0
  91. package/src/elements/currency/internals/Picker.ts +846 -0
  92. package/src/elements/highlight/MarkdownStyles.ts +0 -13
  93. package/src/elements/http/BodyEditor.ts +0 -13
  94. package/src/elements/http/BodyTextEditor.ts +0 -13
  95. package/src/elements/http/BodyUrlEncodedEditor.ts +0 -13
  96. package/src/elements/http/UrlInput.ts +0 -13
  97. package/src/index.ts +17 -0
  98. package/src/md/button/internals/base.ts +7 -0
  99. package/src/md/button/internals/button.styles.ts +1 -1
  100. package/src/md/date/internals/DateTime.ts +0 -14
  101. package/src/md/date-picker/README.md +184 -0
  102. package/src/md/date-picker/index.ts +17 -0
  103. package/src/md/date-picker/internals/DatePicker.styles.ts +411 -0
  104. package/src/md/date-picker/internals/DatePickerCalendar.ts +1031 -0
  105. package/src/md/date-picker/internals/DatePickerUtils.ts +288 -0
  106. package/src/md/date-picker/ui-date-picker-input.ts +333 -0
  107. package/src/md/date-picker/ui-date-picker-modal-input.ts +440 -0
  108. package/src/md/date-picker/ui-date-picker-modal.ts +346 -0
  109. package/src/md/dialog/internals/Dialog.styles.ts +1 -0
  110. package/test/README.md +3 -2
  111. package/test/elements/currency/CurrencyPicker.accessibility.test.ts +328 -0
  112. package/test/elements/currency/CurrencyPicker.core.test.ts +318 -0
  113. package/test/elements/currency/CurrencyPicker.integration.test.ts +482 -0
  114. package/test/elements/currency/CurrencyPicker.test.ts +486 -0
@@ -0,0 +1,346 @@
1
+ import { LitElement, html, TemplateResult } from 'lit'
2
+ import { customElement, property, state } from 'lit/decorators.js'
3
+ import { modalStyles } from './internals/DatePicker.styles.js'
4
+ import { DateRange, formatDate } from './internals/DatePickerUtils.js'
5
+ import type { DateRangeSelectEvent } from './internals/DatePickerCalendar.js'
6
+ import './internals/DatePickerCalendar.js'
7
+ import '../../md/dialog/ui-dialog.js'
8
+ import '../../md/button/ui-button.js'
9
+ import '../../md/icon-button/ui-icon-button.js'
10
+ import '../../md/icons/ui-icon.js'
11
+
12
+ export interface ModalDatePickerChangeEvent {
13
+ range: DateRange
14
+ formattedRange: {
15
+ start: string | null
16
+ end: string | null
17
+ }
18
+ }
19
+
20
+ /**
21
+ * A modal date picker for selecting date ranges.
22
+ * Extends full-screen and is ideal for date range selection like flight bookings.
23
+ *
24
+ * ## Features
25
+ * - Full-screen modal interface
26
+ * - Date range selection with visual feedback
27
+ * - Calendar and input mode toggle
28
+ * - Min/max date constraints
29
+ * - Disabled dates support
30
+ * - Accessible design with proper ARIA attributes and semantic HTML
31
+ * - Real-time date range validation
32
+ *
33
+ * ## Events
34
+ *
35
+ * ### `date-range-change`
36
+ * Fired when a date range is selected or changed.
37
+ *
38
+ * **Detail:**
39
+ * ```typescript
40
+ * {
41
+ * range: DateRange,
42
+ * formattedRange: {
43
+ * start: string | null,
44
+ * end: string | null
45
+ * }
46
+ * }
47
+ * ```
48
+ *
49
+ * ### `close`
50
+ * Fired when the modal is closed.
51
+ *
52
+ * **Detail:**
53
+ * ```typescript
54
+ * {
55
+ * confirmed: boolean,
56
+ * range: DateRange | null
57
+ * }
58
+ * ```
59
+ *
60
+ * ## Usage
61
+ *
62
+ * ### Basic usage
63
+ * ```html
64
+ * <ui-date-picker-modal
65
+ * .open=${true}
66
+ * .selectedRange=${{ start: new Date(), end: null }}
67
+ * @date-range-change=${this.handleRangeChange}
68
+ * @close=${this.handleClose}
69
+ * ></ui-date-picker-modal>
70
+ * ```
71
+ *
72
+ * ### Custom labels and constraints
73
+ * ```html
74
+ * <ui-date-picker-modal
75
+ * title="Select travel dates"
76
+ * startLabel="Check-in"
77
+ * endLabel="Check-out"
78
+ * .minDate=${new Date()}
79
+ * .maxDate=${new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)}
80
+ * .disabledDates=${[new Date('2024-12-25')]}
81
+ * ></ui-date-picker-modal>
82
+ * ```
83
+ *
84
+ * ### With mode toggle disabled
85
+ * ```html
86
+ * <ui-date-picker-modal
87
+ * .showModeToggle=${false}
88
+ * ></ui-date-picker-modal>
89
+ * ```
90
+ */
91
+ @customElement('ui-date-picker-modal')
92
+ export class UiDatePickerModal extends LitElement {
93
+ static override styles = modalStyles
94
+
95
+ /**
96
+ * Whether the modal is open
97
+ */
98
+ @property({ type: Boolean }) accessor open = false
99
+
100
+ /**
101
+ * The modal title
102
+ */
103
+ @property({ type: String }) override accessor title = 'Select dates'
104
+
105
+ /**
106
+ * Label for the start date
107
+ */
108
+ @property({ type: String }) accessor startLabel = 'Start date'
109
+
110
+ /**
111
+ * Label for the end date
112
+ */
113
+ @property({ type: String }) accessor endLabel = 'End date'
114
+
115
+ /**
116
+ * The selected date range
117
+ */
118
+ @property({ type: Object }) accessor selectedRange: DateRange | null = null
119
+
120
+ /**
121
+ * Minimum selectable date
122
+ */
123
+ @property({ type: Object }) accessor minDate: Date | undefined = undefined
124
+
125
+ /**
126
+ * Maximum selectable date
127
+ */
128
+ @property({ type: Object }) accessor maxDate: Date | undefined = undefined
129
+
130
+ /**
131
+ * Array of disabled dates
132
+ */
133
+ @property({ type: Array }) accessor disabledDates: Date[] | undefined = undefined
134
+
135
+ /**
136
+ * Locale for date formatting
137
+ */
138
+ @property({ type: String }) accessor locale: string | undefined = undefined
139
+
140
+ /**
141
+ * Whether to show edit/calendar toggle button
142
+ */
143
+ @property({ type: Boolean }) accessor showModeToggle = true
144
+
145
+ @state() private accessor isInputMode = false
146
+
147
+ constructor() {
148
+ super()
149
+ // Initialize boolean properties to false as per Lit best practices
150
+ this.open = false
151
+ this.showModeToggle = true
152
+ }
153
+
154
+ private handleRangeSelect(event: CustomEvent<DateRangeSelectEvent>): void {
155
+ this.selectedRange = event.detail.range
156
+ this.dispatchChangeEvent()
157
+ }
158
+
159
+ private handleCancel(): void {
160
+ this.dispatchCloseEvent(false)
161
+ }
162
+
163
+ private handleConfirm(): void {
164
+ this.dispatchCloseEvent(true)
165
+ }
166
+
167
+ private handleModeToggle(): void {
168
+ this.isInputMode = !this.isInputMode
169
+ }
170
+
171
+ private dispatchChangeEvent(): void {
172
+ if (!this.selectedRange) return
173
+
174
+ const event: ModalDatePickerChangeEvent = {
175
+ range: this.selectedRange,
176
+ formattedRange: {
177
+ start: this.selectedRange.start ? formatDate(this.selectedRange.start, this.locale) : null,
178
+ end: this.selectedRange.end ? formatDate(this.selectedRange.end, this.locale) : null,
179
+ },
180
+ }
181
+
182
+ this.dispatchEvent(
183
+ new CustomEvent('date-range-change', {
184
+ detail: event,
185
+ bubbles: true,
186
+ composed: true,
187
+ })
188
+ )
189
+ }
190
+
191
+ private dispatchCloseEvent(confirmed: boolean): void {
192
+ this.dispatchEvent(
193
+ new CustomEvent('close', {
194
+ detail: {
195
+ confirmed,
196
+ range: confirmed ? this.selectedRange : null,
197
+ },
198
+ bubbles: true,
199
+ composed: true,
200
+ })
201
+ )
202
+ }
203
+
204
+ private renderHeader(): TemplateResult {
205
+ return html`
206
+ <header class="modal-header">
207
+ <h2 id="modal-title" class="modal-title">${this.title}</h2>
208
+ <div class="header-actions">
209
+ ${this.showModeToggle
210
+ ? html`
211
+ <ui-icon-button
212
+ @click=${this.handleModeToggle}
213
+ aria-label=${this.isInputMode ? 'Show calendar' : 'Show date input'}
214
+ title=${this.isInputMode ? 'Show calendar' : 'Show date input'}
215
+ >
216
+ <ui-icon icon=${this.isInputMode ? 'calendarToday' : 'edit'}></ui-icon>
217
+ </ui-icon-button>
218
+ `
219
+ : ''}
220
+ <ui-icon-button @click=${this.handleCancel} aria-label="Close" title="Close">
221
+ <ui-icon icon="close"></ui-icon>
222
+ </ui-icon-button>
223
+ </div>
224
+ </header>
225
+ `
226
+ }
227
+
228
+ private renderDateDisplay(): TemplateResult {
229
+ const startDate = this.selectedRange?.start
230
+ const endDate = this.selectedRange?.end
231
+
232
+ return html`
233
+ <section class="date-range-display" role="status" aria-live="polite" aria-label="Selected date range">
234
+ <div class="date-display">
235
+ <div class="date-label" id="start-label">${this.startLabel}</div>
236
+ <div class="date-value" aria-labelledby="start-label" aria-describedby="start-description">
237
+ ${startDate ? formatDate(startDate, this.locale) : 'Select date'}
238
+ </div>
239
+ <div id="start-description" class="visually-hidden">
240
+ ${startDate ? `Start date selected: ${formatDate(startDate, this.locale)}` : 'No start date selected'}
241
+ </div>
242
+ </div>
243
+ <div class="date-separator" aria-hidden="true">—</div>
244
+ <div class="date-display">
245
+ <div class="date-label" id="end-label">${this.endLabel}</div>
246
+ <div class="date-value" aria-labelledby="end-label" aria-describedby="end-description">
247
+ ${endDate ? formatDate(endDate, this.locale) : 'Select date'}
248
+ </div>
249
+ <div id="end-description" class="visually-hidden">
250
+ ${endDate ? `End date selected: ${formatDate(endDate, this.locale)}` : 'No end date selected'}
251
+ </div>
252
+ </div>
253
+ </section>
254
+ `
255
+ }
256
+
257
+ private renderCalendar(): TemplateResult {
258
+ const currentDate = this.selectedRange?.start || new Date()
259
+
260
+ return html`
261
+ <ui-date-picker-calendar
262
+ .year=${currentDate.getFullYear()}
263
+ .month=${currentDate.getMonth()}
264
+ .rangeStart=${this.selectedRange?.start || null}
265
+ .rangeEnd=${this.selectedRange?.end || null}
266
+ .rangeSelection=${true}
267
+ .minDate=${this.minDate}
268
+ .maxDate=${this.maxDate}
269
+ .disabledDates=${this.disabledDates}
270
+ .locale=${this.locale}
271
+ @date-range-select=${this.handleRangeSelect}
272
+ ></ui-date-picker-calendar>
273
+ `
274
+ }
275
+
276
+ private renderInputMode(): TemplateResult {
277
+ return html`
278
+ <section class="input-mode-placeholder" role="status" aria-live="polite">
279
+ <ui-icon icon="edit" class="input-mode-icon" aria-hidden="true"></ui-icon>
280
+ <h3>Manual date input mode</h3>
281
+ <p>This feature can be enhanced with date input fields for direct text entry</p>
282
+ <p><em>Use the calendar toggle button to return to calendar view</em></p>
283
+ </section>
284
+ `
285
+ }
286
+
287
+ private renderContent(): TemplateResult {
288
+ return html`
289
+ <main id="modal-content" class="modal-content">
290
+ ${this.renderDateDisplay()} ${this.isInputMode ? this.renderInputMode() : this.renderCalendar()}
291
+ </main>
292
+ `
293
+ }
294
+
295
+ private renderActions(): TemplateResult {
296
+ const hasValidRange = this.selectedRange?.start && this.selectedRange?.end
297
+
298
+ return html`
299
+ <footer class="modal-actions" role="group" aria-label="Date picker actions">
300
+ <ui-button color="text" @click=${this.handleCancel} aria-describedby="cancel-description"> Cancel </ui-button>
301
+ <ui-button
302
+ color="filled"
303
+ @click=${this.handleConfirm}
304
+ .disabled=${!hasValidRange}
305
+ aria-describedby="confirm-description"
306
+ >
307
+ Confirm
308
+ </ui-button>
309
+ <div id="cancel-description" class="visually-hidden">Close the date picker without saving changes</div>
310
+ <div id="confirm-description" class="visually-hidden">
311
+ ${hasValidRange
312
+ ? 'Save the selected date range and close the picker'
313
+ : 'Select both start and end dates to confirm the selection'}
314
+ </div>
315
+ </footer>
316
+ `
317
+ }
318
+
319
+ override render(): TemplateResult {
320
+ return html`
321
+ <ui-dialog
322
+ .open=${this.open}
323
+ @close=${this.handleCancel}
324
+ role="dialog"
325
+ aria-labelledby="modal-title"
326
+ aria-describedby="modal-content"
327
+ >
328
+ ${this.renderHeader()} ${this.renderContent()} ${this.renderActions()}
329
+ </ui-dialog>
330
+ `
331
+ }
332
+ }
333
+
334
+ declare global {
335
+ interface HTMLElementTagNameMap {
336
+ 'ui-date-picker-modal': UiDatePickerModal
337
+ }
338
+
339
+ interface HTMLElementEventMap {
340
+ 'date-range-change': CustomEvent<ModalDatePickerChangeEvent>
341
+ 'close': CustomEvent<{
342
+ confirmed: boolean
343
+ range: DateRange | null
344
+ }>
345
+ }
346
+ }
@@ -35,6 +35,7 @@ export default css`
35
35
  left: 50%;
36
36
  transform: translate(-50%, -50%);
37
37
  margin: 0;
38
+ z-index: 1000;
38
39
  }
39
40
 
40
41
  dialog.non-modal:open {
package/test/README.md CHANGED
@@ -47,9 +47,10 @@ npm run test:watch
47
47
 
48
48
  # Run tests with coverage
49
49
  npm run test:coverage
50
- ```
51
50
 
52
- > ⚠️ **Note**: This project uses Web Test Runner, not Jest. Test filtering options like `--testPathPattern` from Jest are not available. To run specific tests, you can temporarily modify the `files` pattern in `web-test-runner.config.js` or use pattern matching if supported by your Web Test Runner version. For most cases, running all tests is recommended as the test suite is optimized for speed.
51
+ # Run tests for a single file
52
+ npm test -- --files=".tmp/test/**/CurrencyPicker.integration.test.js"
53
+ ```
53
54
 
54
55
  ## Global Test Utilities
55
56
 
@@ -0,0 +1,328 @@
1
+ import { fixture, assert, html } from '@open-wc/testing'
2
+ import { sendKeys } from '@web/test-runner-commands'
3
+ import CurrencyPicker from '../../../src/elements/currency/internals/Picker.js'
4
+ import '../../../src/elements/currency/currency-picker.js'
5
+
6
+ describe('CurrencyPicker - Accessibility', () => {
7
+ async function basicFixture(): Promise<CurrencyPicker> {
8
+ return fixture(html`<currency-picker label="Select Currency"></currency-picker>`)
9
+ }
10
+
11
+ async function multiSelectFixture(): Promise<CurrencyPicker> {
12
+ return fixture(html`<currency-picker multi label="Select Multiple Currencies"></currency-picker>`)
13
+ }
14
+
15
+ async function errorStateFixture(): Promise<CurrencyPicker> {
16
+ return fixture(html`<currency-picker required label="Required Currency Selection"></currency-picker>`)
17
+ }
18
+
19
+ describe('ARIA Attributes', () => {
20
+ let picker: CurrencyPicker
21
+
22
+ beforeEach(async () => {
23
+ picker = await basicFixture()
24
+ })
25
+
26
+ it('should have proper ARIA roles', async () => {
27
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
28
+ assert.equal(container?.getAttribute('role'), 'group')
29
+ })
30
+
31
+ it('should have descriptive ARIA labels', async () => {
32
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
33
+ const ariaLabel = container?.getAttribute('aria-label')
34
+
35
+ assert.exists(ariaLabel)
36
+ assert.include(ariaLabel || '', 'Currency selector')
37
+ })
38
+
39
+ it('should update ARIA labels when selection changes', async () => {
40
+ picker.selected = ['USD']
41
+ await picker.updateComplete
42
+
43
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
44
+ const ariaLabel = container?.getAttribute('aria-label')
45
+
46
+ assert.include(ariaLabel || '', 'USD selected')
47
+ })
48
+
49
+ it('should have proper ARIA invalid state', async () => {
50
+ picker.required = true
51
+ picker.clearSelection()
52
+ await picker.updateComplete
53
+
54
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
55
+ assert.equal(select?.getAttribute('aria-invalid'), 'true')
56
+ })
57
+
58
+ it('should have aria-describedby when supporting text is present', async () => {
59
+ picker.supportingText = 'Choose your preferred currency'
60
+ await picker.updateComplete
61
+
62
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
63
+ assert.exists(select?.getAttribute('aria-describedby'))
64
+ })
65
+ })
66
+
67
+ describe('Multi-select ARIA', () => {
68
+ let picker: CurrencyPicker
69
+
70
+ beforeEach(async () => {
71
+ picker = await multiSelectFixture()
72
+ })
73
+
74
+ it('should announce selection count for multi-select', async () => {
75
+ picker.selected = ['USD', 'EUR', 'GBP']
76
+ await picker.updateComplete
77
+
78
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
79
+ const ariaLabel = container?.getAttribute('aria-label')
80
+
81
+ assert.include(ariaLabel || '', '3 currencies selected')
82
+ })
83
+
84
+ it('should have removable chips with proper labels', async () => {
85
+ picker.selected = ['USD', 'EUR']
86
+ await picker.updateComplete
87
+
88
+ const chips = picker.shadowRoot?.querySelectorAll('ui-chip[removable]')
89
+ assert.equal(chips?.length, 2)
90
+
91
+ chips?.forEach((chip) => {
92
+ assert.isTrue(chip.hasAttribute('removable'))
93
+ })
94
+ })
95
+ })
96
+
97
+ describe('Error Announcements', () => {
98
+ let picker: CurrencyPicker
99
+
100
+ beforeEach(async () => {
101
+ picker = await errorStateFixture()
102
+ })
103
+
104
+ it('should have error alerts with proper ARIA', async () => {
105
+ // Set a value first, then clear to trigger validation
106
+ picker.selected = ['USD']
107
+ await picker.updateComplete
108
+
109
+ // Now set to empty to trigger validation error
110
+ picker.selected = []
111
+ await picker.updateComplete
112
+
113
+ const errorElement = picker.shadowRoot?.querySelector('.error')
114
+ assert.equal(errorElement?.getAttribute('role'), 'alert')
115
+ assert.equal(errorElement?.getAttribute('aria-live'), 'polite')
116
+ })
117
+
118
+ it('should announce errors to screen readers', async () => {
119
+ picker.showErrors = true
120
+
121
+ // Set a value first, then clear to trigger validation
122
+ picker.selected = ['USD']
123
+ await picker.updateComplete
124
+
125
+ // Now set to empty to trigger validation error
126
+ picker.selected = []
127
+ await picker.updateComplete
128
+
129
+ const errorMessage = picker.shadowRoot?.querySelector('.error-message')
130
+ assert.exists(errorMessage)
131
+ assert.exists(errorMessage?.textContent)
132
+ })
133
+ })
134
+
135
+ describe('Keyboard Navigation', () => {
136
+ let picker: CurrencyPicker
137
+
138
+ beforeEach(async () => {
139
+ picker = await basicFixture()
140
+ })
141
+
142
+ it('should be focusable', async () => {
143
+ const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
144
+ select.focus()
145
+
146
+ // Check if the picker or its select element has focus
147
+ const activeElement = picker.shadowRoot?.activeElement || document.activeElement
148
+ assert.isTrue(activeElement === select || activeElement === picker)
149
+ })
150
+
151
+ it('should support keyboard selection', async () => {
152
+ const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
153
+ select.focus()
154
+
155
+ // Simulate opening dropdown and selecting
156
+ await sendKeys({
157
+ press: 'Space',
158
+ })
159
+
160
+ await sendKeys({
161
+ press: 'ArrowDown',
162
+ })
163
+
164
+ await sendKeys({
165
+ press: 'Enter',
166
+ })
167
+
168
+ // Should have some interaction (exact behavior depends on md-outlined-select)
169
+ assert.exists(select)
170
+ })
171
+
172
+ it('should support chip removal via keyboard in multi-select', async () => {
173
+ picker.multi = true
174
+ picker.selected = ['USD', 'EUR']
175
+ await picker.updateComplete
176
+
177
+ const chips = picker.shadowRoot?.querySelectorAll('ui-chip') as NodeListOf<HTMLElement>
178
+ assert.isAtLeast(chips.length, 1)
179
+
180
+ // Focus first chip and simulate removal
181
+ chips[0].focus()
182
+ await sendKeys({
183
+ press: 'Delete',
184
+ })
185
+
186
+ // The chip should be focusable and support keyboard interaction
187
+ assert.exists(chips[0])
188
+ })
189
+ })
190
+
191
+ describe('Focus Management', () => {
192
+ let picker: CurrencyPicker
193
+
194
+ beforeEach(async () => {
195
+ picker = await basicFixture()
196
+ })
197
+
198
+ it('should have visible focus indicators', async () => {
199
+ const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
200
+ select.focus()
201
+
202
+ // Check that focus styles are applied (this may require checking computed styles)
203
+ assert.exists(select)
204
+ })
205
+
206
+ it('should maintain focus appropriately after interactions', async () => {
207
+ picker.multi = true
208
+ picker.selected = ['USD']
209
+ await picker.updateComplete
210
+
211
+ const select = picker.shadowRoot?.querySelector('md-outlined-select') as HTMLElement
212
+ select.focus()
213
+
214
+ // Simulate adding another currency
215
+ picker.selected = ['USD', 'EUR']
216
+ await picker.updateComplete
217
+
218
+ // Focus should still be manageable
219
+ assert.exists(select)
220
+ })
221
+ })
222
+
223
+ describe('Color Contrast and Visual Accessibility', () => {
224
+ let picker: CurrencyPicker
225
+
226
+ beforeEach(async () => {
227
+ picker = await basicFixture()
228
+ })
229
+
230
+ it('should render with proper contrast for text', async () => {
231
+ // This test would ideally check computed styles for contrast ratios
232
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
233
+ assert.exists(select)
234
+
235
+ // Basic check that text content is rendered
236
+ const options = picker.shadowRoot?.querySelectorAll('md-select-option')
237
+ assert.isAtLeast(options?.length || 0, 1)
238
+ })
239
+
240
+ it('should have distinguishable error states', async () => {
241
+ picker.required = true
242
+ picker.showErrors = true
243
+ picker.clearSelection()
244
+ await picker.updateComplete
245
+
246
+ const errorElement = picker.shadowRoot?.querySelector('.error')
247
+ const selectElement = picker.shadowRoot?.querySelector('md-outlined-select')
248
+ const isInvalid = !picker.validity.valid
249
+ const hasAriaInvalid = selectElement?.getAttribute('aria-invalid') === 'true'
250
+
251
+ assert.exists(errorElement, 'Error element should be present for visual accessibility')
252
+ assert.isTrue(isInvalid, 'Component should be in invalid state')
253
+ assert.isTrue(hasAriaInvalid, 'Select should have aria-invalid=true for screen readers')
254
+ })
255
+ })
256
+
257
+ describe('Screen Reader Support', () => {
258
+ let picker: CurrencyPicker
259
+
260
+ beforeEach(async () => {
261
+ picker = await basicFixture()
262
+ })
263
+
264
+ it('should have proper semantic structure', async () => {
265
+ // Check that component has accessible structure
266
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
267
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
268
+
269
+ assert.exists(container)
270
+ assert.equal(container?.getAttribute('role'), 'group')
271
+ assert.exists(container?.getAttribute('aria-label'))
272
+ assert.exists(select)
273
+ })
274
+
275
+ it('should announce state changes', async () => {
276
+ picker.selected = ['USD']
277
+ await picker.updateComplete
278
+
279
+ // Check that ARIA live regions and labels are updated
280
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
281
+ const ariaLabel = container?.getAttribute('aria-label')
282
+
283
+ assert.include(ariaLabel || '', 'selected')
284
+ })
285
+ })
286
+
287
+ describe('WCAG Compliance', () => {
288
+ let picker: CurrencyPicker
289
+
290
+ beforeEach(async () => {
291
+ picker = await basicFixture()
292
+ })
293
+
294
+ it('should have sufficient touch targets', async () => {
295
+ // Check that interactive elements are large enough for touch
296
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
297
+ assert.exists(select)
298
+
299
+ // Material Design select should have proper touch targets
300
+ // This would ideally check computed dimensions
301
+ })
302
+
303
+ it('should work without JavaScript enhancements', async () => {
304
+ // Basic HTML structure should be accessible
305
+ const select = picker.shadowRoot?.querySelector('md-outlined-select')
306
+ const options = picker.shadowRoot?.querySelectorAll('md-select-option')
307
+
308
+ assert.exists(select)
309
+ assert.isAtLeast(options?.length || 0, 1)
310
+ })
311
+
312
+ it('should support high contrast mode', async () => {
313
+ // This would check forced-colors media query support
314
+ // For now, verify basic structure exists
315
+ const container = picker.shadowRoot?.querySelector('.currency-picker')
316
+ assert.exists(container)
317
+ })
318
+
319
+ it('should respect reduced motion preferences', async () => {
320
+ // This would check prefers-reduced-motion media query
321
+ // For now, verify component doesn't rely on motion for function
322
+ picker.selected = ['USD']
323
+ await picker.updateComplete
324
+
325
+ assert.deepEqual(picker.selected, ['USD'])
326
+ })
327
+ })
328
+ })