@zevra/ui 0.43.0 → 0.44.0

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.
@@ -15,11 +15,12 @@
15
15
  // variables. Aucune classe nouvelle n'entre par cette porte : ce sont les
16
16
  // mêmes .zv-table / .zv-row / .zv-cell que le tableau écrit à la main.
17
17
 
18
- import { Fragment, type CSSProperties, type ReactNode } from 'react'
18
+ import { Fragment, type CSSProperties, type DragEvent, type ReactNode } from 'react'
19
19
  import {
20
20
  flexRender,
21
21
  type Column,
22
22
  type Header,
23
+ type Row,
23
24
  type RowData,
24
25
  type Table as TableTanstack,
25
26
  } from '@tanstack/react-table'
@@ -81,6 +82,36 @@ export interface DataTableProps<T> {
81
82
  /** Appelé au clic sur une ligne. Sans lui, la ligne n'est pas cliquable :
82
83
  * et elle ne doit pas en avoir l'air. */
83
84
  onRowClick?: (ligne: T) => void
85
+ /** La ligne dont on regarde le détail (l'aperçu ouvert à côté de la
86
+ * liste). Elle porte un filet d'accent à gauche, distinct de la ligne
87
+ * COCHÉE, qui est une sélection pour agir. */
88
+ ligneActive?: (ligne: T) => boolean
89
+ /** Clic droit sur une ligne : l'appelant ouvre son menu contextuel aux
90
+ * coordonnées données. Le menu du navigateur est alors retenu. */
91
+ onRowContextMenu?: (ligne: T, x: number, y: number) => void
92
+ /** Double clic : « ouvrir pour de bon », quand le simple clic ne fait que
93
+ * montrer. La sélection de texte qu'il provoque est effacée. */
94
+ onRowDoubleClick?: (ligne: T) => void
95
+ /** La ligne se saisit (glisser-déposer NATIF) : l'appelant remplit le
96
+ * `dataTransfer`, pour ranger une pièce dans un dossier par exemple. */
97
+ onRowDragStart?: (ligne: T, e: DragEvent<HTMLDivElement>) => void
98
+ /** Les lignes à rendre, dans leur ordre, à la place du modèle de TanStack :
99
+ * pour un ordre que le tri ne sait pas dire (des lignes épinglées en tête
100
+ * quel que soit le tri). */
101
+ lignes?: Row<T>[]
102
+ /** Rangée posée en tête du corps, avant les lignes : une ligne de SAISIE. */
103
+ avantLignes?: ReactNode
104
+ /** Rangée intercalée AVANT une ligne : l'intitulé d'un groupe (une année,
105
+ * un mois) quand la clé change. Rien pour ne rien intercaler. */
106
+ intercalaire?: (ligne: Row<T>, precedente: Row<T> | undefined) => ReactNode
107
+ /** Sous les lignes : un pied de totaux, le déclencheur d'une liste infinie. */
108
+ apresLignes?: ReactNode
109
+ /** Rangées resserrées, pour un tableau secondaire posé dans une fiche. */
110
+ dense?: boolean
111
+ /** Le tableau REMPLIT la hauteur de son parent (une colonne flex bornée) :
112
+ * seules les lignes défilent, les intitulés et les filtres restent en
113
+ * haut. */
114
+ remplir?: boolean
84
115
  className?: string
85
116
  }
86
117
 
@@ -139,6 +170,16 @@ export function DataTable<T>({
139
170
  onRowClick,
140
171
  celluleEntete,
141
172
  sousEntete,
173
+ ligneActive,
174
+ onRowContextMenu,
175
+ onRowDoubleClick,
176
+ onRowDragStart,
177
+ lignes: lignesImposees,
178
+ avantLignes,
179
+ intercalaire,
180
+ apresLignes,
181
+ dense,
182
+ remplir,
142
183
  className,
143
184
  }: DataTableProps<T>) {
144
185
  const grille = table.options.enableColumnResizing === true
@@ -149,7 +190,7 @@ export function DataTable<T>({
149
190
  grille ? piste(c, reglages[c.id] != null) : (c.columnDef.meta?.largeur ?? 'minmax(0, 1fr)'),
150
191
  )
151
192
  .join(' ')
152
- const lignes = table.getRowModel().rows
193
+ const lignes = lignesImposees ?? table.getRowModel().rows
153
194
  const style: Record<string, string> = { '--zv-cols': gabarit }
154
195
  if (grille) {
155
196
  style['--zv-cols-somme'] = `${colonnes.reduce((s, c) => s + c.getSize(), 0)}px`
@@ -158,9 +199,10 @@ export function DataTable<T>({
158
199
 
159
200
  return (
160
201
  <div
161
- className={cx('zv-table', className)}
202
+ className={cx('zv-table', dense && 'zv-table--dense', remplir && 'zv-table--remplir', className)}
162
203
  style={style as CSSProperties}
163
204
  >
205
+ <Haut actif={remplir}>
164
206
  {table.getHeaderGroups().map((groupe) => (
165
207
  <div className="zv-table__head" key={groupe.id}>
166
208
  {groupe.headers.map((entete) => {
@@ -213,19 +255,47 @@ export function DataTable<T>({
213
255
  ))}
214
256
 
215
257
  {sousEntete}
258
+ </Haut>
259
+
260
+ {avantLignes}
216
261
 
217
262
  {lignes.length === 0
218
263
  ? vide
219
- : lignes.map((ligne) => (
264
+ : lignes.map((ligne, i) => (
265
+ <Fragment key={ligne.id}>
266
+ {intercalaire?.(ligne, lignes[i - 1])}
220
267
  <div
221
- key={ligne.id}
222
- className={cx('zv-row', ligne.getIsSelected() && 'zv-row--selected')}
268
+ className={cx(
269
+ 'zv-row',
270
+ ligne.getIsSelected() && 'zv-row--selected',
271
+ ligneActive?.(ligne.original) && 'zv-row--on',
272
+ )}
273
+ aria-current={ligneActive?.(ligne.original) || undefined}
223
274
  // Le clavier suit le clic : une ligne cliquable qui n'est pas
224
275
  // atteignable au clavier est une action inaccessible.
225
276
  role={onRowClick ? 'button' : undefined}
226
277
  tabIndex={onRowClick ? 0 : undefined}
227
278
  style={onRowClick ? undefined : { cursor: 'default' }}
228
279
  onClick={onRowClick ? () => onRowClick(ligne.original) : undefined}
280
+ onContextMenu={
281
+ onRowContextMenu
282
+ ? (e) => {
283
+ e.preventDefault()
284
+ onRowContextMenu(ligne.original, e.clientX, e.clientY)
285
+ }
286
+ : undefined
287
+ }
288
+ onDoubleClick={
289
+ onRowDoubleClick
290
+ ? (e) => {
291
+ e.preventDefault()
292
+ e.currentTarget.ownerDocument.defaultView?.getSelection()?.removeAllRanges()
293
+ onRowDoubleClick(ligne.original)
294
+ }
295
+ : undefined
296
+ }
297
+ draggable={onRowDragStart ? true : undefined}
298
+ onDragStart={onRowDragStart ? (e) => onRowDragStart(ligne.original, e) : undefined}
229
299
  onKeyDown={
230
300
  onRowClick
231
301
  ? (e) => {
@@ -259,7 +329,18 @@ export function DataTable<T>({
259
329
  )
260
330
  })}
261
331
  </div>
332
+ </Fragment>
262
333
  ))}
334
+
335
+ {apresLignes}
263
336
  </div>
264
337
  )
265
338
  }
339
+
340
+ /** L'en-tête et les filtres, groupés dans un bloc COLLANT quand le tableau
341
+ * remplit son parent : sans ce bloc, chaque rangée collerait au même
342
+ * endroit et la seconde recouvrirait la première. Sinon, rien n'enveloppe :
343
+ * le balisage reste celui d'avant. */
344
+ function Haut({ actif, children }: { actif?: boolean; children: ReactNode }) {
345
+ return actif ? <div className="zv-table__haut">{children}</div> : <>{children}</>
346
+ }