@zevra/ui 0.43.0 → 0.43.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 +17 -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/data-table.d.ts +5 -1
- package/dist/data-table.d.ts.map +1 -1
- package/dist/data-table.js +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/grille.d.ts +3 -1
- package/dist/grille.d.ts.map +1 -1
- package/dist/grille.js +90 -61
- package/dist/grille.js.map +1 -1
- package/package.json +1 -1
- package/react/css-inline.ts +1 -1
- package/react/data-table.tsx +11 -1
- package/react/grille.tsx +214 -145
- package/src/data.css +30 -7
package/react/data-table.tsx
CHANGED
|
@@ -81,6 +81,10 @@ export interface DataTableProps<T> {
|
|
|
81
81
|
/** Appelé au clic sur une ligne. Sans lui, la ligne n'est pas cliquable :
|
|
82
82
|
* et elle ne doit pas en avoir l'air. */
|
|
83
83
|
onRowClick?: (ligne: T) => void
|
|
84
|
+
/** La ligne dont on regarde le détail (l'aperçu ouvert à côté de la
|
|
85
|
+
* liste). Elle porte un filet d'accent à gauche, distinct de la ligne
|
|
86
|
+
* COCHÉE, qui est une sélection pour agir. */
|
|
87
|
+
ligneActive?: (ligne: T) => boolean
|
|
84
88
|
className?: string
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -139,6 +143,7 @@ export function DataTable<T>({
|
|
|
139
143
|
onRowClick,
|
|
140
144
|
celluleEntete,
|
|
141
145
|
sousEntete,
|
|
146
|
+
ligneActive,
|
|
142
147
|
className,
|
|
143
148
|
}: DataTableProps<T>) {
|
|
144
149
|
const grille = table.options.enableColumnResizing === true
|
|
@@ -219,7 +224,12 @@ export function DataTable<T>({
|
|
|
219
224
|
: lignes.map((ligne) => (
|
|
220
225
|
<div
|
|
221
226
|
key={ligne.id}
|
|
222
|
-
className={cx(
|
|
227
|
+
className={cx(
|
|
228
|
+
'zv-row',
|
|
229
|
+
ligne.getIsSelected() && 'zv-row--selected',
|
|
230
|
+
ligneActive?.(ligne.original) && 'zv-row--on',
|
|
231
|
+
)}
|
|
232
|
+
aria-current={ligneActive?.(ligne.original) || undefined}
|
|
223
233
|
// Le clavier suit le clic : une ligne cliquable qui n'est pas
|
|
224
234
|
// atteignable au clavier est une action inaccessible.
|
|
225
235
|
role={onRowClick ? 'button' : undefined}
|
package/react/grille.tsx
CHANGED
|
@@ -70,7 +70,8 @@ import { DataTable, colonnesAffichees, epinglage, type CelluleEnteteProps } from
|
|
|
70
70
|
import { usePersistedState } from './use-persisted-state.js'
|
|
71
71
|
import { Button } from './actions.js'
|
|
72
72
|
import { Icon, IconButton } from './icon.js'
|
|
73
|
-
import { Checkbox, Field, Input, Search,
|
|
73
|
+
import { Checkbox, Field, Input, Search, SelectAnchor } from './forms.js'
|
|
74
|
+
import { SelectMenu } from './select-menu.js'
|
|
74
75
|
import { Menu, MenuItem, Pager, PagerArrow, Popover } from './navigation.js'
|
|
75
76
|
import { Empty } from './feedback.js'
|
|
76
77
|
import { Meta } from './data.js'
|
|
@@ -227,6 +228,8 @@ export interface DataGridProps<T> {
|
|
|
227
228
|
/** Clic (ou Entrée) sur une ligne. Sans lui, la ligne n'est pas
|
|
228
229
|
* cliquable, et elle n'en a pas l'air. */
|
|
229
230
|
onRowClick?: (ligne: T) => void
|
|
231
|
+
/** La ligne dont l'aperçu est ouvert : filet d'accent à gauche. */
|
|
232
|
+
ligneActive?: (ligne: T) => boolean
|
|
230
233
|
/** Identifiant de ligne. Indispensable avec `selection` dès que les
|
|
231
234
|
* données se rechargent : par défaut c'est l'INDEX, et une ligne cochée
|
|
232
235
|
* deviendrait une autre ligne après un tri côté serveur. */
|
|
@@ -832,38 +835,10 @@ function FiltreColonne<T>({
|
|
|
832
835
|
const nom = libelleColonne(colonne)
|
|
833
836
|
const filtre = colonne.getFilterValue() as ValeurFiltre | undefined
|
|
834
837
|
|
|
835
|
-
if (type === 'choix') {
|
|
836
|
-
// Les valeurs du modèle FACETTÉ : celles que les autres filtres laissent
|
|
837
|
-
// encore passer. Une option qui ne rendrait aucune ligne n'est pas offerte.
|
|
838
|
-
const options = [...colonne.getFacetedUniqueValues().keys()]
|
|
839
|
-
.filter((v) => v != null && v !== '')
|
|
840
|
-
.map(String)
|
|
841
|
-
.sort((a, b) => a.localeCompare(b, 'fr'))
|
|
842
|
-
const valeur = filtre?.type === 'choix' ? filtre.valeur : ''
|
|
843
|
-
// L'option choisie reste dans la liste même si un autre filtre l'a
|
|
844
|
-
// fait disparaître : sinon le <select> afficherait « Tous » en mentant.
|
|
845
|
-
if (valeur && !options.includes(valeur)) options.unshift(valeur)
|
|
846
|
-
return (
|
|
847
|
-
<Select
|
|
848
|
-
className="zv-table__filtre"
|
|
849
|
-
value={valeur}
|
|
850
|
-
onChange={(e) => colonne.setFilterValue({ type: 'choix', valeur: e.target.value })}
|
|
851
|
-
aria-label={t.filtrerColonne(nom)}
|
|
852
|
-
>
|
|
853
|
-
<option value="">{t.tous}</option>
|
|
854
|
-
{options.map((o) => (
|
|
855
|
-
<option key={o} value={o}>
|
|
856
|
-
{o}
|
|
857
|
-
</option>
|
|
858
|
-
))}
|
|
859
|
-
</Select>
|
|
860
|
-
)
|
|
861
|
-
}
|
|
862
|
-
|
|
838
|
+
if (type === 'choix') return <FiltreChoix colonne={colonne} nom={nom} t={t} />
|
|
863
839
|
if (type === 'nombre' || type === 'date') {
|
|
864
840
|
return <FiltrePlage colonne={colonne} type={type} nom={nom} t={t} />
|
|
865
841
|
}
|
|
866
|
-
|
|
867
842
|
return (
|
|
868
843
|
<Input
|
|
869
844
|
className="zv-table__filtre"
|
|
@@ -876,55 +851,45 @@ function FiltreColonne<T>({
|
|
|
876
851
|
}
|
|
877
852
|
|
|
878
853
|
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
854
|
+
* Un filtre qui s'ouvre : le champ de la rangée RÉSUME la valeur, et ouvre
|
|
855
|
+
* un panneau (une liste de choix, une plage) sous lui.
|
|
881
856
|
*
|
|
882
|
-
* ⚠️ LE
|
|
883
|
-
* horizontalement, et `overflow-x: auto` force `overflow-y` : un
|
|
857
|
+
* ⚠️ LE PANNEAU SORT DU CADRE, par un portail. `.zv-table` défile
|
|
858
|
+
* horizontalement, et `overflow-x: auto` force `overflow-y` : un panneau
|
|
884
859
|
* posé dans la cellule serait rogné par le bas du tableau, invisible sur un
|
|
885
|
-
* tableau de trois lignes. Il est
|
|
886
|
-
*
|
|
860
|
+
* tableau de trois lignes. Il est rendu dans le <body> et placé sous son
|
|
861
|
+
* déclencheur, en coordonnées d'écran.
|
|
862
|
+
*
|
|
863
|
+
* ⚠️ AUCUN <select> NATIF ICI : sa liste est peinte par le SYSTÈME, hors
|
|
864
|
+
* charte (menu noir sous macOS sombre), et certaines apps l'interdisent.
|
|
887
865
|
*/
|
|
888
|
-
function
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
866
|
+
function FiltreFlottant({
|
|
867
|
+
libelle,
|
|
868
|
+
resume,
|
|
869
|
+
vide,
|
|
870
|
+
icone,
|
|
871
|
+
role,
|
|
872
|
+
panneau: contenu,
|
|
893
873
|
}: {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
874
|
+
libelle: string
|
|
875
|
+
resume: string
|
|
876
|
+
vide: boolean
|
|
877
|
+
icone: 'calendrier' | 'filtre' | 'chevron-bas'
|
|
878
|
+
role: 'dialog' | 'listbox'
|
|
879
|
+
/** Le contenu ; reçoit de quoi refermer le panneau. */
|
|
880
|
+
panneau: (fermer: () => void) => ReactNode
|
|
898
881
|
}) {
|
|
899
882
|
const [ouvert, setOuvert] = useState(false)
|
|
900
883
|
const [position, setPosition] = useState<{ top: number; left: number } | null>(null)
|
|
901
884
|
const declencheur = useRef<HTMLButtonElement>(null)
|
|
902
885
|
const panneau = useRef<HTMLDivElement>(null)
|
|
903
|
-
const idChamp = useId()
|
|
904
886
|
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
colonne.setFilterValue(
|
|
910
|
-
type === 'nombre' ? { type, min: bas, max: haut } : { type, du: bas, au: haut },
|
|
911
|
-
)
|
|
887
|
+
const fermer = useCallback(() => {
|
|
888
|
+
setOuvert(false)
|
|
889
|
+
declencheur.current?.focus()
|
|
890
|
+
}, [])
|
|
912
891
|
|
|
913
|
-
//
|
|
914
|
-
// saisie s'affiche dans le champ sans filtrer le tableau. Resynchronisé
|
|
915
|
-
// quand le filtre change d'ailleurs (« Réinitialiser la vue »).
|
|
916
|
-
const [brouillon, setBrouillon] = useState<[string, string]>([a, b])
|
|
917
|
-
useEffect(() => {
|
|
918
|
-
setBrouillon([a, b])
|
|
919
|
-
}, [a, b])
|
|
920
|
-
const saisir = (bas: string, haut: string) => {
|
|
921
|
-
setBrouillon([bas, haut])
|
|
922
|
-
if (type === 'nombre' || (dateComplete(bas) && dateComplete(haut))) ecrire(bas, haut)
|
|
923
|
-
}
|
|
924
|
-
const resume = type === 'nombre' ? t.resumeNombres(a, b) : t.resumeDates(a, b)
|
|
925
|
-
const vide = !a && !b
|
|
926
|
-
|
|
927
|
-
// Placé AVANT l'affichage (layout effect) : sinon le popover apparaît un
|
|
892
|
+
// Placé AVANT l'affichage (layout effect) : sinon le panneau apparaît un
|
|
928
893
|
// instant en haut à gauche de la page.
|
|
929
894
|
useLayoutEffect(() => {
|
|
930
895
|
if (!ouvert) {
|
|
@@ -939,8 +904,8 @@ function FiltrePlage<T>({
|
|
|
939
904
|
const largeur = (panneau.current?.firstElementChild as HTMLElement | null)?.offsetWidth ?? 0
|
|
940
905
|
const vue = declencheur.current?.ownerDocument.defaultView
|
|
941
906
|
const droite = vue ? vue.innerWidth : r.right + largeur
|
|
942
|
-
// Le décalage de 5 px sous le déclencheur est celui de .zv-popover
|
|
943
|
-
// (top: calc(100% + 5px)) : l'enveloppe se pose au ras
|
|
907
|
+
// Le décalage de 5 px sous le déclencheur est celui de .zv-popover et
|
|
908
|
+
// de .zv-menu (top: calc(100% + 5px)) : l'enveloppe se pose au ras.
|
|
944
909
|
setPosition({ top: r.bottom, left: Math.max(0, Math.min(r.left, droite - largeur - 8)) })
|
|
945
910
|
}
|
|
946
911
|
placer()
|
|
@@ -962,10 +927,7 @@ function FiltrePlage<T>({
|
|
|
962
927
|
if (!panneau.current?.contains(cible) && !declencheur.current?.contains(cible)) setOuvert(false)
|
|
963
928
|
}
|
|
964
929
|
const echap = (e: KeyboardEvent) => {
|
|
965
|
-
if (e.key === 'Escape')
|
|
966
|
-
setOuvert(false)
|
|
967
|
-
declencheur.current?.focus()
|
|
968
|
-
}
|
|
930
|
+
if (e.key === 'Escape') fermer()
|
|
969
931
|
}
|
|
970
932
|
doc.addEventListener('mousedown', dehors)
|
|
971
933
|
doc.addEventListener('keydown', echap)
|
|
@@ -973,16 +935,136 @@ function FiltrePlage<T>({
|
|
|
973
935
|
doc.removeEventListener('mousedown', dehors)
|
|
974
936
|
doc.removeEventListener('keydown', echap)
|
|
975
937
|
}
|
|
976
|
-
}, [ouvert])
|
|
938
|
+
}, [ouvert, fermer])
|
|
977
939
|
|
|
978
|
-
// Le focus entre dans le
|
|
979
|
-
//
|
|
980
|
-
//
|
|
940
|
+
// Le focus entre dans le panneau une fois PLACÉ : avant, il est en
|
|
941
|
+
// `visibility: hidden`, et le navigateur refuse le focus à un élément
|
|
942
|
+
// invisible. La frappe partait alors dans le vide. L'option choisie
|
|
943
|
+
// d'abord, sinon le premier champ ou la première option.
|
|
981
944
|
const place = position !== null
|
|
982
945
|
useEffect(() => {
|
|
983
|
-
if (ouvert
|
|
946
|
+
if (!ouvert || !place) return
|
|
947
|
+
const p = panneau.current
|
|
948
|
+
;(p?.querySelector<HTMLElement>('[aria-selected="true"]') ?? p?.querySelector<HTMLElement>('input, button'))?.focus()
|
|
984
949
|
}, [ouvert, place])
|
|
985
950
|
|
|
951
|
+
return (
|
|
952
|
+
<>
|
|
953
|
+
<button
|
|
954
|
+
ref={declencheur}
|
|
955
|
+
type="button"
|
|
956
|
+
className="zv-input zv-input--trigger zv-table__filtre"
|
|
957
|
+
data-vide={vide ? 'oui' : undefined}
|
|
958
|
+
aria-expanded={ouvert}
|
|
959
|
+
aria-haspopup={role}
|
|
960
|
+
aria-label={`${libelle} : ${resume}`}
|
|
961
|
+
onClick={() => setOuvert((o) => !o)}
|
|
962
|
+
>
|
|
963
|
+
<span className="zv-colhead__label">{resume}</span>
|
|
964
|
+
<Icon nom={icone} taille="sm" discret />
|
|
965
|
+
</button>
|
|
966
|
+
{ouvert &&
|
|
967
|
+
createPortal(
|
|
968
|
+
<div
|
|
969
|
+
ref={panneau}
|
|
970
|
+
className="zv-grille__flottant"
|
|
971
|
+
style={position ? { top: position.top, left: position.left } : { visibility: 'hidden' }}
|
|
972
|
+
>
|
|
973
|
+
{contenu(fermer)}
|
|
974
|
+
</div>,
|
|
975
|
+
declencheur.current?.ownerDocument.body ?? document.body,
|
|
976
|
+
)}
|
|
977
|
+
</>
|
|
978
|
+
)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** Les flèches promènent le focus d'une option à l'autre, Origine et Fin
|
|
982
|
+
* vont aux extrémités : le parcours clavier d'une liste, que le <select>
|
|
983
|
+
* natif donnait et qu'une liste dessinée doit rendre. */
|
|
984
|
+
function parcourirListe(e: EvenementClavier<HTMLElement>) {
|
|
985
|
+
const options = [...e.currentTarget.querySelectorAll<HTMLElement>('[role="option"]')]
|
|
986
|
+
const i = options.indexOf(e.target as HTMLElement)
|
|
987
|
+
const vers =
|
|
988
|
+
e.key === 'ArrowDown' ? i + 1 : e.key === 'ArrowUp' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? options.length - 1 : null
|
|
989
|
+
if (vers === null) return
|
|
990
|
+
e.preventDefault()
|
|
991
|
+
options[Math.max(0, Math.min(options.length - 1, vers))]?.focus()
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function FiltreChoix<T>({ colonne, nom, t }: { colonne: Column<T, unknown>; nom: string; t: TextesGrille }) {
|
|
995
|
+
const filtre = colonne.getFilterValue() as ValeurFiltre | undefined
|
|
996
|
+
// Les valeurs du modèle FACETTÉ : celles que les autres filtres laissent
|
|
997
|
+
// encore passer. Une option qui ne rendrait aucune ligne n'est pas offerte.
|
|
998
|
+
const options = [...colonne.getFacetedUniqueValues().keys()]
|
|
999
|
+
.filter((v) => v != null && v !== '')
|
|
1000
|
+
.map(String)
|
|
1001
|
+
.sort((a, b) => a.localeCompare(b, 'fr'))
|
|
1002
|
+
const valeur = filtre?.type === 'choix' ? filtre.valeur : ''
|
|
1003
|
+
// L'option choisie reste dans la liste même si un autre filtre l'a fait
|
|
1004
|
+
// disparaître : sinon la liste afficherait « Tous » en mentant.
|
|
1005
|
+
if (valeur && !options.includes(valeur)) options.unshift(valeur)
|
|
1006
|
+
|
|
1007
|
+
return (
|
|
1008
|
+
<FiltreFlottant
|
|
1009
|
+
libelle={t.filtrerColonne(nom)}
|
|
1010
|
+
resume={valeur || t.tous}
|
|
1011
|
+
vide={!valeur}
|
|
1012
|
+
icone="chevron-bas"
|
|
1013
|
+
role="listbox"
|
|
1014
|
+
panneau={(fermer) => (
|
|
1015
|
+
<Menu role="listbox" aria-label={t.filtrerColonne(nom)} className="zv-grille__liste" onKeyDown={parcourirListe}>
|
|
1016
|
+
{['', ...options].map((o) => (
|
|
1017
|
+
<MenuItem
|
|
1018
|
+
key={o || '\u0000'}
|
|
1019
|
+
actif={o === valeur}
|
|
1020
|
+
onClick={() => {
|
|
1021
|
+
colonne.setFilterValue({ type: 'choix', valeur: o })
|
|
1022
|
+
fermer()
|
|
1023
|
+
}}
|
|
1024
|
+
>
|
|
1025
|
+
{o || t.tous}
|
|
1026
|
+
</MenuItem>
|
|
1027
|
+
))}
|
|
1028
|
+
</Menu>
|
|
1029
|
+
)}
|
|
1030
|
+
/>
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function FiltrePlage<T>({
|
|
1035
|
+
colonne,
|
|
1036
|
+
type,
|
|
1037
|
+
nom,
|
|
1038
|
+
t,
|
|
1039
|
+
}: {
|
|
1040
|
+
colonne: Column<T, unknown>
|
|
1041
|
+
type: 'nombre' | 'date'
|
|
1042
|
+
nom: string
|
|
1043
|
+
t: TextesGrille
|
|
1044
|
+
}) {
|
|
1045
|
+
const idChamp = useId()
|
|
1046
|
+
const filtre = colonne.getFilterValue() as ValeurFiltre | undefined
|
|
1047
|
+
const [a, b] =
|
|
1048
|
+
filtre?.type === 'nombre' ? [filtre.min, filtre.max] : filtre?.type === 'date' ? [filtre.du, filtre.au] : ['', '']
|
|
1049
|
+
const ecrire = (bas: string, haut: string) =>
|
|
1050
|
+
colonne.setFilterValue(
|
|
1051
|
+
type === 'nombre' ? { type, min: bas, max: haut } : { type, du: bas, au: haut },
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
// Ce qui est TAPÉ, distinct de ce qui est APPLIQUÉ : une date en cours de
|
|
1055
|
+
// saisie s'affiche dans le champ sans filtrer le tableau. Resynchronisé
|
|
1056
|
+
// quand le filtre change d'ailleurs (« Réinitialiser la vue »).
|
|
1057
|
+
const [brouillon, setBrouillon] = useState<[string, string]>([a, b])
|
|
1058
|
+
useEffect(() => {
|
|
1059
|
+
setBrouillon([a, b])
|
|
1060
|
+
}, [a, b])
|
|
1061
|
+
const saisir = (bas: string, haut: string) => {
|
|
1062
|
+
setBrouillon([bas, haut])
|
|
1063
|
+
if (type === 'nombre' || (dateComplete(bas) && dateComplete(haut))) ecrire(bas, haut)
|
|
1064
|
+
}
|
|
1065
|
+
const resume = type === 'nombre' ? t.resumeNombres(a, b) : t.resumeDates(a, b)
|
|
1066
|
+
const vide = !a && !b
|
|
1067
|
+
|
|
986
1068
|
const champ = (
|
|
987
1069
|
valeur: string,
|
|
988
1070
|
changer: (v: string) => void,
|
|
@@ -1014,61 +1096,51 @@ function FiltrePlage<T>({
|
|
|
1014
1096
|
)
|
|
1015
1097
|
|
|
1016
1098
|
return (
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
{!vide && (
|
|
1063
|
-
<Button variante="fantome" compact icone="fermer" onClick={() => saisir('', '')}>
|
|
1064
|
-
{t.effacer}
|
|
1065
|
-
</Button>
|
|
1066
|
-
)}
|
|
1067
|
-
</Popover>
|
|
1068
|
-
</div>,
|
|
1069
|
-
declencheur.current?.ownerDocument.body ?? document.body,
|
|
1070
|
-
)}
|
|
1071
|
-
</>
|
|
1099
|
+
<FiltreFlottant
|
|
1100
|
+
libelle={t.filtrerColonne(nom)}
|
|
1101
|
+
resume={resume}
|
|
1102
|
+
vide={vide}
|
|
1103
|
+
icone={type === 'date' ? 'calendrier' : 'filtre'}
|
|
1104
|
+
role="dialog"
|
|
1105
|
+
panneau={() => (
|
|
1106
|
+
<Popover className="zv-grille__plage" role="dialog" aria-label={t.filtrerColonne(nom)}>
|
|
1107
|
+
{type === 'date' && (
|
|
1108
|
+
<div className="zv-grille__periodes" role="group" aria-label={t.periodes}>
|
|
1109
|
+
{(['aujourdhui', 'septJours', 'trenteJours', 'ceMois', 'cetteAnnee'] as const).map((cle) => {
|
|
1110
|
+
const [du, au] = periodes(new Date())[cle]
|
|
1111
|
+
return (
|
|
1112
|
+
// `role="button"` : ces raccourcis sont des ACTIONS, pas
|
|
1113
|
+
// les options d'une liste (le rôle par défaut de MenuItem).
|
|
1114
|
+
<MenuItem
|
|
1115
|
+
key={cle}
|
|
1116
|
+
role="button"
|
|
1117
|
+
actif={a === du && b === au}
|
|
1118
|
+
aria-selected={undefined}
|
|
1119
|
+
aria-pressed={a === du && b === au}
|
|
1120
|
+
onClick={() => saisir(du, au)}
|
|
1121
|
+
>
|
|
1122
|
+
{t[cle]}
|
|
1123
|
+
</MenuItem>
|
|
1124
|
+
)
|
|
1125
|
+
})}
|
|
1126
|
+
</div>
|
|
1127
|
+
)}
|
|
1128
|
+
<div className="zv-grille__bornes">
|
|
1129
|
+
<Field label={type === 'date' ? t.du : t.min} htmlFor={idChamp + 'a'}>
|
|
1130
|
+
{champ(brouillon[0], (v) => saisir(v, brouillon[1]), 'a', { max: brouillon[1] })}
|
|
1131
|
+
</Field>
|
|
1132
|
+
<Field label={type === 'date' ? t.au : t.max} htmlFor={idChamp + 'b'}>
|
|
1133
|
+
{champ(brouillon[1], (v) => saisir(brouillon[0], v), 'b', { min: brouillon[0] })}
|
|
1134
|
+
</Field>
|
|
1135
|
+
</div>
|
|
1136
|
+
{!vide && (
|
|
1137
|
+
<Button variante="fantome" compact icone="fermer" onClick={() => saisir('', '')}>
|
|
1138
|
+
{t.effacer}
|
|
1139
|
+
</Button>
|
|
1140
|
+
)}
|
|
1141
|
+
</Popover>
|
|
1142
|
+
)}
|
|
1143
|
+
/>
|
|
1072
1144
|
)
|
|
1073
1145
|
}
|
|
1074
1146
|
|
|
@@ -1079,6 +1151,7 @@ export function DataGrid<T>({
|
|
|
1079
1151
|
data,
|
|
1080
1152
|
columns,
|
|
1081
1153
|
onRowClick,
|
|
1154
|
+
ligneActive,
|
|
1082
1155
|
getRowId,
|
|
1083
1156
|
selection = false,
|
|
1084
1157
|
onSelectionChange,
|
|
@@ -1361,6 +1434,7 @@ export function DataGrid<T>({
|
|
|
1361
1434
|
celluleEntete={(p) => <EnteteColonne {...p} t={t} />}
|
|
1362
1435
|
sousEntete={rangeeFiltres}
|
|
1363
1436
|
onRowClick={onRowClick}
|
|
1437
|
+
ligneActive={ligneActive}
|
|
1364
1438
|
vide={
|
|
1365
1439
|
vide ?? (
|
|
1366
1440
|
<Empty>
|
|
@@ -1376,18 +1450,13 @@ export function DataGrid<T>({
|
|
|
1376
1450
|
<Pager pied aria-label={t.pagination}>
|
|
1377
1451
|
<Meta>{t.compte(debut, fin, total)}</Meta>
|
|
1378
1452
|
<div className="zv-grille__pied">
|
|
1379
|
-
<
|
|
1453
|
+
<SelectMenu
|
|
1380
1454
|
className="zv-grille__taille"
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
<option key={taille} value={taille}>
|
|
1387
|
-
{t.parPage(taille)}
|
|
1388
|
-
</option>
|
|
1389
|
-
))}
|
|
1390
|
-
</Select>
|
|
1455
|
+
label={t.lignesParPage}
|
|
1456
|
+
valeur={String(pageSize)}
|
|
1457
|
+
options={TAILLES_PAGE.map((taille) => ({ valeur: String(taille), label: t.parPage(taille) }))}
|
|
1458
|
+
onChange={(v) => table.setPageSize(Number(v))}
|
|
1459
|
+
/>
|
|
1391
1460
|
<PagerArrow
|
|
1392
1461
|
aria-label={t.pagePrecedente}
|
|
1393
1462
|
desactive={!table.getCanPreviousPage()}
|
package/src/data.css
CHANGED
|
@@ -198,9 +198,11 @@
|
|
|
198
198
|
outline-offset: -2px;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/* ⚠️
|
|
202
|
-
*
|
|
203
|
-
*
|
|
201
|
+
/* ⚠️ La source ne contient ni état sélectionné, ni :focus-within, ni ligne
|
|
202
|
+
* active : une liste à LIRE n'en a pas besoin. Une grille qu'on OPÈRE, si :
|
|
203
|
+
* la ligne cochée (--selected) et la ligne active (--on) sont arbitrées
|
|
204
|
+
* plus bas, section « Grille de données », HORS RELEVÉ, sans troisième
|
|
205
|
+
* gris (l'une sur --accent-soft, l'autre sur le fond du survol). */
|
|
204
206
|
|
|
205
207
|
/* ─── En-tête triable ──────────────────────────────────────────────────
|
|
206
208
|
* ⚠️ HORS RELEVÉ : ajouté le 25/08/2026 avec l'adaptateur TanStack Table.
|
|
@@ -1212,7 +1214,8 @@
|
|
|
1212
1214
|
}
|
|
1213
1215
|
|
|
1214
1216
|
.zv-table__head .zv-cell--pin,
|
|
1215
|
-
.zv-row:hover .zv-cell--pin
|
|
1217
|
+
.zv-row:hover .zv-cell--pin,
|
|
1218
|
+
.zv-row--on .zv-cell--pin {
|
|
1216
1219
|
--zv-pin-fond: var(--panel);
|
|
1217
1220
|
}
|
|
1218
1221
|
|
|
@@ -1229,6 +1232,19 @@
|
|
|
1229
1232
|
--zv-pin-fond: var(--accent-soft);
|
|
1230
1233
|
}
|
|
1231
1234
|
|
|
1235
|
+
/* ─── Ligne active ──────────────────────────────────────────────────────
|
|
1236
|
+
* La ligne dont on regarde le détail (un aperçu ouvert à côté de la liste).
|
|
1237
|
+
* ⚠️ HORS RELEVÉ, repris de Sunday (24/09/2026), qui le peignait dans son
|
|
1238
|
+
* propre CSS faute de l'avoir ici. Aucun troisième fond : le fond du
|
|
1239
|
+
* survol, et un filet d'accent à gauche. En ombre INTÉRIEURE et non en
|
|
1240
|
+
* bordure : une bordure décalerait toute la rangée de trois pixels au
|
|
1241
|
+
* moment du clic. Distincte de la ligne cochée (--selected), qui est une
|
|
1242
|
+
* sélection pour agir. */
|
|
1243
|
+
.zv-row--on {
|
|
1244
|
+
background: var(--panel);
|
|
1245
|
+
box-shadow: inset 3px 0 0 var(--accent);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1232
1248
|
/* La case de sélection : centrée dans sa piste, sans troncature. */
|
|
1233
1249
|
.zv-cell--case {
|
|
1234
1250
|
display: flex;
|
|
@@ -1279,11 +1295,18 @@
|
|
|
1279
1295
|
color: var(--accent);
|
|
1280
1296
|
}
|
|
1281
1297
|
|
|
1282
|
-
/* La taille de page : le
|
|
1298
|
+
/* La taille de page : le SelectMenu du paquet, à sa largeur de contenu
|
|
1299
|
+
* (il prend la largeur de son conteneur par défaut). */
|
|
1283
1300
|
.zv-grille__taille {
|
|
1284
1301
|
width: auto;
|
|
1285
|
-
|
|
1286
|
-
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/* La liste d'un filtre à choix : le .zv-menu du paquet, posé dans
|
|
1305
|
+
* l'enveloppe flottante. Il défile au-delà d'une douzaine d'options plutôt
|
|
1306
|
+
* que de couvrir la page. */
|
|
1307
|
+
.zv-grille__liste {
|
|
1308
|
+
max-height: min(320px, 60vh);
|
|
1309
|
+
overflow-y: auto;
|
|
1287
1310
|
}
|
|
1288
1311
|
|
|
1289
1312
|
/* Le résumé d'une plage (nombres, dates) dans la rangée de filtres : un
|