@payglocal_ui/flux-ui 0.2.3 → 0.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payglocal_ui/flux-ui",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Flux UI primitives — inputs, fields, dialog, data table, charts, calendar, and more (Tailwind v4 + Radix).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -72,6 +72,22 @@ interface DataTableProps<T> {
72
72
  * flush with nothing trailing it. Takes precedence over `rowCta`.
73
73
  */
74
74
  rowAction?: ReactNode | ((row: T, index: number) => ReactNode);
75
+ /**
76
+ * Makes the whole row a click target — the row itself opens a drawer, a
77
+ * detail page, whatever the table drills into — instead of that living in a
78
+ * per-cell wrapper or a hover-revealed button.
79
+ *
80
+ * The handler sits on the `<tr>`, so the entire row including cell padding
81
+ * and the empty space between columns is clickable, and the row gets
82
+ * `cursor-pointer` plus keyboard access (focusable, Enter / Space).
83
+ *
84
+ * Clicks that originate inside something interactive — a `<button>`, `<a>`,
85
+ * a form control, a Radix trigger, or anything marked
86
+ * `data-row-click-ignore` — do NOT fire this. Copy buttons, per-row menus
87
+ * and the `rowAction` overlay therefore keep doing only their own job
88
+ * without each having to stop propagation.
89
+ */
90
+ onRowClick?: (row: T, index: number) => void;
75
91
  /** Row / cell vertical rhythm and horizontal gutters */
76
92
  density?: DataTableDensity;
77
93
  /**
@@ -110,6 +126,7 @@ export function DataTable<T>({
110
126
  rowKey,
111
127
  rowCta,
112
128
  rowAction,
129
+ onRowClick,
113
130
  density = "default",
114
131
  tableLayout = "fixed",
115
132
  theadClassName,
@@ -176,6 +193,30 @@ export function DataTable<T>({
176
193
  // edge of the viewport (it has no reserved column — it overlays the row).
177
194
  const actionGutter = comfortable ? 20 : compact ? 12 : 16;
178
195
 
196
+ /**
197
+ * Selector for everything a row-level click must keep its hands off. A click
198
+ * landing inside one of these belongs to that control alone — a copy button,
199
+ * a per-row menu, a link, a checkbox, the `rowAction` overlay — so the row
200
+ * handler ignores it rather than firing as well.
201
+ *
202
+ * `data-row-click-ignore` is the escape hatch for anything not covered here
203
+ * (a custom widget in a cell, a drag handle) without it needing to stop
204
+ * propagation itself.
205
+ */
206
+ const ROW_CLICK_IGNORE =
207
+ 'button, a, input, select, textarea, label, [role="button"], [role="link"], ' +
208
+ '[role="checkbox"], [role="menuitem"], [role="menu"], [role="dialog"], ' +
209
+ "[data-row-click-ignore]";
210
+
211
+ /** Whether a click/keypress inside a row should reach `onRowClick`. */
212
+ const isRowClickTarget = (target: EventTarget | null, rowEl: HTMLElement) => {
213
+ if (!(target instanceof Element)) return false;
214
+ const interactive = target.closest(ROW_CLICK_IGNORE);
215
+ // `closest` can walk out of the row entirely (a portalled menu, say); only
216
+ // a match inside THIS row means the click belonged to that control.
217
+ return !(interactive && rowEl.contains(interactive));
218
+ };
219
+
179
220
  return (
180
221
  <div
181
222
  className={cn(
@@ -285,8 +326,39 @@ export function DataTable<T>({
285
326
  compact && "min-h-[44px]",
286
327
  "hover:bg-muted/40 dark:hover:bg-muted/25",
287
328
  hasAction &&
288
- "hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none"
329
+ "hover:shadow-[0_1px_0_rgba(0,0,0,0.04)] dark:hover:shadow-none",
330
+ // Clickable rows read as clickable, and show a focus ring
331
+ // when reached by keyboard. `focus-visible` only, so a
332
+ // mouse click does not leave a ring behind on the row.
333
+ onRowClick &&
334
+ "cursor-pointer focus-visible:outline-none focus-visible:bg-muted/40 dark:focus-visible:bg-muted/25"
289
335
  )}
336
+ // Keyboard parity with the mouse: the row is reachable by
337
+ // Tab and activated by Enter / Space, which a bare <tr> with
338
+ // an onClick would not be. No role override — the row stays a
339
+ // row for assistive tech rather than claiming to be a button.
340
+ tabIndex={onRowClick ? 0 : undefined}
341
+ onClick={
342
+ onRowClick
343
+ ? (e) => {
344
+ if (!isRowClickTarget(e.target, e.currentTarget)) return;
345
+ onRowClick(row, i);
346
+ }
347
+ : undefined
348
+ }
349
+ onKeyDown={
350
+ onRowClick
351
+ ? (e) => {
352
+ if (e.key !== "Enter" && e.key !== " ") return;
353
+ // Only the row's own focus activates it; a keypress
354
+ // inside a control in the row belongs to that control.
355
+ if (e.target !== e.currentTarget) return;
356
+ // Space scrolls the page by default.
357
+ e.preventDefault();
358
+ onRowClick(row, i);
359
+ }
360
+ : undefined
361
+ }
290
362
  >
291
363
  {columns.map((col) => (
292
364
  <td