@human-kit/svelte-components 1.0.0-alpha.13 → 1.0.0-alpha.14

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.
@@ -24,6 +24,7 @@
24
24
  | 'value'
25
25
  > & {
26
26
  id?: string;
27
+ element?: HTMLSpanElement | null;
27
28
  name?: string;
28
29
  value?: string;
29
30
  isChecked?: boolean;
@@ -39,8 +40,20 @@
39
40
  class?: string;
40
41
  'aria-label'?: string;
41
42
  'aria-labelledby'?: string;
43
+ onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
44
+ onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
42
45
  };
43
46
 
47
+ function composeEventHandlers<TEvent extends Event>(
48
+ internalHandler: ((event: TEvent) => void) | undefined,
49
+ externalHandler: ((event: TEvent) => void) | undefined
50
+ ): (event: TEvent) => void {
51
+ return (event: TEvent) => {
52
+ internalHandler?.(event);
53
+ externalHandler?.(event);
54
+ };
55
+ }
56
+
44
57
  function resolveState(isChecked: boolean, isIndeterminate: boolean): CheckboxState {
45
58
  if (isIndeterminate) return 'indeterminate';
46
59
  return isChecked ? 'checked' : 'unchecked';
@@ -56,6 +69,7 @@
56
69
 
57
70
  let {
58
71
  id,
72
+ element = $bindable(),
59
73
  name,
60
74
  value = 'on',
61
75
  isChecked = $bindable(),
@@ -71,6 +85,8 @@
71
85
  class: className = '',
72
86
  'aria-label': ariaLabel,
73
87
  'aria-labelledby': ariaLabelledby,
88
+ onclick: onClickExternal,
89
+ onkeydown: onKeyDownExternal,
74
90
  ...restProps
75
91
  }: CheckboxRootProps = $props();
76
92
 
@@ -91,6 +107,10 @@
91
107
  let rootRef: HTMLSpanElement | null = $state(null);
92
108
  let inputRef: HTMLInputElement | null = $state(null);
93
109
 
110
+ $effect(() => {
111
+ element = rootRef;
112
+ });
113
+
94
114
  if (untrack(() => isChecked) === undefined) {
95
115
  isChecked = initialState === 'checked';
96
116
  }
@@ -326,8 +346,8 @@
326
346
  data-required={required || undefined}
327
347
  data-focused={focused || undefined}
328
348
  data-focus-visible={focusVisible || undefined}
329
- onclick={handleClick}
330
- onkeydown={handleKeyDown}
349
+ onclick={composeEventHandlers(onClickExternal ?? undefined, handleClick)}
350
+ onkeydown={composeEventHandlers(handleKeyDown, onKeyDownExternal ?? undefined)}
331
351
  onkeyup={handleKeyUp}
332
352
  onpointerdown={handlePointerDown}
333
353
  onmousedown={handlePointerDown}
@@ -2,6 +2,7 @@ import { type Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'class' | 'id' | 'role' | 'tabindex' | 'aria-checked' | 'aria-disabled' | 'aria-readonly' | 'aria-required' | 'onclick' | 'onkeydown' | 'value'> & {
4
4
  id?: string;
5
+ element?: HTMLSpanElement | null;
5
6
  name?: string;
6
7
  value?: string;
7
8
  isChecked?: boolean;
@@ -17,7 +18,9 @@ type CheckboxRootProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'cla
17
18
  class?: string;
18
19
  'aria-label'?: string;
19
20
  'aria-labelledby'?: string;
21
+ onclick?: HTMLAttributes<HTMLSpanElement>['onclick'];
22
+ onkeydown?: HTMLAttributes<HTMLSpanElement>['onkeydown'];
20
23
  };
21
- declare const CheckboxRoot: import("svelte").Component<CheckboxRootProps, {}, "isChecked" | "isIndeterminate">;
24
+ declare const CheckboxRoot: import("svelte").Component<CheckboxRootProps, {}, "element" | "isChecked" | "isIndeterminate">;
22
25
  type CheckboxRoot = ReturnType<typeof CheckboxRoot>;
23
26
  export default CheckboxRoot;
@@ -225,6 +225,7 @@
225
225
 
226
226
  if (selectionMode === 'single') {
227
227
  newSelection = new Set([id]);
228
+ shouldFilter = false;
228
229
  // Save the label persistently for restore on blur/escape
229
230
  selectedLabel = label;
230
231
  // Keep the selected label visible in the input without re-triggering
@@ -159,6 +159,19 @@
159
159
  isFocusVisible = false;
160
160
  });
161
161
 
162
+ $effect(() => {
163
+ if (!isFocusedComputed) {
164
+ if (pressedKey !== null) {
165
+ clearPressedState();
166
+ }
167
+ return;
168
+ }
169
+
170
+ if (listFocusVisible || isFocusVisibleComputed) {
171
+ isHovered = false;
172
+ }
173
+ });
174
+
162
175
  function clearPressedState() {
163
176
  isPressed = false;
164
177
  pressedKey = null;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" generics="T extends object = object">
2
2
  import type { Snippet } from 'svelte';
3
+ import { onMount } from 'svelte';
3
4
  import { createListBoxContext, type ListBoxContext } from './context';
4
5
  import { trackInteractionModality } from '../../primitives/input-modality';
5
6
 
@@ -118,9 +119,14 @@
118
119
 
119
120
  const itemsArray = $derived(items ? Array.from(items) : []);
120
121
  const hasItems = $derived(itemsArray.length > 0 || itemCount > 0);
122
+ let hasMounted = $state(false);
121
123
 
122
124
  let focusWithin = $state(false);
123
125
 
126
+ onMount(() => {
127
+ hasMounted = true;
128
+ });
129
+
124
130
  function syncFocusWithin() {
125
131
  focusWithin =
126
132
  !!listboxElement &&
@@ -175,7 +181,7 @@
175
181
  {@render (children as Snippet)()}
176
182
  {/if}
177
183
 
178
- {#if !hasItems && itemCount === 0}
184
+ {#if hasMounted && !hasItems && itemCount === 0}
179
185
  {#if typeof emptyPlaceholder === 'string'}
180
186
  <div role="option" aria-selected="false" aria-disabled="true" data-empty-placeholder>
181
187
  {emptyPlaceholder}
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## Open questions
4
4
 
5
- - Disabled body rows are currently rendered and keyboard-focusable, but they cannot be selected. We should validate whether this matches the desired UX.
5
+ - Disabled body rows are currently rendered and treated with an all-or-nothing disabled model. The planned `disabledBehavior` API (`'selection' | 'all'`) will require splitting focus/action disabling from selection disabling.
6
6
  - `Table.Column` is implemented as a logical wrapper and currently assumes the intended child is a single `Table.ColumnHeaderCell`.
7
7
  - `Table.Footer` renders semantic table cells but is intentionally excluded from the roving-focus model in v1.
8
8
  - Interactive controls nested inside `Table.Cell` are still intentionally out of scope for v1.
9
+ - `pressRow()` is currently selection-oriented. The planned `onRowAction` feature will require a clearer interaction pipeline so pointer click, double click, `Enter`, and `Space` can route to action and selection independently.
@@ -390,7 +390,7 @@ Responsibilities:
390
390
  - drag and drop
391
391
  - async loading / load more
392
392
  - API pública dinámica con `items` y `columns`
393
- - row actions / row links
393
+ - row links / `href`-style navigation semantics
394
394
  - typeahead
395
395
  - focus management para elementos interactivos dentro de `Cell`
396
396
  - focus management for interactive elements inside `Cell`
@@ -406,22 +406,23 @@ Responsibilities:
406
406
 
407
407
  ## Advanced Feature Matrix
408
408
 
409
- | Feature | Main Complexity | Risk | Recommendation |
410
- | ---------------------------------- | ---------------------------------------------------------- | ----------- | ------------------ |
411
- | Column resizing | width state, handles, pointer + keyboard, persistence | high | next planned phase |
412
- | Drag and drop | reorder, drop targets, SR + keyboard + pointer | very high | keep out of v1 |
413
- | Async loading / load more | scroll state, sentinel rows, partial states | high | keep out of v1 |
414
- | Dynamic `items` / `columns` API | collection, stable ids, render functions, memoization | high | defer |
415
- | Row actions / row links | conflicts between actions, selection, and HTML limitations | medium/high | defer |
416
- | Interactive content inside `Cell` | focus handoff between grid and nested controls | very high | keep out of v1 |
417
- | Typeahead | depends on stable collection and consistent `textValue` | medium | defer |
418
- | Nested headers / column groups | spans, navigation, and complex semantics | high | keep out of v1 |
419
- | Cell selection | changes the entire interaction model | high | keep out of v1 |
420
- | Full `selectionBehavior="replace"` | modifiers and fine-grained focus/selection semantics | medium/high | defer |
421
- | Virtualization | strong decoupling between collection and DOM | very high | keep out of v1 |
422
- | Integrated select-all | useful UX but depends on mature selection behavior | medium | phase 2 |
423
- | Complex `colSpan` / `rowSpan` | breaks the rectangular grid model | high | defer |
424
- | Navigable footer | adds another region to the focus model | medium | avoid in v1 |
409
+ | Feature | Main Complexity | Risk | Recommendation |
410
+ | ---------------------------------- | -------------------------------------------------------- | ----------- | ------------------ |
411
+ | Column resizing | width state, handles, pointer + keyboard, persistence | high | next planned phase |
412
+ | Drag and drop | reorder, drop targets, SR + keyboard + pointer | very high | keep out of v1 |
413
+ | Async loading / load more | scroll state, sentinel rows, partial states | high | keep out of v1 |
414
+ | Dynamic `items` / `columns` API | collection, stable ids, render functions, memoization | high | defer |
415
+ | Row actions / `onRowAction` | action-selection conflicts across mouse and keyboard | medium/high | next planned phase |
416
+ | Row links / `href` semantics | HTML limitations, router integration, native link parity | high | defer |
417
+ | Interactive content inside `Cell` | focus handoff between grid and nested controls | very high | keep out of v1 |
418
+ | Typeahead | depends on stable collection and consistent `textValue` | medium | defer |
419
+ | Nested headers / column groups | spans, navigation, and complex semantics | high | keep out of v1 |
420
+ | Cell selection | changes the entire interaction model | high | keep out of v1 |
421
+ | Full `selectionBehavior="replace"` | modifiers and fine-grained focus/selection semantics | medium/high | defer |
422
+ | Virtualization | strong decoupling between collection and DOM | very high | keep out of v1 |
423
+ | Integrated select-all | useful UX but depends on mature selection behavior | medium | phase 2 |
424
+ | Complex `colSpan` / `rowSpan` | breaks the rectangular grid model | high | defer |
425
+ | Navigable footer | adds another region to the focus model | medium | avoid in v1 |
425
426
 
426
427
  ## Proposed Internal Architecture
427
428
 
@@ -919,6 +920,428 @@ Minimum regression coverage:
919
920
  - tests cover critical behavior
920
921
  - the implementation leaves real room for future phases without breaking the API
921
922
 
923
+ ## Phase 3: Row Actions and Disabled Behavior Plan
924
+
925
+ ### Goal
926
+
927
+ Add row actions in a way that matches the React Aria Components mental model closely enough for consumers to predict behavior, while still fitting the existing `Table` architecture in this repository:
928
+
929
+ - `Table.Root` remains the single owner of interaction state
930
+ - `Table.Row` and `Table.Cell` remain semantic wrappers over native table elements
931
+ - selection and actions are treated as related but distinct interactions
932
+ - disabled state becomes more explicit so selection-only disabling does not accidentally disable focus or actions
933
+
934
+ This phase is specifically about `onRowAction` and `disabledBehavior`. It does not attempt to solve full row-as-link semantics or nested interactive controls inside arbitrary cells.
935
+
936
+ ### Reference Behavior
937
+
938
+ The intended behavioral reference is React Aria's collection model for selection and item actions:
939
+
940
+ - `selectionBehavior="toggle"` keeps action as the primary row press interaction until the user enters a selection state
941
+ - `selectionBehavior="replace"` makes selection the primary pointer interaction and uses double click for row actions
942
+ - keyboard behavior separates selection from action more strictly:
943
+ - `Space` is selection-oriented
944
+ - `Enter` is action-oriented when actions are available
945
+
946
+ The goal is functional parity for the supported cases, not a byte-for-byte clone of RAC internals.
947
+
948
+ ### Public API Recommendation
949
+
950
+ #### `Table.Root`
951
+
952
+ Add the following props:
953
+
954
+ - `onRowAction?: (id: TableSelectionKey) => void`
955
+ - `disabledBehavior?: 'selection' | 'all'`
956
+
957
+ Prerequisite:
958
+
959
+ - `selectionBehavior?: 'toggle' | 'replace'` must already exist and be part of the stable `Table.Root` contract for this phase to make sense. This phase depends on the existing selection-behavior model rather than introducing a parallel row-press API.
960
+
961
+ Defaults:
962
+
963
+ - `onRowAction` default: `undefined`
964
+ - `disabledBehavior` default: `'all'`
965
+
966
+ Rationale:
967
+
968
+ - `onRowAction` belongs at the collection root because the component is built around row ids and centralized interaction state
969
+ - `disabledBehavior` also belongs at the root because the current disabled model is collection-driven (`disabledKeys`) and should stay consistent across row-local and root-provided disabled state
970
+
971
+ #### No New `rowPressBehavior` Prop
972
+
973
+ This phase should not add a new `rowPressBehavior` prop.
974
+
975
+ Reasons:
976
+
977
+ - the desired behavior is already derivable from the combination of:
978
+ - `onRowAction`
979
+ - `selectionMode`
980
+ - `selectionBehavior`
981
+ - React Aria already establishes a recognizable interaction contract based on those inputs
982
+ - a separate `rowPressBehavior` prop would create redundant states and undocumented invalid combinations
983
+
984
+ ### Interaction Model
985
+
986
+ #### Core Principle
987
+
988
+ There are now three distinct row-level concepts:
989
+
990
+ 1. row focus
991
+ 2. row selection
992
+ 3. row action
993
+
994
+ The implementation must stop treating `pressRow()` as if it were synonymous with selection. A row press should first be classified, then routed to either selection logic, action logic, or both depending on the active mode.
995
+
996
+ #### Pointer Behavior Without `onRowAction`
997
+
998
+ When `onRowAction` is not provided, the component should preserve the current selection semantics:
999
+
1000
+ - `selectionMode="none"`: row/cell click does nothing selection-related
1001
+ - `selectionBehavior="toggle"`: row/cell click toggles selection
1002
+ - `selectionBehavior="replace"`: row/cell click replaces selection according to current replace-mode rules
1003
+
1004
+ This preserves backwards compatibility.
1005
+
1006
+ #### Pointer Behavior With `onRowAction`
1007
+
1008
+ ##### `selectionMode="none"`
1009
+
1010
+ Regardless of `selectionBehavior`:
1011
+
1012
+ - single click on row or cell executes `onRowAction(id)`
1013
+ - double click does not have special meaning beyond the browser's normal click sequence
1014
+ - no selection state is changed
1015
+
1016
+ ##### `selectionBehavior="toggle"` with selection enabled
1017
+
1018
+ When `selectionMode` is `single` or `multiple` and `onRowAction` exists:
1019
+
1020
+ - if the table currently has no selected rows:
1021
+ - single click executes `onRowAction(id)`
1022
+ - click does not change row selection
1023
+ - if the table currently has at least one selected row:
1024
+ - single click on a row follows toggle selection semantics
1025
+ - click does not execute `onRowAction`
1026
+
1027
+ This matches the RAC notion that action is the default press interaction until the user has entered a selection workflow.
1028
+
1029
+ Important documentation note:
1030
+
1031
+ - this behavior changes dynamically based on whether the table currently has an active selection
1032
+ - that dynamic switch is powerful but can also surprise consumers and end users if it is not documented clearly
1033
+ - docs should call this out explicitly and describe it as an intentional RAC-aligned interaction model rather than a bug or inconsistency
1034
+ - if future consumer feedback shows this is too implicit, a dedicated escape hatch can be evaluated later, but this phase should ship the RAC-style default first
1035
+
1036
+ ##### `selectionBehavior="replace"` with selection enabled
1037
+
1038
+ When `selectionMode` is `single` or `multiple` and `onRowAction` exists:
1039
+
1040
+ - single click selects the row using replace-mode semantics
1041
+ - double click executes `onRowAction(id)`
1042
+ - the first click of the double-click sequence still performs selection
1043
+
1044
+ Callback ordering requirement:
1045
+
1046
+ - because the first click in the double-click sequence performs selection, `onSelectionChange` must fire before `onRowAction`
1047
+ - docs should state this ordering explicitly so consumers do not assume the action callback is the first observable event in the interaction
1048
+
1049
+ This is the clearest and most familiar desktop-style interaction model for replace mode.
1050
+
1051
+ ### Keyboard Model
1052
+
1053
+ #### Rows and Cells Without `onRowAction`
1054
+
1055
+ Keep existing behavior:
1056
+
1057
+ - `Enter` and `Space` continue to use selection behavior when appropriate
1058
+
1059
+ #### Rows and Cells With `onRowAction`
1060
+
1061
+ Apply the following contract in body rows:
1062
+
1063
+ - `Enter` executes `onRowAction(id)` when the row is actionable
1064
+ - `Space` performs selection when selection is allowed for that row
1065
+ - arrow keys keep their existing focus/navigation behavior
1066
+
1067
+ This keyboard split applies even when pointer behavior differs between `toggle` and `replace`.
1068
+
1069
+ #### Detailed Keyboard Rules
1070
+
1071
+ | State | `Enter` | `Space` |
1072
+ | ------------------------------------------ | -------------------------- | -------------------------- |
1073
+ | `selectionMode="none"` + `onRowAction` | action | no-op |
1074
+ | `selectionMode="single"` + `onRowAction` | action | selection |
1075
+ | `selectionMode="multiple"` + `onRowAction` | action | selection |
1076
+ | any mode without `onRowAction` | current selection behavior | current selection behavior |
1077
+
1078
+ Notes:
1079
+
1080
+ - in `replace` mode, `Space` must not be treated as action even though pointer uses double click for action
1081
+ - `Ctrl/Cmd+Space` in `multiple` + `replace` should preserve the existing non-contiguous selection behavior
1082
+ - `Shift+ArrowUp/Down` in `replace` should continue to extend selection; `onRowAction` must not interfere with that contract
1083
+
1084
+ ### Checkbox Interaction Rules
1085
+
1086
+ `Table.Checkbox` remains the explicit selection affordance.
1087
+
1088
+ Rules:
1089
+
1090
+ - checkbox interactions must never trigger `onRowAction`
1091
+ - checkbox interaction should continue to stop propagation so row presses are not synthesized accidentally
1092
+ - when `disabledBehavior="selection"`, the checkbox is disabled even if the row remains actionable
1093
+ - checkbox semantics remain selection-only, even when `selectionBehavior="replace"`
1094
+
1095
+ This preserves a clean mental model: checkbox equals selection, row press may mean action or selection depending on state.
1096
+
1097
+ ### `disabledBehavior` Semantics
1098
+
1099
+ #### `'all'`
1100
+
1101
+ This is the current effective behavior and should remain the default.
1102
+
1103
+ When a row is disabled by `disabledKeys` or `Table.Row isDisabled` and `disabledBehavior="all"`:
1104
+
1105
+ - row cannot be selected
1106
+ - row cannot trigger `onRowAction`
1107
+ - row is skipped by focus navigation
1108
+ - row should not be tabbable directly
1109
+ - body cells in that row should not be tabbable directly
1110
+ - checkbox is disabled
1111
+
1112
+ #### `'selection'`
1113
+
1114
+ When a row is disabled and `disabledBehavior="selection"`:
1115
+
1116
+ - row cannot be selected
1117
+ - row cannot be added to or removed from selection by click
1118
+ - row cannot be selected by `Space`
1119
+ - row cannot be selected via replace-mode arrow synchronization
1120
+ - checkbox is disabled
1121
+ - row can still receive focus
1122
+ - row can still participate in keyboard navigation
1123
+ - row can still trigger `onRowAction`
1124
+
1125
+ This mode means disabled-for-selection, not disabled-for-interaction.
1126
+
1127
+ ### Disabled State Matrix
1128
+
1129
+ | Condition | Focusable | Selectable | Actionable |
1130
+ | --------------------------------------------- | --------- | ------------------------- | ------------------------------ |
1131
+ | enabled row | yes | yes, per selection config | yes, when `onRowAction` exists |
1132
+ | disabled row + `disabledBehavior="all"` | no | no | no |
1133
+ | disabled row + `disabledBehavior="selection"` | yes | no | yes, when `onRowAction` exists |
1134
+
1135
+ ### Recommended Internal Refactor
1136
+
1137
+ #### Split the Existing Disabled Model
1138
+
1139
+ The current `isRowDisabled()` helper is too coarse for the new behavior. Internally, the root context should introduce separate predicates, or equivalent derived logic, for:
1140
+
1141
+ - row disabled for focus/navigation
1142
+ - row disabled for selection
1143
+ - row disabled for action
1144
+
1145
+ The public API does not need to expose all of these separately, but the internal model should.
1146
+
1147
+ Recommended internal helpers:
1148
+
1149
+ - `isRowSelectionDisabled(id, localDisabled?)`
1150
+ - `isRowActionDisabled(id, localDisabled?)`
1151
+ - `isRowInteractionDisabled(id, localDisabled?)`
1152
+ - `canRowReceiveFocus(id, localDisabled?)`
1153
+
1154
+ These names are illustrative; exact naming can be refined during implementation.
1155
+
1156
+ #### Split the Existing Press Pipeline
1157
+
1158
+ The current `pressRow()` API is selection-oriented. This phase should replace that single concept with a more explicit pipeline.
1159
+
1160
+ Recommended root-level methods:
1161
+
1162
+ - `performRowAction(id)`
1163
+ - `pressRowSelection(id, interaction)`
1164
+ - `pressRow(id, source, interaction)` as a coordinator, or equivalent separate handlers
1165
+
1166
+ The coordinator should decide behavior using:
1167
+
1168
+ - presence of `onRowAction`
1169
+ - `selectionMode`
1170
+ - `selectionBehavior`
1171
+ - whether there is an active selection
1172
+ - whether the row is disabled for selection or for all interactions
1173
+ - whether the source was pointer single click, pointer double click, `Enter`, or `Space`
1174
+
1175
+ ### Affected Parts
1176
+
1177
+ #### `root/context.ts`
1178
+
1179
+ Will need to own:
1180
+
1181
+ - `onRowAction`
1182
+ - `disabledBehavior`
1183
+ - selection-disabled vs action-disabled resolution
1184
+ - row action dispatch helpers
1185
+ - press classification helpers
1186
+
1187
+ #### `root/table-root.svelte`
1188
+
1189
+ Will need to:
1190
+
1191
+ - accept and sync the new props into context
1192
+ - expose any new root-level data attributes if useful for styling/debugging
1193
+
1194
+ #### `row/table-row.svelte`
1195
+
1196
+ Will need to:
1197
+
1198
+ - update row tabbability based on `disabledBehavior`
1199
+ - handle `Enter` as action when available
1200
+ - handle `Space` as selection when available
1201
+ - ensure row-level keydown no longer assumes `Enter` and `Space` are always equivalent
1202
+
1203
+ #### `cell/table-cell.svelte`
1204
+
1205
+ Will need to:
1206
+
1207
+ - classify pointer click behavior differently when `onRowAction` exists
1208
+ - support double-click action in `replace` mode
1209
+ - align keydown behavior with the row contract so focus target does not change semantics
1210
+
1211
+ #### `checkbox/table-checkbox.svelte`
1212
+
1213
+ Will need to:
1214
+
1215
+ - disable itself from selection-only disabled rows
1216
+ - keep stopping propagation so row actions are not accidentally fired
1217
+ - preserve explicit selection behavior independently from row-action semantics
1218
+
1219
+ ### Event Contract Recommendation
1220
+
1221
+ For the first release of this feature, `onRowAction` should receive only the row id:
1222
+
1223
+ ```ts
1224
+ onRowAction?: (id: TableSelectionKey) => void;
1225
+ ```
1226
+
1227
+ Reasons:
1228
+
1229
+ - aligns with the current root-level API style (`onSelectionChange`, `onSortChange`)
1230
+ - keeps the surface simple while the interaction model is still stabilizing
1231
+ - avoids prematurely freezing an event-detail contract that may need more nuance later
1232
+
1233
+ If consumers later need trigger metadata, a future non-breaking addition could evolve this to an object payload or add a second callback.
1234
+
1235
+ ### Data Attribute Plan
1236
+
1237
+ This phase should add row-level state markers for styling and debugging:
1238
+
1239
+ - `data-actionable="true"` when the row can trigger `onRowAction`
1240
+ - `data-disabled-behavior="selection" | "all"` on `Table.Root`
1241
+ - `data-selection-disabled="true"` on rows/cells when selection is blocked but action remains available
1242
+
1243
+ `data-actionable` should be treated as required for this phase rather than optional.
1244
+
1245
+ Reasons:
1246
+
1247
+ - consumers need a reliable styling hook for actionable rows
1248
+ - cursor styling depends on this (`cursor: pointer` vs default)
1249
+ - once row actions exist, actionable state is part of the public styling contract rather than an internal implementation detail
1250
+
1251
+ ### Accessibility Plan
1252
+
1253
+ Requirements for this phase:
1254
+
1255
+ - rows that are action-enabled but selection-disabled must still expose coherent keyboard interaction
1256
+ - `aria-disabled` should only reflect non-interactive rows under `disabledBehavior="all"`
1257
+ - rows disabled only for selection should not be presented as fully disabled if they remain actionable and focusable
1258
+ - checkbox disabled state must remain announced correctly
1259
+
1260
+ Explicit decision:
1261
+
1262
+ - rows under `disabledBehavior="selection"` should not add compensating `aria-roledescription` just to explain partial disabled state
1263
+ - the row should remain announced according to its normal table/grid semantics
1264
+ - the disabled checkbox remains the primary assistive-technology signal that selection is unavailable
1265
+ - docs should explain that selection-only disabled rows are still actionable and focusable, while accessibility semantics stay conservative rather than inventing a custom roledescription
1266
+
1267
+ This is important because reusing the current all-or-nothing `aria-disabled` contract under `disabledBehavior="selection"` would misrepresent the row to assistive technology.
1268
+
1269
+ ### Behavior Matrix
1270
+
1271
+ #### Pointer Summary
1272
+
1273
+ | `selectionMode` | `selectionBehavior` | `onRowAction` | row click | row double click |
1274
+ | --------------------- | --------------------- | ---------------------------- | ----------------- | ---------------------- |
1275
+ | `none` | `toggle` or `replace` | no | no-op | no-op |
1276
+ | `none` | `toggle` or `replace` | yes | action | same as click sequence |
1277
+ | `single` / `multiple` | `toggle` | no | selection toggle | same as click sequence |
1278
+ | `single` / `multiple` | `toggle` | yes, no active selection | action | same as click sequence |
1279
+ | `single` / `multiple` | `toggle` | yes, active selection exists | selection toggle | same as click sequence |
1280
+ | `single` / `multiple` | `replace` | no | replace selection | same as click sequence |
1281
+ | `single` / `multiple` | `replace` | yes | replace selection | action |
1282
+
1283
+ #### Keyboard Summary
1284
+
1285
+ | `selectionMode` | `onRowAction` | `Enter` | `Space` |
1286
+ | --------------------- | ------------- | --------------------------- | --------------------------- |
1287
+ | `none` | no | no-op | no-op |
1288
+ | `none` | yes | action | no-op |
1289
+ | `single` / `multiple` | no | existing selection behavior | existing selection behavior |
1290
+ | `single` / `multiple` | yes | action | selection |
1291
+
1292
+ ### Testing Plan
1293
+
1294
+ Minimum regression coverage:
1295
+
1296
+ - `selectionMode="none"` + `onRowAction` triggers action on row click
1297
+ - basic pointer action tests should land before double-click support so the action pipeline is validated incrementally
1298
+ - `selectionBehavior="toggle"` + `onRowAction` triggers action on click when selection is empty
1299
+ - `selectionBehavior="toggle"` + `onRowAction` toggles selection on click once a selection exists
1300
+ - `selectionBehavior="replace"` + `onRowAction` selects on click and acts on double click
1301
+ - `Enter` triggers action and `Space` triggers selection when both are available
1302
+ - `disabledBehavior="selection"` disables checkbox and selection changes but still allows action
1303
+ - `disabledBehavior="all"` blocks focus, selection, and action
1304
+ - disabled rows under `selection` are skipped by selection sync but not by pure focus navigation
1305
+ - disabled row + `disabledBehavior="selection"` + `onRowAction` + `Enter` triggers action
1306
+ - checkbox never triggers `onRowAction`
1307
+ - existing replace-mode selection extension (`Shift+Arrow`, `Ctrl/Cmd+Space`) remains intact
1308
+
1309
+ ### Documentation Plan
1310
+
1311
+ Docs and README updates should include:
1312
+
1313
+ - a new section explaining the difference between row actions and row selection
1314
+ - examples for:
1315
+ - action-only rows (`selectionMode="none"`)
1316
+ - mixed action + selection in `toggle`
1317
+ - mixed action + selection in `replace`
1318
+ - `disabledBehavior="selection"`
1319
+ - an explicit note that in `toggle` mode the meaning of row click changes when a selection becomes active
1320
+ - an explicit note that in `replace` mode double click emits callbacks in the order `onSelectionChange` then `onRowAction`
1321
+ - an explicit note that this phase does not implement full row link semantics
1322
+
1323
+ ### Explicit Non-Goals
1324
+
1325
+ This phase should not attempt to solve:
1326
+
1327
+ - `href`, `target`, or router-aware row navigation APIs
1328
+ - native-link-equivalent semantics for rows
1329
+ - nested buttons, links, inputs, or menus inside arbitrary body cells
1330
+ - touch-specific long-press selection mode switching
1331
+
1332
+ These can be revisited later, but they should not block the collection-level row action API.
1333
+
1334
+ ### Recommended Implementation Order
1335
+
1336
+ 1. Add `onRowAction` and `disabledBehavior` to `Table.Root` and root context.
1337
+ 2. Refactor disabled-state helpers into selection-vs-action-aware logic.
1338
+ 3. Refactor row press handling so action and selection are separate pathways, and land basic pointer single-click action tests immediately.
1339
+ 4. Update `Table.Row` and `Table.Cell` keyboard semantics (`Enter` vs `Space`).
1340
+ 5. Add pointer double-click support for `replace` mode only after the basic action pipeline is covered by tests.
1341
+ 6. Update `Table.Checkbox` for selection-only disabled rows.
1342
+ 7. Add regression tests for the full matrix, including callback ordering and disabled-row keyboard action coverage.
1343
+ 8. Document examples and caveats, especially the dynamic `toggle`-mode switch, callback ordering in `replace`, and the non-goal of row link semantics.
1344
+
922
1345
  ## Recommended Next Step
923
1346
 
924
1347
  Turn this plan into a more concrete API specification, part by part and prop by prop, before creating implementation files.