@sentropic/design-system-svelte 0.25.0 → 0.25.1
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/AppHeader.svelte +22 -1
- package/dist/AppHeader.svelte.d.ts +5 -0
- package/dist/AppHeader.svelte.d.ts.map +1 -1
- package/dist/IdentityMenu.svelte +86 -15
- package/dist/IdentityMenu.svelte.d.ts +11 -1
- package/dist/IdentityMenu.svelte.d.ts.map +1 -1
- package/dist/LanguageToggle.svelte +31 -1
- package/dist/LanguageToggle.svelte.d.ts +3 -1
- package/dist/LanguageToggle.svelte.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/AppHeader.svelte
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
onMenuToggle?: () => void;
|
|
14
14
|
/** aria-label du bouton burger. */
|
|
15
15
|
menuLabel?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Id du tiroir, partagé entre `aria-controls` (burger) et `id` (drawer).
|
|
18
|
+
* Auto-généré et stable si non fourni.
|
|
19
|
+
*/
|
|
20
|
+
drawerId?: string;
|
|
16
21
|
/** Logo (décision actée : logo SENT + sous-titre). */
|
|
17
22
|
logo?: Snippet;
|
|
18
23
|
/** Liens de navigation (rendus dans le <nav> desktop). */
|
|
@@ -23,9 +28,17 @@
|
|
|
23
28
|
drawer?: Snippet;
|
|
24
29
|
class?: string;
|
|
25
30
|
}
|
|
31
|
+
|
|
32
|
+
// Compteur module pour générer un id de tiroir stable, déterministe et
|
|
33
|
+
// SSR-safe (pas de crypto). Aligné sur le pattern des 3 fw.
|
|
34
|
+
let appHeaderIdCounter = 0;
|
|
35
|
+
function nextAppHeaderId(): number {
|
|
36
|
+
return ++appHeaderIdCounter;
|
|
37
|
+
}
|
|
26
38
|
</script>
|
|
27
39
|
|
|
28
40
|
<script lang="ts">
|
|
41
|
+
import { untrack } from "svelte";
|
|
29
42
|
import { Menu, X } from "@lucide/svelte";
|
|
30
43
|
|
|
31
44
|
let {
|
|
@@ -33,6 +46,7 @@
|
|
|
33
46
|
menuOpen = false,
|
|
34
47
|
onMenuToggle,
|
|
35
48
|
menuLabel = "Menu",
|
|
49
|
+
drawerId,
|
|
36
50
|
logo,
|
|
37
51
|
nav,
|
|
38
52
|
actions,
|
|
@@ -40,6 +54,12 @@
|
|
|
40
54
|
class: className,
|
|
41
55
|
}: AppHeaderProps = $props();
|
|
42
56
|
|
|
57
|
+
// Id stable du tiroir : prop fournie sinon compteur module (SSR-safe, sans
|
|
58
|
+
// crypto). Capturé une seule fois (untrack) : un id stable ne doit pas réagir.
|
|
59
|
+
const resolvedDrawerId = untrack(
|
|
60
|
+
() => drawerId ?? `st-appHeader-drawer-${nextAppHeaderId()}`,
|
|
61
|
+
);
|
|
62
|
+
|
|
43
63
|
const classes = () => ["st-appHeader", className].filter(Boolean).join(" ");
|
|
44
64
|
</script>
|
|
45
65
|
|
|
@@ -54,6 +74,7 @@
|
|
|
54
74
|
onclick={onMenuToggle}
|
|
55
75
|
aria-label={menuLabel}
|
|
56
76
|
aria-expanded={menuOpen}
|
|
77
|
+
aria-controls={resolvedDrawerId}
|
|
57
78
|
aria-haspopup="menu"
|
|
58
79
|
>
|
|
59
80
|
{#if menuOpen}
|
|
@@ -90,7 +111,7 @@
|
|
|
90
111
|
aria-label={menuLabel}
|
|
91
112
|
onclick={onMenuToggle}
|
|
92
113
|
></button>
|
|
93
|
-
<aside class="st-appHeader__drawer">
|
|
114
|
+
<aside id={resolvedDrawerId} class="st-appHeader__drawer">
|
|
94
115
|
{@render drawer()}
|
|
95
116
|
</aside>
|
|
96
117
|
{/if}
|
|
@@ -11,6 +11,11 @@ export interface AppHeaderProps {
|
|
|
11
11
|
onMenuToggle?: () => void;
|
|
12
12
|
/** aria-label du bouton burger. */
|
|
13
13
|
menuLabel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Id du tiroir, partagé entre `aria-controls` (burger) et `id` (drawer).
|
|
16
|
+
* Auto-généré et stable si non fourni.
|
|
17
|
+
*/
|
|
18
|
+
drawerId?: string;
|
|
14
19
|
/** Logo (décision actée : logo SENT + sous-titre). */
|
|
15
20
|
logo?: Snippet;
|
|
16
21
|
/** Liens de navigation (rendus dans le <nav> desktop). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppHeader.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppHeader.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"AppHeader.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppHeader.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0DAA0D;IAC1D,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqFH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
package/dist/IdentityMenu.svelte
CHANGED
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
user?: IdentityUser | null;
|
|
11
11
|
/** État d'authentification (contrôlé : aucun état auth interne). */
|
|
12
12
|
isAuthenticated?: boolean;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* État ouvert du dropdown (optionnellement contrôlé). Si fourni, le
|
|
15
|
+
* composant suit la valeur du parent ; sinon il gère un état interne.
|
|
16
|
+
* Reste $bindable pour l'idiome Svelte.
|
|
17
|
+
*/
|
|
14
18
|
open?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Notifié à chaque demande de changement d'état ouvert (pattern
|
|
21
|
+
* contrôlé/non-contrôlé, aligné React/Vue). Le composant met aussi à jour
|
|
22
|
+
* `open` (bindable) quand il est non contrôlé.
|
|
23
|
+
*/
|
|
24
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
25
|
/** Notifié au clic « Se connecter ». */
|
|
16
26
|
onLogin?: () => void;
|
|
17
27
|
/** Notifié au clic « Se déconnecter ». */
|
|
@@ -50,7 +60,8 @@
|
|
|
50
60
|
let {
|
|
51
61
|
user = null,
|
|
52
62
|
isAuthenticated = false,
|
|
53
|
-
open = $bindable(
|
|
63
|
+
open = $bindable(),
|
|
64
|
+
onOpenChange,
|
|
54
65
|
onLogin,
|
|
55
66
|
onLogout,
|
|
56
67
|
devicesHref = "#",
|
|
@@ -66,6 +77,20 @@
|
|
|
66
77
|
let root: HTMLDivElement | undefined = $state();
|
|
67
78
|
let triggerEl: HTMLButtonElement | undefined = $state();
|
|
68
79
|
|
|
80
|
+
// Pattern contrôlé/non-contrôlé, IDENTIQUE aux 3 fw : si `open` est fourni en
|
|
81
|
+
// prop, le parent contrôle ; sinon un état interne prend le relais.
|
|
82
|
+
let internalOpen = $state(false);
|
|
83
|
+
const isOpen = $derived(open ?? internalOpen);
|
|
84
|
+
// Quel item focuser à la prochaine ouverture : "first" (défaut) ou "last"
|
|
85
|
+
// (ArrowUp depuis le trigger). Lu par l'$effect d'ouverture.
|
|
86
|
+
let pendingFocus: "first" | "last" = "first";
|
|
87
|
+
|
|
88
|
+
function setOpen(next: boolean) {
|
|
89
|
+
if (open === undefined) internalOpen = next;
|
|
90
|
+
else open = next; // bindable : reflète aussi côté parent en mode contrôlé
|
|
91
|
+
onOpenChange?.(next);
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
const classes = () => ["st-identityMenu", className].filter(Boolean).join(" ");
|
|
70
95
|
const initial = $derived(identityInitial(user));
|
|
71
96
|
const displayName = $derived(user?.displayName || user?.email || "User");
|
|
@@ -85,25 +110,33 @@
|
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
function toggle() {
|
|
88
|
-
|
|
113
|
+
setOpen(!isOpen);
|
|
89
114
|
}
|
|
90
115
|
|
|
91
116
|
function closeAndFocusTrigger() {
|
|
92
|
-
|
|
117
|
+
setOpen(false);
|
|
93
118
|
triggerEl?.focus();
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
function onTriggerKeydown(event: KeyboardEvent) {
|
|
97
122
|
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
|
98
123
|
event.preventDefault();
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
pendingFocus = "first";
|
|
125
|
+
if (!isOpen) setOpen(true);
|
|
126
|
+
else queueMicrotask(() => focusItem(0));
|
|
101
127
|
return;
|
|
102
128
|
}
|
|
103
129
|
if (event.key === "ArrowUp") {
|
|
104
130
|
event.preventDefault();
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
pendingFocus = "last";
|
|
132
|
+
if (!isOpen) setOpen(true);
|
|
133
|
+
else queueMicrotask(() => focusItem(-1));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (event.key === "Escape" && isOpen) {
|
|
137
|
+
// Esc ferme aussi depuis le trigger (global au composant ouvert).
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
closeAndFocusTrigger();
|
|
107
140
|
}
|
|
108
141
|
}
|
|
109
142
|
|
|
@@ -130,21 +163,57 @@
|
|
|
130
163
|
focusItem(items.length - 1);
|
|
131
164
|
return;
|
|
132
165
|
}
|
|
166
|
+
if (event.key === "Tab") {
|
|
167
|
+
// Piège de focus : Tab/Shift+Tab bouclent DANS le menu.
|
|
168
|
+
if (!items.length) return;
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
focusItem(current + (event.shiftKey ? -1 : 1));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
133
173
|
if (event.key === "Escape") {
|
|
134
174
|
event.preventDefault();
|
|
135
175
|
closeAndFocusTrigger();
|
|
136
176
|
}
|
|
137
177
|
}
|
|
138
178
|
|
|
179
|
+
// Enter/Space activent l'item courant. Sur un <a>, on suit le href en
|
|
180
|
+
// déclenchant un clic natif (preventDefault sur Space pour éviter le scroll).
|
|
181
|
+
function onItemKeydown(event: KeyboardEvent) {
|
|
182
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
(event.currentTarget as HTMLElement).click();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectAndClose() {
|
|
189
|
+
setOpen(false);
|
|
190
|
+
triggerEl?.focus();
|
|
191
|
+
}
|
|
192
|
+
|
|
139
193
|
function handleLogout() {
|
|
140
|
-
|
|
194
|
+
setOpen(false);
|
|
195
|
+
triggerEl?.focus();
|
|
141
196
|
onLogout?.();
|
|
142
197
|
}
|
|
198
|
+
|
|
199
|
+
// À l'ouverture : focus le 1er item (ou le dernier sur ArrowUp). Client-only
|
|
200
|
+
// (guard typeof document). Restaure `pendingFocus` à "first" ensuite.
|
|
201
|
+
$effect(() => {
|
|
202
|
+
if (isOpen && typeof document !== "undefined") {
|
|
203
|
+
const which = pendingFocus;
|
|
204
|
+
queueMicrotask(() => focusItem(which === "last" ? -1 : 0));
|
|
205
|
+
pendingFocus = "first";
|
|
206
|
+
}
|
|
207
|
+
});
|
|
143
208
|
</script>
|
|
144
209
|
|
|
145
210
|
<svelte:window
|
|
146
211
|
onpointerdown={(e) => {
|
|
147
|
-
if (
|
|
212
|
+
if (isOpen && root && e.target instanceof Node && !root.contains(e.target)) {
|
|
213
|
+
// Clic extérieur : ferme ET restaure le focus sur le trigger.
|
|
214
|
+
setOpen(false);
|
|
215
|
+
triggerEl?.focus();
|
|
216
|
+
}
|
|
148
217
|
}}
|
|
149
218
|
/>
|
|
150
219
|
|
|
@@ -159,7 +228,7 @@
|
|
|
159
228
|
class="st-identityMenu__trigger"
|
|
160
229
|
bind:this={triggerEl}
|
|
161
230
|
aria-haspopup="menu"
|
|
162
|
-
aria-expanded={
|
|
231
|
+
aria-expanded={isOpen}
|
|
163
232
|
aria-label={`Compte de ${displayName}`}
|
|
164
233
|
onclick={toggle}
|
|
165
234
|
onkeydown={onTriggerKeydown}
|
|
@@ -172,13 +241,13 @@
|
|
|
172
241
|
{/if}
|
|
173
242
|
</span>
|
|
174
243
|
<ChevronDown
|
|
175
|
-
class={`st-identityMenu__chevron${
|
|
244
|
+
class={`st-identityMenu__chevron${isOpen ? " st-identityMenu__chevron--open" : ""}`}
|
|
176
245
|
size={16}
|
|
177
246
|
aria-hidden="true"
|
|
178
247
|
/>
|
|
179
248
|
</button>
|
|
180
249
|
|
|
181
|
-
{#if
|
|
250
|
+
{#if isOpen}
|
|
182
251
|
<div
|
|
183
252
|
class="st-identityMenu__menu"
|
|
184
253
|
role="menu"
|
|
@@ -191,7 +260,8 @@
|
|
|
191
260
|
class="st-identityMenu__item"
|
|
192
261
|
role="menuitem"
|
|
193
262
|
tabindex="-1"
|
|
194
|
-
onclick={
|
|
263
|
+
onclick={selectAndClose}
|
|
264
|
+
onkeydown={onItemKeydown}
|
|
195
265
|
>
|
|
196
266
|
{devicesLabel}
|
|
197
267
|
</a>
|
|
@@ -200,7 +270,8 @@
|
|
|
200
270
|
class="st-identityMenu__item"
|
|
201
271
|
role="menuitem"
|
|
202
272
|
tabindex="-1"
|
|
203
|
-
onclick={
|
|
273
|
+
onclick={selectAndClose}
|
|
274
|
+
onkeydown={onItemKeydown}
|
|
204
275
|
>
|
|
205
276
|
{settingsLabel}
|
|
206
277
|
</a>
|
|
@@ -8,8 +8,18 @@ export interface IdentityMenuProps {
|
|
|
8
8
|
user?: IdentityUser | null;
|
|
9
9
|
/** État d'authentification (contrôlé : aucun état auth interne). */
|
|
10
10
|
isAuthenticated?: boolean;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* État ouvert du dropdown (optionnellement contrôlé). Si fourni, le
|
|
13
|
+
* composant suit la valeur du parent ; sinon il gère un état interne.
|
|
14
|
+
* Reste $bindable pour l'idiome Svelte.
|
|
15
|
+
*/
|
|
12
16
|
open?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Notifié à chaque demande de changement d'état ouvert (pattern
|
|
19
|
+
* contrôlé/non-contrôlé, aligné React/Vue). Le composant met aussi à jour
|
|
20
|
+
* `open` (bindable) quand il est non contrôlé.
|
|
21
|
+
*/
|
|
22
|
+
onOpenChange?: (open: boolean) => void;
|
|
13
23
|
/** Notifié au clic « Se connecter ». */
|
|
14
24
|
onLogin?: () => void;
|
|
15
25
|
/** Notifié au clic « Se déconnecter ». */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentityMenu.svelte.d.ts","sourceRoot":"","sources":["../src/lib/IdentityMenu.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"IdentityMenu.svelte.d.ts","sourceRoot":"","sources":["../src/lib/IdentityMenu.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3B,oEAAoE;IACpE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kCAAkC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAG7E;AA8MH,QAAA,MAAM,YAAY,2DAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
export type LanguageToggleLocale = "fr" | "en";
|
|
3
3
|
|
|
4
|
+
// Compteur module pour un id de <select> stable, déterministe et SSR-safe.
|
|
5
|
+
let languageToggleIdCounter = 0;
|
|
6
|
+
function nextLanguageToggleId(): number {
|
|
7
|
+
return ++languageToggleIdCounter;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export interface LanguageToggleProps {
|
|
5
11
|
/** Langue courante (contrôlé). */
|
|
6
12
|
locale?: LanguageToggleLocale;
|
|
@@ -10,8 +16,10 @@
|
|
|
10
16
|
frLabel?: string;
|
|
11
17
|
/** Libellé EN (i18n-agnostique : fourni par le parent). */
|
|
12
18
|
enLabel?: string;
|
|
13
|
-
/**
|
|
19
|
+
/** Libellé du sélecteur (associé via <label for> + aria-label). */
|
|
14
20
|
label?: string;
|
|
21
|
+
/** Id du <select> ; auto-généré et stable si non fourni. */
|
|
22
|
+
selectId?: string;
|
|
15
23
|
/**
|
|
16
24
|
* Variante d'affichage :
|
|
17
25
|
* - `select` (défaut) : le <select> de la source (header desktop).
|
|
@@ -25,6 +33,7 @@
|
|
|
25
33
|
</script>
|
|
26
34
|
|
|
27
35
|
<script lang="ts">
|
|
36
|
+
import { untrack } from "svelte";
|
|
28
37
|
import { ChevronDown } from "@lucide/svelte";
|
|
29
38
|
|
|
30
39
|
let {
|
|
@@ -33,11 +42,17 @@
|
|
|
33
42
|
frLabel = "FR",
|
|
34
43
|
enLabel = "EN",
|
|
35
44
|
label = "Langue",
|
|
45
|
+
selectId,
|
|
36
46
|
variant = "select",
|
|
37
47
|
accordionLabel = "Langue",
|
|
38
48
|
class: className,
|
|
39
49
|
}: LanguageToggleProps = $props();
|
|
40
50
|
|
|
51
|
+
// Capturé une seule fois (untrack) : un id stable ne doit pas réagir.
|
|
52
|
+
const resolvedSelectId = untrack(
|
|
53
|
+
() => selectId ?? `st-languageToggle-${nextLanguageToggleId()}`,
|
|
54
|
+
);
|
|
55
|
+
|
|
41
56
|
let open = $state(false);
|
|
42
57
|
|
|
43
58
|
const classes = () => ["st-languageToggle", className].filter(Boolean).join(" ");
|
|
@@ -91,7 +106,9 @@
|
|
|
91
106
|
{/if}
|
|
92
107
|
</div>
|
|
93
108
|
{:else}
|
|
109
|
+
<label class="st-languageToggle__srLabel" for={resolvedSelectId}>{label}</label>
|
|
94
110
|
<select
|
|
111
|
+
id={resolvedSelectId}
|
|
95
112
|
class={`st-languageToggle__select${className ? ` ${className}` : ""}`}
|
|
96
113
|
value={locale}
|
|
97
114
|
aria-label={label}
|
|
@@ -107,6 +124,19 @@
|
|
|
107
124
|
width: 100%;
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
/* Label associé au <select>, masqué visuellement (a11y : <label for>). */
|
|
128
|
+
.st-languageToggle__srLabel {
|
|
129
|
+
border: 0;
|
|
130
|
+
clip: rect(0 0 0 0);
|
|
131
|
+
height: 1px;
|
|
132
|
+
margin: -1px;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
padding: 0;
|
|
135
|
+
position: absolute;
|
|
136
|
+
white-space: nowrap;
|
|
137
|
+
width: 1px;
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
/* Variante <select> — calque du <select> source (header desktop). */
|
|
111
141
|
.st-languageToggle__select {
|
|
112
142
|
background: var(--st-semantic-surface-default);
|
|
@@ -8,8 +8,10 @@ export interface LanguageToggleProps {
|
|
|
8
8
|
frLabel?: string;
|
|
9
9
|
/** Libellé EN (i18n-agnostique : fourni par le parent). */
|
|
10
10
|
enLabel?: string;
|
|
11
|
-
/**
|
|
11
|
+
/** Libellé du sélecteur (associé via <label for> + aria-label). */
|
|
12
12
|
label?: string;
|
|
13
|
+
/** Id du <select> ; auto-généré et stable si non fourni. */
|
|
14
|
+
selectId?: string;
|
|
13
15
|
/**
|
|
14
16
|
* Variante d'affichage :
|
|
15
17
|
* - `select` (défaut) : le <select> de la source (header desktop).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LanguageToggle.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LanguageToggle.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"LanguageToggle.svelte.d.ts","sourceRoot":"","sources":["../src/lib/LanguageToggle.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAAC;AAQ/C,MAAM,WAAW,mBAAmB;IAClC,kCAAkC;IAClC,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,uCAAuC;IACvC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACxD,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IACjC,qCAAqC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwEH,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|