@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.
- package/.cursor/rules/html-and-css-best-practices.mdc +63 -0
- package/.cursor/rules/lit-best-practices.mdc +78 -0
- package/.github/instructions/html-and-css-best-practices.instructions.md +70 -0
- package/.github/instructions/lit-best-practices.instructions.md +86 -0
- package/build/src/elements/currency/currency-picker.d.ts +10 -0
- package/build/src/elements/currency/currency-picker.d.ts.map +1 -0
- package/build/src/elements/currency/currency-picker.js +27 -0
- package/build/src/elements/currency/currency-picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.d.ts +311 -0
- package/build/src/elements/currency/internals/Picker.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.js +857 -0
- package/build/src/elements/currency/internals/Picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts +3 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.js +58 -0
- package/build/src/elements/currency/internals/Picker.styles.js.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts +216 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js +1037 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts +3 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js +274 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts +13 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js +28 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js.map +1 -0
- package/build/src/md/button/internals/base.d.ts +1 -0
- package/build/src/md/button/internals/base.d.ts.map +1 -1
- package/build/src/md/button/internals/base.js +7 -0
- package/build/src/md/button/internals/base.js.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.d.ts.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.js +2 -0
- package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.js +73 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +164 -51
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +660 -368
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +65 -13
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.js +143 -76
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +76 -17
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +192 -127
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +63 -15
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.js +143 -64
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -1
- package/demo/elements/currency/index.html +91 -0
- package/demo/elements/currency/index.ts +272 -0
- package/demo/elements/index.html +6 -0
- package/demo/elements/mention-textarea/index.html +19 -0
- package/demo/elements/mention-textarea/index.ts +205 -0
- package/demo/md/date-picker/date-picker.ts +138 -103
- package/package.json +2 -2
- package/src/elements/currency/currency-picker.ts +14 -0
- package/src/elements/currency/internals/Picker.styles.ts +58 -0
- package/src/elements/currency/internals/Picker.ts +846 -0
- package/src/elements/mention-textarea/internals/MentionTextArea.styles.ts +274 -0
- package/src/elements/mention-textarea/internals/MentionTextArea.ts +1036 -0
- package/src/elements/mention-textarea/ui-mention-textarea.ts +18 -0
- package/src/md/button/internals/base.ts +7 -0
- package/src/md/chip/internals/Chip.styles.ts +2 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +73 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +643 -309
- package/src/md/date-picker/ui-date-picker-input.ts +110 -49
- package/src/md/date-picker/ui-date-picker-modal-input.ts +168 -99
- package/src/md/date-picker/ui-date-picker-modal.ts +136 -53
- package/test/README.md +3 -2
- package/test/elements/currency/CurrencyPicker.accessibility.test.ts +328 -0
- package/test/elements/currency/CurrencyPicker.core.test.ts +318 -0
- package/test/elements/currency/CurrencyPicker.integration.test.ts +482 -0
- package/test/elements/currency/CurrencyPicker.test.ts +486 -0
- package/test/elements/mention-textarea/MentionTextArea.basic.test.ts +63 -0
- package/test/elements/mention-textarea/MentionTextArea.test.ts +321 -0
- 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
|
|
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
|
|
154
|
+
private handleRangeSelect(event: CustomEvent<DateRangeSelectEvent>): void {
|
|
101
155
|
this.selectedRange = event.detail.range
|
|
102
|
-
this.
|
|
156
|
+
this.dispatchChangeEvent()
|
|
103
157
|
}
|
|
104
158
|
|
|
105
|
-
private
|
|
106
|
-
this.
|
|
159
|
+
private handleCancel(): void {
|
|
160
|
+
this.dispatchCloseEvent(false)
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
private
|
|
110
|
-
this.
|
|
163
|
+
private handleConfirm(): void {
|
|
164
|
+
this.dispatchCloseEvent(true)
|
|
111
165
|
}
|
|
112
166
|
|
|
113
|
-
private
|
|
114
|
-
this.
|
|
167
|
+
private handleModeToggle(): void {
|
|
168
|
+
this.isInputMode = !this.isInputMode
|
|
115
169
|
}
|
|
116
170
|
|
|
117
|
-
private
|
|
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
|
|
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
|
|
204
|
+
private renderHeader(): TemplateResult {
|
|
151
205
|
return html`
|
|
152
|
-
<
|
|
153
|
-
<h2 class="modal-title">${this.title}</h2>
|
|
154
|
-
<div
|
|
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.
|
|
159
|
-
aria-label=${this.
|
|
160
|
-
title=${this.
|
|
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.
|
|
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.
|
|
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
|
-
</
|
|
224
|
+
</header>
|
|
171
225
|
`
|
|
172
226
|
}
|
|
173
227
|
|
|
174
|
-
private
|
|
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
|
-
<
|
|
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"
|
|
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"
|
|
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
|
-
</
|
|
253
|
+
</section>
|
|
190
254
|
`
|
|
191
255
|
}
|
|
192
256
|
|
|
193
|
-
private
|
|
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
|
-
.
|
|
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.
|
|
271
|
+
@date-range-select=${this.handleRangeSelect}
|
|
207
272
|
></ui-date-picker-calendar>
|
|
208
273
|
`
|
|
209
274
|
}
|
|
210
275
|
|
|
211
|
-
private
|
|
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
|
-
<
|
|
216
|
-
<ui-icon icon="edit"
|
|
217
|
-
<
|
|
218
|
-
<p>This feature can be enhanced with date input fields</p>
|
|
219
|
-
|
|
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
|
|
287
|
+
private renderContent(): TemplateResult {
|
|
224
288
|
return html`
|
|
225
|
-
<
|
|
226
|
-
${this.
|
|
227
|
-
</
|
|
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
|
|
295
|
+
private renderActions(): TemplateResult {
|
|
232
296
|
const hasValidRange = this.selectedRange?.start && this.selectedRange?.end
|
|
233
297
|
|
|
234
298
|
return html`
|
|
235
|
-
<
|
|
236
|
-
<ui-button color="text" @click=${this.
|
|
237
|
-
<ui-button
|
|
238
|
-
|
|
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
|
|
245
|
-
|
|
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
|
-
|
|
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
|
+
})
|