@sentropic/design-system-svelte 0.16.0 → 0.18.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/Calendar.svelte +237 -42
- package/dist/Calendar.svelte.d.ts.map +1 -1
- package/dist/ComboChart.svelte +620 -0
- package/dist/ComboChart.svelte.d.ts +28 -0
- package/dist/ComboChart.svelte.d.ts.map +1 -0
- package/dist/FunnelChart.svelte +358 -0
- package/dist/FunnelChart.svelte.d.ts +21 -0
- package/dist/FunnelChart.svelte.d.ts.map +1 -0
- package/dist/GaugeChart.svelte +300 -0
- package/dist/GaugeChart.svelte.d.ts +36 -0
- package/dist/GaugeChart.svelte.d.ts.map +1 -0
- package/dist/KpiCard.svelte +318 -0
- package/dist/KpiCard.svelte.d.ts +36 -0
- package/dist/KpiCard.svelte.d.ts.map +1 -0
- package/dist/Popper.svelte +157 -0
- package/dist/Popper.svelte.d.ts +17 -0
- package/dist/Popper.svelte.d.ts.map +1 -1
- package/dist/Rating.svelte +130 -35
- package/dist/Rating.svelte.d.ts.map +1 -1
- package/dist/SelectableList.svelte +60 -12
- package/dist/SelectableList.svelte.d.ts.map +1 -1
- package/dist/SelectableRow.svelte +23 -8
- package/dist/SelectableRow.svelte.d.ts +5 -4
- package/dist/SelectableRow.svelte.d.ts.map +1 -1
- package/dist/SlideIndicator.svelte +17 -3
- package/dist/SlideIndicator.svelte.d.ts.map +1 -1
- package/dist/TimePicker.svelte +176 -13
- package/dist/TimePicker.svelte.d.ts.map +1 -1
- package/dist/TreemapChart.svelte +448 -0
- package/dist/TreemapChart.svelte.d.ts +26 -0
- package/dist/TreemapChart.svelte.d.ts.map +1 -0
- package/dist/WaterfallChart.svelte +469 -0
- package/dist/WaterfallChart.svelte.d.ts +19 -0
- package/dist/WaterfallChart.svelte.d.ts.map +1 -0
- package/dist/chartContrast.d.ts +6 -0
- package/dist/chartContrast.d.ts.map +1 -0
- package/dist/chartContrast.js +58 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/package.json +1 -1
package/dist/Rating.svelte
CHANGED
|
@@ -49,8 +49,21 @@
|
|
|
49
49
|
const stars = $derived(Array.from({ length: max }, (_, i) => i + 1));
|
|
50
50
|
|
|
51
51
|
// L'étoile « focusable » (tabindex 0) suit la valeur ; à 0 c'est la première.
|
|
52
|
+
// En mode allowHalf, la valeur peut être un demi-entier : on focus l'étoile plafond.
|
|
52
53
|
const focusedStar = $derived(value > 0 ? Math.ceil(value) : 1);
|
|
53
54
|
|
|
55
|
+
// Refs des boutons radio pour déplacer le focus programmatiquement.
|
|
56
|
+
let radioRefs = $state<Record<number, HTMLElement | null>>({});
|
|
57
|
+
|
|
58
|
+
// Texte accessible décrivant la valeur courante (utilisé pour aria-valuetext et aria-label readonly).
|
|
59
|
+
const valueText = $derived(
|
|
60
|
+
value === 0
|
|
61
|
+
? `0 / ${max}`
|
|
62
|
+
: allowHalf && value % 1 !== 0
|
|
63
|
+
? `${value} / ${max}`
|
|
64
|
+
: `${value} / ${max}`
|
|
65
|
+
);
|
|
66
|
+
|
|
54
67
|
function fill(star: number): "full" | "half" | "empty" {
|
|
55
68
|
if (value >= star) return "full";
|
|
56
69
|
if (allowHalf && value >= star - 0.5) return "half";
|
|
@@ -85,64 +98,140 @@
|
|
|
85
98
|
if (readonly) return;
|
|
86
99
|
const step = allowHalf ? 0.5 : 1;
|
|
87
100
|
let handled = true;
|
|
101
|
+
let next: number | null = null;
|
|
88
102
|
switch (event.key) {
|
|
89
103
|
case "ArrowRight":
|
|
90
104
|
case "ArrowUp":
|
|
91
|
-
|
|
105
|
+
next = Math.min(max, value + step);
|
|
92
106
|
break;
|
|
93
107
|
case "ArrowLeft":
|
|
94
108
|
case "ArrowDown":
|
|
95
|
-
|
|
109
|
+
// En mode entier, ne pas descendre sous 1 (pas de radio "0").
|
|
110
|
+
next = allowHalf ? Math.max(0, value - step) : Math.max(1, value - step);
|
|
96
111
|
break;
|
|
97
112
|
case "Home":
|
|
98
|
-
|
|
113
|
+
// Home → première étoile (1), pas 0 (aucun radio "0" n'existe).
|
|
114
|
+
next = allowHalf ? 0 : 1;
|
|
99
115
|
break;
|
|
100
116
|
case "End":
|
|
101
|
-
|
|
117
|
+
next = max;
|
|
102
118
|
break;
|
|
103
119
|
default:
|
|
104
120
|
handled = false;
|
|
105
121
|
}
|
|
106
|
-
if (handled)
|
|
122
|
+
if (handled) {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
if (next !== null) {
|
|
125
|
+
commit(next);
|
|
126
|
+
// En mode entier, déplacer le focus DOM vers le radio cible.
|
|
127
|
+
if (!allowHalf) {
|
|
128
|
+
const targetStar = next > 0 ? Math.ceil(next) : 1;
|
|
129
|
+
const targetEl = radioRefs[targetStar];
|
|
130
|
+
if (targetEl) targetEl.focus();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
107
134
|
}
|
|
135
|
+
|
|
136
|
+
// En mode allowHalf, on expose un slider ARIA (valeurs fractionnaires non représentables
|
|
137
|
+
// fidèlement par un radiogroup). En mode entier, on garde radiogroup/radio.
|
|
138
|
+
// Readonly : rendu non interactif avec span + aria-label global pour éviter les boutons disabled
|
|
139
|
+
// qui disparaissent de l'arbre d'accessibilité interactif.
|
|
108
140
|
</script>
|
|
109
141
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
{#if readonly}
|
|
143
|
+
<!-- Readonly : pas d'éléments interactifs disabled — on expose la note via aria-label sur le groupe. -->
|
|
144
|
+
<div
|
|
145
|
+
{...rest}
|
|
146
|
+
class={classes}
|
|
147
|
+
role="img"
|
|
148
|
+
aria-label={label ? `${label} : ${valueText}` : valueText}
|
|
149
|
+
>
|
|
150
|
+
{#each stars as star (star)}
|
|
151
|
+
{@const state = fill(star)}
|
|
152
|
+
<span class="st-rating__star" class:st-rating__star--full={state === "full"} class:st-rating__star--half={state === "half"} aria-hidden="true">
|
|
153
|
+
{#if state === "half"}
|
|
154
|
+
<StarHalf size={iconSize} strokeWidth={1.75} aria-hidden="true" />
|
|
155
|
+
{:else}
|
|
156
|
+
<Star
|
|
157
|
+
size={iconSize}
|
|
158
|
+
strokeWidth={1.75}
|
|
159
|
+
fill={state === "full" ? "currentColor" : "none"}
|
|
160
|
+
aria-hidden="true"
|
|
161
|
+
/>
|
|
162
|
+
{/if}
|
|
163
|
+
</span>
|
|
164
|
+
{/each}
|
|
165
|
+
</div>
|
|
166
|
+
{:else if allowHalf}
|
|
167
|
+
<!-- allowHalf : slider ARIA — valeurs fractionnaires (0.5 step), plus fidèle que radiogroup. -->
|
|
168
|
+
<div
|
|
169
|
+
{...rest}
|
|
170
|
+
class={classes}
|
|
171
|
+
role="slider"
|
|
172
|
+
aria-label={label}
|
|
173
|
+
aria-valuemin={0}
|
|
174
|
+
aria-valuemax={max}
|
|
175
|
+
aria-valuenow={value}
|
|
176
|
+
aria-valuetext={valueText}
|
|
177
|
+
tabindex={0}
|
|
178
|
+
onkeydown={onKeyDown}
|
|
179
|
+
>
|
|
180
|
+
{#each stars as star (star)}
|
|
181
|
+
{@const state = fill(star)}
|
|
182
|
+
<span
|
|
183
|
+
class="st-rating__star"
|
|
184
|
+
class:st-rating__star--full={state === "full"}
|
|
185
|
+
class:st-rating__star--half={state === "half"}
|
|
186
|
+
aria-hidden="true"
|
|
187
|
+
onclick={(event) => onStarClick(event, star)}
|
|
188
|
+
>
|
|
189
|
+
{#if state === "half"}
|
|
190
|
+
<StarHalf size={iconSize} strokeWidth={1.75} aria-hidden="true" />
|
|
191
|
+
{:else}
|
|
192
|
+
<Star
|
|
193
|
+
size={iconSize}
|
|
194
|
+
strokeWidth={1.75}
|
|
195
|
+
fill={state === "full" ? "currentColor" : "none"}
|
|
196
|
+
aria-hidden="true"
|
|
197
|
+
/>
|
|
198
|
+
{/if}
|
|
199
|
+
</span>
|
|
200
|
+
{/each}
|
|
201
|
+
</div>
|
|
202
|
+
{:else}
|
|
203
|
+
<!-- Mode entier : radiogroup / radio. aria-checked=true uniquement sur l'étoile == value. -->
|
|
204
|
+
<div
|
|
205
|
+
{...rest}
|
|
206
|
+
class={classes}
|
|
207
|
+
role="radiogroup"
|
|
208
|
+
aria-label={label}
|
|
209
|
+
>
|
|
210
|
+
{#each stars as star (star)}
|
|
211
|
+
{@const state = fill(star)}
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
class="st-rating__star"
|
|
215
|
+
class:st-rating__star--full={state === "full"}
|
|
216
|
+
role="radio"
|
|
217
|
+
name={name}
|
|
218
|
+
aria-checked={value === star ? "true" : "false"}
|
|
219
|
+
aria-label={`${star} / ${max}`}
|
|
220
|
+
tabindex={star === focusedStar ? 0 : -1}
|
|
221
|
+
bind:this={radioRefs[star]}
|
|
222
|
+
onclick={(event) => onStarClick(event, star)}
|
|
223
|
+
onkeydown={onKeyDown}
|
|
224
|
+
>
|
|
136
225
|
<Star
|
|
137
226
|
size={iconSize}
|
|
138
227
|
strokeWidth={1.75}
|
|
139
228
|
fill={state === "full" ? "currentColor" : "none"}
|
|
140
229
|
aria-hidden="true"
|
|
141
230
|
/>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
231
|
+
</button>
|
|
232
|
+
{/each}
|
|
233
|
+
</div>
|
|
234
|
+
{/if}
|
|
146
235
|
|
|
147
236
|
<style>
|
|
148
237
|
.st-rating {
|
|
@@ -186,4 +275,10 @@
|
|
|
186
275
|
.st-rating--readonly .st-rating__star {
|
|
187
276
|
cursor: default;
|
|
188
277
|
}
|
|
278
|
+
|
|
279
|
+
/* Mode allowHalf : le slider (conteneur) doit afficher un focus-visible. */
|
|
280
|
+
[role="slider"].st-rating:focus-visible {
|
|
281
|
+
outline: 2px solid var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
|
|
282
|
+
outline-offset: 2px;
|
|
283
|
+
}
|
|
189
284
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Rating.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Rating.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAI9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAC9E,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"Rating.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Rating.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAI9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IAC9E,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAsKJ,QAAA,MAAM,MAAM,iDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
// --- Row registry: ordered by DOM position so arrow nav matches the visual
|
|
62
62
|
// order regardless of registration timing. -------------------------------
|
|
63
|
-
type Entry = { el: HTMLElement; value: string | undefined };
|
|
63
|
+
type Entry = { el: HTMLElement; value: string | undefined; disabled?: boolean };
|
|
64
64
|
let entries = $state<Entry[]>([]);
|
|
65
65
|
|
|
66
66
|
// The element that currently holds the roving tab stop (tabindex 0). Null until
|
|
@@ -79,9 +79,14 @@
|
|
|
79
79
|
// register/unregister are called from each row's $effect. They read AND write
|
|
80
80
|
// `entries`, so the read must be untracked — otherwise the calling effect would
|
|
81
81
|
// subscribe to `entries`, and writing it would re-run the effect forever.
|
|
82
|
-
|
|
82
|
+
// Disabled rows are registered with disabled:true so navigate() can skip them
|
|
83
|
+
// explicitly, making the skip correct even when disabled state changes mid-session.
|
|
84
|
+
function register(el: HTMLElement, rowValue: string | undefined, rowDisabled = false): () => void {
|
|
83
85
|
untrack(() => {
|
|
84
|
-
entries = sortByDom([
|
|
86
|
+
entries = sortByDom([
|
|
87
|
+
...entries.filter((e) => e.el !== el),
|
|
88
|
+
{ el, value: rowValue, disabled: rowDisabled }
|
|
89
|
+
]);
|
|
85
90
|
});
|
|
86
91
|
return () => {
|
|
87
92
|
untrack(() => {
|
|
@@ -91,8 +96,30 @@
|
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
// Default roving stop = first
|
|
95
|
-
|
|
99
|
+
// Default roving stop = first non-disabled DOM-ordered row when none focused,
|
|
100
|
+
// or when the current tabStopEl has become disabled.
|
|
101
|
+
const effectiveTabStop = $derived.by((): HTMLElement | null => {
|
|
102
|
+
if (tabStopEl) {
|
|
103
|
+
const entry = entries.find((e) => e.el === tabStopEl);
|
|
104
|
+
if (entry && !entry.disabled) return tabStopEl;
|
|
105
|
+
}
|
|
106
|
+
return entries.find((e) => !e.disabled)?.el ?? null;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Si la row qui détient le focus DOM devient disabled (in-place, sans unmount),
|
|
110
|
+
// transférer le focus vers la nouvelle cible de roving tabindex.
|
|
111
|
+
// Note : le cas du cycle unregister/register est géré dans SelectableRow via
|
|
112
|
+
// l'$effect sur `disabled` qui appelle navigate() AVANT le cleanup.
|
|
113
|
+
$effect(() => {
|
|
114
|
+
const newStop = effectiveTabStop;
|
|
115
|
+
if (!newStop) return;
|
|
116
|
+
if (tabStopEl !== null) {
|
|
117
|
+
const disabledEntry = entries.find((e) => e.el === tabStopEl && e.disabled);
|
|
118
|
+
if (disabledEntry && tabStopEl.contains(document.activeElement ?? null)) {
|
|
119
|
+
newStop.focus();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
96
123
|
|
|
97
124
|
function valueOf(el: HTMLElement): string | undefined {
|
|
98
125
|
return entries.find((e) => e.el === el)?.value;
|
|
@@ -137,13 +164,34 @@
|
|
|
137
164
|
if (entries.length === 0) return;
|
|
138
165
|
const idx = entries.findIndex((e) => e.el === el);
|
|
139
166
|
if (idx === -1) return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
|
|
168
|
+
let targetIdx: number | null = null;
|
|
169
|
+
|
|
170
|
+
if (key === "ArrowDown" || key === "ArrowRight") {
|
|
171
|
+
// Walk forward from current position, find the next non-disabled entry.
|
|
172
|
+
for (let i = idx + 1; i < entries.length; i++) {
|
|
173
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
174
|
+
}
|
|
175
|
+
} else if (key === "ArrowUp" || key === "ArrowLeft") {
|
|
176
|
+
// Walk backward from current position, find the previous non-disabled entry.
|
|
177
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
178
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
179
|
+
}
|
|
180
|
+
} else if (key === "Home") {
|
|
181
|
+
// First non-disabled entry.
|
|
182
|
+
for (let i = 0; i < entries.length; i++) {
|
|
183
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
184
|
+
}
|
|
185
|
+
} else if (key === "End") {
|
|
186
|
+
// Last non-disabled entry.
|
|
187
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
188
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If no target found (all remaining are disabled, or already at boundary), stay put.
|
|
193
|
+
if (targetIdx === null) return;
|
|
194
|
+
|
|
147
195
|
const target = entries[targetIdx]?.el;
|
|
148
196
|
if (target) {
|
|
149
197
|
tabStopEl = target;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA0MJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
readonly managed: true;
|
|
17
17
|
/** listbox role for the wrapper → rows are "option". */
|
|
18
18
|
readonly itemRole: "option";
|
|
19
|
-
/** Register a row element; returns an unregister callback. */
|
|
20
|
-
register: (el: HTMLElement, value: string | undefined) => () => void;
|
|
19
|
+
/** Register a row element; returns an unregister callback. disabled is forwarded so the list can skip it during keyboard navigation. */
|
|
20
|
+
register: (el: HTMLElement, value: string | undefined, disabled?: boolean) => () => void;
|
|
21
21
|
/** Is the row with this element currently selected? */
|
|
22
22
|
isSelected: (el: HTMLElement) => boolean;
|
|
23
23
|
/** Should the row with this element be the roving-tabindex stop (tabindex 0)? */
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
/** Stable value, surfaced as `data-value` and used by the list for `value`. */
|
|
45
45
|
value?: string;
|
|
46
46
|
/**
|
|
47
|
-
* ARIA role for the standalone row. Defaults to "
|
|
48
|
-
*
|
|
47
|
+
* ARIA role for the standalone row. Defaults to "button" for standalone use —
|
|
48
|
+
* "option" is only valid inside a listbox and would be invalid without one.
|
|
49
|
+
* Inside a SelectableList the role is always forced to "option".
|
|
49
50
|
*/
|
|
50
51
|
role?: string;
|
|
51
52
|
/**
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
onselect,
|
|
72
73
|
disabled = false,
|
|
73
74
|
value,
|
|
74
|
-
role = "
|
|
75
|
+
role = "button",
|
|
75
76
|
accentBar = false,
|
|
76
77
|
leading,
|
|
77
78
|
trailing,
|
|
@@ -86,10 +87,23 @@
|
|
|
86
87
|
let el: HTMLElement | null = $state(null);
|
|
87
88
|
|
|
88
89
|
// Register with the parent list (if any) so it can order rows for arrow nav
|
|
89
|
-
// and compute the roving tab stop.
|
|
90
|
+
// and compute the roving tab stop. Disabled rows are registered too so the
|
|
91
|
+
// list can skip them during navigation; the list owns the skip logic.
|
|
90
92
|
$effect(() => {
|
|
91
|
-
if (!list || !el
|
|
92
|
-
return list.register(el, value);
|
|
93
|
+
if (!list || !el) return;
|
|
94
|
+
return list.register(el, value, disabled);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// A11y edge-case : quand cette row passe à disabled=true ET qu'elle détient le
|
|
98
|
+
// focus DOM, transférer le focus vers la prochaine row enabled via navigate().
|
|
99
|
+
// On le fait ici (dans SelectableRow) pour avoir accès au focus DOM AVANT que
|
|
100
|
+
// le cycle unregister/register dans SelectableList ne perturbe l'état.
|
|
101
|
+
$effect(() => {
|
|
102
|
+
if (!disabled || !list || !el) return;
|
|
103
|
+
if (!el.contains(document.activeElement ?? null)) return;
|
|
104
|
+
// Déléguer via navigate ArrowDown (cherche prochaine row enabled vers l'avant,
|
|
105
|
+
// puis vers l'arrière si aucune). navigate appelle target.focus() directement.
|
|
106
|
+
list.navigate(el, "ArrowDown");
|
|
93
107
|
});
|
|
94
108
|
|
|
95
109
|
// Effective selected state: list-managed rows read the list; standalone rows
|
|
@@ -165,6 +179,7 @@
|
|
|
165
179
|
class={classes}
|
|
166
180
|
role={effectiveRole}
|
|
167
181
|
aria-selected={effectiveRole === "option" ? isSelected : undefined}
|
|
182
|
+
aria-pressed={effectiveRole === "button" ? isSelected : undefined}
|
|
168
183
|
aria-disabled={disabled ? "true" : undefined}
|
|
169
184
|
data-value={value}
|
|
170
185
|
{tabindex}
|
|
@@ -13,8 +13,8 @@ export type SelectableListContext = {
|
|
|
13
13
|
readonly managed: true;
|
|
14
14
|
/** listbox role for the wrapper → rows are "option". */
|
|
15
15
|
readonly itemRole: "option";
|
|
16
|
-
/** Register a row element; returns an unregister callback. */
|
|
17
|
-
register: (el: HTMLElement, value: string | undefined) => () => void;
|
|
16
|
+
/** Register a row element; returns an unregister callback. disabled is forwarded so the list can skip it during keyboard navigation. */
|
|
17
|
+
register: (el: HTMLElement, value: string | undefined, disabled?: boolean) => () => void;
|
|
18
18
|
/** Is the row with this element currently selected? */
|
|
19
19
|
isSelected: (el: HTMLElement) => boolean;
|
|
20
20
|
/** Should the row with this element be the roving-tabindex stop (tabindex 0)? */
|
|
@@ -40,8 +40,9 @@ export type SelectableRowProps = {
|
|
|
40
40
|
/** Stable value, surfaced as `data-value` and used by the list for `value`. */
|
|
41
41
|
value?: string;
|
|
42
42
|
/**
|
|
43
|
-
* ARIA role for the standalone row. Defaults to "
|
|
44
|
-
*
|
|
43
|
+
* ARIA role for the standalone row. Defaults to "button" for standalone use —
|
|
44
|
+
* "option" is only valid inside a listbox and would be invalid without one.
|
|
45
|
+
* Inside a SelectableList the role is always forced to "option".
|
|
45
46
|
*/
|
|
46
47
|
role?: string;
|
|
47
48
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wIAAwI;IACxI,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IACzF,uDAAuD;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,wDAAwD;IACxD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,gDAAgD;IAChD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiIJ,QAAA,MAAM,aAAa,gEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
const items = $derived(Array.from({ length: Math.max(0, count) }, (_, i) => i));
|
|
45
45
|
|
|
46
|
+
// Refs des boutons pour déplacer le focus programmatiquement lors de la navigation clavier.
|
|
47
|
+
let buttonRefs = $state<Record<number, HTMLElement | null>>({});
|
|
48
|
+
|
|
46
49
|
function select(index: number) {
|
|
47
50
|
if (index < 0 || index >= count || index === current) return;
|
|
48
51
|
onChange?.(index);
|
|
@@ -69,21 +72,32 @@
|
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
74
|
event.preventDefault();
|
|
75
|
+
// Déplacer le focus DOM vers le bouton cible (roving tabindex correct).
|
|
76
|
+
const targetEl = buttonRefs[target];
|
|
77
|
+
if (targetEl) targetEl.focus();
|
|
72
78
|
select(target);
|
|
73
79
|
}
|
|
74
80
|
</script>
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
<!--
|
|
83
|
+
Choix de pattern : role="group" + boutons avec aria-current.
|
|
84
|
+
Justification : un indicateur de carrousel/pagination n'est PAS un tablist — il n'y a pas de
|
|
85
|
+
tabpanel associé. Utiliser role="tab" sans aria-controls/tabpanel trompe les lecteurs d'écran
|
|
86
|
+
qui annoncent « onglet X sur N » sans panneau contrôlé.
|
|
87
|
+
Pattern retenu (ARIA Authoring Practices Guide — Carousel) : role="group" nommé + boutons natifs
|
|
88
|
+
avec aria-current="true" sur le point courant + roving tabindex.
|
|
89
|
+
Le SR annonce « Groupe [label] — [label] 1, bouton ; [label] 2, courant, bouton ; … »
|
|
90
|
+
-->
|
|
91
|
+
<div {...rest} class={classes} role="group" aria-label={label}>
|
|
77
92
|
{#each items as index (index)}
|
|
78
93
|
<button
|
|
79
94
|
type="button"
|
|
80
95
|
class="st-slideIndicator__dot"
|
|
81
96
|
class:st-slideIndicator__dot--current={index === current}
|
|
82
|
-
role="tab"
|
|
83
|
-
aria-selected={index === current ? "true" : "false"}
|
|
84
97
|
aria-current={index === current ? "true" : undefined}
|
|
85
98
|
aria-label={`${label} ${index + 1}`}
|
|
86
99
|
tabindex={index === current ? 0 : -1}
|
|
100
|
+
bind:this={buttonRefs[index]}
|
|
87
101
|
onclick={() => select(index)}
|
|
88
102
|
onkeydown={(event) => onKeyDown(event, index)}
|
|
89
103
|
></button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SlideIndicator.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SlideIndicator.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,mBAAmB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACtF,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"SlideIndicator.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SlideIndicator.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,mBAAmB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACtF,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA8EJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|