@dile/ui 3.0.7 → 3.1.0
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.
|
Binary file
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import './select.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-select', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function renderSelect(html) {
|
|
10
|
+
document.body.innerHTML = html;
|
|
11
|
+
const el = document.body.querySelector('dile-select');
|
|
12
|
+
await el.updateComplete;
|
|
13
|
+
return el;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('Rendering', () => {
|
|
17
|
+
it('renders with slotted native select element', async () => {
|
|
18
|
+
const el = await renderSelect(`
|
|
19
|
+
<dile-select>
|
|
20
|
+
<select slot="select">
|
|
21
|
+
<option value="1">Option 1</option>
|
|
22
|
+
<option value="2">Option 2</option>
|
|
23
|
+
</select>
|
|
24
|
+
</dile-select>
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
expect(el.querySelector('select')).toBeTruthy();
|
|
28
|
+
expect(el.querySelector('select').getAttribute('slot')).toBe('select');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders with a label when provided', async () => {
|
|
32
|
+
const el = await renderSelect(`
|
|
33
|
+
<dile-select label="Choose option">
|
|
34
|
+
<select slot="select">
|
|
35
|
+
<option value="1">Option 1</option>
|
|
36
|
+
</select>
|
|
37
|
+
</dile-select>
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
const label = el.shadowRoot.querySelector('label');
|
|
41
|
+
expect(label).toBeTruthy();
|
|
42
|
+
expect(label.textContent).toBe('Choose option');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not render label when not provided', async () => {
|
|
46
|
+
const el = await renderSelect(`
|
|
47
|
+
<dile-select>
|
|
48
|
+
<select slot="select">
|
|
49
|
+
<option value="1">Option 1</option>
|
|
50
|
+
</select>
|
|
51
|
+
</dile-select>
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
expect(el.shadowRoot.querySelector('label')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders an arrow icon inside select-wrapper', async () => {
|
|
58
|
+
const el = await renderSelect(`
|
|
59
|
+
<dile-select>
|
|
60
|
+
<select slot="select">
|
|
61
|
+
<option value="1">Option 1</option>
|
|
62
|
+
</select>
|
|
63
|
+
</dile-select>
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
const arrowIcon = el.shadowRoot.querySelector('.arrow-icon');
|
|
67
|
+
expect(arrowIcon).toBeTruthy();
|
|
68
|
+
expect(arrowIcon.querySelector('svg')).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('renders message when provided', async () => {
|
|
72
|
+
const el = await renderSelect(`
|
|
73
|
+
<dile-select message="This is a message">
|
|
74
|
+
<select slot="select">
|
|
75
|
+
<option value="1">Option 1</option>
|
|
76
|
+
</select>
|
|
77
|
+
</dile-select>
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
const message = el.shadowRoot.querySelector('.message');
|
|
81
|
+
expect(message).toBeTruthy();
|
|
82
|
+
expect(message.textContent.trim()).toBe('This is a message');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('applies errored-msg class to message when errored is true', async () => {
|
|
86
|
+
const el = await renderSelect(`
|
|
87
|
+
<dile-select errored message="Error message">
|
|
88
|
+
<select slot="select">
|
|
89
|
+
<option value="1">Option 1</option>
|
|
90
|
+
</select>
|
|
91
|
+
</dile-select>
|
|
92
|
+
`);
|
|
93
|
+
|
|
94
|
+
const message = el.shadowRoot.querySelector('.message');
|
|
95
|
+
expect(message.classList.contains('errored-msg')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Properties', () => {
|
|
100
|
+
it('reflects value property to the native select', async () => {
|
|
101
|
+
const el = await renderSelect(`
|
|
102
|
+
<dile-select value="2">
|
|
103
|
+
<select slot="select">
|
|
104
|
+
<option value="1">Option 1</option>
|
|
105
|
+
<option value="2">Option 2</option>
|
|
106
|
+
</select>
|
|
107
|
+
</dile-select>
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
expect(el.value).toBe('2');
|
|
111
|
+
expect(el.querySelector('select').value).toBe('2');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('updates select value when value property changes', async () => {
|
|
115
|
+
const el = await renderSelect(`
|
|
116
|
+
<dile-select>
|
|
117
|
+
<select slot="select">
|
|
118
|
+
<option value="1">Option 1</option>
|
|
119
|
+
<option value="2">Option 2</option>
|
|
120
|
+
</select>
|
|
121
|
+
</dile-select>
|
|
122
|
+
`);
|
|
123
|
+
|
|
124
|
+
el.value = '2';
|
|
125
|
+
await el.updateComplete;
|
|
126
|
+
|
|
127
|
+
expect(el.querySelector('select').value).toBe('2');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('reflects disabled property to native select', async () => {
|
|
131
|
+
const el = await renderSelect(`
|
|
132
|
+
<dile-select disabled>
|
|
133
|
+
<select slot="select">
|
|
134
|
+
<option value="1">Option 1</option>
|
|
135
|
+
</select>
|
|
136
|
+
</dile-select>
|
|
137
|
+
`);
|
|
138
|
+
|
|
139
|
+
expect(el.querySelector('select').disabled).toBe(true);
|
|
140
|
+
expect(el.querySelector('select').classList.contains('disabled')).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('updates disabled state dynamically', async () => {
|
|
144
|
+
const el = await renderSelect(`
|
|
145
|
+
<dile-select>
|
|
146
|
+
<select slot="select">
|
|
147
|
+
<option value="1">Option 1</option>
|
|
148
|
+
</select>
|
|
149
|
+
</dile-select>
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
expect(el.querySelector('select').disabled).toBe(false);
|
|
153
|
+
|
|
154
|
+
el.disabled = true;
|
|
155
|
+
await el.updateComplete;
|
|
156
|
+
|
|
157
|
+
expect(el.querySelector('select').disabled).toBe(true);
|
|
158
|
+
expect(el.querySelector('select').classList.contains('disabled')).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('applies errored class to select when errored is true', async () => {
|
|
162
|
+
const el = await renderSelect(`
|
|
163
|
+
<dile-select errored>
|
|
164
|
+
<select slot="select">
|
|
165
|
+
<option value="1">Option 1</option>
|
|
166
|
+
</select>
|
|
167
|
+
</dile-select>
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
expect(el.querySelector('select').classList.contains('errored')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('removes errored class when errored is set to false', async () => {
|
|
174
|
+
const el = await renderSelect(`
|
|
175
|
+
<dile-select errored>
|
|
176
|
+
<select slot="select">
|
|
177
|
+
<option value="1">Option 1</option>
|
|
178
|
+
</select>
|
|
179
|
+
</dile-select>
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
el.errored = false;
|
|
183
|
+
await el.updateComplete;
|
|
184
|
+
|
|
185
|
+
expect(el.querySelector('select').classList.contains('errored')).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Accessibility Attributes', () => {
|
|
190
|
+
it('generates unique ID for label association', async () => {
|
|
191
|
+
const el = await renderSelect(`
|
|
192
|
+
<dile-select label="Select option">
|
|
193
|
+
<select slot="select">
|
|
194
|
+
<option value="1">Option 1</option>
|
|
195
|
+
</select>
|
|
196
|
+
</dile-select>
|
|
197
|
+
`);
|
|
198
|
+
|
|
199
|
+
const label = el.shadowRoot.querySelector('label');
|
|
200
|
+
const selectId = el.querySelector('select').id;
|
|
201
|
+
|
|
202
|
+
expect(selectId).toMatch(/^dile-select-/);
|
|
203
|
+
expect(label.getAttribute('for')).toBe(selectId);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('assigns ID to slotted select element', async () => {
|
|
207
|
+
const el = await renderSelect(`
|
|
208
|
+
<dile-select>
|
|
209
|
+
<select slot="select">
|
|
210
|
+
<option value="1">Option 1</option>
|
|
211
|
+
</select>
|
|
212
|
+
</dile-select>
|
|
213
|
+
`);
|
|
214
|
+
|
|
215
|
+
expect(el.querySelector('select').id).toMatch(/^dile-select-/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('preserves existing ID on select element', async () => {
|
|
219
|
+
const el = await renderSelect(`
|
|
220
|
+
<dile-select>
|
|
221
|
+
<select slot="select" id="my-custom-id">
|
|
222
|
+
<option value="1">Option 1</option>
|
|
223
|
+
</select>
|
|
224
|
+
</dile-select>
|
|
225
|
+
`);
|
|
226
|
+
|
|
227
|
+
expect(el.querySelector('select').id).toBe('my-custom-id');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('sets aria-invalid="true" when errored is true', async () => {
|
|
231
|
+
const el = await renderSelect(`
|
|
232
|
+
<dile-select errored>
|
|
233
|
+
<select slot="select">
|
|
234
|
+
<option value="1">Option 1</option>
|
|
235
|
+
</select>
|
|
236
|
+
</dile-select>
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
expect(el.querySelector('select').getAttribute('aria-invalid')).toBe('true');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('sets aria-invalid="false" when errored is false', async () => {
|
|
243
|
+
const el = await renderSelect(`
|
|
244
|
+
<dile-select>
|
|
245
|
+
<select slot="select">
|
|
246
|
+
<option value="1">Option 1</option>
|
|
247
|
+
</select>
|
|
248
|
+
</dile-select>
|
|
249
|
+
`);
|
|
250
|
+
|
|
251
|
+
expect(el.querySelector('select').getAttribute('aria-invalid')).toBe('false');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('sets aria-describedby when message exists', async () => {
|
|
255
|
+
const el = await renderSelect(`
|
|
256
|
+
<dile-select message="Error message">
|
|
257
|
+
<select slot="select">
|
|
258
|
+
<option value="1">Option 1</option>
|
|
259
|
+
</select>
|
|
260
|
+
</dile-select>
|
|
261
|
+
`);
|
|
262
|
+
|
|
263
|
+
const messageId = el.shadowRoot.querySelector('.message').id;
|
|
264
|
+
expect(el.querySelector('select').getAttribute('aria-describedby')).toBe(messageId);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('removes aria-describedby when message is cleared', async () => {
|
|
268
|
+
const el = await renderSelect(`
|
|
269
|
+
<dile-select message="Error message">
|
|
270
|
+
<select slot="select">
|
|
271
|
+
<option value="1">Option 1</option>
|
|
272
|
+
</select>
|
|
273
|
+
</dile-select>
|
|
274
|
+
`);
|
|
275
|
+
|
|
276
|
+
el.message = '';
|
|
277
|
+
await el.updateComplete;
|
|
278
|
+
|
|
279
|
+
expect(el.querySelector('select').hasAttribute('aria-describedby')).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('sets aria-disabled attribute when disabled', async () => {
|
|
283
|
+
const el = await renderSelect(`
|
|
284
|
+
<dile-select disabled>
|
|
285
|
+
<select slot="select">
|
|
286
|
+
<option value="1">Option 1</option>
|
|
287
|
+
</select>
|
|
288
|
+
</dile-select>
|
|
289
|
+
`);
|
|
290
|
+
|
|
291
|
+
expect(el.querySelector('select').getAttribute('aria-disabled')).toBe('true');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('has aria-hidden on arrow icon', async () => {
|
|
295
|
+
const el = await renderSelect(`
|
|
296
|
+
<dile-select>
|
|
297
|
+
<select slot="select">
|
|
298
|
+
<option value="1">Option 1</option>
|
|
299
|
+
</select>
|
|
300
|
+
</dile-select>
|
|
301
|
+
`);
|
|
302
|
+
|
|
303
|
+
const arrowIcon = el.shadowRoot.querySelector('.arrow-icon');
|
|
304
|
+
expect(arrowIcon.getAttribute('aria-hidden')).toBe('true');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('sets message as role="status" with aria-live="polite"', async () => {
|
|
308
|
+
const el = await renderSelect(`
|
|
309
|
+
<dile-select message="Message">
|
|
310
|
+
<select slot="select">
|
|
311
|
+
<option value="1">Option 1</option>
|
|
312
|
+
</select>
|
|
313
|
+
</dile-select>
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
const message = el.shadowRoot.querySelector('.message');
|
|
317
|
+
expect(message.getAttribute('role')).toBe('status');
|
|
318
|
+
expect(message.getAttribute('aria-live')).toBe('polite');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('Events', () => {
|
|
323
|
+
it('dispatches element-changed event on value change', async () => {
|
|
324
|
+
const el = await renderSelect(`
|
|
325
|
+
<dile-select>
|
|
326
|
+
<select slot="select">
|
|
327
|
+
<option value="1">Option 1</option>
|
|
328
|
+
<option value="2">Option 2</option>
|
|
329
|
+
</select>
|
|
330
|
+
</dile-select>
|
|
331
|
+
`);
|
|
332
|
+
|
|
333
|
+
let eventFired = false;
|
|
334
|
+
let eventDetail = null;
|
|
335
|
+
el.addEventListener('element-changed', (e) => {
|
|
336
|
+
eventFired = true;
|
|
337
|
+
eventDetail = e.detail;
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
el.querySelector('select').value = '2';
|
|
341
|
+
el.querySelector('select').dispatchEvent(new Event('change', { bubbles: true }));
|
|
342
|
+
await el.updateComplete;
|
|
343
|
+
|
|
344
|
+
expect(eventFired).toBe(true);
|
|
345
|
+
expect(eventDetail.value).toBe('2');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('does not dispatch element-changed on quietChange', async () => {
|
|
349
|
+
const el = await renderSelect(`
|
|
350
|
+
<dile-select value="1">
|
|
351
|
+
<select slot="select">
|
|
352
|
+
<option value="1">Option 1</option>
|
|
353
|
+
<option value="2">Option 2</option>
|
|
354
|
+
</select>
|
|
355
|
+
</dile-select>
|
|
356
|
+
`);
|
|
357
|
+
|
|
358
|
+
let eventCount = 0;
|
|
359
|
+
el.addEventListener('element-changed', () => {
|
|
360
|
+
eventCount++;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
el.quietChange('2');
|
|
364
|
+
await el.updateComplete;
|
|
365
|
+
|
|
366
|
+
expect(el.value).toBe('2');
|
|
367
|
+
expect(eventCount).toBe(0);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('Methods', () => {
|
|
372
|
+
it('clear() sets value to undefined', async () => {
|
|
373
|
+
const el = await renderSelect(`
|
|
374
|
+
<dile-select value="1">
|
|
375
|
+
<select slot="select">
|
|
376
|
+
<option value="1">Option 1</option>
|
|
377
|
+
<option value="2">Option 2</option>
|
|
378
|
+
</select>
|
|
379
|
+
</dile-select>
|
|
380
|
+
`);
|
|
381
|
+
|
|
382
|
+
el.clear();
|
|
383
|
+
await el.updateComplete;
|
|
384
|
+
|
|
385
|
+
expect(el.value).toBeUndefined();
|
|
386
|
+
expect(el.querySelector('select').value).toBe('');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('getOptionByValue() returns the correct option', async () => {
|
|
390
|
+
const el = await renderSelect(`
|
|
391
|
+
<dile-select>
|
|
392
|
+
<select slot="select">
|
|
393
|
+
<option value="1">Option 1</option>
|
|
394
|
+
<option value="2">Option 2</option>
|
|
395
|
+
<option value="3">Option 3</option>
|
|
396
|
+
</select>
|
|
397
|
+
</dile-select>
|
|
398
|
+
`);
|
|
399
|
+
|
|
400
|
+
const option = el.getOptionByValue('2');
|
|
401
|
+
expect(option).toBeTruthy();
|
|
402
|
+
expect(option.value).toBe('2');
|
|
403
|
+
expect(option.textContent).toBe('Option 2');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('getOptionByValue() returns undefined for non-existent value', async () => {
|
|
407
|
+
const el = await renderSelect(`
|
|
408
|
+
<dile-select>
|
|
409
|
+
<select slot="select">
|
|
410
|
+
<option value="1">Option 1</option>
|
|
411
|
+
</select>
|
|
412
|
+
</dile-select>
|
|
413
|
+
`);
|
|
414
|
+
|
|
415
|
+
const option = el.getOptionByValue('999');
|
|
416
|
+
expect(option).toBeUndefined();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('quietChange() updates value without emitting event', async () => {
|
|
420
|
+
const el = await renderSelect(`
|
|
421
|
+
<dile-select value="1">
|
|
422
|
+
<select slot="select">
|
|
423
|
+
<option value="1">Option 1</option>
|
|
424
|
+
<option value="2">Option 2</option>
|
|
425
|
+
</select>
|
|
426
|
+
</dile-select>
|
|
427
|
+
`);
|
|
428
|
+
|
|
429
|
+
let eventCount = 0;
|
|
430
|
+
el.addEventListener('element-changed', () => {
|
|
431
|
+
eventCount++;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
el.quietChange('2');
|
|
435
|
+
await el.updateComplete;
|
|
436
|
+
|
|
437
|
+
expect(el.value).toBe('2');
|
|
438
|
+
expect(el.querySelector('select').value).toBe('2');
|
|
439
|
+
expect(eventCount).toBe(0);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('hideErrorOnInput', () => {
|
|
444
|
+
it('clears error message when value changes with hideErrorOnInput', async () => {
|
|
445
|
+
const el = await renderSelect(`
|
|
446
|
+
<dile-select errored message="Error" hideErrorOnInput>
|
|
447
|
+
<select slot="select">
|
|
448
|
+
<option value="1">Option 1</option>
|
|
449
|
+
<option value="2">Option 2</option>
|
|
450
|
+
</select>
|
|
451
|
+
</dile-select>
|
|
452
|
+
`);
|
|
453
|
+
|
|
454
|
+
expect(el.errored).toBe(true);
|
|
455
|
+
expect(el.message).toBe('Error');
|
|
456
|
+
|
|
457
|
+
el.querySelector('select').value = '2';
|
|
458
|
+
el.querySelector('select').dispatchEvent(new Event('change', { bubbles: true }));
|
|
459
|
+
await el.updateComplete;
|
|
460
|
+
|
|
461
|
+
expect(el.errored).toBe(false);
|
|
462
|
+
expect(el.message).toBe('');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('does not clear error when hideErrorOnInput is false', async () => {
|
|
466
|
+
const el = await renderSelect(`
|
|
467
|
+
<dile-select errored message="Error">
|
|
468
|
+
<select slot="select">
|
|
469
|
+
<option value="1">Option 1</option>
|
|
470
|
+
<option value="2">Option 2</option>
|
|
471
|
+
</select>
|
|
472
|
+
</dile-select>
|
|
473
|
+
`);
|
|
474
|
+
|
|
475
|
+
el.querySelector('select').value = '2';
|
|
476
|
+
el.querySelector('select').dispatchEvent(new Event('change', { bubbles: true }));
|
|
477
|
+
await el.updateComplete;
|
|
478
|
+
|
|
479
|
+
expect(el.errored).toBe(true);
|
|
480
|
+
expect(el.message).toBe('Error');
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('quietOnStart', () => {
|
|
485
|
+
it('does not emit element-changed on initialization with quietOnStart', async () => {
|
|
486
|
+
let eventCount = 0;
|
|
487
|
+
document.body.innerHTML = `
|
|
488
|
+
<dile-select value="1" quietOnStart>
|
|
489
|
+
<select slot="select">
|
|
490
|
+
<option value="1">Option 1</option>
|
|
491
|
+
<option value="2">Option 2</option>
|
|
492
|
+
</select>
|
|
493
|
+
</dile-select>
|
|
494
|
+
`;
|
|
495
|
+
const el = document.body.querySelector('dile-select');
|
|
496
|
+
el.addEventListener('element-changed', () => {
|
|
497
|
+
eventCount++;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await el.updateComplete;
|
|
501
|
+
|
|
502
|
+
// Initial setup should not emit event
|
|
503
|
+
expect(eventCount).toBe(0);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe('Form Association', () => {
|
|
508
|
+
it('is form-associated', async () => {
|
|
509
|
+
const el = await renderSelect(`
|
|
510
|
+
<dile-select>
|
|
511
|
+
<select slot="select">
|
|
512
|
+
<option value="1">Option 1</option>
|
|
513
|
+
</select>
|
|
514
|
+
</dile-select>
|
|
515
|
+
`);
|
|
516
|
+
|
|
517
|
+
expect(el.internals).toBeTruthy();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('sets form value through ElementInternals', async () => {
|
|
521
|
+
const el = await renderSelect(`
|
|
522
|
+
<dile-select value="1">
|
|
523
|
+
<select slot="select">
|
|
524
|
+
<option value="1">Option 1</option>
|
|
525
|
+
</select>
|
|
526
|
+
</dile-select>
|
|
527
|
+
`);
|
|
528
|
+
|
|
529
|
+
el.value = '1';
|
|
530
|
+
await el.updateComplete;
|
|
531
|
+
|
|
532
|
+
expect(el.internals.form).toBeDefined();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -11,42 +11,82 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
11
11
|
:host {
|
|
12
12
|
display: block;
|
|
13
13
|
margin-bottom: 10px;
|
|
14
|
+
--dile-select-arrow-color: #303030;
|
|
14
15
|
}
|
|
15
16
|
* {
|
|
16
17
|
box-sizing: border-box;
|
|
17
18
|
}
|
|
19
|
+
.select-wrapper {
|
|
20
|
+
position: relative;
|
|
21
|
+
display: inline-block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
18
24
|
::slotted(select) {
|
|
19
25
|
box-sizing: border-box;
|
|
20
26
|
width: var(--dile-input-width, 100%);
|
|
21
27
|
padding: var(--dile-input-padding, 7px 5px);
|
|
28
|
+
padding-right: 30px;
|
|
22
29
|
line-height: var(--dile-input-line-height, 1.5em);
|
|
23
30
|
font-size: var(--dile-select-font-size, 0.875em);
|
|
24
31
|
border-radius: var(--dile-input-border-radius, 5px);
|
|
25
32
|
border: var(--dile-input-border-width, 1px) solid var(--dile-input-border-color, #888);
|
|
26
33
|
background-color: var(--dile-input-background-color, #fff);
|
|
27
34
|
color: var(--dile-input-color, #303030);
|
|
28
|
-
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20448%22%20enable-background%3D%22new%200%200%20256%20448%22%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E.arrow%7Bfill%3A%23303030%3B%7D%3C%2Fstyle%3E%3Cpath%20class%3D%22arrow%22%20d%3D%22M255.9%20168c0-4.2-1.6-7.9-4.8-11.2-3.2-3.2-6.9-4.8-11.2-4.8H16c-4.2%200-7.9%201.6-11.2%204.8S0%20163.8%200%20168c0%204.4%201.6%208.2%204.8%2011.4l112%20112c3.1%203.1%206.8%204.6%2011.2%204.6%204.4%200%208.2-1.5%2011.4-4.6l112-112c3-3.2%204.5-7%204.5-11.4z%22%2F%3E%3C%2Fsvg%3E%0A");
|
|
29
|
-
background-position: right 10px center;
|
|
30
|
-
background-repeat: no-repeat;
|
|
31
|
-
background-size: auto 50%;
|
|
32
35
|
-moz-appearance: none;
|
|
33
36
|
-webkit-appearance: none;
|
|
34
37
|
appearance: none;
|
|
35
38
|
}
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
.arrow-icon {
|
|
40
|
+
position: absolute;
|
|
41
|
+
right: 10px;
|
|
42
|
+
top: 50%;
|
|
43
|
+
transform: translateY(-50%);
|
|
44
|
+
width: 18px;
|
|
45
|
+
height: 18px;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
}
|
|
51
|
+
.arrow-icon svg {
|
|
52
|
+
width: 100%;
|
|
53
|
+
height: 100%;
|
|
54
|
+
}
|
|
55
|
+
.arrow-icon path {
|
|
56
|
+
fill: var(--dile-select-arrow-color);
|
|
38
57
|
}
|
|
39
58
|
:host(:focus-within) ::slotted(select) {
|
|
40
59
|
outline: var(--dile-input-focus-ring-width, 3px) solid var(--dile-input-focus-ring-color, rgba(102, 170, 255, 0.5));
|
|
41
60
|
outline-offset: calc(-1 * var(--dile-input-focus-ring-width, 3px));
|
|
42
61
|
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
43
62
|
}
|
|
63
|
+
::slotted(select:focus-visible) {
|
|
64
|
+
outline: var(--dile-input-focus-ring-width, 3px) solid var(--dile-input-focus-ring-color, rgba(102, 170, 255, 0.5));
|
|
65
|
+
outline-offset: calc(-1 * var(--dile-input-focus-ring-width, 3px));
|
|
66
|
+
}
|
|
44
67
|
::slotted(select.errored) {
|
|
45
68
|
border-color: var(--dile-input-error-border-color, #c00);
|
|
46
69
|
}
|
|
70
|
+
::slotted(select[aria-invalid="true"]) {
|
|
71
|
+
border-color: var(--dile-input-error-border-color, #c00);
|
|
72
|
+
}
|
|
47
73
|
::slotted(select.disabled) {
|
|
48
74
|
background-color: #f5f5f5;
|
|
49
75
|
border-color: var(--dile-input-disabled-border-color, #eee);
|
|
76
|
+
cursor: not-allowed;
|
|
77
|
+
}
|
|
78
|
+
/* High contrast mode support (WCAG 2.2 2.1.3) */
|
|
79
|
+
@media (forced-colors: active) {
|
|
80
|
+
::slotted(select) {
|
|
81
|
+
border-width: var(--dile-input-border-width, 1px);
|
|
82
|
+
}
|
|
83
|
+
:host(:focus-within) ::slotted(select) {
|
|
84
|
+
outline: 3px solid;
|
|
85
|
+
}
|
|
86
|
+
.arrow-icon {
|
|
87
|
+
forced-color-adjust: none;
|
|
88
|
+
color: CanvasText;
|
|
89
|
+
}
|
|
50
90
|
}
|
|
51
91
|
`];
|
|
52
92
|
}
|
|
@@ -69,16 +109,26 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
69
109
|
}
|
|
70
110
|
|
|
71
111
|
render() {
|
|
112
|
+
const messageId = this.message ? `${this.selectId}-message` : null;
|
|
113
|
+
const ariaDescribedBy = messageId ? messageId : null;
|
|
114
|
+
|
|
72
115
|
return html`
|
|
73
116
|
<div>
|
|
74
117
|
${this.label
|
|
75
|
-
? html`<label for="
|
|
118
|
+
? html`<label for="${this.selectId}">${this.label}</label>`
|
|
76
119
|
: ""
|
|
77
120
|
}
|
|
78
|
-
<
|
|
121
|
+
<div class="select-wrapper">
|
|
122
|
+
<slot name="select"></slot>
|
|
123
|
+
<div class="arrow-icon" aria-hidden="true">
|
|
124
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 448" enable-background="new 0 0 256 448">
|
|
125
|
+
<path d="M255.9 168c0-4.2-1.6-7.9-4.8-11.2-3.2-3.2-6.9-4.8-11.2-4.8H16c-4.2 0-7.9 1.6-11.2 4.8S0 163.8 0 168c0 4.4 1.6 8.2 4.8 11.4l112 112c3.1 3.1 6.8 4.6 11.2 4.6 4.4 0 8.2-1.5 11.4-4.6l112-112c3-3.2 4.5-7 4.5-11.4z"></path>
|
|
126
|
+
</svg>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
79
129
|
</div>
|
|
80
130
|
${this.message
|
|
81
|
-
? html`<div class="message ${this.errored ? 'errored-msg' : ''}"><span>${this.message}</span></div>`
|
|
131
|
+
? html`<div class="message ${this.errored ? 'errored-msg' : ''}" id="${messageId}" role="status" aria-live="polite"><span>${this.message}</span></div>`
|
|
82
132
|
: ''
|
|
83
133
|
}
|
|
84
134
|
`;
|
|
@@ -95,6 +145,8 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
95
145
|
this.changeHandler = this.onChange.bind(this);
|
|
96
146
|
this.quiet = false;
|
|
97
147
|
this.internals = this.attachInternals();
|
|
148
|
+
// Generate unique ID for label association
|
|
149
|
+
this.selectId = `dile-select-${Math.random().toString(36).substr(2, 9)}`;
|
|
98
150
|
}
|
|
99
151
|
|
|
100
152
|
|
|
@@ -104,7 +156,12 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
104
156
|
if (!this.elselect) {
|
|
105
157
|
throw new Error('Use dile-select with a select element in the slot "select"');
|
|
106
158
|
} else {
|
|
159
|
+
// Assign unique ID for label association (a11y)
|
|
160
|
+
if (!this.elselect.id) {
|
|
161
|
+
this.elselect.id = this.selectId;
|
|
162
|
+
}
|
|
107
163
|
this.elselect.addEventListener("change", this.changeHandler);
|
|
164
|
+
this.updateSelectA11y();
|
|
108
165
|
}
|
|
109
166
|
}
|
|
110
167
|
|
|
@@ -136,10 +193,29 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
136
193
|
if(changedProperties.has("disabled")) {
|
|
137
194
|
this.elselect.disabled = this.disabled;
|
|
138
195
|
this.elselect.classList.toggle("disabled", this.disabled);
|
|
196
|
+
this.elselect.setAttribute("aria-disabled", this.disabled);
|
|
139
197
|
}
|
|
140
|
-
if(changedProperties.has("errored")) {
|
|
141
|
-
this.
|
|
198
|
+
if(changedProperties.has("errored") || changedProperties.has("message")) {
|
|
199
|
+
this.updateSelectA11y();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
updateSelectA11y() {
|
|
204
|
+
if (!this.elselect) return;
|
|
205
|
+
|
|
206
|
+
// Set aria-invalid when errored
|
|
207
|
+
this.elselect.setAttribute("aria-invalid", this.errored);
|
|
208
|
+
|
|
209
|
+
// Set aria-describedby when message exists
|
|
210
|
+
const messageId = this.message ? `${this.selectId}-message` : null;
|
|
211
|
+
if (messageId) {
|
|
212
|
+
this.elselect.setAttribute("aria-describedby", messageId);
|
|
213
|
+
} else {
|
|
214
|
+
this.elselect.removeAttribute("aria-describedby");
|
|
142
215
|
}
|
|
216
|
+
|
|
217
|
+
// Update visual state
|
|
218
|
+
this.elselect.classList.toggle("errored", this.errored);
|
|
143
219
|
}
|
|
144
220
|
|
|
145
221
|
firstUpdated() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "8cbccc74037ddf6004d03daf6726adca91481d95"
|
|
31
31
|
}
|