@human-kit/svelte-components 1.0.0-alpha.12 → 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.
Files changed (29) hide show
  1. package/dist/checkbox/root/checkbox-root.svelte +22 -2
  2. package/dist/checkbox/root/checkbox-root.svelte.d.ts +4 -1
  3. package/dist/combobox/input/combobox-input.svelte +1 -0
  4. package/dist/combobox/list/combobox-listbox.svelte +1 -0
  5. package/dist/combobox/list/combobox-listbox.svelte.d.ts +1 -0
  6. package/dist/combobox/root/combobox-test.svelte +8 -2
  7. package/dist/combobox/root/combobox-test.svelte.d.ts +1 -0
  8. package/dist/combobox/root/combobox.svelte +16 -9
  9. package/dist/listbox/item/listbox-item.svelte +24 -2
  10. package/dist/listbox/root/listbox.svelte +14 -2
  11. package/dist/listbox/root/listbox.svelte.d.ts +2 -0
  12. package/dist/table/IMPLEMENTATION_NOTES.md +2 -1
  13. package/dist/table/PLAN.md +440 -17
  14. package/dist/table/TODO.md +39 -1
  15. package/dist/table/cell/table-cell.svelte +86 -79
  16. package/dist/table/checkbox/table-checkbox-test.svelte +7 -0
  17. package/dist/table/checkbox/table-checkbox-test.svelte.d.ts +3 -1
  18. package/dist/table/checkbox/table-checkbox.svelte +55 -30
  19. package/dist/table/index.d.ts +1 -1
  20. package/dist/table/root/context.d.ts +16 -1
  21. package/dist/table/root/context.js +199 -24
  22. package/dist/table/root/table-root.svelte +30 -0
  23. package/dist/table/root/table-root.svelte.d.ts +4 -1
  24. package/dist/table/root/table-test.svelte +29 -0
  25. package/dist/table/root/table-test.svelte.d.ts +5 -1
  26. package/dist/table/row/table-row.svelte +44 -67
  27. package/dist/table/utils/handle-body-keydown.d.ts +13 -0
  28. package/dist/table/utils/handle-body-keydown.js +67 -0
  29. package/package.json +1 -1
@@ -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.
@@ -61,6 +61,9 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
61
61
  - [x] [S][P2][Area: Behavior][Owner: Unassigned][Target: Done] Confirm whether disabled body rows should remain keyboard-focusable or be skipped by navigation.
62
62
  - [ ] [S][P2][Area: API][Owner: Unassigned][Target: TBD] Decide whether `Table.Column` should hard-enforce a single `Table.ColumnHeaderCell` child.
63
63
  - [ ] [S][P2][Area: API][Owner: Unassigned][Target: TBD] Decide whether clipboard-related behavior should remain fully browser-native in v1 or be deferred behind a future explicit cell-selection model.
64
+ - [x] [C][P1][Area: API][Owner: Unassigned][Target: Done] Add `onRowAction?: (id: TableSelectionKey) => void` to `Table.Root` and document its collection-level semantics across `selectionMode` and `selectionBehavior`.
65
+ - [x] [C][P1][Area: API][Owner: Unassigned][Target: Done] Add `disabledBehavior?: 'selection' | 'all'` to `Table.Root` with default `'all'`, and document that `'selection'` disables selection while preserving focus and row actions.
66
+ - [x] [S][P1][Area: API][Owner: Unassigned][Target: Done] Treat stable `selectionBehavior?: 'toggle' | 'replace'` support in `Table.Root` as an explicit prerequisite for the row-actions phase rather than an implicit assumption.
64
67
 
65
68
  ### Features
66
69
 
@@ -74,6 +77,7 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
74
77
  - [ ] [C][P2][Area: Features][Owner: Unassigned][Target: TBD] Add inline cell and row editing primitives — define an opt-in editing model that can coexist with current focus, selection, and keyboard navigation contracts.
75
78
  - [ ] [M][P2][Area: Features][Owner: Unassigned][Target: TBD] Add column action primitives — expose a path for header menus, quick sort/filter actions, and future column management UI without requiring consumers to hand-roll header action composition every time.
76
79
  - [ ] [M][P3][Area: Features][Owner: Unassigned][Target: TBD] Add row actions patterns — document or expose a composable pattern for common trailing actions columns so selection, row press, and nested interactive controls do not conflict.
80
+ - [x] [C][P1][Area: Features][Owner: Unassigned][Target: Done] Implement collection-level row actions in `Table.Root` so body rows can trigger `onRowAction` using RAC-style interaction rules without introducing a separate `rowPressBehavior` prop.
77
81
 
78
82
  ### Tests
79
83
 
@@ -81,12 +85,21 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
81
85
  - [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add test for full sort cycle via keyboard (ascending → descending) and verify no way to clear sort with keyboard is intentional.
82
86
  - [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add test verifying disabled rows are not selected when arrow-navigated in `replace` mode.
83
87
  - [x] [S][P2][Area: Tests][Owner: Unassigned][Target: Done] Add tests covering `selectionMode` transitions after mount, including collapsing multiple selections to one on `single`.
88
+ - [x] [C][P1][Area: Tests][Owner: Unassigned][Target: Done] Add the full row action matrix to `Table.Root` tests: click/action in `selectionMode="none"`, toggle-mode action when selection is empty, toggle-mode selection when selection is active, and replace-mode double-click actions.
89
+ - [x] [M][P1][Area: Tests][Owner: Unassigned][Target: Done] Add keyboard interaction tests proving `Enter` triggers `onRowAction` and `Space` triggers selection when both behaviors are available.
90
+ - [x] [M][P1][Area: Tests][Owner: Unassigned][Target: Done] Add `disabledBehavior` regression tests covering `'selection'` vs `'all'` for row focusability, checkbox disabled state, row action availability, and selection blocking.
91
+ - [x] [S][P1][Area: Tests][Owner: Unassigned][Target: Done] Add an explicit edge-case test proving a disabled row under `disabledBehavior="selection"` still fires `onRowAction` on `Enter` while remaining non-selectable.
92
+ - [x] [S][P1][Area: Tests][Owner: Unassigned][Target: Done] Add a callback-ordering test proving `selectionBehavior="replace"` double click fires `onSelectionChange` before `onRowAction`.
84
93
 
85
94
  ### Docs
86
95
 
87
96
  - [ ] [C][P2][Area: Docs][Owner: Unassigned][Target: TBD] Add richer styling examples and sorting guidance to the docs page.
88
97
  - [x] [C][P3][Area: Docs][Owner: Unassigned][Target: Done] Document that `Table.Column` is a logical-only wrapper (no DOM output) prominently in the README anatomy section.
89
98
  - [x] [S][P2][Area: Docs][Owner: Unassigned][Target: Done] Document `selectionMode="none"` normalization behavior and clarify that selection is cleared internally when selection is disabled.
99
+ - [ ] [C][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document row actions vs row selection in the Table README and docs page, including the different pointer rules for `toggle` and `replace` when `onRowAction` is provided.
100
+ - [ ] [M][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document `disabledBehavior="selection"` and clarify that it disables selection only, not row actions or focus.
101
+ - [ ] [S][P1][Area: Docs][Owner: Unassigned][Target: TBD] Call out explicitly that `toggle` mode changes row-click behavior dynamically once an active selection exists, and treat this as an intentional RAC-style model rather than an accidental inconsistency.
102
+ - [ ] [S][P1][Area: Docs][Owner: Unassigned][Target: TBD] Document callback ordering for `replace`-mode double click: `onSelectionChange` fires before `onRowAction`.
90
103
 
91
104
  ### Selection Checkbox
92
105
 
@@ -94,7 +107,32 @@ Ship a stable `Table` v1 with keyboard navigation, row selection, sorting, docum
94
107
  - [ ] [M][P1][Area: Accessibility][Owner: Unassigned][Target: TBD] Validate `Table.Checkbox` screen reader announcements across NVDA and VoiceOver — verify that `aria-checked="mixed"` transitions in the header checkbox and row selection toggles are announced correctly.
95
108
  - [ ] [M][P1][Area: Correctness][Owner: Unassigned][Target: TBD] Document or resolve `Table.Checkbox` bypass of `pressRow()` — the checkbox always calls `toggleRowSelection()` directly, ignoring `selectionBehavior="replace"` semantics (Shift+click range, Ctrl+click toggle). This is intentional for checkbox UX but undocumented and inconsistent with row-click behavior.
96
109
  - [ ] [M][P1][Area: Correctness][Owner: Unassigned][Target: TBD] Add dev-time structural validation for `Table.Checkbox` placement — warn when header includes a selection checkbox column but body rows do not, or when the checkbox is placed in a footer cell where it has no behavior.
110
+ - [x] [M][P1][Area: Correctness][Owner: Unassigned][Target: Done] Ensure `Table.Checkbox` respects `disabledBehavior="selection"` by disabling explicit selection controls without suppressing row actions on the rest of the row.
111
+
112
+ ### Row Actions
113
+
114
+ - [x] [C][P1][Area: Correctness][Owner: Unassigned][Target: Done] Refactor the current `pressRow()` pipeline so selection and row actions are distinct internal pathways rather than a single selection-oriented press abstraction.
115
+ - [x] [C][P1][Area: Correctness][Owner: Unassigned][Target: Done] Split row disabled-state helpers into selection-disabled vs action-disabled semantics so `disabledBehavior="selection"` does not incorrectly suppress focus or actions.
116
+ - [x] [M][P1][Area: Behavior][Owner: Unassigned][Target: Done] Preserve existing `replace`-mode selection extension (`Shift+Arrow`, `Ctrl/Cmd+Space`, click replace) while layering `onRowAction` on top.
117
+ - [x] [M][P1][Area: Accessibility][Owner: Unassigned][Target: Done] Revisit `aria-disabled` semantics for rows under `disabledBehavior="selection"` so actionable rows are not announced as fully disabled.
118
+ - [x] [M][P1][Area: DX][Owner: Unassigned][Target: Done] Evaluate whether row/cell data attributes should expose actionable and selection-disabled states for styling and debugging once `onRowAction` ships.
119
+ - [x] [S][P1][Area: DX][Owner: Unassigned][Target: Done] Make `data-actionable` part of the required styling contract for actionable rows so consumers can reliably style cursor and affordances.
97
120
 
98
121
  ### Code Quality
99
122
 
100
- - [ ] [C][P3][Area: Code Quality][Owner: Unassigned][Target: TBD] Remove unused keyboard/click handlers from `Table.Cell` when rendering in footer scope — currently handlers are bound but short-circuit via guards.
123
+ - [x] [C][P3][Area: Code Quality][Owner: Unassigned][Target: Done] Remove unused keyboard/click handlers from `Table.Cell` when rendering in footer scope — currently handlers are bound but short-circuit via guards.
124
+ - [x] [M][P2][Area: Code Quality][Owner: Unassigned][Target: Done] Extract shared keyboard handler between `Table.Row` and `Table.Cell` — both components duplicate ~60 lines of nearly identical `handleKeyDown` logic (ArrowUp/Down/Left/Right, Home/End, Ctrl+Home/End, Ctrl+A, Enter, Space) with only minor differences in Home/End targets.
125
+ - [x] [S][P2][Area: Code Quality][Owner: Unassigned][Target: Done] Add a defensive `isRowDisabled` guard inside `pressRow()` in context — currently callers (Row, Cell) check disabled state before calling, but `pressRow` itself does not validate, so a direct call bypasses the check.
126
+
127
+ ### Bugs / Phase 3
128
+
129
+ - [x] [M][P1][Area: Correctness][Owner: Unassigned][Target: Done] Fix `keyboard-space` ignoring interaction modifiers in `pressRow()` — the `keyboard-space` branch calls `toggleRowSelection(id)` directly, discarding `shiftKey`/`ctrlKey`/`metaKey`. This breaks `Shift+Space` (should extend selection) and `Ctrl+Space` semantics in `replace` + `multiple` mode. Should route through `pressRowSelection(id, interaction)` instead.
130
+
131
+ ### Accessibility / Phase 3
132
+
133
+ - [x] [S][P2][Area: Accessibility][Owner: Unassigned][Target: Done] Clarify ARIA semantics for rows under `disabledBehavior="selection"` without a checkbox — a selection-disabled but actionable row has no `aria-disabled` (correct) but also no ARIA hint that selection is unavailable; the checkbox disabled state announces it, but rows without a checkbox column have no signal for assistive technology.
134
+
135
+ ### Performance / Phase 3
136
+
137
+ - [x] [S][P2][Area: Performance][Owner: Unassigned][Target: Done] Use O(1) id-to-token lookup in `isColumnSortable()` — currently iterates all columns with `Array.from(columns.values()).some(...)` instead of using `getColumnRegistrationById(columnId)?.allowsSorting`.
138
+ - [x] [S][P2][Area: Performance][Owner: Unassigned][Target: Done] Cache `getVisibleColumnWidths()` result — it filters `getColumnWidths()` on every call but is used in the render path of `Table.Root` for `managedTableWidth` derivation; should share a cache invalidated alongside `columnWidthsCache`.