@lotics/ui 1.25.0 → 1.26.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
package/src/table.web.tsx CHANGED
@@ -7,13 +7,22 @@ import type { Column, TableProps } from "./table_types";
7
7
  export type { SortDir, TableSort, Column, TableProps } from "./table_types";
8
8
 
9
9
  // Web table. Built from raw <div>s (not RN Pressable) so a click-to-expand row
10
- // can legally contain interactive cells: the row is a <div role="row">, never a
11
- // <button>, and an `interactive` cell's clicks are skipped via a centralized
12
- // `closest('[data-interactive]')` guardno stopPropagation, no nested <button>,
13
- // no absolute overlay. The detail panel renders full-width below the row.
10
+ // can legally contain interactive controls: the row is a <div role="row">, never
11
+ // a <button>. A row click toggles expansion EXCEPT when it lands on an actual
12
+ // interactive elementthose keep their own behaviour. This is the web mirror of
13
+ // RN's responder model (the innermost control wins): only the control suppresses
14
+ // the toggle, so empty space anywhere in the row — including around a control in a
15
+ // wide action column — still expands. No stopPropagation, no nested <button>, no
16
+ // absolute overlay, no dead zones. The detail panel renders full-width below.
14
17
 
15
18
  const CHEVRON_W = 44;
16
19
 
20
+ // Clicks landing on (or inside) one of these keep their own behaviour instead of
21
+ // toggling the row — the standard interactive HTML tags + ARIA interactive roles,
22
+ // plus an explicit [data-interactive] escape hatch for a non-element control.
23
+ const INTERACTIVE_SELECTOR =
24
+ 'a[href], button, input, select, textarea, label, summary, [role="button"], [role="link"], [role="checkbox"], [role="switch"], [role="radio"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="tab"], [role="slider"], [role="spinbutton"], [contenteditable="true"], [data-interactive]';
25
+
17
26
  function colWidth<TRow extends Record<string, unknown>>(col: Column<TRow>): CSSProperties {
18
27
  return col.width ? { width: col.width, flexShrink: 0 } : { flex: 1, minWidth: 0 };
19
28
  }
@@ -113,10 +122,10 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
113
122
  onClick={
114
123
  pressable
115
124
  ? (e: React.MouseEvent) => {
116
- // Whole-row click acts (expand or select), EXCEPT clicks inside an
117
- // interactive cell (pickers/buttons) they keep their own behaviour.
118
- // The disclosure chevron is the keyboard/AT affordance for expansion.
119
- if ((e.target as HTMLElement).closest("[data-interactive]")) return;
125
+ // Whole-row click acts (expand or select), EXCEPT when it lands on an
126
+ // actual interactive elementthose keep their own behaviour. The
127
+ // disclosure chevron is the keyboard/AT affordance for expansion.
128
+ if ((e.target as HTMLElement).closest(INTERACTIVE_SELECTOR)) return;
120
129
  if (expandable) toggle(key, row);
121
130
  else onRowPress?.(row);
122
131
  }
@@ -130,7 +139,6 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
130
139
  <div
131
140
  key={col.key as string}
132
141
  role="cell"
133
- data-interactive={col.interactive || undefined}
134
142
  style={{
135
143
  ...bodyCellStyle,
136
144
  ...colWidth(col),
@@ -17,10 +17,6 @@ export interface Column<TRow extends Record<string, unknown>> {
17
17
  /** When true (and `onSortChange` is set), the header is pressable + shows a
18
18
  * sort arrow when this column is the active `sort`. */
19
19
  sortable?: boolean;
20
- /** Cells whose own controls (buttons, pickers) must NOT toggle the row. The
21
- * row's expand handler ignores clicks originating inside an interactive cell,
22
- * so the rest of the row stays click-to-expand. */
23
- interactive?: boolean;
24
20
  renderCell?: (params: { row: TRow; column: Column<TRow> }) => ReactNode;
25
21
  }
26
22
 
@@ -35,13 +31,14 @@ export interface TableProps<TRow extends Record<string, unknown>> {
35
31
  * parent owns the actual sorting of `rows` — this only drives the indicator. */
36
32
  sort?: TableSort<TRow> | null;
37
33
  onSortChange?: (key: keyof TRow) => void;
38
- /** Click-to-select: pressing a row (except `interactive` cells) calls this —
39
- * e.g. a picker that returns the chosen row. Mutually exclusive with
34
+ /** Click-to-select: pressing a row (except its interactive controls) calls
35
+ * this — e.g. a picker that returns the chosen row. Mutually exclusive with
40
36
  * `renderDetail` (a row either expands or selects); `renderDetail` wins if
41
37
  * both are set. Pair with `rowStyle` to highlight the selected row. */
42
38
  onRowPress?: (row: TRow) => void;
43
39
  /** Render an inline detail panel, full-width below the row. When set, the
44
- * whole row (except `interactive` cells) is click-to-expand. */
40
+ * whole row (except its interactive controls) is click-to-expand. The row
41
+ * detects controls by element/role, so cells need no special marking. */
45
42
  renderDetail?: (row: TRow) => ReactNode;
46
43
  /** Controlled set of expanded row keys. Omit for internal (uncontrolled) state. */
47
44
  expandedKeys?: Set<string>;