fuma 2.6.0 → 2.6.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/command/useCommand.svelte.js +6 -1
- package/dist/ui/drawer/Drawer.svelte +5 -2
- package/dist/ui/drawer/Drawer.svelte.d.ts +2 -0
- package/dist/ui/drawer/focusTarget.d.ts +5 -0
- package/dist/ui/drawer/focusTarget.js +36 -0
- package/dist/ui/drawer/inertBackground.d.ts +3 -1
- package/dist/ui/drawer/inertBackground.js +8 -5
- package/package.json +1 -1
|
@@ -82,8 +82,13 @@ export function useCommand({ onSelect, onFocus, isEnable = () => true, hotKey }
|
|
|
82
82
|
[createAttachmentKey()]: (node) => {
|
|
83
83
|
if (!items.includes(node))
|
|
84
84
|
items.splice(index, 0, node);
|
|
85
|
-
const cleanup = on(node, 'click', () => {
|
|
85
|
+
const cleanup = on(node, 'click', (event) => {
|
|
86
86
|
focusIndex = selectedIndex = items.indexOf(node);
|
|
87
|
+
// Un lien navigue par son `href`, modificateurs compris (cmd+clic, nouvel
|
|
88
|
+
// onglet): le clic ne fait que poser le curseur. `onSelect` reste la voie du
|
|
89
|
+
// clavier, et celle d'un clic dont le lien a été annulé.
|
|
90
|
+
if (node instanceof HTMLAnchorElement && node.href && !event.defaultPrevented)
|
|
91
|
+
return;
|
|
87
92
|
onSelect?.(selectedIndex);
|
|
88
93
|
});
|
|
89
94
|
return () => {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
classBody = '',
|
|
25
25
|
duration = 180,
|
|
26
26
|
noOverlay = false,
|
|
27
|
+
autofocus = true,
|
|
27
28
|
zIndex = 50,
|
|
28
29
|
inertSkip = '[data-sonner-toaster], :has(> [data-sonner-toaster])',
|
|
29
30
|
children
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
classBody?: ClassValue
|
|
37
38
|
duration?: number
|
|
38
39
|
noOverlay?: boolean
|
|
40
|
+
/** Poser le focus sur le premier champ du corps à l'ouverture, s'il y en a un. */
|
|
41
|
+
autofocus?: boolean
|
|
39
42
|
zIndex?: number
|
|
40
43
|
/** Sélecteur des éléments d'arrière-plan qui restent interactifs quand le tiroir est ouvert. */
|
|
41
44
|
inertSkip?: string
|
|
@@ -102,7 +105,7 @@
|
|
|
102
105
|
aria-modal={!noOverlay}
|
|
103
106
|
aria-label={title}
|
|
104
107
|
tabindex={-1}
|
|
105
|
-
{@attach noOverlay ? () => {} : inertBackground({ key, skip: inertSkip })}
|
|
108
|
+
{@attach noOverlay ? () => {} : inertBackground({ key, skip: inertSkip, autofocus })}
|
|
106
109
|
bind:clientWidth
|
|
107
110
|
transition:drawerFly|local={{
|
|
108
111
|
x: clientWidth,
|
|
@@ -138,7 +141,7 @@
|
|
|
138
141
|
</button>
|
|
139
142
|
</div>
|
|
140
143
|
|
|
141
|
-
<div class={['grow px-4', classBody]}>
|
|
144
|
+
<div data-drawer-body class={['grow px-4', classBody]}>
|
|
142
145
|
{@render children({ open, close })}
|
|
143
146
|
</div>
|
|
144
147
|
</div>
|
|
@@ -13,6 +13,8 @@ declare const Drawer: import("svelte").Component<{
|
|
|
13
13
|
classBody?: ClassValue;
|
|
14
14
|
duration?: number;
|
|
15
15
|
noOverlay?: boolean;
|
|
16
|
+
/** Poser le focus sur le premier champ du corps à l'ouverture, s'il y en a un. */
|
|
17
|
+
autofocus?: boolean;
|
|
16
18
|
zIndex?: number;
|
|
17
19
|
/** Sélecteur des éléments d'arrière-plan qui restent interactifs quand le tiroir est ouvert. */
|
|
18
20
|
inertSkip?: string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Champs de saisie: le focus s'y pose sans effet de bord. */
|
|
2
|
+
const ENTRY = 'input:not([type="hidden"], [type="submit"], [type="button"], [type="reset"], [type="image"]), textarea, select';
|
|
3
|
+
/**
|
|
4
|
+
* Tout ce qui compte comme premier champ. Les déclencheurs d'`InputSelect` et d'`InputMultiSelect`
|
|
5
|
+
* en font partie sans être focalisables ici: ils comptent pour décider qu'on ne descend pas, sans
|
|
6
|
+
* quoi le focus sauterait par-dessus le premier champ pour atterrir au milieu du formulaire.
|
|
7
|
+
*/
|
|
8
|
+
const FIELD = `${ENTRY}, .input, [role="combobox"]`;
|
|
9
|
+
function isEligible(element) {
|
|
10
|
+
if (element.matches('[disabled], [aria-disabled="true"], [readonly], [tabindex="-1"]')) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
// Écarte ce que le CSS cache: l'`input[type=file]` d'un sélecteur d'image, et les champs d'un
|
|
14
|
+
// `<dialog>` fermé rendu à l'intérieur du tiroir.
|
|
15
|
+
return element.checkVisibility();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Le premier champ de saisie du corps du tiroir, ou le panneau lui-même à défaut — un tiroir sans
|
|
19
|
+
* formulaire garde le focus sur son conteneur.
|
|
20
|
+
*/
|
|
21
|
+
export function drawerFocusTarget(node) {
|
|
22
|
+
// Le clavier virtuel recouvrirait la moitié du tiroir à peine ouvert.
|
|
23
|
+
if (window.matchMedia('(pointer: coarse)').matches)
|
|
24
|
+
return node;
|
|
25
|
+
const body = node.querySelector('[data-drawer-body]') ?? node;
|
|
26
|
+
for (const field of body.querySelectorAll(FIELD)) {
|
|
27
|
+
if (!isEligible(field))
|
|
28
|
+
continue;
|
|
29
|
+
// Le premier champ n'est pas une saisie: on s'arrête sur le panneau plutôt que de chercher
|
|
30
|
+
// plus bas.
|
|
31
|
+
if (!field.matches(ENTRY))
|
|
32
|
+
return node;
|
|
33
|
+
return field;
|
|
34
|
+
}
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
@@ -4,10 +4,12 @@ export type InertBackgroundOptions = {
|
|
|
4
4
|
key: string;
|
|
5
5
|
/** Sélecteur des frères à laisser interactifs, par ex. le conteneur des toasts. */
|
|
6
6
|
skip?: string;
|
|
7
|
+
/** Descendre au premier champ du corps, plutôt que de s'arrêter sur le panneau. */
|
|
8
|
+
autofocus?: boolean;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
9
11
|
* Rend inerte tout le document sauf la branche qui mène au nœud attaché: à chaque niveau on marque
|
|
10
12
|
* les frères, jusqu'à `<body>`. Aucun nœud n'est déplacé — poser `inert` sur un conteneur racine
|
|
11
13
|
* serait impossible ici, le tiroir étant rendu à l'intérieur de ce conteneur.
|
|
12
14
|
*/
|
|
13
|
-
export declare function inertBackground({ key, skip }: InertBackgroundOptions): Attachment<HTMLElement>;
|
|
15
|
+
export declare function inertBackground({ key, skip, autofocus }: InertBackgroundOptions): Attachment<HTMLElement>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { drawerFocusTarget } from './focusTarget.js';
|
|
1
2
|
// Compteur par élément: deux tiroirs empilés marquent les mêmes frères, et le second à se fermer
|
|
2
3
|
// ne doit lever `inert` que si le premier l'a déjà relâché. Un `WeakMap` évite de retenir les
|
|
3
4
|
// nœuds détachés entre-temps.
|
|
@@ -7,7 +8,7 @@ const marks = new WeakMap();
|
|
|
7
8
|
* les frères, jusqu'à `<body>`. Aucun nœud n'est déplacé — poser `inert` sur un conteneur racine
|
|
8
9
|
* serait impossible ici, le tiroir étant rendu à l'intérieur de ce conteneur.
|
|
9
10
|
*/
|
|
10
|
-
export function inertBackground({ key, skip }) {
|
|
11
|
+
export function inertBackground({ key, skip, autofocus = true }) {
|
|
11
12
|
return (node) => {
|
|
12
13
|
const marked = [];
|
|
13
14
|
for (let el = node; el && el !== document.body; el = el.parentElement) {
|
|
@@ -25,18 +26,20 @@ export function inertBackground({ key, skip }) {
|
|
|
25
26
|
marked.push(sibling);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
const focus = () => (autofocus ? drawerFocusTarget(node) : node).focus({ preventScroll: true });
|
|
28
30
|
// Relevé avant la mise en `inert`: le focus courant est souvent sur le déclencheur, qui
|
|
29
31
|
// appartient à l'arrière-plan et perdrait le focus au profit de `<body>`.
|
|
30
32
|
const previous = document.activeElement;
|
|
31
|
-
|
|
33
|
+
focus();
|
|
32
34
|
// SvelteKit remet le focus sur `<body>` à la fin de la navigation qui ouvre le tiroir. Les
|
|
33
35
|
// `goto` de `open()`/`close()` passent `keepFocus`, mais un tiroir ouvert par un simple
|
|
34
36
|
// `<a href>` n'en profite pas: on repasse après coup, et seulement si personne d'autre n'a
|
|
35
|
-
// pris le focus entre-temps.
|
|
37
|
+
// pris le focus entre-temps. Le panneau ne compte pas comme quelqu'un: le corps peut n'avoir
|
|
38
|
+
// été monté qu'après l'attachement, et le champ visé n'existait alors pas encore.
|
|
36
39
|
const timer = setTimeout(() => {
|
|
37
40
|
const active = document.activeElement;
|
|
38
|
-
if (!active || active === document.body)
|
|
39
|
-
|
|
41
|
+
if (!active || active === document.body || active === node)
|
|
42
|
+
focus();
|
|
40
43
|
});
|
|
41
44
|
return () => {
|
|
42
45
|
clearTimeout(timer);
|