@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
@@ -12,6 +12,7 @@
12
12
  shouldShowFocusVisible,
13
13
  trackInteractionModality
14
14
  } from '../../primitives/input-modality';
15
+ import { handleTableBodyKeydown } from '../utils/handle-body-keydown';
15
16
 
16
17
  type TableCellProps = Omit<HTMLAttributes<HTMLTableCellElement>, 'children'> & {
17
18
  children?: Snippet;
@@ -109,6 +110,27 @@
109
110
  void $selectionVersion;
110
111
  return row.section === 'body' ? table.isRowDisabled(row.rowId, row.isDisabled) : row.isDisabled;
111
112
  });
113
+ const isRowSelectionDisabled = $derived.by(() => {
114
+ void $selectionVersion;
115
+ return row.section === 'body'
116
+ ? table.isRowSelectionDisabled(row.rowId, row.isDisabled)
117
+ : row.isDisabled;
118
+ });
119
+ const isRowActionable = $derived.by(() => {
120
+ void $selectionVersion;
121
+ return row.section === 'body' ? table.isRowActionable(row.rowId, row.isDisabled) : false;
122
+ });
123
+ const selectionUnavailableDescription = $derived.by(() => {
124
+ return row.section === 'body' &&
125
+ table.selectionMode !== 'none' &&
126
+ !isRowDisabled &&
127
+ isRowSelectionDisabled
128
+ ? 'Selection unavailable for this row.'
129
+ : undefined;
130
+ });
131
+ const selectionUnavailableDescriptionId = $derived(
132
+ selectionUnavailableDescription ? table.selectionUnavailableDescriptionId : undefined
133
+ );
112
134
  const isCellFocusable = $derived(row.section !== 'body' || !isRowDisabled);
113
135
  const cellTabIndex = $derived.by(() => {
114
136
  if (row.section !== 'body') return undefined;
@@ -128,12 +150,29 @@
128
150
  if (row.section !== 'body') return;
129
151
  if (isRowDisabled) return;
130
152
  table.focusCellByKey(key);
131
- table.pressRow(row.rowId as TableSelectionKey | undefined, {
132
- shiftKey: event.shiftKey,
133
- ctrlKey: event.ctrlKey,
134
- metaKey: event.metaKey,
135
- altKey: event.altKey
136
- });
153
+ table.pressRow(
154
+ row.rowId as TableSelectionKey | undefined,
155
+ 'pointer',
156
+ {
157
+ shiftKey: event.shiftKey,
158
+ ctrlKey: event.ctrlKey,
159
+ metaKey: event.metaKey,
160
+ altKey: event.altKey
161
+ },
162
+ row.isDisabled
163
+ );
164
+ }
165
+
166
+ function handleDoubleClick() {
167
+ if (row.section !== 'body') return;
168
+ if (isRowDisabled) return;
169
+ table.focusCellByKey(key);
170
+ table.pressRow(
171
+ row.rowId as TableSelectionKey | undefined,
172
+ 'pointer-double',
173
+ {},
174
+ row.isDisabled
175
+ );
137
176
  }
138
177
 
139
178
  function handleMouseDown(event: MouseEvent) {
@@ -143,79 +182,38 @@
143
182
 
144
183
  function handleKeyDown(event: KeyboardEvent) {
145
184
  if (row.section !== 'body') return;
146
- trackInteractionModality(event, element ?? null);
147
-
148
- if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
149
- if (table.selectionMode === 'multiple') {
150
- event.preventDefault();
151
- table.selectAllRows();
152
- }
153
- return;
154
- }
155
-
156
- if ((event.ctrlKey || event.metaKey) && event.key === 'Home') {
157
- event.preventDefault();
158
- table.moveToGridStart();
159
- return;
160
- }
161
-
162
- if ((event.ctrlKey || event.metaKey) && event.key === 'End') {
163
- event.preventDefault();
164
- table.moveToGridEnd();
165
- return;
166
- }
167
-
168
- switch (event.key) {
169
- case 'ArrowUp':
170
- event.preventDefault();
171
- table.moveFocus('up', {
172
- shiftKey: event.shiftKey,
173
- ctrlKey: event.ctrlKey,
174
- metaKey: event.metaKey,
175
- altKey: event.altKey
176
- });
177
- return;
178
- case 'ArrowDown':
179
- event.preventDefault();
180
- table.moveFocus('down', {
181
- shiftKey: event.shiftKey,
182
- ctrlKey: event.ctrlKey,
183
- metaKey: event.metaKey,
184
- altKey: event.altKey
185
- });
186
- return;
187
- case 'ArrowLeft':
188
- event.preventDefault();
189
- table.moveFocus('left');
190
- return;
191
- case 'ArrowRight':
192
- event.preventDefault();
193
- table.moveFocus('right');
194
- return;
195
- case 'Home':
196
- event.preventDefault();
197
- table.moveToRowStart();
198
- return;
199
- case 'End':
200
- event.preventDefault();
201
- table.moveToRowEnd();
202
- return;
203
- case 'Enter':
204
- case ' ':
205
- event.preventDefault();
206
- if (event.repeat) {
207
- return;
208
- }
209
- if (!isRowDisabled) {
210
- table.pressRow(row.rowId as TableSelectionKey | undefined, {
185
+ handleTableBodyKeydown({
186
+ event,
187
+ table,
188
+ focusTarget: element,
189
+ isDisabled: isRowDisabled,
190
+ onHome: () => table.moveToRowStart(),
191
+ onEnd: () => table.moveToRowEnd(),
192
+ onEnter: () =>
193
+ table.pressRow(
194
+ row.rowId as TableSelectionKey | undefined,
195
+ 'keyboard-enter',
196
+ {
211
197
  shiftKey: event.shiftKey,
212
198
  ctrlKey: event.ctrlKey,
213
199
  metaKey: event.metaKey,
214
200
  altKey: event.altKey
215
- });
216
- }
217
- return;
218
- }
201
+ },
202
+ row.isDisabled
203
+ ),
204
+ onSpace: () =>
205
+ table.pressRow(
206
+ row.rowId as TableSelectionKey | undefined,
207
+ 'keyboard-space',
208
+ {
209
+ shiftKey: event.shiftKey,
210
+ ctrlKey: event.ctrlKey,
211
+ metaKey: event.metaKey,
212
+ altKey: event.altKey
213
+ },
214
+ row.isDisabled
215
+ )
216
+ });
219
217
  }
220
218
  </script>
221
219
 
@@ -228,17 +226,26 @@
228
226
  scope={row.section === 'body' && column?.isRowHeader ? 'row' : undefined}
229
227
  aria-colindex={!isColumnHidden && visibleColumnIndex >= 0 ? visibleColumnIndex + 1 : undefined}
230
228
  aria-hidden={isColumnHidden ? true : undefined}
229
+ aria-describedby={selectionUnavailableDescriptionId}
231
230
  aria-disabled={row.section === 'body' && isRowDisabled ? true : undefined}
232
231
  data-focused={isFocused ? 'true' : undefined}
233
232
  data-focus-visible={isFocusVisible ? 'true' : undefined}
233
+ data-actionable={isRowActionable ? 'true' : undefined}
234
234
  data-row-selected={isRowSelected ? 'true' : undefined}
235
+ data-selection-disabled={row.section === 'body' &&
236
+ table.selectionMode !== 'none' &&
237
+ !isRowDisabled &&
238
+ isRowSelectionDisabled
239
+ ? 'true'
240
+ : undefined}
235
241
  data-disabled={isRowDisabled || undefined}
236
242
  data-column-index={visibleColumnIndex >= 0 ? visibleColumnIndex : undefined}
237
243
  style:display={isColumnHidden ? 'none' : undefined}
238
- onfocus={handleFocus}
239
- onclick={handleClick}
240
- onmousedown={handleMouseDown}
241
- onkeydown={handleKeyDown}
244
+ onfocus={row.section === 'body' ? handleFocus : undefined}
245
+ onclick={row.section === 'body' ? handleClick : undefined}
246
+ ondblclick={row.section === 'body' ? handleDoubleClick : undefined}
247
+ onmousedown={row.section === 'body' ? handleMouseDown : undefined}
248
+ onkeydown={row.section === 'body' ? handleKeyDown : undefined}
242
249
  {...restProps}
243
250
  >
244
251
  {#if children}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { Table } from '../index';
3
3
  import type {
4
+ TableDisabledBehavior,
4
5
  TableSelectionBehavior,
5
6
  TableSelectionKey,
6
7
  TableSelectionMode
@@ -27,6 +28,8 @@
27
28
  rows?: DemoRow[];
28
29
  selectionMode?: TableSelectionMode;
29
30
  selectionBehavior?: TableSelectionBehavior;
31
+ disabledBehavior?: TableDisabledBehavior;
32
+ disallowEmptySelection?: boolean;
30
33
  disabledKeys?: Iterable<TableSelectionKey>;
31
34
  initialSelectedKeys?: Iterable<TableSelectionKey>;
32
35
  };
@@ -35,6 +38,8 @@
35
38
  rows = defaultRows,
36
39
  selectionMode = 'multiple',
37
40
  selectionBehavior = 'toggle',
41
+ disabledBehavior = 'all',
42
+ disallowEmptySelection = false,
38
43
  disabledKeys,
39
44
  initialSelectedKeys
40
45
  }: CheckboxTestProps = $props();
@@ -48,6 +53,8 @@
48
53
  aria-label="Users table"
49
54
  {selectionMode}
50
55
  {selectionBehavior}
56
+ {disabledBehavior}
57
+ {disallowEmptySelection}
51
58
  bind:selectedKeys={currentSelectedKeys}
52
59
  {disabledKeys}
53
60
  >
@@ -1,4 +1,4 @@
1
- import type { TableSelectionBehavior, TableSelectionKey, TableSelectionMode } from '../root/context';
1
+ import type { TableDisabledBehavior, TableSelectionBehavior, TableSelectionKey, TableSelectionMode } from '../root/context';
2
2
  type DemoRow = {
3
3
  id: string;
4
4
  email: string;
@@ -8,6 +8,8 @@ type CheckboxTestProps = {
8
8
  rows?: DemoRow[];
9
9
  selectionMode?: TableSelectionMode;
10
10
  selectionBehavior?: TableSelectionBehavior;
11
+ disabledBehavior?: TableDisabledBehavior;
12
+ disallowEmptySelection?: boolean;
11
13
  disabledKeys?: Iterable<TableSelectionKey>;
12
14
  initialSelectedKeys?: Iterable<TableSelectionKey>;
13
15
  };
@@ -50,7 +50,9 @@
50
50
  const selectionVersion = table.selectionVersion;
51
51
  const layoutVersion = table.layoutVersion;
52
52
 
53
- let wrapperElement = $state<HTMLElement | undefined>(undefined);
53
+ let checkboxElement = $state<HTMLSpanElement | null>(null);
54
+ let checkboxChecked = $state(false);
55
+ let checkboxIndeterminate = $state(false);
54
56
 
55
57
  const isVisible = $derived.by(() => {
56
58
  if (table.selectionMode === 'none') return false;
@@ -83,7 +85,7 @@
83
85
  return !table.hasSelectableRows();
84
86
  }
85
87
  if (section.section === 'body') {
86
- return table.isRowDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
88
+ return table.isRowSelectionDisabled(row.rowId, row.isDisabled) || row.rowId === undefined;
87
89
  }
88
90
  return true;
89
91
  });
@@ -101,9 +103,20 @@
101
103
  });
102
104
 
103
105
  function getCheckboxRootElement() {
104
- return wrapperElement?.querySelector<HTMLElement>('[data-checkbox-root="true"]') ?? undefined;
106
+ return checkboxElement ?? undefined;
105
107
  }
106
108
 
109
+ $effect(() => {
110
+ void $selectionVersion;
111
+ checkboxChecked = isChecked;
112
+ });
113
+
114
+ $effect(() => {
115
+ void $selectionVersion;
116
+ void $layoutVersion;
117
+ checkboxIndeterminate = isIndeterminate;
118
+ });
119
+
107
120
  $effect(() => {
108
121
  if (!isVisible || isDisabled) {
109
122
  cell.unregisterFocusDelegate();
@@ -129,6 +142,27 @@
129
142
  checkboxElement.tabIndex = tabIndex;
130
143
  });
131
144
 
145
+ $effect(() => {
146
+ const checkboxElement = getCheckboxRootElement();
147
+ if (!checkboxElement) return;
148
+
149
+ const handleElementFocus = (event: FocusEvent) => {
150
+ handleFocusIn(event);
151
+ };
152
+
153
+ const handleElementMouseDown = (event: MouseEvent) => {
154
+ handleMouseDown(event);
155
+ };
156
+
157
+ checkboxElement.addEventListener('focus', handleElementFocus);
158
+ checkboxElement.addEventListener('mousedown', handleElementMouseDown);
159
+
160
+ return () => {
161
+ checkboxElement.removeEventListener('focus', handleElementFocus);
162
+ checkboxElement.removeEventListener('mousedown', handleElementMouseDown);
163
+ };
164
+ });
165
+
132
166
  function applySelection(nextChecked: boolean) {
133
167
  if (isDisabled) return;
134
168
 
@@ -152,11 +186,6 @@
152
186
  table.setFocusVisible(shouldShowFocusVisible(target ?? null));
153
187
  }
154
188
 
155
- function handleFocusOut(event: FocusEvent) {
156
- const nextFocused = event.relatedTarget;
157
- if (nextFocused instanceof Node && wrapperElement?.contains(nextFocused)) return;
158
- }
159
-
160
189
  function handleMouseDown(event: MouseEvent) {
161
190
  trackInteractionModality(event, getCheckboxRootElement() ?? null);
162
191
  table.setFocusVisible(false);
@@ -166,6 +195,9 @@
166
195
  function handleClick(event: MouseEvent) {
167
196
  event.stopPropagation();
168
197
  if (!isVisible || isDisabled) return;
198
+ event.preventDefault();
199
+ const nextChecked = section.section === 'header' ? checkboxState !== 'all' : !isChecked;
200
+ applySelection(nextChecked);
169
201
  table.focusCellByKey(cell.cellKey);
170
202
  }
171
203
 
@@ -246,29 +278,22 @@
246
278
  </script>
247
279
 
248
280
  {#if isVisible}
249
- <div
250
- bind:this={wrapperElement}
251
- role="presentation"
252
- onfocusin={handleFocusIn}
253
- onfocusout={handleFocusOut}
254
- onmousedown={handleMouseDown}
281
+ <Checkbox.Root
282
+ {id}
283
+ bind:element={checkboxElement}
284
+ bind:isChecked={checkboxChecked}
285
+ bind:isIndeterminate={checkboxIndeterminate}
286
+ {isDisabled}
287
+ onCheckedChange={applySelection}
288
+ {title}
289
+ aria-label={accessibleLabel}
290
+ aria-labelledby={ariaLabelledby}
291
+ data-table-checkbox="true"
255
292
  onclick={handleClick}
256
293
  onkeydown={handleKeyDown}
294
+ class={className}
295
+ {...restProps}
257
296
  >
258
- <Checkbox.Root
259
- {id}
260
- {isChecked}
261
- {isIndeterminate}
262
- {isDisabled}
263
- onCheckedChange={applySelection}
264
- {title}
265
- aria-label={accessibleLabel}
266
- aria-labelledby={ariaLabelledby}
267
- data-table-checkbox="true"
268
- class={className}
269
- {...restProps}
270
- >
271
- {@render children?.()}
272
- </Checkbox.Root>
273
- </div>
297
+ {@render children?.()}
298
+ </Checkbox.Root>
274
299
  {/if}
@@ -11,6 +11,6 @@ export { default as TableColumnResizer } from './column-resizer/table-column-res
11
11
  export { default as TableCheckbox } from './checkbox/table-checkbox.svelte';
12
12
  export { default as TableCheckboxIndicator } from './checkbox-indicator/table-checkbox-indicator.svelte';
13
13
  export { default as TableCell } from './cell/table-cell.svelte';
14
- export { createTableContext, getTableContext, setTableContext, useTableContext, getTableSectionContext, setTableSectionContext, useTableSectionContext, getTableRowContext, setTableRowContext, useTableRowContext, getTableColumnContext, setTableColumnContext, useTableColumnContext, type TableContext, type TableSelectionBehavior, type TableSelectionCheckboxState, type TableSelectionKey, type TableSelectionMode, type TableSortDirection, type TableSortDescriptor, type TableColumnWidth, type TableGridCoord, type TableColumnRegistration, type TableSectionKind, type TableSectionContext, type TableRowContext, type TableColumnContext, type CreateTableContextOptions } from './root/context.js';
14
+ export { createTableContext, getTableContext, setTableContext, useTableContext, getTableSectionContext, setTableSectionContext, useTableSectionContext, getTableRowContext, setTableRowContext, useTableRowContext, getTableColumnContext, setTableColumnContext, useTableColumnContext, type TableContext, type TableDisabledBehavior, type TableSelectionBehavior, type TableSelectionCheckboxState, type TableSelectionKey, type TableSelectionMode, type TableRowActionHandler, type TableSortDirection, type TableSortDescriptor, type TableColumnWidth, type TableGridCoord, type TableColumnRegistration, type TableSectionKind, type TableSectionContext, type TableRowContext, type TableColumnContext, type CreateTableContextOptions } from './root/context.js';
15
15
  import * as TableParts from './index.parts.js';
16
16
  export default TableParts;
@@ -2,14 +2,17 @@ import { type Readable } from 'svelte/store';
2
2
  export type TableSelectionKey = string | number;
3
3
  export type TableSelectionMode = 'none' | 'single' | 'multiple';
4
4
  export type TableSelectionBehavior = 'toggle' | 'replace';
5
+ export type TableDisabledBehavior = 'selection' | 'all';
5
6
  export type TableSortDirection = 'ascending' | 'descending';
6
7
  export type TableSectionKind = 'header' | 'body' | 'footer';
8
+ export type TableRowActionHandler = (id: TableSelectionKey) => void;
7
9
  type TableSelectionInteraction = {
8
10
  shiftKey?: boolean;
9
11
  ctrlKey?: boolean;
10
12
  metaKey?: boolean;
11
13
  altKey?: boolean;
12
14
  };
15
+ type TableRowPressSource = 'pointer' | 'pointer-double' | 'keyboard-enter' | 'keyboard-space';
13
16
  export type TableSortDescriptor = {
14
17
  column: string;
15
18
  direction: TableSortDirection;
@@ -52,11 +55,14 @@ type TableCellRegistration = {
52
55
  export type CreateTableContextOptions = {
53
56
  selectionMode?: TableSelectionMode;
54
57
  selectionBehavior?: TableSelectionBehavior;
58
+ disabledBehavior?: TableDisabledBehavior;
59
+ disallowEmptySelection?: boolean;
55
60
  initialSelectedKeys?: Iterable<TableSelectionKey>;
56
61
  initialSortDescriptor?: TableSortDescriptor;
57
62
  initialColumnWidths?: Iterable<readonly [string, number]>;
58
63
  initialHiddenColumns?: Iterable<string>;
59
64
  disabledKeys?: Iterable<TableSelectionKey>;
65
+ onRowAction?: TableRowActionHandler;
60
66
  onSelectionChange?: (keys: Set<TableSelectionKey>) => void;
61
67
  onSortChange?: (descriptor: TableSortDescriptor | undefined) => void;
62
68
  onColumnWidthsChange?: (widths: Map<string, number>) => void;
@@ -74,6 +80,9 @@ export type TableContext = {
74
80
  createInstanceToken: (prefix: string) => string;
75
81
  selectionMode: TableSelectionMode;
76
82
  selectionBehavior: TableSelectionBehavior;
83
+ disabledBehavior: TableDisabledBehavior;
84
+ disallowEmptySelection: boolean;
85
+ selectionUnavailableDescriptionId: string;
77
86
  disabledKeys: Set<TableSelectionKey>;
78
87
  focusedCellKey: string | null;
79
88
  focusVisible: boolean;
@@ -114,6 +123,9 @@ export type TableContext = {
114
123
  isRowFocusTarget: (token: string) => boolean;
115
124
  getRowFocusEdge: (token: string) => TableRowFocusEdge | null;
116
125
  isRowDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
126
+ isRowSelectionDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
127
+ isRowActionDisabled: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
128
+ isRowActionable: (id: TableSelectionKey | undefined, localDisabled?: boolean) => boolean;
117
129
  hasSelectableRows: () => boolean;
118
130
  getSelectionCheckboxState: () => TableSelectionCheckboxState;
119
131
  registerCell: (cell: TableCellRegistration) => void;
@@ -123,7 +135,7 @@ export type TableContext = {
123
135
  isRowTabStop: (token: string) => boolean;
124
136
  focusCellByKey: (key: string | null) => void;
125
137
  focusRowByToken: (token: string, edge: TableRowFocusEdge) => void;
126
- pressRow: (id: TableSelectionKey | undefined, interaction?: TableSelectionInteraction) => void;
138
+ pressRow: (id: TableSelectionKey | undefined, source: TableRowPressSource, interaction?: TableSelectionInteraction, localDisabled?: boolean) => void;
127
139
  setFocusedCell: (key: string | null) => void;
128
140
  setFocusedRow: (token: string | null, edge?: TableRowFocusEdge) => void;
129
141
  setFocusVisible: (visible: boolean) => void;
@@ -140,7 +152,10 @@ export type TableContext = {
140
152
  setSelection: (keys: Iterable<TableSelectionKey>) => void;
141
153
  setSelectionMode: (mode: TableSelectionMode) => void;
142
154
  setSelectionBehavior: (behavior: TableSelectionBehavior) => void;
155
+ setDisabledBehavior: (behavior: TableDisabledBehavior) => void;
156
+ setDisallowEmptySelection: (disallow: boolean) => void;
143
157
  setDisabledKeys: (keys?: Iterable<TableSelectionKey>) => void;
158
+ setRowActionHandler: (handler?: TableRowActionHandler) => void;
144
159
  setSortDescriptor: (descriptor: TableSortDescriptor | undefined) => void;
145
160
  toggleSort: (columnId: string) => void;
146
161
  isColumnSortable: (columnId: string) => boolean;