@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.
- 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/highlight/MarkdownStyles.d.ts.map +1 -1
- package/build/src/elements/highlight/MarkdownStyles.js +0 -13
- package/build/src/elements/highlight/MarkdownStyles.js.map +1 -1
- package/build/src/elements/http/BodyEditor.d.ts +0 -13
- package/build/src/elements/http/BodyEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyEditor.js +0 -13
- package/build/src/elements/http/BodyEditor.js.map +1 -1
- package/build/src/elements/http/BodyTextEditor.d.ts +0 -13
- package/build/src/elements/http/BodyTextEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyTextEditor.js +0 -13
- package/build/src/elements/http/BodyTextEditor.js.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.d.ts.map +1 -1
- package/build/src/elements/http/BodyUrlEncodedEditor.js +0 -13
- package/build/src/elements/http/BodyUrlEncodedEditor.js.map +1 -1
- package/build/src/elements/http/UrlInput.d.ts +0 -13
- package/build/src/elements/http/UrlInput.d.ts.map +1 -1
- package/build/src/elements/http/UrlInput.js +0 -13
- package/build/src/elements/http/UrlInput.js.map +1 -1
- package/build/src/index.d.ts +2 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +2 -0
- package/build/src/index.js.map +1 -1
- 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/button/internals/button.styles.js +1 -1
- package/build/src/md/button/internals/button.styles.js.map +1 -1
- package/build/src/md/date/internals/DateTime.d.ts +0 -13
- package/build/src/md/date/internals/DateTime.d.ts.map +1 -1
- package/build/src/md/date/internals/DateTime.js +0 -13
- package/build/src/md/date/internals/DateTime.js.map +1 -1
- package/build/src/md/date-picker/index.d.ts +13 -0
- package/build/src/md/date-picker/index.d.ts.map +1 -0
- package/build/src/md/date-picker/index.js +13 -0
- package/build/src/md/date-picker/index.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts +4 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js +409 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +272 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +1062 -0
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts +93 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.d.ts.map +1 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js +221 -0
- package/build/src/md/date-picker/internals/DatePickerUtils.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +160 -0
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-input.js +464 -0
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +178 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +538 -0
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +156 -0
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js +423 -0
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.d.ts.map +1 -1
- package/build/src/md/dialog/internals/Dialog.styles.js +1 -0
- package/build/src/md/dialog/internals/Dialog.styles.js.map +1 -1
- package/demo/elements/currency/index.html +91 -0
- package/demo/elements/currency/index.ts +272 -0
- package/demo/elements/har/har2.json +1 -1
- package/demo/elements/index.html +3 -0
- package/demo/md/date-picker/date-picker.ts +336 -0
- package/demo/md/date-picker/index.html +171 -0
- package/demo/md/index.html +2 -0
- package/package.json +1 -1
- 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/highlight/MarkdownStyles.ts +0 -13
- package/src/elements/http/BodyEditor.ts +0 -13
- package/src/elements/http/BodyTextEditor.ts +0 -13
- package/src/elements/http/BodyUrlEncodedEditor.ts +0 -13
- package/src/elements/http/UrlInput.ts +0 -13
- package/src/index.ts +17 -0
- package/src/md/button/internals/base.ts +7 -0
- package/src/md/button/internals/button.styles.ts +1 -1
- package/src/md/date/internals/DateTime.ts +0 -14
- package/src/md/date-picker/README.md +184 -0
- package/src/md/date-picker/index.ts +17 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +411 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +1031 -0
- package/src/md/date-picker/internals/DatePickerUtils.ts +288 -0
- package/src/md/date-picker/ui-date-picker-input.ts +333 -0
- package/src/md/date-picker/ui-date-picker-modal-input.ts +440 -0
- package/src/md/date-picker/ui-date-picker-modal.ts +346 -0
- package/src/md/dialog/internals/Dialog.styles.ts +1 -0
- 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
|
@@ -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
|
+
}
|
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
|
+
})
|