@lilydesignsystem/svelte-locale-picker 0.1.0 → 0.1.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.
- package/dist/LocalePicker.svelte +39 -105
- package/package.json +4 -1
package/dist/LocalePicker.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
+
import { IconButton, Listbox } from "@lilydesignsystem/svelte-headless";
|
|
3
4
|
import {
|
|
4
5
|
defaultLocaleLabels,
|
|
5
6
|
RTL_LANGUAGE_TAGS,
|
|
@@ -185,13 +186,9 @@
|
|
|
185
186
|
let open = $state(false);
|
|
186
187
|
let activeIndex = $state(-1);
|
|
187
188
|
let buttonEl: HTMLButtonElement | undefined = $state();
|
|
188
|
-
let listEl:
|
|
189
|
+
let listEl: HTMLElement | undefined = $state();
|
|
189
190
|
let rootEl: HTMLDivElement | undefined = $state();
|
|
190
191
|
|
|
191
|
-
// Typeahead buffer: APG listbox behaviour. Reset after a pause.
|
|
192
|
-
let typeahead = "";
|
|
193
|
-
let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
194
|
-
|
|
195
192
|
function labelFor(locale: string): string {
|
|
196
193
|
if (locale in localeLabels) return localeLabels[locale];
|
|
197
194
|
// Endonym first: a language menu names each language in itself,
|
|
@@ -307,37 +304,26 @@
|
|
|
307
304
|
el?.scrollIntoView?.({ block: "nearest" });
|
|
308
305
|
}
|
|
309
306
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
// Arrow/Home/End/PageUp/PageDown/typeahead/Escape/Tab keyboard handling
|
|
308
|
+
// inside the open list is owned by Listbox's "active-descendant" mode
|
|
309
|
+
// (see @lilydesignsystem/svelte-headless); this component only decides
|
|
310
|
+
// what open/close/choose/scroll mean. Keep the highlighted option in
|
|
311
|
+
// view for every reason activeIndex can change.
|
|
312
|
+
$effect(() => {
|
|
313
|
+
activeIndex;
|
|
314
314
|
scrollActiveIntoView();
|
|
315
|
-
}
|
|
315
|
+
});
|
|
316
316
|
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
typeaheadTimer = setTimeout(() => (typeahead = ""), 500);
|
|
328
|
-
const query = sameCharRun ? lower : typeahead;
|
|
329
|
-
const anchor = activeIndex < 0 ? 0 : activeIndex;
|
|
330
|
-
const start = sameCharRun ? anchor + 1 : anchor;
|
|
331
|
-
// Search forward, wrapping once — typeahead wraps even though the
|
|
332
|
-
// arrows clamp, or options above the cursor would be untypable.
|
|
333
|
-
for (let n = 0; n < locales.length; n++) {
|
|
334
|
-
const i = (start + n) % locales.length;
|
|
335
|
-
if (labelFor(locales[i]).toLowerCase().startsWith(query)) {
|
|
336
|
-
activeIndex = i;
|
|
337
|
-
scrollActiveIntoView();
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
317
|
+
function handleTabOut(): void {
|
|
318
|
+
// Tab moves on — but focus goes to the button FIRST, without
|
|
319
|
+
// cancelling the key (Listbox's onTabOut never preventDefaults
|
|
320
|
+
// Tab). Hiding the focused list drops focus to <body>, and the
|
|
321
|
+
// browser then computes the default Tab move from the top of the
|
|
322
|
+
// document, so tabbing out of an open picker teleported the user to
|
|
323
|
+
// the page's first tab stop. From the button, the default Tab
|
|
324
|
+
// lands exactly where leaving the picker should.
|
|
325
|
+
buttonEl?.focus?.({ preventScroll: true });
|
|
326
|
+
closeList(false);
|
|
341
327
|
}
|
|
342
328
|
|
|
343
329
|
function onButtonKeydown(event: KeyboardEvent): void {
|
|
@@ -355,62 +341,6 @@
|
|
|
355
341
|
}
|
|
356
342
|
}
|
|
357
343
|
|
|
358
|
-
function onListKeydown(event: KeyboardEvent): void {
|
|
359
|
-
switch (event.key) {
|
|
360
|
-
case "ArrowDown":
|
|
361
|
-
event.preventDefault();
|
|
362
|
-
moveActive(1);
|
|
363
|
-
break;
|
|
364
|
-
case "ArrowUp":
|
|
365
|
-
event.preventDefault();
|
|
366
|
-
moveActive(-1);
|
|
367
|
-
break;
|
|
368
|
-
case "Home":
|
|
369
|
-
event.preventDefault();
|
|
370
|
-
activeIndex = 0;
|
|
371
|
-
scrollActiveIntoView();
|
|
372
|
-
break;
|
|
373
|
-
case "End":
|
|
374
|
-
event.preventDefault();
|
|
375
|
-
activeIndex = locales.length - 1;
|
|
376
|
-
scrollActiveIntoView();
|
|
377
|
-
break;
|
|
378
|
-
case "Enter":
|
|
379
|
-
case " ":
|
|
380
|
-
event.preventDefault();
|
|
381
|
-
if (activeIndex >= 0) choose(activeIndex);
|
|
382
|
-
break;
|
|
383
|
-
case "Escape":
|
|
384
|
-
event.preventDefault();
|
|
385
|
-
closeList();
|
|
386
|
-
break;
|
|
387
|
-
case "PageUp":
|
|
388
|
-
event.preventDefault();
|
|
389
|
-
moveActive(-10);
|
|
390
|
-
break;
|
|
391
|
-
case "PageDown":
|
|
392
|
-
// ±10, clamped: an APG-optional key for long locale lists.
|
|
393
|
-
event.preventDefault();
|
|
394
|
-
moveActive(10);
|
|
395
|
-
break;
|
|
396
|
-
case "Tab":
|
|
397
|
-
// Tab moves on — but focus goes to the button FIRST,
|
|
398
|
-
// without cancelling the key. Hiding the focused list
|
|
399
|
-
// drops focus to <body>, and the browser then computes
|
|
400
|
-
// the default Tab move from the top of the document, so
|
|
401
|
-
// tabbing out of an open picker teleported the user to
|
|
402
|
-
// the page's first tab stop. From the button, the default
|
|
403
|
-
// Tab lands exactly where leaving the picker should.
|
|
404
|
-
buttonEl?.focus?.({ preventScroll: true });
|
|
405
|
-
closeList(false);
|
|
406
|
-
break;
|
|
407
|
-
default:
|
|
408
|
-
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
409
|
-
runTypeahead(event.key);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
344
|
function onRootFocusOut(event: FocusEvent): void {
|
|
415
345
|
const next = event.relatedTarget as Node | null;
|
|
416
346
|
if (next && rootEl?.contains(next)) return;
|
|
@@ -481,11 +411,10 @@
|
|
|
481
411
|
>
|
|
482
412
|
<input type="hidden" {name} {value} />
|
|
483
413
|
|
|
484
|
-
<
|
|
485
|
-
bind:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
aria-label={label}
|
|
414
|
+
<IconButton
|
|
415
|
+
bind:ref={buttonEl}
|
|
416
|
+
baseClass="locale-picker-button"
|
|
417
|
+
label={label}
|
|
489
418
|
aria-haspopup="listbox"
|
|
490
419
|
aria-expanded={open}
|
|
491
420
|
aria-controls={listId}
|
|
@@ -512,18 +441,23 @@
|
|
|
512
441
|
<path d="M8 2c2.2 0 4 2.7 4 6s-1.8 6-4 6-4-2.7-4-6 1.8-6 4-6z" />
|
|
513
442
|
</svg>
|
|
514
443
|
{/if}
|
|
515
|
-
</
|
|
444
|
+
</IconButton>
|
|
516
445
|
|
|
517
|
-
<
|
|
518
|
-
bind:
|
|
519
|
-
|
|
446
|
+
<Listbox
|
|
447
|
+
bind:ref={listEl}
|
|
448
|
+
as="ul"
|
|
449
|
+
baseClass="locale-picker-list"
|
|
520
450
|
id={listId}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
451
|
+
label={label}
|
|
452
|
+
navigation="active-descendant"
|
|
453
|
+
clamp
|
|
454
|
+
typeahead
|
|
455
|
+
pageSize={10}
|
|
456
|
+
bind:activeIndex
|
|
525
457
|
hidden={!open}
|
|
526
|
-
|
|
458
|
+
onActivate={choose}
|
|
459
|
+
onEscape={() => closeList()}
|
|
460
|
+
onTabOut={handleTabOut}
|
|
527
461
|
>
|
|
528
462
|
{#each locales as locale, i (locale)}
|
|
529
463
|
<!-- The option's keyboard interaction lives on the listbox
|
|
@@ -543,5 +477,5 @@
|
|
|
543
477
|
{labelFor(locale)}
|
|
544
478
|
</li>
|
|
545
479
|
{/each}
|
|
546
|
-
</
|
|
480
|
+
</Listbox>
|
|
547
481
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lilydesignsystem/svelte-locale-picker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": "=26"
|
|
6
6
|
},
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"prepublishOnly": "cd .. && npm run build"
|
|
25
25
|
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@lilydesignsystem/svelte-headless": "^0.2.0"
|
|
28
|
+
},
|
|
26
29
|
"peerDependencies": {
|
|
27
30
|
"svelte": "^5.0.0"
|
|
28
31
|
},
|