@zevra/ui 0.50.0 → 0.50.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/DECISIONS.md +41 -0
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/dist/overlays.d.ts.map +1 -1
- package/dist/overlays.js +9 -3
- package/dist/overlays.js.map +1 -1
- package/dist/select-menu.d.ts.map +1 -1
- package/dist/select-menu.js +66 -5
- package/dist/select-menu.js.map +1 -1
- package/package.json +1 -1
- package/react/css-inline.ts +1 -1
- package/react/overlays.tsx +9 -3
- package/react/select-menu.tsx +71 -5
- package/src/forms.css +15 -2
package/react/overlays.tsx
CHANGED
|
@@ -66,8 +66,14 @@ export function useOverlay({ ouvert, onClose, panneau, verrouille = true }: UseO
|
|
|
66
66
|
|
|
67
67
|
// ⚠️ La valeur PRÉCÉDENTE est restaurée, pas remise à '' : une page qui
|
|
68
68
|
// gérait déjà son propre défilement serait cassée par un reset aveugle.
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// ⚠️ Sur `html`, JAMAIS sur `body` (DECISIONS, 03/11/2026) : overlays.css
|
|
70
|
+
// verrouille déjà `html` ; un `overflow` de plus sur `body` ne remonte
|
|
71
|
+
// alors plus au viewport, `body` devient un conteneur de défilement à
|
|
72
|
+
// zéro, et toute colonne `sticky` de la page saute en haut de `body`,
|
|
73
|
+
// hors de l'écran.
|
|
74
|
+
const racine = docu.documentElement
|
|
75
|
+
const overflowPrecedent = verrouille ? racine.style.overflow : null
|
|
76
|
+
if (verrouille) racine.style.overflow = 'hidden'
|
|
71
77
|
|
|
72
78
|
const premier = cible?.querySelector<HTMLElement>(FOCUSABLES)
|
|
73
79
|
premier?.focus()
|
|
@@ -99,7 +105,7 @@ export function useOverlay({ ouvert, onClose, panneau, verrouille = true }: UseO
|
|
|
99
105
|
docu.addEventListener('keydown', onKey)
|
|
100
106
|
return () => {
|
|
101
107
|
docu.removeEventListener('keydown', onKey)
|
|
102
|
-
if (overflowPrecedent !== null)
|
|
108
|
+
if (overflowPrecedent !== null) racine.style.overflow = overflowPrecedent
|
|
103
109
|
origine.current?.focus?.()
|
|
104
110
|
}
|
|
105
111
|
}, [ouvert, panneau, verrouille])
|
package/react/select-menu.tsx
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// liste système est plus rapide à manipuler que n'importe quelle liste
|
|
21
21
|
// maison, et où le clavier logiciel s'adapte tout seul.
|
|
22
22
|
|
|
23
|
-
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
|
23
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
|
|
24
|
+
import type { CSSProperties } from 'react'
|
|
24
25
|
import type { ReactNode } from 'react'
|
|
25
26
|
import { cx } from './cx.js'
|
|
26
27
|
import { Icon } from './icon.js'
|
|
@@ -118,13 +119,70 @@ export function SelectMenu<T extends string = string>({
|
|
|
118
119
|
return () => doc.removeEventListener('pointerdown', dehors)
|
|
119
120
|
}, [ouvert])
|
|
120
121
|
|
|
122
|
+
// La place de la liste à l'écran (DECISIONS, 03/11/2026) : en `fixed`, elle
|
|
123
|
+
// échappe à tout conteneur qui rogne (un tableau qui défile, une carte en
|
|
124
|
+
// `overflow: hidden`). Calculée d'après la boîte du déclencheur, et
|
|
125
|
+
// recalculée quand la page ou un conteneur défile, ou que la fenêtre change.
|
|
126
|
+
const [place, setPlace] = useState<CSSProperties | null>(null)
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
if (!ouvert) {
|
|
129
|
+
setPlace(null)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const vue = declencheur.current?.ownerDocument.defaultView ?? window
|
|
133
|
+
const ECART = 5
|
|
134
|
+
const MARGE = 8
|
|
135
|
+
function placer(e?: Event) {
|
|
136
|
+
// Le défilement de la liste ELLE-MÊME ne la déplace pas : sans ce
|
|
137
|
+
// filtre, chaque tour de molette dans une longue liste la replaçait.
|
|
138
|
+
if (e && e.target === liste.current) return
|
|
139
|
+
const d = declencheur.current
|
|
140
|
+
if (!d) return
|
|
141
|
+
const r = d.getBoundingClientRect()
|
|
142
|
+
// La largeur AVANT la mesure : sans elle, la liste vaut encore 100 % de
|
|
143
|
+
// la fenêtre au premier passage, et le garde-fou de droite la plaque à
|
|
144
|
+
// gauche de l'écran.
|
|
145
|
+
liste.current?.style.setProperty('--zv-menu-largeur', `${r.width}px`)
|
|
146
|
+
const hauteur = liste.current?.scrollHeight ?? 0
|
|
147
|
+
const largeur = Math.max(liste.current?.offsetWidth ?? 0, r.width)
|
|
148
|
+
const dessous = vue.innerHeight - r.bottom - ECART - MARGE
|
|
149
|
+
const dessus = r.top - ECART - MARGE
|
|
150
|
+
// Au-dessus seulement si la liste ne tient pas dessous ET qu'il y a
|
|
151
|
+
// davantage de place au-dessus : sinon on garde le sens attendu.
|
|
152
|
+
const enHaut = hauteur > dessous && dessus > dessous
|
|
153
|
+
const dispo = Math.max(120, enHaut ? dessus : dessous)
|
|
154
|
+
const visible = Math.min(hauteur, dispo)
|
|
155
|
+
setPlace({
|
|
156
|
+
position: 'fixed',
|
|
157
|
+
top: enHaut ? r.top - ECART - visible : r.bottom + ECART,
|
|
158
|
+
// Jamais hors de la fenêtre à droite.
|
|
159
|
+
left: Math.max(MARGE, Math.min(r.left, vue.innerWidth - largeur - MARGE)),
|
|
160
|
+
maxHeight: dispo,
|
|
161
|
+
'--zv-menu-largeur': `${r.width}px`,
|
|
162
|
+
} as CSSProperties)
|
|
163
|
+
}
|
|
164
|
+
placer()
|
|
165
|
+
vue.addEventListener('scroll', placer, true)
|
|
166
|
+
vue.addEventListener('resize', placer)
|
|
167
|
+
return () => {
|
|
168
|
+
vue.removeEventListener('scroll', placer, true)
|
|
169
|
+
vue.removeEventListener('resize', placer)
|
|
170
|
+
}
|
|
171
|
+
}, [ouvert, options.length])
|
|
172
|
+
|
|
121
173
|
// L'option active reste visible quand on parcourt une liste plus haute que
|
|
122
|
-
// son cadre.
|
|
174
|
+
// son cadre. `place` : la liste n'est bornée (hauteur maximale) qu'une fois
|
|
175
|
+
// placée ; avant, il n'y a rien à faire défiler, et une liste longue
|
|
176
|
+
// s'ouvrait en haut (« Algérie ») au lieu de l'option choisie (« France »).
|
|
177
|
+
// Sur le PASSAGE à « placée », pas sur chaque nouvelle place : la place
|
|
178
|
+
// change à chaque défilement de la page, et ramener l'option active à
|
|
179
|
+
// chaque fois contrarierait qui parcourt la liste.
|
|
180
|
+
const placee = place !== null
|
|
123
181
|
useEffect(() => {
|
|
124
|
-
if (!ouvert) return
|
|
182
|
+
if (!ouvert || !placee) return
|
|
125
183
|
const el = liste.current?.children[actif] as HTMLElement | undefined
|
|
126
184
|
el?.scrollIntoView({ block: 'nearest' })
|
|
127
|
-
}, [ouvert, actif])
|
|
185
|
+
}, [ouvert, actif, placee])
|
|
128
186
|
|
|
129
187
|
function suivant(depuis: number, pas: number): number {
|
|
130
188
|
const n = options.length
|
|
@@ -221,7 +279,15 @@ export function SelectMenu<T extends string = string>({
|
|
|
221
279
|
</button>
|
|
222
280
|
|
|
223
281
|
{ouvert && (
|
|
224
|
-
<div
|
|
282
|
+
<div
|
|
283
|
+
ref={liste}
|
|
284
|
+
id={idListe}
|
|
285
|
+
role="listbox"
|
|
286
|
+
className="zv-menu zv-menu--place"
|
|
287
|
+
// Tant que la place n'est pas calculée (premier rendu), la liste est
|
|
288
|
+
// posée mais invisible : elle ne clignote pas à sa place d'origine.
|
|
289
|
+
style={place ?? { position: 'fixed', visibility: 'hidden' }}
|
|
290
|
+
>
|
|
225
291
|
{options.map((o, i) => (
|
|
226
292
|
<button
|
|
227
293
|
key={o.valeur}
|
package/src/forms.css
CHANGED
|
@@ -383,8 +383,21 @@
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
.zv-select-anchor--block .zv-menu {
|
|
386
|
-
|
|
387
|
-
|
|
386
|
+
/* En `fixed` (DECISIONS, 03/11/2026), 100 % vaudrait la fenêtre : la
|
|
387
|
+
* largeur du déclencheur arrive par `--zv-menu-largeur`. La liste en est
|
|
388
|
+
* le MINIMUM, pas la mesure : sous un déclencheur calé sur son contenu
|
|
389
|
+
* (« Membre »), l'option longue (« Administrateur ») débordait. Elle prend
|
|
390
|
+
* la largeur de ses options, jamais plus que la fenêtre. */
|
|
391
|
+
width: max-content;
|
|
392
|
+
min-width: var(--zv-menu-largeur, 100%);
|
|
393
|
+
max-width: calc(100vw - 16px);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* La liste de SelectMenu, placée par le composant (`fixed`, haut, gauche,
|
|
397
|
+
* hauteur maximale) : elle défile au-delà de la place disponible. */
|
|
398
|
+
.zv-menu--place {
|
|
399
|
+
overflow-y: auto;
|
|
400
|
+
overscroll-behavior: contain;
|
|
388
401
|
}
|
|
389
402
|
|
|
390
403
|
/* ─── Déclencheur de sélection DESSINÉE ─────────────────────────────────
|