@sveltia/ui 0.72.1 → 0.73.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.
- package/dist/components/alert/alert.svelte +29 -5
- package/dist/components/alert/alert.svelte.d.ts +8 -4
- package/dist/components/alert/infobar.svelte +21 -5
- package/dist/components/alert/infobar.svelte.d.ts +10 -4
- package/dist/components/button/button.svelte +5 -1
- package/dist/components/button/select-button-group.svelte +1 -1
- package/dist/components/calendar/calendar.d.ts +1 -4
- package/dist/components/calendar/calendar.js +17 -0
- package/dist/components/calendar/calendar.svelte +67 -11
- package/dist/components/checkbox/checkbox-group.svelte +3 -1
- package/dist/components/checkbox/checkbox.svelte +3 -1
- package/dist/components/dialog/dialog.svelte +14 -2
- package/dist/components/dialog/prompt-dialog.svelte +9 -2
- package/dist/components/dialog/prompt-dialog.svelte.d.ts +4 -2
- package/dist/components/disclosure/disclosure.svelte +2 -1
- package/dist/components/drawer/drawer.svelte +28 -3
- package/dist/components/drawer/drawer.svelte.d.ts +16 -2
- package/dist/components/grid/grid-body.svelte +3 -1
- package/dist/components/grid/grid-foot.svelte +3 -1
- package/dist/components/grid/grid-head.svelte +3 -1
- package/dist/components/listbox/option-group.svelte +2 -1
- package/dist/components/listbox/option.svelte +4 -0
- package/dist/components/menu/menu-item-group.svelte +3 -1
- package/dist/components/radio/radio.svelte +21 -3
- package/dist/components/radio/radio.svelte.d.ts +12 -2
- package/dist/components/resizable-pane/resizable-handle.svelte +12 -0
- package/dist/components/select/combobox.svelte +69 -1
- package/dist/components/select/select-tags.svelte +34 -23
- package/dist/components/slider/slider.d.ts +6 -0
- package/dist/components/slider/slider.js +40 -0
- package/dist/components/slider/slider.svelte +45 -11
- package/dist/components/slider/slider.svelte.d.ts +14 -2
- package/dist/components/switch/switch.svelte +1 -1
- package/dist/components/table/table-body.svelte +3 -1
- package/dist/components/table/table-foot.svelte +3 -1
- package/dist/components/table/table-head.svelte +3 -1
- package/dist/components/text-editor/code-editor.svelte +22 -1
- package/dist/components/text-editor/code-editor.svelte.d.ts +28 -0
- package/dist/components/text-editor/shiki/version.d.ts +1 -1
- package/dist/components/text-editor/shiki/version.js +1 -1
- package/dist/components/text-editor/text-editor.svelte +23 -0
- package/dist/components/text-editor/text-editor.svelte.d.ts +28 -0
- package/dist/components/text-field/number-input.svelte +7 -17
- package/dist/components/text-field/search-bar.svelte +1 -0
- package/dist/components/text-field/text-input.svelte +1 -1
- package/dist/components/toast/toast.svelte +58 -4
- package/dist/components/toast/toast.svelte.d.ts +4 -2
- package/dist/components/tree/tree-item.svelte +3 -2
- package/dist/components/util/app-shell.svelte +290 -5
- package/dist/components/util/popup.svelte +1 -1
- package/dist/locales/ar.yaml +20 -0
- package/dist/locales/bg.yaml +20 -0
- package/dist/locales/ca.yaml +20 -0
- package/dist/locales/cs.yaml +20 -0
- package/dist/locales/da.yaml +20 -0
- package/dist/locales/de.yaml +20 -0
- package/dist/locales/el.yaml +20 -0
- package/dist/locales/en-CA.yaml +20 -0
- package/dist/locales/en-GB.yaml +20 -0
- package/dist/locales/en-US.yaml +20 -0
- package/dist/locales/es-CO.yaml +20 -0
- package/dist/locales/fa.yaml +20 -0
- package/dist/locales/fi.yaml +20 -0
- package/dist/locales/fr.yaml +20 -0
- package/dist/locales/hr.yaml +20 -0
- package/dist/locales/it.yaml +20 -0
- package/dist/locales/ja.yaml +20 -0
- package/dist/locales/ko.yaml +20 -0
- package/dist/locales/nl.yaml +20 -0
- package/dist/locales/pl.yaml +20 -0
- package/dist/locales/pt-BR.yaml +20 -0
- package/dist/locales/pt-PT.yaml +20 -0
- package/dist/locales/ru.yaml +20 -0
- package/dist/locales/sv.yaml +20 -0
- package/dist/locales/tr.yaml +20 -0
- package/dist/locales/uk.yaml +20 -0
- package/dist/locales/vi.yaml +20 -0
- package/dist/locales/zh-CN.yaml +20 -0
- package/dist/locales/zh-TW.yaml +20 -0
- package/dist/services/group.svelte.js +60 -0
- package/dist/services/popup.svelte.js +24 -1
- package/dist/services/select.svelte.d.ts +5 -0
- package/dist/services/select.svelte.js +8 -0
- package/dist/services/type-ahead.d.ts +26 -0
- package/dist/services/type-ahead.js +81 -0
- package/dist/styles/core.scss +14 -3
- package/dist/styles/variables.scss +119 -1
- package/dist/typedefs.d.ts +7 -1
- package/dist/typedefs.js +4 -1
- package/package.json +7 -7
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
@see https://w3c.github.io/aria/#rowgroup
|
|
6
6
|
-->
|
|
7
7
|
<script>
|
|
8
|
+
import { _ } from '@sveltia/i18n';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @import { Snippet } from 'svelte';
|
|
10
12
|
*/
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
{...restProps}
|
|
32
34
|
role="rowgroup"
|
|
33
35
|
class="sui grid-foot {className}"
|
|
34
|
-
aria-roledescription=
|
|
36
|
+
aria-roledescription={_('_sui.role_descriptions.grid_foot')}
|
|
35
37
|
>
|
|
36
38
|
{@render children?.()}
|
|
37
39
|
</div>
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
@see https://w3c.github.io/aria/#rowgroup
|
|
6
6
|
-->
|
|
7
7
|
<script>
|
|
8
|
+
import { _ } from '@sveltia/i18n';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @import { Snippet } from 'svelte';
|
|
10
12
|
*/
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
{...restProps}
|
|
32
34
|
role="rowgroup"
|
|
33
35
|
class="sui grid-head {className}"
|
|
34
|
-
aria-roledescription=
|
|
36
|
+
aria-roledescription={_('_sui.role_descriptions.grid_head')}
|
|
35
37
|
>
|
|
36
38
|
{@render children?.()}
|
|
37
39
|
</div>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
@see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-grouped/
|
|
8
8
|
-->
|
|
9
9
|
<script>
|
|
10
|
+
import { _ } from '@sveltia/i18n';
|
|
10
11
|
import TruncatedText from '../typography/truncated-text.svelte';
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
aria-hidden={hidden}
|
|
51
52
|
aria-disabled={disabled}
|
|
52
53
|
aria-labelledby="{id}-label"
|
|
53
|
-
aria-roledescription=
|
|
54
|
+
aria-roledescription={_('_sui.role_descriptions.option_group')}
|
|
54
55
|
>
|
|
55
56
|
<div role="none" id="{id}-label" class="label">
|
|
56
57
|
<TruncatedText>
|
|
@@ -202,6 +202,10 @@
|
|
|
202
202
|
color: var(--sui-highlight-foreground-color);
|
|
203
203
|
background-color: var(--sui-hover-background-color);
|
|
204
204
|
}
|
|
205
|
+
.option :global(.focused) {
|
|
206
|
+
outline-color: var(--sui-focus-ring-color);
|
|
207
|
+
outline-offset: calc(var(--sui-focus-ring-width) * -1);
|
|
208
|
+
}
|
|
205
209
|
.option :global(.icon.check) {
|
|
206
210
|
margin: -2px;
|
|
207
211
|
}</style>
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
A menu item group.
|
|
4
4
|
-->
|
|
5
5
|
<script>
|
|
6
|
+
import { _ } from '@sveltia/i18n';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* @import { CommonEventHandlers } from '../../typedefs';
|
|
8
10
|
*/
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
aria-hidden={hidden}
|
|
44
46
|
aria-disabled={disabled}
|
|
45
47
|
aria-labelledby={title ? `${id}-title` : undefined}
|
|
46
|
-
aria-roledescription=
|
|
48
|
+
aria-roledescription={_('_sui.role_descriptions.menu_item_group')}
|
|
47
49
|
>
|
|
48
50
|
{#if title}
|
|
49
51
|
<div role="none" class="title" id="{id}-title">{title}</div>
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* @property {any} [value] The `data-value` attribute on the `<button>` element.
|
|
25
25
|
* @property {string} [valueType] Data type of the `value`. Typically `string`, `number` or
|
|
26
26
|
* `boolean`. Default: auto detect.
|
|
27
|
-
* @property {string} [label] Text label displayed next to the
|
|
27
|
+
* @property {string} [label] Text label displayed next to the radio button.
|
|
28
|
+
* @property {string} [ariaLabel] `aria-label` attribute on the `<button>` element, for a radio
|
|
29
|
+
* button without a visible label.
|
|
28
30
|
* @property {string} [group] The two-way bound variable to manage the state of a group of radio
|
|
29
31
|
* buttons. It works in the same way as the [`<input
|
|
30
32
|
* bind:group>`](https://svelte.dev/docs/svelte/bind#input-bind:group) of Svelte.
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
value = undefined,
|
|
48
50
|
valueType = undefined,
|
|
49
51
|
label = undefined,
|
|
52
|
+
ariaLabel = undefined,
|
|
50
53
|
group = $bindable(),
|
|
51
54
|
children,
|
|
52
55
|
onChange,
|
|
@@ -63,6 +66,14 @@
|
|
|
63
66
|
*/
|
|
64
67
|
let buttonElement = $state();
|
|
65
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Accessible name given as a prop or as a plain `aria-label` attribute. It takes the place of the
|
|
71
|
+
* visible label, which the button otherwise points at — but only when there is one to point at.
|
|
72
|
+
* @type {string | undefined}
|
|
73
|
+
*/
|
|
74
|
+
const accessibleLabel = $derived(ariaLabel ?? restProps['aria-label']);
|
|
75
|
+
const hasVisibleLabel = $derived(!!(children || label));
|
|
76
|
+
|
|
66
77
|
// Sync `checked` with `group` and `value`
|
|
67
78
|
$effect(() => {
|
|
68
79
|
if (typeof group === 'string') {
|
|
@@ -77,8 +88,11 @@
|
|
|
77
88
|
});
|
|
78
89
|
</script>
|
|
79
90
|
|
|
91
|
+
<!--
|
|
92
|
+
`restProps`, including any `aria-*` attribute, go on the `<button>`: ARIA ignores them on the
|
|
93
|
+
presentational wrapper.
|
|
94
|
+
-->
|
|
80
95
|
<span
|
|
81
|
-
{...restProps}
|
|
82
96
|
role="none"
|
|
83
97
|
class="sui radio {className}"
|
|
84
98
|
class:disabled
|
|
@@ -90,6 +104,7 @@
|
|
|
90
104
|
}}
|
|
91
105
|
>
|
|
92
106
|
<Button
|
|
107
|
+
{...restProps}
|
|
93
108
|
bind:element={buttonElement}
|
|
94
109
|
role="radio"
|
|
95
110
|
{id}
|
|
@@ -99,7 +114,8 @@
|
|
|
99
114
|
{value}
|
|
100
115
|
{valueType}
|
|
101
116
|
aria-checked={checked}
|
|
102
|
-
aria-
|
|
117
|
+
aria-label={accessibleLabel || undefined}
|
|
118
|
+
aria-labelledby={!accessibleLabel && hasVisibleLabel ? `${id}-label` : undefined}
|
|
103
119
|
onclick={(event) => {
|
|
104
120
|
event.preventDefault();
|
|
105
121
|
|
|
@@ -132,6 +148,8 @@
|
|
|
132
148
|
align-items: center;
|
|
133
149
|
gap: 8px;
|
|
134
150
|
margin: var(--sui-focus-ring-width);
|
|
151
|
+
min-width: 24px;
|
|
152
|
+
min-height: 24px;
|
|
135
153
|
color: var(--sui-control-foreground-color);
|
|
136
154
|
font-family: var(--sui-control-font-family);
|
|
137
155
|
font-size: var(--sui-control-font-size);
|
|
@@ -42,9 +42,14 @@ declare const Radio: import("svelte").Component<{
|
|
|
42
42
|
*/
|
|
43
43
|
valueType?: string | undefined;
|
|
44
44
|
/**
|
|
45
|
-
* Text label displayed next to the
|
|
45
|
+
* Text label displayed next to the radio button.
|
|
46
46
|
*/
|
|
47
47
|
label?: string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* `aria-label` attribute on the `<button>` element, for a radio
|
|
50
|
+
* button without a visible label.
|
|
51
|
+
*/
|
|
52
|
+
ariaLabel?: string | undefined;
|
|
48
53
|
/**
|
|
49
54
|
* The two-way bound variable to manage the state of a group of radio
|
|
50
55
|
* buttons. It works in the same way as the [`<input
|
|
@@ -101,9 +106,14 @@ type Props = {
|
|
|
101
106
|
*/
|
|
102
107
|
valueType?: string | undefined;
|
|
103
108
|
/**
|
|
104
|
-
* Text label displayed next to the
|
|
109
|
+
* Text label displayed next to the radio button.
|
|
105
110
|
*/
|
|
106
111
|
label?: string | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* `aria-label` attribute on the `<button>` element, for a radio
|
|
114
|
+
* button without a visible label.
|
|
115
|
+
*/
|
|
116
|
+
ariaLabel?: string | undefined;
|
|
107
117
|
/**
|
|
108
118
|
* The two-way bound variable to manage the state of a group of radio
|
|
109
119
|
* buttons. It works in the same way as the [`<input
|
|
@@ -284,11 +284,20 @@
|
|
|
284
284
|
pointer-events: none;
|
|
285
285
|
opacity: 0.4;
|
|
286
286
|
}
|
|
287
|
+
.resizable-handle::before {
|
|
288
|
+
content: "";
|
|
289
|
+
position: absolute;
|
|
290
|
+
inset: 0;
|
|
291
|
+
z-index: 1;
|
|
292
|
+
}
|
|
287
293
|
.resizable-handle.horizontal {
|
|
288
294
|
width: var(--sui-resizable-handle-size, 4px);
|
|
289
295
|
height: 100%;
|
|
290
296
|
cursor: col-resize;
|
|
291
297
|
}
|
|
298
|
+
.resizable-handle.horizontal::before {
|
|
299
|
+
inset-inline: calc((var(--sui-resizable-handle-size, 4px) - 24px) / 2);
|
|
300
|
+
}
|
|
292
301
|
.resizable-handle.horizontal .handle-bar {
|
|
293
302
|
width: 2px;
|
|
294
303
|
height: 40%;
|
|
@@ -299,6 +308,9 @@
|
|
|
299
308
|
height: var(--sui-resizable-handle-size, 4px);
|
|
300
309
|
cursor: row-resize;
|
|
301
310
|
}
|
|
311
|
+
.resizable-handle.vertical::before {
|
|
312
|
+
inset-block: calc((var(--sui-resizable-handle-size, 4px) - 24px) / 2);
|
|
313
|
+
}
|
|
302
314
|
.resizable-handle.vertical .handle-bar {
|
|
303
315
|
height: 2px;
|
|
304
316
|
width: 40%;
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
<script>
|
|
9
9
|
import { _ } from '@sveltia/i18n';
|
|
10
10
|
import { onMount } from 'svelte';
|
|
11
|
+
import { normalize } from '../../services/group.svelte.js';
|
|
11
12
|
import { createOptionRegistry, getSelectedItemDetail } from '../../services/select.svelte.js';
|
|
13
|
+
import { findTypeAheadMatch, TypeAhead } from '../../services/type-ahead.js';
|
|
12
14
|
import Button from '../button/button.svelte';
|
|
13
15
|
import Icon from '../icon/icon.svelte';
|
|
14
16
|
import Listbox from '../listbox/listbox.svelte';
|
|
@@ -124,6 +126,71 @@
|
|
|
124
126
|
onChange?.(new CustomEvent('Change', { detail }));
|
|
125
127
|
};
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Keystroke buffer for type-ahead on the collapsed select.
|
|
131
|
+
*/
|
|
132
|
+
const typeAhead = new TypeAhead();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Type-ahead on the collapsed, select-only combobox: a printable character picks the next option
|
|
136
|
+
* whose label starts with what has been typed, without expanding the dropdown — the way a native
|
|
137
|
+
* `<select>` behaves and the Select-Only Combobox pattern expects. There is no option element to
|
|
138
|
+
* report while the dropdown is collapsed, so the `Change` event carries the registry entry.
|
|
139
|
+
* @param {KeyboardEvent} event The `keydown` event.
|
|
140
|
+
*/
|
|
141
|
+
const onComboboxKeyDown = (event) => {
|
|
142
|
+
const { key, ctrlKey, metaKey, altKey } = event;
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
disabled ||
|
|
146
|
+
readonly ||
|
|
147
|
+
isPopupOpen ||
|
|
148
|
+
key.length !== 1 ||
|
|
149
|
+
key === ' ' ||
|
|
150
|
+
ctrlKey ||
|
|
151
|
+
metaKey ||
|
|
152
|
+
altKey
|
|
153
|
+
) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Swallow the key whether or not it matches, as a native `<select>` does. Left to the browser,
|
|
158
|
+
// a printable key on a non-editable element starts Firefox’s find-as-you-type.
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
|
|
161
|
+
const entries = registry.entries.filter((entry) => !entry.disabled);
|
|
162
|
+
|
|
163
|
+
const index = findTypeAheadMatch(
|
|
164
|
+
entries.map((entry) => normalize(entry.label ?? '')),
|
|
165
|
+
typeAhead.push(key),
|
|
166
|
+
entries.findIndex((entry) => entry.value === value),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (index === -1) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const entry = entries[index];
|
|
174
|
+
|
|
175
|
+
if (entry.value === value) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
value = entry.value;
|
|
180
|
+
_onChange();
|
|
181
|
+
onChange?.(
|
|
182
|
+
new CustomEvent('Change', {
|
|
183
|
+
detail: {
|
|
184
|
+
target: undefined,
|
|
185
|
+
type: entry.type,
|
|
186
|
+
name: entry.name,
|
|
187
|
+
label: entry.label,
|
|
188
|
+
value: entry.value,
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
127
194
|
/**
|
|
128
195
|
* The `<Listbox>` rendered for this combobox. It’s reached through {@link optionHost}, which owns
|
|
129
196
|
* it wherever the options currently live, rather than through the popup, which only holds it
|
|
@@ -254,6 +321,7 @@
|
|
|
254
321
|
aria-invalid={invalid}
|
|
255
322
|
aria-haspopup="listbox"
|
|
256
323
|
aria-label={ariaLabel}
|
|
324
|
+
onkeydown={onComboboxKeyDown}
|
|
257
325
|
>
|
|
258
326
|
<div role="none" class="label">
|
|
259
327
|
<TruncatedText>
|
|
@@ -364,7 +432,7 @@
|
|
|
364
432
|
{/if}
|
|
365
433
|
<div bind:this={listboxSlot} role="none" class="listbox-slot"></div>
|
|
366
434
|
{#if !hasMatchingOptions}
|
|
367
|
-
<div role="
|
|
435
|
+
<div role="status" class="no-options">
|
|
368
436
|
{_('_sui.combobox.no_matching_options')}
|
|
369
437
|
</div>
|
|
370
438
|
{/if}
|
|
@@ -130,16 +130,23 @@
|
|
|
130
130
|
{hidden}
|
|
131
131
|
bind:this={wrapperElement}
|
|
132
132
|
>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
<!--
|
|
134
|
+
The tags follow the APG layout grid pattern rather than a listbox: a listbox may only hold
|
|
135
|
+
options, and each tag also carries its Remove button. Each tag is a row whose first cell, the
|
|
136
|
+
label, is the focusable part that the arrow keys reorder.
|
|
137
|
+
@see https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/
|
|
138
|
+
-->
|
|
139
|
+
<span role="grid" aria-label={_('_sui.select_tags.selected_options')}>
|
|
138
140
|
{#each values as value, index (value)}
|
|
139
141
|
{@const option = optionMap.get(value)}
|
|
140
142
|
{@const label = option?.label || option?.value || value}
|
|
143
|
+
<!--
|
|
144
|
+
The drag handlers are the pointer path only; the keyboard path is the focusable label cell
|
|
145
|
+
below, so the row itself stays out of the tab order
|
|
146
|
+
-->
|
|
147
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
141
148
|
<span
|
|
142
|
-
role="
|
|
149
|
+
role="row"
|
|
143
150
|
draggable={!disabled && !readonly}
|
|
144
151
|
class:drag-source={dragIndex === index}
|
|
145
152
|
class:drop-before={dropIndex === index && dragIndex !== index && dragIndex !== index - 1}
|
|
@@ -195,8 +202,7 @@
|
|
|
195
202
|
>
|
|
196
203
|
<span
|
|
197
204
|
class="label"
|
|
198
|
-
role="
|
|
199
|
-
aria-selected="true"
|
|
205
|
+
role="gridcell"
|
|
200
206
|
tabindex={disabled || readonly ? undefined : 0}
|
|
201
207
|
onkeydown={async (event) => {
|
|
202
208
|
const targetIndex = getKeyboardMoveTarget({
|
|
@@ -215,20 +221,22 @@
|
|
|
215
221
|
{label}
|
|
216
222
|
</span>
|
|
217
223
|
{#if option}
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
<span role="gridcell">
|
|
225
|
+
<Button
|
|
226
|
+
iconic
|
|
227
|
+
size="small"
|
|
228
|
+
disabled={disabled || readonly}
|
|
229
|
+
aria-label={_('_sui.select_tags.remove_x', { values: { name: label } })}
|
|
230
|
+
onclick={() => {
|
|
231
|
+
values = values.filter((v) => v !== value);
|
|
232
|
+
onRemoveValue?.(new CustomEvent('RemoveValue', { detail: { value } }));
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
{#snippet startIcon()}
|
|
236
|
+
<Icon name="close" />
|
|
237
|
+
{/snippet}
|
|
238
|
+
</Button>
|
|
239
|
+
</span>
|
|
232
240
|
{/if}
|
|
233
241
|
</span>
|
|
234
242
|
{/each}
|
|
@@ -272,7 +280,7 @@
|
|
|
272
280
|
.select-tags.disabled > * {
|
|
273
281
|
opacity: 0.5;
|
|
274
282
|
}
|
|
275
|
-
.select-tags span[role=
|
|
283
|
+
.select-tags span[role=grid] {
|
|
276
284
|
display: contents;
|
|
277
285
|
}
|
|
278
286
|
.select-tags span[draggable] {
|
|
@@ -314,6 +322,9 @@
|
|
|
314
322
|
.select-tags span[draggable] .label {
|
|
315
323
|
outline: none;
|
|
316
324
|
}
|
|
325
|
+
.select-tags span[draggable] span[role=gridcell]:not(.label) {
|
|
326
|
+
display: contents;
|
|
327
|
+
}
|
|
317
328
|
.select-tags span[draggable] :global(button) {
|
|
318
329
|
outline-offset: -2px;
|
|
319
330
|
}
|
|
@@ -10,6 +10,12 @@ export function getSliderSteps({ min, max, step, barWidth }: {
|
|
|
10
10
|
export function toLogicalX(physicalX: number, barWidth: number, rtl: boolean): number;
|
|
11
11
|
export function findNearestStepIndex(positionList: number[], logicalX: number): number;
|
|
12
12
|
export function getSliderKeyDirection(key: string, rtl: boolean): 1 | -1 | 0;
|
|
13
|
+
export function getSliderKeyTargetIndex({ key, rtl, currentIndex, length }: {
|
|
14
|
+
key: string;
|
|
15
|
+
rtl: boolean;
|
|
16
|
+
currentIndex: number;
|
|
17
|
+
length: number;
|
|
18
|
+
}): number;
|
|
13
19
|
export function wouldCrossThumbs({ valueIndex, targetPosition, sliderPositions }: {
|
|
14
20
|
valueIndex: number;
|
|
15
21
|
targetPosition: number;
|
|
@@ -82,6 +82,46 @@ export const getSliderKeyDirection = (key, rtl) => {
|
|
|
82
82
|
return 0;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Get the step index a key moves the thumb to, per the APG Slider pattern: the arrow keys move by
|
|
87
|
+
* one step, Page Up/Down by a tenth of the range, and Home/End jump to either end.
|
|
88
|
+
* @param {object} options Options.
|
|
89
|
+
* @param {string} options.key The key pressed.
|
|
90
|
+
* @param {boolean} options.rtl Whether the layout is right-to-left.
|
|
91
|
+
* @param {number} options.currentIndex Index of the current step.
|
|
92
|
+
* @param {number} options.length Number of steps.
|
|
93
|
+
* @returns {number} Target index, clamped to the range, or `-1` if the key doesn’t move the thumb
|
|
94
|
+
* or the thumb is already where the key would take it.
|
|
95
|
+
*/
|
|
96
|
+
export const getSliderKeyTargetIndex = ({ key, rtl, currentIndex, length }) => {
|
|
97
|
+
const lastIndex = length - 1;
|
|
98
|
+
const page = Math.max(1, Math.round(length / 10));
|
|
99
|
+
/** @type {number} */
|
|
100
|
+
let index;
|
|
101
|
+
|
|
102
|
+
if (key === 'Home') {
|
|
103
|
+
index = 0;
|
|
104
|
+
} else if (key === 'End') {
|
|
105
|
+
index = lastIndex;
|
|
106
|
+
} else if (key === 'PageUp') {
|
|
107
|
+
index = currentIndex + page;
|
|
108
|
+
} else if (key === 'PageDown') {
|
|
109
|
+
index = currentIndex - page;
|
|
110
|
+
} else {
|
|
111
|
+
const direction = getSliderKeyDirection(key, rtl);
|
|
112
|
+
|
|
113
|
+
if (!direction) {
|
|
114
|
+
return -1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
index = currentIndex + direction;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
index = Math.min(lastIndex, Math.max(0, index));
|
|
121
|
+
|
|
122
|
+
return index === currentIndex ? -1 : index;
|
|
123
|
+
};
|
|
124
|
+
|
|
85
125
|
/**
|
|
86
126
|
* Whether moving one thumb of a multi-thumb slider to the given position would make it cross, or
|
|
87
127
|
* land on, the other thumb.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { onMount } from 'svelte';
|
|
13
13
|
import {
|
|
14
14
|
findNearestStepIndex,
|
|
15
|
-
|
|
15
|
+
getSliderKeyTargetIndex,
|
|
16
16
|
getSliderSteps,
|
|
17
17
|
toLogicalX,
|
|
18
18
|
wouldCrossThumbs,
|
|
@@ -28,10 +28,13 @@
|
|
|
28
28
|
* @property {number} [min] Minimum allowed value. An alias of the `aria-valuemin` attribute.
|
|
29
29
|
* @property {number} [max] Maximum allowed value. An alias of the `aria-valuemax` attribute.
|
|
30
30
|
* @property {string} [sliderLabel] `aria-label` on the slider.
|
|
31
|
+
* @property {string} [ariaLabelledby] `aria-labelledby` on a single-thumb slider, for a visible
|
|
32
|
+
* label elsewhere on the page.
|
|
31
33
|
* @property {[number, number]} [values] Value list for a multi-thumb slider.
|
|
32
34
|
* @property {[string, string]} [sliderLabels] `aria-label` on a multi-thumb slider.
|
|
33
35
|
* @property {number} [step] Step option like `<input type="range">`.
|
|
34
|
-
* @property {(string[] | number[])} [optionLabels] Visible labels on the slider.
|
|
36
|
+
* @property {(string[] | number[])} [optionLabels] Visible labels on the slider. When there is
|
|
37
|
+
* one per step, the current one is also read out as the value (`aria-valuetext`).
|
|
35
38
|
* @property {boolean} [flex] Make the text input container flexible.
|
|
36
39
|
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
37
40
|
* @property {boolean} [hidden] Whether to hide the widget.
|
|
@@ -55,6 +58,7 @@
|
|
|
55
58
|
min = 0,
|
|
56
59
|
max = 100,
|
|
57
60
|
sliderLabel = '',
|
|
61
|
+
ariaLabelledby = undefined,
|
|
58
62
|
values = $bindable(undefined),
|
|
59
63
|
sliderLabels = undefined,
|
|
60
64
|
step = 1,
|
|
@@ -79,6 +83,21 @@
|
|
|
79
83
|
let positionList = $state([]);
|
|
80
84
|
/** @type {number[]} */
|
|
81
85
|
let valueList = $state([]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the text read out for a value, when the visible option labels line up with the steps.
|
|
89
|
+
* @param {number | undefined} _value Value.
|
|
90
|
+
* @returns {string | undefined} Matching label, if any.
|
|
91
|
+
*/
|
|
92
|
+
const getValueText = (_value) => {
|
|
93
|
+
if (optionLabels.length !== valueList.length || _value === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const index = valueList.indexOf(_value);
|
|
98
|
+
|
|
99
|
+
return index === -1 ? undefined : String(optionLabels[index]);
|
|
100
|
+
};
|
|
82
101
|
let startX = $state(0);
|
|
83
102
|
let startScreenX = $state(0);
|
|
84
103
|
// eslint-disable-next-line prefer-const
|
|
@@ -128,20 +147,26 @@
|
|
|
128
147
|
}
|
|
129
148
|
|
|
130
149
|
const _value = multiThumb ? /** @type {[number, number]} */ (values)[valueIndex] : value;
|
|
131
|
-
const direction = getSliderKeyDirection(key, isRTL());
|
|
132
150
|
|
|
133
|
-
|
|
151
|
+
const index = getSliderKeyTargetIndex({
|
|
152
|
+
key,
|
|
153
|
+
rtl: isRTL(),
|
|
154
|
+
currentIndex: valueList.indexOf(_value),
|
|
155
|
+
length: valueList.length,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (index === -1) {
|
|
159
|
+
// Still swallow the key if it’s one of the slider’s, so Home/End don’t scroll the page
|
|
160
|
+
if (['Home', 'End', 'PageUp', 'PageDown'].includes(key)) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
}
|
|
163
|
+
|
|
134
164
|
return;
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
event.preventDefault();
|
|
138
168
|
event.stopPropagation();
|
|
139
169
|
|
|
140
|
-
const index =
|
|
141
|
-
(direction < 0 && _value > min) || (direction > 0 && _value < max)
|
|
142
|
-
? valueList.indexOf(_value) + direction
|
|
143
|
-
: -1;
|
|
144
|
-
|
|
145
170
|
if (index > -1) {
|
|
146
171
|
if (
|
|
147
172
|
multiThumb &&
|
|
@@ -325,7 +350,8 @@
|
|
|
325
350
|
<div
|
|
326
351
|
role="slider"
|
|
327
352
|
tabindex={disabled ? -1 : 0}
|
|
328
|
-
aria-label={multiThumb ? sliderLabels?.[0] : sliderLabel}
|
|
353
|
+
aria-label={(multiThumb ? sliderLabels?.[0] : sliderLabel) || undefined}
|
|
354
|
+
aria-labelledby={multiThumb ? undefined : ariaLabelledby}
|
|
329
355
|
aria-hidden={hidden}
|
|
330
356
|
aria-disabled={disabled}
|
|
331
357
|
aria-readonly={readonly}
|
|
@@ -333,6 +359,7 @@
|
|
|
333
359
|
aria-valuemin={min}
|
|
334
360
|
aria-valuemax={max}
|
|
335
361
|
aria-valuenow={multiThumb ? values?.[0] : value}
|
|
362
|
+
aria-valuetext={getValueText(multiThumb ? values?.[0] : value)}
|
|
336
363
|
style:inset-inline-start={`${sliderPositions[0]}px`}
|
|
337
364
|
onpointerdown={(event) => onPointerDown(event, 0)}
|
|
338
365
|
onkeydown={(event) => onKeyDown(event, 0)}
|
|
@@ -341,7 +368,7 @@
|
|
|
341
368
|
<div
|
|
342
369
|
role="slider"
|
|
343
370
|
tabindex={disabled ? -1 : 0}
|
|
344
|
-
aria-label={sliderLabels?.[1]}
|
|
371
|
+
aria-label={sliderLabels?.[1] || undefined}
|
|
345
372
|
aria-hidden={hidden}
|
|
346
373
|
aria-disabled={disabled}
|
|
347
374
|
aria-readonly={readonly}
|
|
@@ -349,6 +376,7 @@
|
|
|
349
376
|
aria-valuemin={min}
|
|
350
377
|
aria-valuemax={max}
|
|
351
378
|
aria-valuenow={values?.[1]}
|
|
379
|
+
aria-valuetext={getValueText(values?.[1])}
|
|
352
380
|
style:inset-inline-start={`${sliderPositions[1]}px`}
|
|
353
381
|
onpointerdown={(event) => onPointerDown(event, 1)}
|
|
354
382
|
onkeydown={(event) => onKeyDown(event, 1)}
|
|
@@ -421,6 +449,12 @@
|
|
|
421
449
|
background-color: var(--sui-primary-accent-color-inverted);
|
|
422
450
|
cursor: pointer;
|
|
423
451
|
}
|
|
452
|
+
[role=slider]::before {
|
|
453
|
+
content: "";
|
|
454
|
+
position: absolute;
|
|
455
|
+
inset: -4px;
|
|
456
|
+
border-radius: inherit;
|
|
457
|
+
}
|
|
424
458
|
[role=slider]:dir(ltr) {
|
|
425
459
|
transform: translate(calc((var(--sui-checkbox-height) / 2 - 1px) * -1), calc((var(--sui-checkbox-height) / 4 - 1px) * -1));
|
|
426
460
|
}
|
|
@@ -28,6 +28,11 @@ declare const Slider: import("svelte").Component<{
|
|
|
28
28
|
* `aria-label` on the slider.
|
|
29
29
|
*/
|
|
30
30
|
sliderLabel?: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* `aria-labelledby` on a single-thumb slider, for a visible
|
|
33
|
+
* label elsewhere on the page.
|
|
34
|
+
*/
|
|
35
|
+
ariaLabelledby?: string | undefined;
|
|
31
36
|
/**
|
|
32
37
|
* Value list for a multi-thumb slider.
|
|
33
38
|
*/
|
|
@@ -41,7 +46,8 @@ declare const Slider: import("svelte").Component<{
|
|
|
41
46
|
*/
|
|
42
47
|
step?: number | undefined;
|
|
43
48
|
/**
|
|
44
|
-
* Visible labels on the slider.
|
|
49
|
+
* Visible labels on the slider. When there is
|
|
50
|
+
* one per step, the current one is also read out as the value (`aria-valuetext`).
|
|
45
51
|
*/
|
|
46
52
|
optionLabels?: string[] | number[] | undefined;
|
|
47
53
|
/**
|
|
@@ -101,6 +107,11 @@ type Props = {
|
|
|
101
107
|
* `aria-label` on the slider.
|
|
102
108
|
*/
|
|
103
109
|
sliderLabel?: string | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* `aria-labelledby` on a single-thumb slider, for a visible
|
|
112
|
+
* label elsewhere on the page.
|
|
113
|
+
*/
|
|
114
|
+
ariaLabelledby?: string | undefined;
|
|
104
115
|
/**
|
|
105
116
|
* Value list for a multi-thumb slider.
|
|
106
117
|
*/
|
|
@@ -114,7 +125,8 @@ type Props = {
|
|
|
114
125
|
*/
|
|
115
126
|
step?: number | undefined;
|
|
116
127
|
/**
|
|
117
|
-
* Visible labels on the slider.
|
|
128
|
+
* Visible labels on the slider. When there is
|
|
129
|
+
* one per step, the current one is also read out as the value (`aria-valuetext`).
|
|
118
130
|
*/
|
|
119
131
|
optionLabels?: string[] | number[] | undefined;
|
|
120
132
|
/**
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
@see https://w3c.github.io/aria/#rowgroup
|
|
6
6
|
-->
|
|
7
7
|
<script>
|
|
8
|
+
import { _ } from '@sveltia/i18n';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* @import { Snippet } from 'svelte';
|
|
10
12
|
*/
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
role="rowgroup"
|
|
37
39
|
class="sui table-body row-group {className}"
|
|
38
40
|
aria-labelledby={label ? `${id}-label` : undefined}
|
|
39
|
-
aria-roledescription=
|
|
41
|
+
aria-roledescription={_('_sui.role_descriptions.table_body')}
|
|
40
42
|
>
|
|
41
43
|
{#if label}
|
|
42
44
|
<div role="row" class="row-group-caption">
|