@warp-ds/elements 2.6.0 → 2.7.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/custom-elements.json +1552 -0
  2. package/dist/index.d.ts +561 -0
  3. package/dist/packages/affix/affix.js +11 -11
  4. package/dist/packages/affix/affix.js.map +3 -3
  5. package/dist/packages/alert/alert.js +7 -7
  6. package/dist/packages/alert/alert.js.map +3 -3
  7. package/dist/packages/attention/attention.js +22 -22
  8. package/dist/packages/attention/attention.js.map +3 -3
  9. package/dist/packages/breadcrumbs/breadcrumbs.js +9 -9
  10. package/dist/packages/breadcrumbs/breadcrumbs.js.map +3 -3
  11. package/dist/packages/button/button.js +11 -11
  12. package/dist/packages/button/button.js.map +3 -3
  13. package/dist/packages/card/card.js +8 -8
  14. package/dist/packages/card/card.js.map +3 -3
  15. package/dist/packages/checkbox/checkbox.a11y.test.d.ts +1 -0
  16. package/dist/packages/checkbox/checkbox.a11y.test.js +73 -0
  17. package/dist/packages/checkbox/checkbox.d.ts +49 -0
  18. package/dist/packages/checkbox/checkbox.js +2594 -0
  19. package/dist/packages/checkbox/checkbox.js.map +7 -0
  20. package/dist/packages/checkbox/checkbox.react.stories.d.ts +12 -0
  21. package/dist/packages/checkbox/checkbox.react.stories.js +10 -0
  22. package/dist/packages/checkbox/checkbox.stories.d.ts +25 -0
  23. package/dist/packages/checkbox/checkbox.stories.js +183 -0
  24. package/dist/packages/checkbox/checkbox.test.d.ts +1 -0
  25. package/dist/packages/checkbox/checkbox.test.js +142 -0
  26. package/dist/packages/checkbox/react.d.ts +5 -0
  27. package/dist/packages/checkbox/react.js +15 -0
  28. package/dist/packages/checkbox/styles.d.ts +1 -0
  29. package/dist/packages/checkbox/styles.js +134 -0
  30. package/dist/packages/checkbox-group/checkbox-group.a11y.test.d.ts +2 -0
  31. package/dist/packages/checkbox-group/checkbox-group.a11y.test.js +109 -0
  32. package/dist/packages/checkbox-group/checkbox-group.d.ts +33 -0
  33. package/dist/packages/checkbox-group/checkbox-group.js +71 -0
  34. package/dist/packages/checkbox-group/checkbox-group.js.map +7 -0
  35. package/dist/packages/checkbox-group/checkbox-group.test.d.ts +2 -0
  36. package/dist/packages/checkbox-group/checkbox-group.test.js +112 -0
  37. package/dist/packages/checkbox-group/locales/da/messages.d.mts +1 -0
  38. package/dist/packages/checkbox-group/locales/da/messages.mjs +1 -0
  39. package/dist/packages/checkbox-group/locales/en/messages.d.mts +1 -0
  40. package/dist/packages/checkbox-group/locales/en/messages.mjs +1 -0
  41. package/dist/packages/checkbox-group/locales/fi/messages.d.mts +1 -0
  42. package/dist/packages/checkbox-group/locales/fi/messages.mjs +1 -0
  43. package/dist/packages/checkbox-group/locales/nb/messages.d.mts +1 -0
  44. package/dist/packages/checkbox-group/locales/nb/messages.mjs +1 -0
  45. package/dist/packages/checkbox-group/locales/sv/messages.d.mts +1 -0
  46. package/dist/packages/checkbox-group/locales/sv/messages.mjs +1 -0
  47. package/dist/packages/checkbox-group/react.d.ts +2 -0
  48. package/dist/packages/checkbox-group/react.js +11 -0
  49. package/dist/packages/combobox/combobox.js +11 -11
  50. package/dist/packages/combobox/combobox.js.map +3 -3
  51. package/dist/packages/datepicker/datepicker.js +42 -42
  52. package/dist/packages/datepicker/datepicker.js.map +3 -3
  53. package/dist/packages/expandable/expandable.js +11 -11
  54. package/dist/packages/expandable/expandable.js.map +3 -3
  55. package/dist/packages/i18n.d.ts +2 -0
  56. package/dist/packages/i18n.js +87 -2
  57. package/dist/packages/modal-header/modal-header.js +15 -15
  58. package/dist/packages/modal-header/modal-header.js.map +3 -3
  59. package/dist/packages/page-indicator/page-indicator.js +7 -7
  60. package/dist/packages/page-indicator/page-indicator.js.map +3 -3
  61. package/dist/packages/pagination/pagination.js +24 -24
  62. package/dist/packages/pagination/pagination.js.map +3 -3
  63. package/dist/packages/pill/pill.js +10 -10
  64. package/dist/packages/pill/pill.js.map +3 -3
  65. package/dist/packages/select/select.js +20 -20
  66. package/dist/packages/select/select.js.map +3 -3
  67. package/dist/packages/slider/Slider.d.ts +2 -0
  68. package/dist/packages/slider/Slider.js +8 -0
  69. package/dist/packages/slider/index.d.ts +2 -0
  70. package/dist/packages/slider/index.js +2 -0
  71. package/dist/packages/slider/locales/da/messages.d.mts +1 -0
  72. package/dist/packages/slider/locales/da/messages.mjs +1 -0
  73. package/dist/packages/slider/locales/en/messages.d.mts +1 -0
  74. package/dist/packages/slider/locales/en/messages.mjs +1 -0
  75. package/dist/packages/slider/locales/fi/messages.d.mts +1 -0
  76. package/dist/packages/slider/locales/fi/messages.mjs +1 -0
  77. package/dist/packages/slider/locales/nb/messages.d.mts +1 -0
  78. package/dist/packages/slider/locales/nb/messages.mjs +1 -0
  79. package/dist/packages/slider/locales/sv/messages.d.mts +1 -0
  80. package/dist/packages/slider/locales/sv/messages.mjs +1 -0
  81. package/dist/packages/slider/react.d.ts +3 -0
  82. package/dist/packages/slider/react.js +13 -0
  83. package/dist/packages/slider/slider.d.ts +64 -0
  84. package/dist/packages/slider/slider.js +2641 -0
  85. package/dist/packages/slider/slider.js.map +7 -0
  86. package/dist/packages/slider/slider.react.stories.d.ts +19 -0
  87. package/dist/packages/slider/slider.react.stories.js +161 -0
  88. package/dist/packages/slider/slider.stories.d.ts +26 -0
  89. package/dist/packages/slider/slider.stories.js +464 -0
  90. package/dist/packages/slider/slider.test.d.ts +5 -0
  91. package/dist/packages/slider/slider.test.js +380 -0
  92. package/dist/packages/slider/styles/w-slider.styles.d.ts +1 -0
  93. package/dist/packages/slider/styles/w-slider.styles.js +154 -0
  94. package/dist/packages/slider/styles.d.ts +1 -0
  95. package/dist/packages/slider/styles.js +2 -0
  96. package/dist/packages/slider-thumb/SliderThumb.d.ts +2 -0
  97. package/dist/packages/slider-thumb/SliderThumb.js +8 -0
  98. package/dist/packages/slider-thumb/oddbird-css-anchor-positioning.d.ts +2 -0
  99. package/dist/packages/slider-thumb/oddbird-css-anchor-positioning.js +3 -0
  100. package/dist/packages/slider-thumb/react.d.ts +6 -0
  101. package/dist/packages/slider-thumb/react.js +15 -0
  102. package/dist/packages/slider-thumb/slider-thumb.d.ts +72 -0
  103. package/dist/packages/slider-thumb/slider-thumb.js +2774 -0
  104. package/dist/packages/slider-thumb/slider-thumb.js.map +7 -0
  105. package/dist/packages/slider-thumb/styles/w-slider-thumb.styles.d.ts +1 -0
  106. package/dist/packages/slider-thumb/styles/w-slider-thumb.styles.js +194 -0
  107. package/dist/packages/step/step.js +13 -13
  108. package/dist/packages/step/step.js.map +3 -3
  109. package/dist/packages/step-indicator/step-indicator.a11y.test.js +6 -2
  110. package/dist/packages/textarea/textarea.a11y.test.js +1 -1
  111. package/dist/packages/textarea/textarea.test.js +2 -1
  112. package/dist/packages/textfield/styles/w-textfield.styles.js +6 -0
  113. package/dist/packages/textfield/textfield.js +7 -1
  114. package/dist/packages/textfield/textfield.js.map +2 -2
  115. package/dist/packages/toast/toast.js +13 -13
  116. package/dist/packages/toast/toast.js.map +3 -3
  117. package/dist/setup-tests.js +1 -1
  118. package/dist/web-types.json +328 -1
  119. package/package.json +1 -1
@@ -0,0 +1,380 @@
1
+ import { userEvent } from '@vitest/browser/context';
2
+ import { html } from 'lit';
3
+ import { expect, test } from 'vitest';
4
+ import { render } from 'vitest-browser-lit';
5
+ import '../attention/attention.js';
6
+ import '../affix/affix.js';
7
+ import '../textfield/textfield.js';
8
+ import './slider.js';
9
+ import '../slider-thumb/slider-thumb.js';
10
+ test('single slider starts with a default value equal to max', async () => {
11
+ const component = html `
12
+ <form data-testid="form">
13
+ <w-slider label="Single" min="0" max="100">
14
+ <w-slider-thumb name="value"></w-slider-thumb>
15
+ </w-slider>
16
+ </form>
17
+ `;
18
+ const page = render(component);
19
+ await expect.element(page.getByLabelText('Single').first()).toHaveValue('100');
20
+ });
21
+ test('range slider starts with a default from value equal to min, and to value equal to max', async () => {
22
+ const component = html `
23
+ <form data-testid="form">
24
+ <w-slider label="Range" min="0" max="100">
25
+ <w-slider-thumb aria-label="From" name="rangefrom" slot="from"></w-slider-thumb>
26
+ <w-slider-thumb aria-label="To" name="rangeto" slot="to"></w-slider-thumb>
27
+ </w-slider>
28
+ </form>
29
+ `;
30
+ const page = render(component);
31
+ await expect.element(page.getByLabelText('From').first()).toHaveValue('0');
32
+ await expect.element(page.getByLabelText('To').first()).toHaveValue('100');
33
+ });
34
+ test('can set slider value via the range input', async () => {
35
+ const component = html `
36
+ <form data-testid="form">
37
+ <w-slider label="Single" min="0" max="100">
38
+ <w-slider-thumb name="value"></w-slider-thumb>
39
+ </w-slider>
40
+ </form>
41
+ `;
42
+ const page = render(component);
43
+ await userEvent.type(page.getByLabelText('Single').first(), '{ArrowLeft}{ArrowLeft}{ArrowLeft}');
44
+ await expect.element(page.getByLabelText('Single').first()).toHaveValue('97');
45
+ await expect.element(page.getByRole('spinbutton')).toHaveValue(97); // keeps value in sync between inputs
46
+ const formData = new FormData(page.getByTestId('form').element());
47
+ expect(formData.get('value')).toBe('97');
48
+ });
49
+ test('can set slider value via the number input', async () => {
50
+ const component = html `
51
+ <form data-testid="form">
52
+ <w-slider label="Single" min="0" max="100">
53
+ <w-slider-thumb name="value"></w-slider-thumb>
54
+ </w-slider>
55
+ </form>
56
+ `;
57
+ const page = render(component);
58
+ await page.getByRole('spinbutton').fill('50');
59
+ await expect.element(page.getByRole('spinbutton')).toHaveValue(50);
60
+ await expect.element(page.getByLabelText('Single').first()).toHaveValue('50'); // keeps value in sync between inputs
61
+ const formData = new FormData(page.getByTestId('form').element());
62
+ expect(formData.get('value')).toBe('50');
63
+ });
64
+ test('deleting from number input works as expected', async () => {
65
+ const component = html `
66
+ <form data-testid="form">
67
+ <w-slider label="Production year" min="1950" max="2025" over under>
68
+ <p slot="description">Try typing a from value higher than a to value</p>
69
+ <w-slider-thumb slot="from" name="from"></w-slider-thumb>
70
+ <w-slider-thumb slot="to" name="to"></w-slider-thumb>
71
+ </w-slider>
72
+ </form>
73
+ `;
74
+ const page = render(component);
75
+ await expect.element(page.getByRole('spinbutton').last()).toHaveValue(2025);
76
+ await userEvent.type(page.getByRole('spinbutton').last(), '{Backspace}');
77
+ await expect.element(page.getByRole('spinbutton').last()).toHaveValue(202);
78
+ await userEvent.type(page.getByRole('spinbutton').last(), '{Backspace}');
79
+ await expect.element(page.getByRole('spinbutton').last()).toHaveValue(20);
80
+ await userEvent.type(page.getByRole('spinbutton').last(), '{Backspace}');
81
+ await expect.element(page.getByRole('spinbutton').last()).toHaveValue(2);
82
+ await userEvent.type(page.getByRole('spinbutton').last(), '{Backspace}');
83
+ await expect.element(page.getByRole('spinbutton').last()).not.toHaveValue();
84
+ });
85
+ test('can reset slider by resetting surrounding form', async () => {
86
+ render(html `
87
+ <form>
88
+ <w-slider label="Slider from 0 - 10" min="0" max="10">
89
+ <p slot="description">If you want to slide between 0 and 10, this slider has you covered.</p>
90
+ <w-slider-thumb name="zero-ten" value="3"></w-slider-thumb>
91
+ </w-slider>
92
+ </form>
93
+ `);
94
+ const form = document.querySelector('form');
95
+ const wSlider = document.querySelector('w-slider');
96
+ const wSliderThumb = wSlider.querySelector('w-slider-thumb');
97
+ // sanity
98
+ expect(form).not.toBeNull();
99
+ expect(wSlider).not.toBeNull();
100
+ expect(wSliderThumb).not.toBeNull();
101
+ expect(wSliderThumb.value).toBe('3');
102
+ expect(Object.fromEntries(new FormData(form).entries())['zero-ten']).toBe('3');
103
+ wSliderThumb.value = '5';
104
+ await wSliderThumb.updateComplete;
105
+ expect(wSliderThumb.value).toBe('5');
106
+ expect(Object.fromEntries(new FormData(form).entries())['zero-ten']).toBe('5');
107
+ // Reset the form
108
+ form.reset();
109
+ await wSliderThumb.updateComplete;
110
+ expect(wSliderThumb.value).toBe('3');
111
+ expect(Object.fromEntries(new FormData(form).entries())['zero-ten']).toBe('3');
112
+ });
113
+ // labelFormatter tests
114
+ test('labelFormatter formats min and max labels', async () => {
115
+ const component = html `
116
+ <w-slider label="Production year" min="1950" max="2025">
117
+ <w-slider-thumb slot="from" name="from"></w-slider-thumb>
118
+ <w-slider-thumb slot="to" name="to"></w-slider-thumb>
119
+ </w-slider>
120
+ `;
121
+ render(component);
122
+ const slider = document.querySelector('w-slider');
123
+ slider.labelFormatter = (slot) => {
124
+ if (slot === 'from')
125
+ return 'Before 1950';
126
+ return '2025+';
127
+ };
128
+ await slider.updateComplete;
129
+ const fromThumb = document.querySelector('w-slider-thumb[slot="from"]');
130
+ const toThumb = document.querySelector('w-slider-thumb[slot="to"]');
131
+ await fromThumb.updateComplete;
132
+ await toThumb.updateComplete;
133
+ // Check the visible labels are formatted
134
+ const fromMarker = fromThumb.shadowRoot.querySelector('.w-slider-thumb__from-marker');
135
+ const toMarker = toThumb.shadowRoot.querySelector('.w-slider-thumb__to-marker');
136
+ expect(fromMarker.textContent.trim()).toBe('Before 1950');
137
+ expect(toMarker.textContent.trim()).toBe('2025+');
138
+ });
139
+ test('labelFormatter can hide labels by returning empty string', async () => {
140
+ const component = html `
141
+ <w-slider label="Hidden labels" min="0" max="100">
142
+ <w-slider-thumb name="value"></w-slider-thumb>
143
+ </w-slider>
144
+ `;
145
+ render(component);
146
+ const slider = document.querySelector('w-slider');
147
+ slider.labelFormatter = () => '';
148
+ await slider.updateComplete;
149
+ const thumb = document.querySelector('w-slider-thumb');
150
+ await thumb.updateComplete;
151
+ const fromMarker = thumb.shadowRoot.querySelector('.w-slider-thumb__from-marker');
152
+ const toMarker = thumb.shadowRoot.querySelector('.w-slider-thumb__to-marker');
153
+ expect(fromMarker.textContent.trim()).toBe('');
154
+ expect(toMarker.textContent.trim()).toBe('');
155
+ });
156
+ // valueFormatter tests
157
+ test('valueFormatter formats tooltip display value', async () => {
158
+ const component = html `
159
+ <w-slider label="Price" min="0" max="100000">
160
+ <w-slider-thumb name="price" value="50000"></w-slider-thumb>
161
+ </w-slider>
162
+ `;
163
+ render(component);
164
+ const slider = document.querySelector('w-slider');
165
+ // Format with custom suffix
166
+ slider.valueFormatter = (value) => {
167
+ if (!value)
168
+ return '0';
169
+ return `${value} formatted`;
170
+ };
171
+ await slider.updateComplete;
172
+ const thumb = document.querySelector('w-slider-thumb');
173
+ await thumb.updateComplete;
174
+ // Check the tooltip message content in w-attention
175
+ const tooltipMessage = thumb.shadowRoot.querySelector('w-attention span[slot="message"]');
176
+ expect(tooltipMessage.textContent.trim()).toBe('50000 formatted');
177
+ });
178
+ // WCAG 2.1 Accessibility Tests
179
+ // Fieldset and legend tests (WCAG 1.3.1 Info and Relationships, 4.1.2 Name, Role, Value)
180
+ test('slider uses fieldset with legend for proper grouping', async () => {
181
+ const component = html `
182
+ <w-slider label="Volume control" min="0" max="100">
183
+ <w-slider-thumb name="volume"></w-slider-thumb>
184
+ </w-slider>
185
+ `;
186
+ render(component);
187
+ const slider = document.querySelector('w-slider');
188
+ await slider.updateComplete;
189
+ const fieldset = slider.shadowRoot.querySelector('fieldset');
190
+ const legend = fieldset.querySelector('legend');
191
+ expect(fieldset).not.toBeNull();
192
+ expect(legend).not.toBeNull();
193
+ expect(legend.textContent.trim()).toBe('Volume control');
194
+ });
195
+ test('range slider fieldset groups both thumbs together', async () => {
196
+ const component = html `
197
+ <w-slider label="Price range" min="0" max="1000">
198
+ <w-slider-thumb slot="from" aria-label="Minimum price" name="min"></w-slider-thumb>
199
+ <w-slider-thumb slot="to" aria-label="Maximum price" name="max"></w-slider-thumb>
200
+ </w-slider>
201
+ `;
202
+ render(component);
203
+ const slider = document.querySelector('w-slider');
204
+ await slider.updateComplete;
205
+ const fieldset = slider.shadowRoot.querySelector('fieldset');
206
+ const legend = fieldset.querySelector('legend');
207
+ expect(fieldset).not.toBeNull();
208
+ expect(legend.textContent.trim()).toBe('Price range');
209
+ // Both thumbs should be slotted within the fieldset
210
+ const slots = fieldset.querySelectorAll('slot');
211
+ expect(slots.length).toBeGreaterThan(0);
212
+ });
213
+ // Input type range accessibility (WCAG 4.1.2 Name, Role, Value)
214
+ test('range input has proper aria-label', async () => {
215
+ const component = html `
216
+ <w-slider label="Brightness" min="0" max="100">
217
+ <w-slider-thumb name="brightness"></w-slider-thumb>
218
+ </w-slider>
219
+ `;
220
+ render(component);
221
+ const thumb = document.querySelector('w-slider-thumb');
222
+ await thumb.updateComplete;
223
+ const rangeInput = thumb.shadowRoot.querySelector('input[type="range"]');
224
+ expect(rangeInput.getAttribute('aria-label')).toBe('Brightness');
225
+ });
226
+ test('range input uses explicit aria-label when provided', async () => {
227
+ const component = html `
228
+ <w-slider label="Volume" min="0" max="100">
229
+ <w-slider-thumb aria-label="Custom volume control" name="volume"></w-slider-thumb>
230
+ </w-slider>
231
+ `;
232
+ render(component);
233
+ const thumb = document.querySelector('w-slider-thumb');
234
+ await thumb.updateComplete;
235
+ const rangeInput = thumb.shadowRoot.querySelector('input[type="range"]');
236
+ expect(rangeInput.getAttribute('aria-label')).toBe('Custom volume control');
237
+ });
238
+ // Range slider accessibility for from/to labels
239
+ test('range slider thumbs get appropriate aria-labels when not explicitly set', async () => {
240
+ const component = html `
241
+ <w-slider label="Year range" min="2000" max="2025">
242
+ <w-slider-thumb slot="from" name="from-year"></w-slider-thumb>
243
+ <w-slider-thumb slot="to" name="to-year"></w-slider-thumb>
244
+ </w-slider>
245
+ `;
246
+ render(component);
247
+ const fromThumb = document.querySelector('w-slider-thumb[slot="from"]');
248
+ const toThumb = document.querySelector('w-slider-thumb[slot="to"]');
249
+ await fromThumb.updateComplete;
250
+ await toThumb.updateComplete;
251
+ const fromRange = fromThumb.shadowRoot.querySelector('input[type="range"]');
252
+ const toRange = toThumb.shadowRoot.querySelector('input[type="range"]');
253
+ // Should append min/max to the parent label
254
+ expect(fromRange.getAttribute('aria-label')).toBe('Year range min');
255
+ expect(toRange.getAttribute('aria-label')).toBe('Year range max');
256
+ });
257
+ // Input type number accessibility (WCAG 4.1.2 Name, Role, Value)
258
+ test('number input (textfield) has proper aria-label', async () => {
259
+ const component = html `
260
+ <w-slider label="Quantity" min="0" max="100">
261
+ <w-slider-thumb name="qty"></w-slider-thumb>
262
+ </w-slider>
263
+ `;
264
+ render(component);
265
+ const thumb = document.querySelector('w-slider-thumb');
266
+ await thumb.updateComplete;
267
+ const textfield = thumb.shadowRoot.querySelector('w-textfield');
268
+ expect(textfield.getAttribute('aria-label')).toBe('Quantity');
269
+ });
270
+ // Disabled state accessibility
271
+ test('disabled slider marks all inputs as disabled', async () => {
272
+ const component = html `
273
+ <w-slider label="Disabled slider" min="0" max="100" disabled>
274
+ <w-slider-thumb name="value"></w-slider-thumb>
275
+ </w-slider>
276
+ `;
277
+ render(component);
278
+ const thumb = document.querySelector('w-slider-thumb');
279
+ await thumb.updateComplete;
280
+ const rangeInput = thumb.shadowRoot.querySelector('input[type="range"]');
281
+ const textfield = thumb.shadowRoot.querySelector('w-textfield');
282
+ expect(rangeInput.disabled).toBe(true);
283
+ expect(textfield.hasAttribute('disabled')).toBe(true);
284
+ });
285
+ test('disabled fieldset communicates disabled state', async () => {
286
+ const component = html `
287
+ <w-slider label="Disabled control" min="0" max="100" disabled>
288
+ <w-slider-thumb name="value"></w-slider-thumb>
289
+ </w-slider>
290
+ `;
291
+ render(component);
292
+ const slider = document.querySelector('w-slider');
293
+ await slider.updateComplete;
294
+ const fieldset = slider.shadowRoot.querySelector('fieldset');
295
+ expect(fieldset.disabled).toBe(true);
296
+ });
297
+ // Error state accessibility (WCAG 3.3.1 Error Identification)
298
+ test('invalid slider sets aria-invalid on fieldset', async () => {
299
+ const component = html `
300
+ <w-slider label="Invalid slider" min="0" max="100" invalid error="Please select a value">
301
+ <w-slider-thumb name="value"></w-slider-thumb>
302
+ </w-slider>
303
+ `;
304
+ render(component);
305
+ const slider = document.querySelector('w-slider');
306
+ await slider.updateComplete;
307
+ // Wait for the state update triggered in connectedCallback
308
+ await slider.updateComplete;
309
+ const fieldset = slider.shadowRoot.querySelector('fieldset');
310
+ expect(fieldset.getAttribute('aria-invalid')).toBe('true');
311
+ });
312
+ test('error message is visible when slider is invalid', async () => {
313
+ const component = html `
314
+ <w-slider label="Error slider" min="0" max="100" invalid error="Value is required">
315
+ <w-slider-thumb name="value"></w-slider-thumb>
316
+ </w-slider>
317
+ `;
318
+ render(component);
319
+ const slider = document.querySelector('w-slider');
320
+ await slider.updateComplete;
321
+ // Wait for the state update triggered in connectedCallback
322
+ await slider.updateComplete;
323
+ const errorMessage = slider.shadowRoot.querySelector('.w-slider__error');
324
+ expect(errorMessage).not.toBeNull();
325
+ expect(errorMessage.textContent.trim()).toBe('Value is required');
326
+ });
327
+ // Screen reader min/max range announcement
328
+ test('screen reader can access min and max range values', async () => {
329
+ const component = html `
330
+ <w-slider label="Range with bounds" min="10" max="90">
331
+ <w-slider-thumb name="value"></w-slider-thumb>
332
+ </w-slider>
333
+ `;
334
+ render(component);
335
+ const slider = document.querySelector('w-slider');
336
+ await slider.updateComplete;
337
+ const thumb = document.querySelector('w-slider-thumb');
338
+ await thumb.updateComplete;
339
+ // Screen reader only text should contain min and max info
340
+ const srOnlyText = thumb.shadowRoot.querySelector('.sr-only');
341
+ expect(srOnlyText).not.toBeNull();
342
+ expect(srOnlyText.textContent).toContain('10');
343
+ expect(srOnlyText.textContent).toContain('90');
344
+ });
345
+ test('screen reader range announcement uses labelFormatter values', async () => {
346
+ const component = html `
347
+ <w-slider label="Formatted range" min="0" max="100">
348
+ <w-slider-thumb name="value"></w-slider-thumb>
349
+ </w-slider>
350
+ `;
351
+ render(component);
352
+ const slider = document.querySelector('w-slider');
353
+ slider.labelFormatter = (slot) => {
354
+ if (slot === 'from')
355
+ return 'Zero';
356
+ return 'One hundred';
357
+ };
358
+ await slider.updateComplete;
359
+ const thumb = document.querySelector('w-slider-thumb');
360
+ await thumb.updateComplete;
361
+ const srOnlyText = thumb.shadowRoot.querySelector('.sr-only');
362
+ expect(srOnlyText.textContent).toContain('Zero');
363
+ expect(srOnlyText.textContent).toContain('One hundred');
364
+ });
365
+ // Required field accessibility (WCAG 3.3.2 Labels or Instructions)
366
+ test('required slider passes required state to thumb', async () => {
367
+ render(html `
368
+ <w-slider label="Required slider" min="0" max="100" required>
369
+ <w-slider-thumb name="value"></w-slider-thumb>
370
+ </w-slider>
371
+ `);
372
+ const slider = document.querySelector('w-slider');
373
+ await slider.updateComplete;
374
+ const thumb = document.querySelector('w-slider-thumb');
375
+ await thumb.updateComplete;
376
+ // The required state should be synced from slider to thumb
377
+ expect(thumb.required).toBe(true);
378
+ // Verify the slider has required attribute in HTML
379
+ expect(slider.hasAttribute('required')).toBe(true);
380
+ });
@@ -0,0 +1 @@
1
+ export declare const wSliderStyles: import("lit").CSSResult;
@@ -0,0 +1,154 @@
1
+ import { css } from 'lit';
2
+ export const wSliderStyles = css `
3
+ .w-slider {
4
+ position: relative;
5
+ border: none;
6
+ padding: 0;
7
+ margin: 0;
8
+ display: grid;
9
+ width: 100%;
10
+ grid-template-areas:
11
+ "label"
12
+ "description"
13
+ "slider"
14
+ "error";
15
+ grid-template-columns: 1fr;
16
+
17
+ --w-slider-track-background: var(--w-s-color-background-disabled-subtle);
18
+ --w-slider-track-active-background: var(--w-s-color-background-primary);
19
+ --w-slider-track-height: 4px;
20
+ --w-slider-track-active-height: 6px;
21
+ --w-slider-thumb-background: var(--w-s-color-background-primary);
22
+ --w-slider-thumb-background-hover: var(
23
+ --w-s-color-background-primary-hover
24
+ );
25
+ --w-slider-thumb-size: 28px;
26
+ --w-slider-thumb-offset: calc(var(--w-slider-thumb-size) / 2);
27
+ --w-slider-marker-color: var(--w-s-color-border);
28
+
29
+
30
+ /* Vertical position of range and markers */
31
+ --_range-top: calc(
32
+ var(--w-slider-thumb-size) / 2 +
33
+ calc(
34
+ var(--w-slider-track-active-height) - calc(
35
+ var(--w-slider-track-height) / 2
36
+ ) + 1px
37
+ )
38
+ );
39
+ }
40
+
41
+ .w-slider[disabled] {
42
+ --w-slider-track-active-background: var(--w-s-color-background-disabled-subtle);
43
+ --w-slider-thumb-background: var(--w-s-color-background-disabled-subtle);
44
+ --w-slider-thumb-background-hover: var(--w-s-color-background-disabled-subtle);
45
+ }
46
+
47
+ .w-slider__label {
48
+ grid-area: label;
49
+ font-size: var(--w-font-size-s);
50
+ line-height: var(--w-line-height-s);
51
+ font-weight: bold;
52
+ padding-bottom: 8px;
53
+ color: var(--w-s-color-text);
54
+ }
55
+
56
+ .w-slider__description {
57
+ grid-area: description;
58
+ }
59
+
60
+ .w-slider__range {
61
+ align-self: center;
62
+ background: var(--w-slider-track-background);
63
+ border-radius: 4px;
64
+ height: var(--w-slider-track-active-height);
65
+ position: absolute;
66
+ /* For range sliders to avoid overlapping the slider thumbs we transform them to
67
+ be visually to the left and right of their respective input[type="range"]. This
68
+ padding is here so the active-range element is the same width as the input fields. */
69
+ padding-inline-start: var(--active-range-inline-start-padding, 0);
70
+ padding-inline-end: var(--active-range-inline-end-padding, 0);
71
+ top: var(--_range-top);
72
+ left: 0;
73
+ right: 0;
74
+ grid-area: slider;
75
+ }
76
+
77
+ .w-slider__active-range {
78
+ box-sizing: content-box;
79
+ background: var(--w-slider-track-active-background);
80
+ height: var(--w-slider-track-active-height);
81
+
82
+ border-start-start-radius: var(--active-range-border-radius, 0);
83
+ border-end-start-radius: var(--active-range-border-radius, 0);
84
+
85
+ /* takes over-under into the calculation if set, as this makes the ranges longer in reality */
86
+ --max-with-offset: calc(var(--max) + var(--over-under-offset, 0));
87
+
88
+ /* calculate the offset for the "from" thumb in percentage to move the range visualisation from the left edge, using max() to avoid going off screen */
89
+ --offset-percentage: calc(calc(var(--from) - var(--min)) / calc(var(--max-with-offset) - var(--min)) * 100);
90
+ margin-left: calc(max(var(--offset-percentage) * 1%, 0px) - var(--range-slider-magic-pixel, 0px));
91
+
92
+ /* calculate the width of the selected range in percentage, clamped between min/max */
93
+ --value-range: calc(var(--max-with-offset) - var(--min));
94
+ --range-span-percentage: calc(calc(min(var(--to), var(--max-with-offset)) - max(var(--min), var(--from))) / var(--value-range) * 100 );
95
+ width: calc(var(--range-span-percentage) * 1% + var(--range-slider-magic-pixel, 0px));
96
+
97
+
98
+ z-index: 1;
99
+ }
100
+
101
+ .w-slider__markers {
102
+ --_marker-height: 7px;
103
+ --_marker-width: 1px;
104
+
105
+ align-self: center;
106
+
107
+ /* Creates a linear gradient with --_marker-width wide markers
108
+ followed by enough transparent that we can repeat the gradient
109
+ along the X axis and have markers visible where we want them. */
110
+ background: linear-gradient(
111
+ to right,
112
+ var(--w-slider-marker-color) var(--_marker-width),
113
+ rgba(0, 0, 0, 0) 1px calc(100% - 1px),
114
+ var(--w-slider-marker-color) 100%
115
+ )
116
+ repeat-x;
117
+ --_step-width-as-percent: calc(var(--markers) / var(--_value-range) * 100);
118
+ background-size: calc(var(--_step-width-as-percent) * 1%)
119
+ var(--_marker-height);
120
+
121
+ background-position: bottom 0 left 8px right 8px;
122
+ position: absolute;
123
+ top: calc(var(--_range-top) + 2px);
124
+ left: 1px;
125
+ right: 1px;
126
+ grid-area: slider;
127
+ height: var(--_marker-height);
128
+ margin-inline: var(--w-slider-thumb-offset);
129
+ }
130
+
131
+ .w-slider__error {
132
+ grid-area: error;
133
+ padding-top: 8px;
134
+ font-size: var(--w-font-size-xs);
135
+ line-height: var(--w-line-height-xs);
136
+ color: var(--w-s-color-text-negative);
137
+ }
138
+
139
+ .w-slider__help-text {
140
+ grid-area: error;
141
+ padding-top: 8px;
142
+ font-size: var(--w-font-size-xs);
143
+ line-height: var(--w-line-height-xs);
144
+ }
145
+
146
+ slot::slotted(w-slider-thumb) {
147
+ position: static;
148
+ top: 0;
149
+ left: 0;
150
+ right: 0;
151
+ bottom: 0;
152
+ grid-area: slider;
153
+ }
154
+ `;
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,2 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `*,:before,:after{--w-rotate:0;--w-rotate-x:0;--w-rotate-y:0;--w-rotate-z:0;--w-scale-x:1;--w-scale-y:1;--w-scale-z:1;--w-skew-x:0;--w-skew-y:0;--w-translate-x:0;--w-translate-y:0;--w-translate-z:0}.hidden{display:none}.absolute{position:absolute}.relative{position:relative}.static{position:static}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}`;
@@ -0,0 +1,2 @@
1
+ import { WarpSliderThumb } from './slider-thumb.js';
2
+ export declare const SliderThumb: import("@lit/react").ReactWebComponent<WarpSliderThumb, {}>;
@@ -0,0 +1,8 @@
1
+ import { createComponent } from '@lit/react';
2
+ import React from 'react';
3
+ import { WarpSliderThumb } from './slider-thumb.js';
4
+ export const SliderThumb = createComponent({
5
+ tagName: 'w-slider-thumb',
6
+ elementClass: WarpSliderThumb,
7
+ react: React,
8
+ });
@@ -0,0 +1,2 @@
1
+ import polyfill from '@oddbird/css-anchor-positioning/fn';
2
+ export default polyfill;
@@ -0,0 +1,3 @@
1
+ // This file is only here to give the Storybook Vite dev server a target
2
+ import polyfill from '@oddbird/css-anchor-positioning/fn';
3
+ export default polyfill;
@@ -0,0 +1,6 @@
1
+ import { EventName } from '@lit/react';
2
+ import { WarpSliderThumb } from './slider-thumb.js';
3
+ export declare const SliderThumb: import("@lit/react").ReactWebComponent<WarpSliderThumb, {
4
+ onSliderValidity: EventName<CustomEvent>;
5
+ 'onslider-validity': EventName<CustomEvent>;
6
+ }>;
@@ -0,0 +1,15 @@
1
+ import { createComponent } from '@lit/react';
2
+ import { LitElement } from 'lit';
3
+ import React from 'react';
4
+ // decouple from CDN by providing a dummy class
5
+ class Component extends LitElement {
6
+ }
7
+ export const SliderThumb = createComponent({
8
+ tagName: 'w-slider-thumb',
9
+ elementClass: Component,
10
+ react: React,
11
+ events: {
12
+ onSliderValidity: 'slidervalidity',
13
+ 'onslider-validity': 'slidervalidity', // should be slider-validity
14
+ },
15
+ });
@@ -0,0 +1,72 @@
1
+ import { LitElement, PropertyValues } from 'lit';
2
+ import type { WarpTextField } from '../textfield/textfield.js';
3
+ export type SliderSlot = 'to' | 'from';
4
+ declare const WarpSliderThumb_base: import("@open-wc/form-control").Constructor<import("@open-wc/form-control").FormControlInterface> & typeof LitElement;
5
+ /**
6
+ * Component to place inside a `<w-slider>`.
7
+ *
8
+ * [See Storybook for usage examples](https://warp-ds.github.io/elements/?path=/docs/forms-slider-and-range-slider--docs)
9
+ */
10
+ declare class WarpSliderThumb extends WarpSliderThumb_base {
11
+ #private;
12
+ static shadowRootOptions: {
13
+ delegatesFocus: boolean;
14
+ mode: ShadowRootMode;
15
+ serializable?: boolean;
16
+ slotAssignment?: SlotAssignmentMode;
17
+ };
18
+ static styles: import("lit").CSSResult[];
19
+ ariaLabel: string;
20
+ ariaDescription: string;
21
+ name: string;
22
+ value: string;
23
+ /** @internal Set by `<w-slider>` */
24
+ disabled: boolean;
25
+ /** @internal Set by `<w-slider>` */
26
+ invalid: boolean;
27
+ /** @internal Set by `<w-slider>` */
28
+ openEnded: boolean;
29
+ placeholder: string;
30
+ /** @internal Set by `<w-slider>` */
31
+ markers: string;
32
+ /** @internal Set by `<w-slider>` */
33
+ required: boolean;
34
+ /** @internal Set by `<w-slider>` */
35
+ step: number;
36
+ /** @internal Set by `<w-slider>` */
37
+ min: string;
38
+ /** @internal Set by `<w-slider>` */
39
+ max: string;
40
+ /** @internal Set by `<w-slider>` */
41
+ suffix: string;
42
+ /** @internal Formatter for the tooltip and input mask values. Set by `<w-slider>`. */
43
+ valueFormatter: (value: string, slot: SliderSlot) => string;
44
+ /** @internal Formatter for the min and max labels below the range. Set by `<w-slider>`. */
45
+ labelFormatter: (slot: SliderSlot) => string;
46
+ range: HTMLInputElement;
47
+ tooltipTarget: HTMLOutputElement;
48
+ textfield: WarpTextField;
49
+ /** @internal */
50
+ _showTooltip: boolean;
51
+ /** @internal */
52
+ _inputHasFocus: boolean;
53
+ /** @internal */
54
+ _hiddenTextfield: boolean;
55
+ resetFormControl(): void;
56
+ /**
57
+ * Reference to the anchor positioning style element used by the polyfill.
58
+ * @internal
59
+ */
60
+ anchorPositioningStyleElement: HTMLStyleElement | null;
61
+ updateFieldAfterValidation(): Promise<void>;
62
+ connectedCallback(): Promise<void>;
63
+ get boundaryValue(): string;
64
+ /** Value to display in the textfield (shows boundary when focused on empty value) */
65
+ get textFieldDisplayValue(): string;
66
+ /** Value to display in the tooltip */
67
+ get tooltipDisplayValue(): string | number;
68
+ get ariaDescriptionText(): string;
69
+ updated(changedProperties: PropertyValues<this>): void;
70
+ render(): import("lit").TemplateResult<1>;
71
+ }
72
+ export { WarpSliderThumb };