fuma 2.5.4 → 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.
@@ -11,7 +11,11 @@ export declare function useCommand({ onSelect, onFocus, isEnable, hotKey }?: Com
11
11
  [x: symbol]: (node: HTMLElement) => () => void;
12
12
  };
13
13
  focusTrigger: () => void;
14
- item: (index: number, scrollMargin?: string) => {
14
+ /**
15
+ * Le dégagement du défilement appartient à l'appelant: `scroll-mt-*` / `scroll-mb-*`
16
+ * sur l'item, puisque lui seul connaît la hauteur de son en-tête collant.
17
+ */
18
+ item: (index: number) => {
15
19
  [x: symbol]: (node: HTMLElement) => () => void;
16
20
  };
17
21
  };
@@ -74,13 +74,21 @@ export function useCommand({ onSelect, onFocus, isEnable = () => true, hotKey }
74
74
  }
75
75
  },
76
76
  focusTrigger,
77
- item: (index, scrollMargin = '8px') => ({
77
+ /**
78
+ * Le dégagement du défilement appartient à l'appelant: `scroll-mt-*` / `scroll-mb-*`
79
+ * sur l'item, puisque lui seul connaît la hauteur de son en-tête collant.
80
+ */
81
+ item: (index) => ({
78
82
  [createAttachmentKey()]: (node) => {
79
- node.style.scrollMargin = scrollMargin;
80
83
  if (!items.includes(node))
81
84
  items.splice(index, 0, node);
82
- const cleanup = on(node, 'click', () => {
85
+ const cleanup = on(node, 'click', (event) => {
83
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;
84
92
  onSelect?.(selectedIndex);
85
93
  });
86
94
  return () => {
@@ -293,7 +293,7 @@
293
293
  {@const isFocus = index === command.focusIndex}
294
294
  <li>
295
295
  <button
296
- {...command.item(index, '')}
296
+ {...command.item(index)}
297
297
  class={[isSelected && 'border', isFocus && 'menu-focus', 'scroll-mt-13 scroll-mb-2']}
298
298
  type="button"
299
299
  tabindex="-1"
@@ -62,7 +62,7 @@ declare function $$render<Item>(): {
62
62
  [x: symbol]: (node: HTMLElement) => () => void;
63
63
  };
64
64
  focusTrigger: () => void;
65
- item: (index: number, scrollMargin?: string) => {
65
+ item: (index: number) => {
66
66
  [x: symbol]: (node: HTMLElement) => () => void;
67
67
  };
68
68
  };
@@ -96,7 +96,7 @@ declare class __sveltets_Render<Item> {
96
96
  [x: symbol]: (node: HTMLElement) => () => void;
97
97
  };
98
98
  focusTrigger: () => void;
99
- item: (index: number, scrollMargin?: string) => {
99
+ item: (index: number) => {
100
100
  [x: symbol]: (node: HTMLElement) => () => void;
101
101
  };
102
102
  };
@@ -252,7 +252,7 @@
252
252
  {@const isFocus = index === command.focusIndex}
253
253
  <li {...propsLi}>
254
254
  <button
255
- {...command.item(index, '')}
255
+ {...command.item(index)}
256
256
  class={[
257
257
  isSelected && 'border-hard border',
258
258
  isFocus && 'menu-focus',
@@ -70,7 +70,7 @@ declare function $$render<Item>(): {
70
70
  [x: symbol]: (node: HTMLElement) => () => void;
71
71
  };
72
72
  focusTrigger: () => void;
73
- item: (index: number, scrollMargin?: string) => {
73
+ item: (index: number) => {
74
74
  [x: symbol]: (node: HTMLElement) => () => void;
75
75
  };
76
76
  };
@@ -104,7 +104,7 @@ declare class __sveltets_Render<Item> {
104
104
  [x: symbol]: (node: HTMLElement) => () => void;
105
105
  };
106
106
  focusTrigger: () => void;
107
- item: (index: number, scrollMargin?: string) => {
107
+ item: (index: number) => {
108
108
  [x: symbol]: (node: HTMLElement) => () => void;
109
109
  };
110
110
  };
@@ -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,5 @@
1
+ /**
2
+ * Le premier champ de saisie du corps du tiroir, ou le panneau lui-même à défaut — un tiroir sans
3
+ * formulaire garde le focus sur son conteneur.
4
+ */
5
+ export declare function drawerFocusTarget(node: HTMLElement): HTMLElement;
@@ -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
- node.focus({ preventScroll: true });
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
- node.focus({ preventScroll: true });
41
+ if (!active || active === document.body || active === node)
42
+ focus();
40
43
  });
41
44
  return () => {
42
45
  clearTimeout(timer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fuma",
3
- "version": "2.5.4",
3
+ "version": "2.6.2",
4
4
  "description": "My fullstack material build with sveltekit, daisyui, zod, prisma, lucia",
5
5
  "author": {
6
6
  "name": "Jonas Voisard",