@api-client/ui 0.5.6 → 0.5.8

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 (82) 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/mention-textarea/internals/MentionTextArea.d.ts +216 -0
  18. package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts.map +1 -0
  19. package/build/src/elements/mention-textarea/internals/MentionTextArea.js +1037 -0
  20. package/build/src/elements/mention-textarea/internals/MentionTextArea.js.map +1 -0
  21. package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts +3 -0
  22. package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts.map +1 -0
  23. package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js +274 -0
  24. package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js.map +1 -0
  25. package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts +13 -0
  26. package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts.map +1 -0
  27. package/build/src/elements/mention-textarea/ui-mention-textarea.js +28 -0
  28. package/build/src/elements/mention-textarea/ui-mention-textarea.js.map +1 -0
  29. package/build/src/md/button/internals/base.d.ts +1 -0
  30. package/build/src/md/button/internals/base.d.ts.map +1 -1
  31. package/build/src/md/button/internals/base.js +7 -0
  32. package/build/src/md/button/internals/base.js.map +1 -1
  33. package/build/src/md/chip/internals/Chip.styles.d.ts.map +1 -1
  34. package/build/src/md/chip/internals/Chip.styles.js +2 -0
  35. package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
  36. package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -1
  37. package/build/src/md/date-picker/internals/DatePicker.styles.js +73 -0
  38. package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -1
  39. package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +164 -51
  40. package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -1
  41. package/build/src/md/date-picker/internals/DatePickerCalendar.js +660 -368
  42. package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -1
  43. package/build/src/md/date-picker/ui-date-picker-input.d.ts +65 -13
  44. package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -1
  45. package/build/src/md/date-picker/ui-date-picker-input.js +143 -76
  46. package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -1
  47. package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +76 -17
  48. package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -1
  49. package/build/src/md/date-picker/ui-date-picker-modal-input.js +192 -127
  50. package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -1
  51. package/build/src/md/date-picker/ui-date-picker-modal.d.ts +63 -15
  52. package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -1
  53. package/build/src/md/date-picker/ui-date-picker-modal.js +143 -64
  54. package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -1
  55. package/demo/elements/currency/index.html +91 -0
  56. package/demo/elements/currency/index.ts +272 -0
  57. package/demo/elements/index.html +6 -0
  58. package/demo/elements/mention-textarea/index.html +19 -0
  59. package/demo/elements/mention-textarea/index.ts +205 -0
  60. package/demo/md/date-picker/date-picker.ts +138 -103
  61. package/package.json +2 -2
  62. package/src/elements/currency/currency-picker.ts +14 -0
  63. package/src/elements/currency/internals/Picker.styles.ts +58 -0
  64. package/src/elements/currency/internals/Picker.ts +846 -0
  65. package/src/elements/mention-textarea/internals/MentionTextArea.styles.ts +274 -0
  66. package/src/elements/mention-textarea/internals/MentionTextArea.ts +1036 -0
  67. package/src/elements/mention-textarea/ui-mention-textarea.ts +18 -0
  68. package/src/md/button/internals/base.ts +7 -0
  69. package/src/md/chip/internals/Chip.styles.ts +2 -0
  70. package/src/md/date-picker/internals/DatePicker.styles.ts +73 -0
  71. package/src/md/date-picker/internals/DatePickerCalendar.ts +643 -309
  72. package/src/md/date-picker/ui-date-picker-input.ts +110 -49
  73. package/src/md/date-picker/ui-date-picker-modal-input.ts +168 -99
  74. package/src/md/date-picker/ui-date-picker-modal.ts +136 -53
  75. package/test/README.md +3 -2
  76. package/test/elements/currency/CurrencyPicker.accessibility.test.ts +328 -0
  77. package/test/elements/currency/CurrencyPicker.core.test.ts +318 -0
  78. package/test/elements/currency/CurrencyPicker.integration.test.ts +482 -0
  79. package/test/elements/currency/CurrencyPicker.test.ts +486 -0
  80. package/test/elements/mention-textarea/MentionTextArea.basic.test.ts +63 -0
  81. package/test/elements/mention-textarea/MentionTextArea.test.ts +321 -0
  82. package/tsconfig.json +1 -1
@@ -21,23 +21,70 @@ export interface ModalDatePickerChangeEvent {
21
21
  * A modal date picker for selecting date ranges.
22
22
  * Extends full-screen and is ideal for date range selection like flight bookings.
23
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
+ *
24
60
  * ## Usage
25
61
  *
62
+ * ### Basic usage
26
63
  * ```html
27
64
  * <ui-date-picker-modal
28
65
  * .open=${true}
29
66
  * .selectedRange=${{ start: new Date(), end: null }}
30
- * @change=${this.handleRangeChange}
67
+ * @date-range-change=${this.handleRangeChange}
31
68
  * @close=${this.handleClose}
32
69
  * ></ui-date-picker-modal>
33
70
  * ```
34
71
  *
35
- * ### Custom labels
72
+ * ### Custom labels and constraints
36
73
  * ```html
37
74
  * <ui-date-picker-modal
38
75
  * title="Select travel dates"
39
76
  * startLabel="Check-in"
40
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}
41
88
  * ></ui-date-picker-modal>
42
89
  * ```
43
90
  */
@@ -95,26 +142,33 @@ export class UiDatePickerModal extends LitElement {
95
142
  */
96
143
  @property({ type: Boolean }) accessor showModeToggle = true
97
144
 
98
- @state() private accessor _isInputMode = false
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
+ }
99
153
 
100
- private _handleRangeSelect(event: CustomEvent<DateRangeSelectEvent>): void {
154
+ private handleRangeSelect(event: CustomEvent<DateRangeSelectEvent>): void {
101
155
  this.selectedRange = event.detail.range
102
- this._dispatchChangeEvent()
156
+ this.dispatchChangeEvent()
103
157
  }
104
158
 
105
- private _handleCancel(): void {
106
- this._dispatchCloseEvent(false)
159
+ private handleCancel(): void {
160
+ this.dispatchCloseEvent(false)
107
161
  }
108
162
 
109
- private _handleConfirm(): void {
110
- this._dispatchCloseEvent(true)
163
+ private handleConfirm(): void {
164
+ this.dispatchCloseEvent(true)
111
165
  }
112
166
 
113
- private _handleModeToggle(): void {
114
- this._isInputMode = !this._isInputMode
167
+ private handleModeToggle(): void {
168
+ this.isInputMode = !this.isInputMode
115
169
  }
116
170
 
117
- private _dispatchChangeEvent(): void {
171
+ private dispatchChangeEvent(): void {
118
172
  if (!this.selectedRange) return
119
173
 
120
174
  const event: ModalDatePickerChangeEvent = {
@@ -134,7 +188,7 @@ export class UiDatePickerModal extends LitElement {
134
188
  )
135
189
  }
136
190
 
137
- private _dispatchCloseEvent(confirmed: boolean): void {
191
+ private dispatchCloseEvent(confirmed: boolean): void {
138
192
  this.dispatchEvent(
139
193
  new CustomEvent('close', {
140
194
  detail: {
@@ -147,102 +201,131 @@ export class UiDatePickerModal extends LitElement {
147
201
  )
148
202
  }
149
203
 
150
- private _renderHeader(): TemplateResult {
204
+ private renderHeader(): TemplateResult {
151
205
  return html`
152
- <div class="modal-header">
153
- <h2 class="modal-title">${this.title}</h2>
154
- <div style="display: flex; gap: 8px;">
206
+ <header class="modal-header">
207
+ <h2 id="modal-title" class="modal-title">${this.title}</h2>
208
+ <div class="header-actions">
155
209
  ${this.showModeToggle
156
210
  ? html`
157
211
  <ui-icon-button
158
- @click=${this._handleModeToggle}
159
- aria-label=${this._isInputMode ? 'Show calendar' : 'Show date input'}
160
- title=${this._isInputMode ? 'Show calendar' : 'Show date input'}
212
+ @click=${this.handleModeToggle}
213
+ aria-label=${this.isInputMode ? 'Show calendar' : 'Show date input'}
214
+ title=${this.isInputMode ? 'Show calendar' : 'Show date input'}
161
215
  >
162
- <ui-icon icon=${this._isInputMode ? 'calendarToday' : 'edit'}></ui-icon>
216
+ <ui-icon icon=${this.isInputMode ? 'calendarToday' : 'edit'}></ui-icon>
163
217
  </ui-icon-button>
164
218
  `
165
219
  : ''}
166
- <ui-icon-button @click=${this._handleCancel} aria-label="Close" title="Close">
220
+ <ui-icon-button @click=${this.handleCancel} aria-label="Close" title="Close">
167
221
  <ui-icon icon="close"></ui-icon>
168
222
  </ui-icon-button>
169
223
  </div>
170
- </div>
224
+ </header>
171
225
  `
172
226
  }
173
227
 
174
- private _renderDateDisplay(): TemplateResult {
228
+ private renderDateDisplay(): TemplateResult {
175
229
  const startDate = this.selectedRange?.start
176
230
  const endDate = this.selectedRange?.end
177
231
 
178
232
  return html`
179
- <div class="date-range-display">
233
+ <section class="date-range-display" role="status" aria-live="polite" aria-label="Selected date range">
180
234
  <div class="date-display">
181
- <div class="date-label">${this.startLabel}</div>
182
- <div class="date-value">${startDate ? formatDate(startDate, this.locale) : 'Select date'}</div>
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>
183
242
  </div>
184
- <div class="date-separator">—</div>
243
+ <div class="date-separator" aria-hidden="true">—</div>
185
244
  <div class="date-display">
186
- <div class="date-label">${this.endLabel}</div>
187
- <div class="date-value">${endDate ? formatDate(endDate, this.locale) : 'Select date'}</div>
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>
188
252
  </div>
189
- </div>
253
+ </section>
190
254
  `
191
255
  }
192
256
 
193
- private _renderCalendar(): TemplateResult {
257
+ private renderCalendar(): TemplateResult {
194
258
  const currentDate = this.selectedRange?.start || new Date()
195
259
 
196
260
  return html`
197
261
  <ui-date-picker-calendar
198
262
  .year=${currentDate.getFullYear()}
199
263
  .month=${currentDate.getMonth()}
200
- .selectedRange=${this.selectedRange}
264
+ .rangeStart=${this.selectedRange?.start || null}
265
+ .rangeEnd=${this.selectedRange?.end || null}
201
266
  .rangeSelection=${true}
202
267
  .minDate=${this.minDate}
203
268
  .maxDate=${this.maxDate}
204
269
  .disabledDates=${this.disabledDates}
205
270
  .locale=${this.locale}
206
- @date-range-select=${this._handleRangeSelect}
271
+ @date-range-select=${this.handleRangeSelect}
207
272
  ></ui-date-picker-calendar>
208
273
  `
209
274
  }
210
275
 
211
- private _renderInputMode(): TemplateResult {
212
- // For now, we'll show a placeholder for input mode
213
- // This could be enhanced with actual date input fields
276
+ private renderInputMode(): TemplateResult {
214
277
  return html`
215
- <div style="padding: 24px; text-align: center; color: var(--ui-color-on-surface-variant);">
216
- <ui-icon icon="edit" style="font-size: 48px; margin-bottom: 16px;"></ui-icon>
217
- <p>Manual date input mode</p>
218
- <p>This feature can be enhanced with date input fields</p>
219
- </div>
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>
220
284
  `
221
285
  }
222
286
 
223
- private _renderContent(): TemplateResult {
287
+ private renderContent(): TemplateResult {
224
288
  return html`
225
- <div class="modal-content">
226
- ${this._renderDateDisplay()} ${this._isInputMode ? this._renderInputMode() : this._renderCalendar()}
227
- </div>
289
+ <main id="modal-content" class="modal-content">
290
+ ${this.renderDateDisplay()} ${this.isInputMode ? this.renderInputMode() : this.renderCalendar()}
291
+ </main>
228
292
  `
229
293
  }
230
294
 
231
- private _renderActions(): TemplateResult {
295
+ private renderActions(): TemplateResult {
232
296
  const hasValidRange = this.selectedRange?.start && this.selectedRange?.end
233
297
 
234
298
  return html`
235
- <div class="modal-actions">
236
- <ui-button color="text" @click=${this._handleCancel}>Cancel</ui-button>
237
- <ui-button color="filled" @click=${this._handleConfirm} .disabled=${!hasValidRange}> Confirm </ui-button>
238
- </div>
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>
239
316
  `
240
317
  }
241
318
 
242
319
  override render(): TemplateResult {
243
320
  return html`
244
- <ui-dialog .open=${this.open} @close=${this._handleCancel}>
245
- ${this._renderHeader()} ${this._renderContent()} ${this._renderActions()}
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()}
246
329
  </ui-dialog>
247
330
  `
248
331
  }
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
+ })