sveltacular 1.0.15 → 1.0.16

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.
@@ -12,6 +12,7 @@
12
12
  name = undefined,
13
13
  onChange = undefined,
14
14
  label,
15
+ ariaLabel,
15
16
  children,
16
17
  size = 'full' as FormFieldSizeOptions,
17
18
  helperText = undefined,
@@ -25,6 +26,7 @@
25
26
  name?: string | undefined;
26
27
  onChange?: ((data: { isChecked: boolean; value: string }) => void) | undefined;
27
28
  label?: string;
29
+ ariaLabel?: string;
28
30
  children?: Snippet;
29
31
  size?: FormFieldSizeOptions;
30
32
  helperText?: string;
@@ -33,6 +35,9 @@
33
35
  inline?: boolean;
34
36
  } = $props();
35
37
 
38
+ // Use ariaLabel if provided, otherwise fall back to label for accessibility
39
+ let inputAriaLabel = $derived(ariaLabel ?? label);
40
+
36
41
  const id = uniqueId();
37
42
 
38
43
  const onChecked = (event: Event) => {
@@ -56,18 +61,21 @@
56
61
  bind:checked={isChecked}
57
62
  onchange={onChecked}
58
63
  {required}
64
+ aria-label={inputAriaLabel}
59
65
  />
60
66
  <span class="checkbox">
61
67
  <span class="checkmark"><Icon type="check" size="sm" fill="#fff" mask /></span>
62
68
  </span>
63
- {#if children}
64
- <div class="text">
65
- {@render children()}
66
- </div>
67
- {:else if label}
68
- <div class="text">
69
- {label}
70
- </div>
69
+ {#if !ariaLabel}
70
+ {#if children}
71
+ <div class="text">
72
+ {@render children()}
73
+ </div>
74
+ {:else if label}
75
+ <div class="text">
76
+ {label}
77
+ </div>
78
+ {/if}
71
79
  {/if}
72
80
  </label>
73
81
  {:else}
@@ -82,6 +90,7 @@
82
90
  bind:checked={isChecked}
83
91
  onchange={onChecked}
84
92
  {required}
93
+ aria-label={inputAriaLabel}
85
94
  />
86
95
  <span class="checkbox">
87
96
  <span class="checkmark"> <Icon type="check" size="sm" fill="#fff" mask /></span>
@@ -123,7 +132,7 @@
123
132
  user-select: none;
124
133
  }
125
134
  .checkbox-label .checkbox .checkmark {
126
- display: block;
135
+ display: none;
127
136
  position: absolute;
128
137
  top: 0;
129
138
  left: 0;
@@ -145,6 +154,7 @@
145
154
  border-color: var(--form-input-border);
146
155
  }
147
156
  .checkbox-label input:checked + .checkbox .checkmark {
157
+ display: block;
148
158
  width: 100%;
149
159
  height: 100%;
150
160
  }</style>
@@ -11,6 +11,7 @@ type $$ComponentProps = {
11
11
  value: string;
12
12
  }) => void) | undefined;
13
13
  label?: string;
14
+ ariaLabel?: string;
14
15
  children?: Snippet;
15
16
  size?: FormFieldSizeOptions;
16
17
  helperText?: string;
@@ -9,12 +9,14 @@
9
9
  value = undefined as RadioValue,
10
10
  group = $bindable(undefined as string | undefined),
11
11
  disabled = false,
12
+ ariaLabel,
12
13
  children = undefined,
13
14
  onChange = undefined
14
15
  }: {
15
16
  value?: RadioValue;
16
17
  group?: string | undefined;
17
18
  disabled?: boolean;
19
+ ariaLabel?: string;
18
20
  children?: Snippet;
19
21
  onChange?: ((value: string) => void) | undefined;
20
22
  } = $props();
@@ -29,12 +31,13 @@
29
31
  {value}
30
32
  {disabled}
31
33
  {id}
34
+ aria-label={ariaLabel}
32
35
  onchange={() => onChange?.(String(value || ''))}
33
36
  />
34
- <span class="checkbox">
35
- <span class="checkmark"><Icon type="check" size="sm" /></span>
37
+ <span class="radio-circle">
38
+ <span class="radio-dot"></span>
36
39
  </span>
37
- {#if children}
40
+ {#if !ariaLabel && children}
38
41
  <div class="text">
39
42
  {@render children?.()}
40
43
  </div>
@@ -49,43 +52,35 @@
49
52
  font-size: 1rem;
50
53
  cursor: pointer;
51
54
  }
52
- label .checkbox {
55
+ label .radio-circle {
53
56
  position: relative;
54
57
  width: 1.2rem;
55
58
  height: 1.2rem;
56
- border-radius: 0.6rem;
59
+ border-radius: 50%;
57
60
  border: 1px solid var(--form-input-border, black);
58
61
  background-color: var(--form-input-bg, white);
59
- color: var(--form-input-fg, black);
60
- font-size: 0.875rem;
61
- font-weight: 500;
62
- line-height: 1.25rem;
63
- transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out, fill 0.2s ease-in-out, stroke 0.2s ease-in-out;
62
+ transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
64
63
  user-select: none;
65
64
  display: flex;
66
65
  align-items: center;
67
66
  justify-content: center;
68
- padding-top: 0.1rem;
69
67
  }
70
- label .checkbox .checkmark {
71
- display: block;
72
- width: 0;
73
- height: 0;
74
- line-height: 100%;
75
- color: var(--form-input-selected-fg, white);
76
- fill: var(--form-input-selected-bg, #3182ce);
77
- stroke: var(--form-input-selected-fg, white);
78
- transition: width 0.2s ease-in-out, height 0.2s ease-in-out;
68
+ label .radio-circle .radio-dot {
69
+ display: none;
70
+ width: 0.5rem;
71
+ height: 0.5rem;
72
+ border-radius: 50%;
73
+ background-color: var(--form-input-selected-bg, #3182ce);
74
+ transition: opacity 0.2s ease-in-out;
79
75
  }
80
76
  label input {
81
77
  width: 0;
82
78
  height: 0;
83
79
  position: absolute;
84
80
  }
85
- label input:checked + .checkbox {
86
- background-color: var(--form-input-selected-bg, #3182ce);
81
+ label input:checked + .radio-circle {
82
+ border-color: var(--form-input-selected-bg, #3182ce);
87
83
  }
88
- label input:checked + .checkbox .checkmark {
89
- width: 100%;
90
- height: 100%;
84
+ label input:checked + .radio-circle .radio-dot {
85
+ display: block;
91
86
  }</style>
@@ -4,6 +4,7 @@ type $$ComponentProps = {
4
4
  value?: RadioValue;
5
5
  group?: string | undefined;
6
6
  disabled?: boolean;
7
+ ariaLabel?: string;
7
8
  children?: Snippet;
8
9
  onChange?: ((value: string) => void) | undefined;
9
10
  };
@@ -4,6 +4,8 @@
4
4
  import TableHeader from './table-header.svelte';
5
5
  import TableRow from './table-row.svelte';
6
6
  import Table from './table.svelte';
7
+ import TableSelectionCell from './table-selection-cell.svelte';
8
+ import TableSelectionHeaderCell from './table-selection-header-cell.svelte';
7
9
  import type { ColumnDef, JsonObject, PaginationProperties } from '../types/data.js';
8
10
  import Button from '../forms/button/button.svelte';
9
11
  import DropdownItem from '../generic/dropdown-item/dropdown-item.svelte';
@@ -21,10 +23,8 @@
21
23
  getCellTypeClass,
22
24
  sortRows
23
25
  } from './cell-renderers.js';
24
- import { getTableContext } from './table-context.svelte.js';
25
26
  import type { Snippet } from 'svelte';
26
27
  import { useVirtualList } from '../helpers/use-virtual-list.svelte.js';
27
- import { onMount } from 'svelte';
28
28
 
29
29
  type PaginationEvent = (pagination: PaginationProperties) => void;
30
30
 
@@ -48,12 +48,12 @@
48
48
  actions = undefined,
49
49
  stickyHeader = false,
50
50
  enableSorting = true,
51
- enableSelection = false,
52
- selectionMode = 'multi',
51
+ selectionMode = 'none',
53
52
  rowIdKey = 'id',
54
53
  onPageChange = null,
55
54
  onSort = undefined,
56
55
  onSelectionChange = undefined,
56
+ selectedCount = $bindable(0),
57
57
  children = undefined,
58
58
  virtualScroll = false,
59
59
  rowHeight = 48,
@@ -67,12 +67,12 @@
67
67
  actions?: Actions;
68
68
  stickyHeader?: boolean;
69
69
  enableSorting?: boolean;
70
- enableSelection?: boolean;
71
- selectionMode?: 'single' | 'multi';
70
+ selectionMode?: 'none' | 'single' | 'multi';
72
71
  rowIdKey?: string;
73
72
  onPageChange?: PaginationEvent | null;
74
73
  onSort?: (column: string, direction: 'asc' | 'desc') => void;
75
- onSelectionChange?: (selectedIds: Set<string | number>) => void;
74
+ onSelectionChange?: (selectedRows: JsonObject[]) => void;
75
+ selectedCount?: number;
76
76
  children?: Snippet;
77
77
  virtualScroll?: boolean;
78
78
  rowHeight?: number;
@@ -108,8 +108,19 @@
108
108
 
109
109
  // Computed values
110
110
  let hasActionCol = $derived(actions?.items && actions.items.length > 0);
111
+ let hasSelectionCol = $derived(selectionMode !== 'none');
111
112
  let visibleCols = $derived(cols.filter((col) => !col.hidden));
112
- let colCount = $derived(Math.max(1, visibleCols.length) + (hasActionCol ? 1 : 0));
113
+ let colCount = $derived(
114
+ Math.max(1, visibleCols.length) + (hasActionCol ? 1 : 0) + (hasSelectionCol ? 1 : 0)
115
+ );
116
+
117
+ // Track selected count from selection change callbacks
118
+ let internalSelectedCount = $state(0);
119
+
120
+ // Sync selectedCount with internal tracking
121
+ $effect(() => {
122
+ selectedCount = internalSelectedCount;
123
+ });
113
124
 
114
125
  // Manage sort state directly in DataGrid (not via context)
115
126
  let currentSortColumn = $state<string | null>(null);
@@ -181,13 +192,17 @@
181
192
  </script>
182
193
 
183
194
  <Table
195
+ rows={filteredRows ?? []}
184
196
  {stickyHeader}
185
197
  enableSorting={false}
186
- {enableSelection}
187
198
  {selectionMode}
188
199
  {rowIdKey}
189
200
  onSort={handleSortChange}
190
- {onSelectionChange}
201
+ onSelectionChange={(selectedRows) => {
202
+ onSelectionChange?.(selectedRows);
203
+ // Track selected count from the callback
204
+ internalSelectedCount = selectedRows.length;
205
+ }}
191
206
  >
192
207
  {#if children}
193
208
  <TableCaption side={captionSide} align={captionAlign}>{@render children?.()}</TableCaption>
@@ -195,6 +210,9 @@
195
210
 
196
211
  <TableHeader sticky={stickyHeader}>
197
212
  <tr>
213
+ {#if hasSelectionCol}
214
+ <TableSelectionHeaderCell />
215
+ {/if}
198
216
  {#each visibleCols as col}
199
217
  <TableHeaderCell
200
218
  type={col.type}
@@ -245,7 +263,10 @@
245
263
  <div
246
264
  style="position: absolute; top: {vItem.offsetTop}px; height: {vItem.height}px; width: 100%; display: table; table-layout: fixed;"
247
265
  >
248
- <TableRow {row} rowIndex={index} selectable={enableSelection}>
266
+ <TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
267
+ {#if hasSelectionCol}
268
+ <TableSelectionCell {row} rowIndex={index} />
269
+ {/if}
249
270
  {#each visibleCols as col}
250
271
  {@const cellValue = formatCell(row, col)}
251
272
  {@const cellLink = getCellLink(row, col)}
@@ -294,7 +315,10 @@
294
315
  {:else}
295
316
  <!-- Regular rendering mode -->
296
317
  {#each filteredRows as row, index}
297
- <TableRow {row} rowIndex={index} selectable={enableSelection}>
318
+ <TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
319
+ {#if hasSelectionCol}
320
+ <TableSelectionCell {row} rowIndex={index} />
321
+ {/if}
298
322
  {#each visibleCols as col}
299
323
  {@const cellValue = formatCell(row, col)}
300
324
  {@const cellLink = getCellLink(row, col)}
@@ -19,17 +19,17 @@ type $$ComponentProps = {
19
19
  actions?: Actions;
20
20
  stickyHeader?: boolean;
21
21
  enableSorting?: boolean;
22
- enableSelection?: boolean;
23
- selectionMode?: 'single' | 'multi';
22
+ selectionMode?: 'none' | 'single' | 'multi';
24
23
  rowIdKey?: string;
25
24
  onPageChange?: PaginationEvent | null;
26
25
  onSort?: (column: string, direction: 'asc' | 'desc') => void;
27
- onSelectionChange?: (selectedIds: Set<string | number>) => void;
26
+ onSelectionChange?: (selectedRows: JsonObject[]) => void;
27
+ selectedCount?: number;
28
28
  children?: Snippet;
29
29
  virtualScroll?: boolean;
30
30
  rowHeight?: number;
31
31
  maxHeight?: string;
32
32
  };
33
- declare const DataGrid: import("svelte").Component<$$ComponentProps, {}, "">;
33
+ declare const DataGrid: import("svelte").Component<$$ComponentProps, {}, "selectedCount">;
34
34
  type DataGrid = ReturnType<typeof DataGrid>;
35
35
  export default DataGrid;
@@ -1,27 +1,31 @@
1
1
  import type { JsonObject, SortDirection, SortState } from '../types/data.js';
2
2
  export interface TableContextConfig<T extends JsonObject = JsonObject> {
3
3
  enableSorting?: boolean;
4
- enableSelection?: boolean;
5
- selectionMode?: 'single' | 'multi';
4
+ selectionMode?: 'none' | 'single' | 'multi';
6
5
  rowIdKey?: keyof T & string;
7
6
  onSort?: (column: string, direction: SortDirection) => void;
8
- onSelectionChange?: (selectedIds: Set<string | number>) => void;
7
+ onSelectionChange?: (selectedRows: T[]) => void;
8
+ rows?: T[];
9
9
  }
10
10
  export declare class TableContext<T extends JsonObject = JsonObject> {
11
11
  sortColumn: string | null;
12
12
  sortDirection: SortDirection;
13
13
  selectedIds: Set<string | number>;
14
14
  lastSelectedIndex: number | null;
15
+ radioGroup: string | undefined;
16
+ selectedCount: number;
15
17
  config: TableContextConfig<T>;
16
18
  constructor(config?: TableContextConfig<T>);
17
19
  toggleSort(columnKey: string): void;
18
20
  setSortColumn(columnKey: string | null, direction?: SortDirection): void;
19
21
  clearSort(): void;
20
22
  getSortState(): SortState;
21
- toggleRow(id: string | number, index: number, shiftKey?: boolean): void;
23
+ toggleRow(id: string | number, index: number, shiftKey?: boolean, rows?: T[]): void;
24
+ private notifySelectionChange;
22
25
  selectRange(startIndex: number, endIndex: number, rows?: T[]): void;
23
26
  selectAll(rows: T[]): void;
24
- deselectAll(): void;
27
+ deselectAll(rows?: T[]): void;
28
+ setRadioSelection(value: string | undefined, rows?: T[]): void;
25
29
  isRowSelected(id: string | number): boolean;
26
30
  isAllSelected(rows: T[]): boolean;
27
31
  isSomeSelected(rows: T[]): boolean;
@@ -7,13 +7,16 @@ export class TableContext {
7
7
  // Selection state
8
8
  selectedIds = $state(new Set());
9
9
  lastSelectedIndex = $state(null);
10
+ // Radio button group state (for single selection mode)
11
+ radioGroup = $state(undefined);
12
+ // Reactive selected count
13
+ selectedCount = $derived(this.selectedIds.size);
10
14
  // Configuration
11
15
  config;
12
16
  constructor(config = {}) {
13
17
  this.config = {
14
18
  enableSorting: true,
15
- enableSelection: false,
16
- selectionMode: 'multi',
19
+ selectionMode: 'none',
17
20
  rowIdKey: 'id',
18
21
  ...config
19
22
  };
@@ -56,25 +59,31 @@ export class TableContext {
56
59
  };
57
60
  }
58
61
  // Selection methods
59
- toggleRow(id, index, shiftKey = false) {
60
- if (!this.config.enableSelection)
62
+ toggleRow(id, index, shiftKey = false, rows) {
63
+ if (this.config.selectionMode === 'none')
61
64
  return;
62
65
  if (this.config.selectionMode === 'single') {
63
66
  // Single selection mode
64
67
  if (this.selectedIds.has(id)) {
65
68
  this.selectedIds.delete(id);
69
+ this.radioGroup = undefined;
70
+ // Trigger reactivity
71
+ this.selectedIds = new Set(this.selectedIds);
66
72
  }
67
73
  else {
68
74
  this.selectedIds.clear();
69
75
  this.selectedIds.add(id);
76
+ this.radioGroup = String(id);
77
+ // Trigger reactivity
78
+ this.selectedIds = new Set(this.selectedIds);
70
79
  }
71
80
  this.lastSelectedIndex = index;
72
81
  }
73
82
  else {
74
83
  // Multi selection mode
75
- if (shiftKey && this.lastSelectedIndex !== null) {
84
+ if (shiftKey && this.lastSelectedIndex !== null && rows) {
76
85
  // Range selection
77
- this.selectRange(this.lastSelectedIndex, index);
86
+ this.selectRange(this.lastSelectedIndex, index, rows);
78
87
  }
79
88
  else {
80
89
  // Toggle single row
@@ -84,10 +93,18 @@ export class TableContext {
84
93
  else {
85
94
  this.selectedIds.add(id);
86
95
  }
96
+ // Trigger reactivity
97
+ this.selectedIds = new Set(this.selectedIds);
87
98
  this.lastSelectedIndex = index;
88
99
  }
89
100
  }
90
- this.config.onSelectionChange?.(new Set(this.selectedIds));
101
+ this.notifySelectionChange(rows);
102
+ }
103
+ notifySelectionChange(rows) {
104
+ if (this.config.onSelectionChange && rows) {
105
+ const selectedRows = this.getSelectedRows(rows);
106
+ this.config.onSelectionChange(selectedRows);
107
+ }
91
108
  }
92
109
  selectRange(startIndex, endIndex, rows) {
93
110
  if (!rows)
@@ -103,23 +120,52 @@ export class TableContext {
103
120
  }
104
121
  }
105
122
  }
106
- this.config.onSelectionChange?.(new Set(this.selectedIds));
123
+ // Trigger reactivity
124
+ this.selectedIds = new Set(this.selectedIds);
125
+ this.notifySelectionChange(rows);
107
126
  }
108
127
  selectAll(rows) {
109
- if (!this.config.enableSelection)
128
+ if (this.config.selectionMode === 'none')
110
129
  return;
130
+ // Clear first, then add all to ensure proper state update
131
+ this.selectedIds.clear();
111
132
  rows.forEach((row) => {
112
133
  const id = row[this.config.rowIdKey];
113
134
  if (id !== undefined) {
114
135
  this.selectedIds.add(id);
115
136
  }
116
137
  });
117
- this.config.onSelectionChange?.(new Set(this.selectedIds));
138
+ // Trigger reactivity by reassigning (Svelte 5 tracks Set mutations, but ensure it's reactive)
139
+ this.selectedIds = new Set(this.selectedIds);
140
+ this.notifySelectionChange(rows);
118
141
  }
119
- deselectAll() {
142
+ deselectAll(rows) {
120
143
  this.selectedIds.clear();
144
+ // Trigger reactivity by reassigning
145
+ this.selectedIds = new Set(this.selectedIds);
121
146
  this.lastSelectedIndex = null;
122
- this.config.onSelectionChange?.(new Set(this.selectedIds));
147
+ this.radioGroup = undefined;
148
+ this.notifySelectionChange(rows);
149
+ }
150
+ // Method to handle radio group changes (from radio button bindings)
151
+ setRadioSelection(value, rows) {
152
+ if (this.config.selectionMode !== 'single')
153
+ return;
154
+ if (value) {
155
+ this.selectedIds.clear();
156
+ this.selectedIds.add(value);
157
+ // Trigger reactivity
158
+ this.selectedIds = new Set(this.selectedIds);
159
+ this.radioGroup = value;
160
+ this.notifySelectionChange(rows);
161
+ }
162
+ else {
163
+ this.selectedIds.clear();
164
+ // Trigger reactivity
165
+ this.selectedIds = new Set(this.selectedIds);
166
+ this.radioGroup = undefined;
167
+ this.notifySelectionChange(rows);
168
+ }
123
169
  }
124
170
  isRowSelected(id) {
125
171
  return this.selectedIds.has(id);
@@ -140,6 +186,7 @@ export class TableContext {
140
186
  return id !== undefined && this.selectedIds.has(id);
141
187
  });
142
188
  }
189
+ // selectedCount is now a reactive derived state, but keep this method for backwards compatibility
143
190
  getSelectedCount() {
144
191
  return this.selectedIds.size;
145
192
  }
@@ -22,36 +22,11 @@
22
22
  row && context?.config.rowIdKey ? (row[context.config.rowIdKey] as string | number) : undefined
23
23
  );
24
24
 
25
- let isSelected = $derived(rowId !== undefined && context ? context.isRowSelected(rowId) : false);
26
-
27
- let canSelect = $derived(selectable && context?.config.enableSelection && rowId !== undefined);
28
-
29
- function handleClick(event: MouseEvent) {
30
- if (canSelect && rowId !== undefined && rowIndex !== undefined) {
31
- const shiftKey = event.shiftKey;
32
- context?.toggleRow(rowId, rowIndex, shiftKey);
33
- }
34
- }
35
-
36
- function handleKeyDown(event: KeyboardEvent) {
37
- if (canSelect && (event.key === 'Enter' || event.key === ' ')) {
38
- event.preventDefault();
39
- if (rowId !== undefined && rowIndex !== undefined) {
40
- context?.toggleRow(rowId, rowIndex, event.shiftKey);
41
- }
42
- }
43
- }
25
+ let selectionMode = $derived(context?.config.selectionMode ?? 'none');
26
+ let canSelect = $derived(selectable && selectionMode !== 'none' && rowId !== undefined);
44
27
  </script>
45
28
 
46
- <tr
47
- class:selectable={canSelect}
48
- class:selected={isSelected}
49
- aria-selected={canSelect ? isSelected : undefined}
50
- tabindex={canSelect ? 0 : undefined}
51
- onclick={handleClick}
52
- onkeydown={handleKeyDown}
53
- role={canSelect ? 'row' : undefined}
54
- >
29
+ <tr class:selectable={canSelect}>
55
30
  {@render children?.()}
56
31
  </tr>
57
32
 
@@ -69,8 +44,6 @@
69
44
  }
70
45
 
71
46
  tr.selectable {
72
- cursor: pointer;
73
-
74
47
  &:hover {
75
48
  background-color: var(--table-row-hover-bg, rgba(0, 0, 0, 0.05));
76
49
  }
@@ -80,13 +53,4 @@
80
53
  outline-offset: -2px;
81
54
  }
82
55
  }
83
-
84
- tr.selected {
85
- background-color: var(--table-row-selected-bg, rgba(0, 102, 204, 0.1)) !important;
86
- border-color: var(--table-row-selected-border, #0066cc);
87
- }
88
-
89
- tr.selected.selectable:hover {
90
- background-color: var(--table-row-selected-hover-bg, rgba(0, 102, 204, 0.15)) !important;
91
- }
92
56
  </style>
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import type { JsonObject } from '../types/data.js';
3
+ import { getTableContext } from './table-context.svelte.js';
4
+ import CheckBox from '../forms/check-box/check-box.svelte';
5
+ import RadioBox from '../forms/radio-group/radio-box.svelte';
6
+
7
+ let {
8
+ row = undefined,
9
+ rowIndex = undefined
10
+ }: {
11
+ row?: JsonObject;
12
+ rowIndex?: number;
13
+ } = $props();
14
+
15
+ const context = getTableContext();
16
+
17
+ if (!context) {
18
+ throw new Error('TableSelectionCell must be used within a Table component');
19
+ }
20
+
21
+ // Get row ID for selection
22
+ let rowId = $derived(
23
+ row && context?.config.rowIdKey ? (row[context.config.rowIdKey] as string | number) : undefined
24
+ );
25
+
26
+ // Derive selection state from context - this is the source of truth
27
+ // Access selectedIds directly to ensure reactivity is tracked properly
28
+ let isSelectedInContext = $derived.by(() => {
29
+ if (rowId === undefined || !context) return false;
30
+ // Explicitly access selectedIds to set up reactivity tracking
31
+ const selectedIds = context.selectedIds;
32
+ return selectedIds.has(rowId);
33
+ });
34
+
35
+ let selectionMode = $derived(context?.config.selectionMode ?? 'none');
36
+
37
+ // Local state for checkbox binding - synced from context
38
+ let localChecked = $state(false);
39
+
40
+ // Sync local state FROM context whenever context selection changes
41
+ // This ensures external changes (like "select all") update the checkbox
42
+ $effect(() => {
43
+ localChecked = isSelectedInContext;
44
+ });
45
+
46
+ // For radio buttons in single selection mode
47
+ let radioGroupLocal = $state<string | undefined>(undefined);
48
+
49
+ // Sync radio state FROM context
50
+ $effect(() => {
51
+ if (selectionMode === 'single' && context) {
52
+ radioGroupLocal = context.radioGroup;
53
+ }
54
+ });
55
+
56
+ // Handle checkbox change - update context (source of truth)
57
+ function handleCheckboxChange(data: { isChecked: boolean; value: string }) {
58
+ if (rowId !== undefined && rowIndex !== undefined && context) {
59
+ const rows = context.config.rows ?? [];
60
+ // Toggle the row in context - context is the source of truth
61
+ context.toggleRow(rowId, rowIndex, false, rows);
62
+ }
63
+ }
64
+
65
+ // Handle radio button change
66
+ function handleRadioChange(value: string) {
67
+ if (context && selectionMode === 'single') {
68
+ const rows = context.config.rows ?? [];
69
+ // Toggle: if already selected, deselect; otherwise select
70
+ if (context.radioGroup === value) {
71
+ context.setRadioSelection(undefined, rows);
72
+ } else {
73
+ context.setRadioSelection(value, rows);
74
+ }
75
+ }
76
+ }
77
+
78
+ // Handle cell click - toggle selection when clicking anywhere in the cell
79
+ function handleCellClick(event: MouseEvent) {
80
+ const target = event.target as HTMLElement;
81
+
82
+ if (selectionMode === 'multi') {
83
+ // For checkboxes: skip if clicking on input to avoid double-toggle
84
+ if (target.tagName === 'INPUT') return;
85
+ handleCheckboxChange({ isChecked: !localChecked, value: '' });
86
+ } else if (selectionMode === 'single' && rowId !== undefined) {
87
+ // For radios: always handle the click ourselves to enable deselection
88
+ // Native radio buttons don't allow unchecking, so we take control
89
+ if (target.tagName === 'INPUT') {
90
+ event.preventDefault();
91
+ }
92
+ handleRadioChange(String(rowId));
93
+ }
94
+ }
95
+ </script>
96
+
97
+ {#if selectionMode === 'multi'}
98
+ <td class="selection-cell" role="gridcell" onclick={handleCellClick}>
99
+ <CheckBox
100
+ bind:isChecked={localChecked}
101
+ onChange={handleCheckboxChange}
102
+ inline={true}
103
+ ariaLabel="Select row"
104
+ />
105
+ </td>
106
+ {:else if selectionMode === 'single'}
107
+ <td class="selection-cell" role="gridcell" onclick={handleCellClick}>
108
+ <RadioBox
109
+ value={String(rowId ?? '')}
110
+ bind:group={radioGroupLocal}
111
+ onChange={handleRadioChange}
112
+ ariaLabel="Select row"
113
+ />
114
+ </td>
115
+ {/if}
116
+
117
+ <style>.selection-cell {
118
+ text-align: center;
119
+ padding: 0.5rem 0.25rem;
120
+ width: 48px;
121
+ min-width: 48px;
122
+ vertical-align: middle;
123
+ cursor: pointer;
124
+ display: table-cell;
125
+ }
126
+
127
+ :global(.selection-cell .checkbox-label),
128
+ :global(.selection-cell label) {
129
+ margin: 0 auto;
130
+ cursor: pointer;
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ }</style>
@@ -0,0 +1,8 @@
1
+ import type { JsonObject } from '../types/data.js';
2
+ type $$ComponentProps = {
3
+ row?: JsonObject;
4
+ rowIndex?: number;
5
+ };
6
+ declare const TableSelectionCell: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type TableSelectionCell = ReturnType<typeof TableSelectionCell>;
8
+ export default TableSelectionCell;
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ import { getTableContext } from './table-context.svelte.js';
3
+ import CheckBox from '../forms/check-box/check-box.svelte';
4
+
5
+ const context = getTableContext();
6
+
7
+ if (!context) {
8
+ throw new Error('TableSelectionHeaderCell must be used within a Table component');
9
+ }
10
+
11
+ let selectionMode = $derived(context?.config.selectionMode ?? 'none');
12
+ let rows = $derived(context?.config.rows ?? []);
13
+
14
+ // Compute whether all rows are selected - source of truth from context
15
+ let isAllSelectedInContext = $derived.by(() => {
16
+ if (!context || !rows.length) return false;
17
+ // Access selectedIds directly to ensure reactivity is tracked
18
+ const selectedIds = context.selectedIds;
19
+ const rowIdKey = context.config.rowIdKey!;
20
+ // Check if every row's ID is in the selected set
21
+ return rows.every((row) => {
22
+ const id = row[rowIdKey] as string | number;
23
+ return id !== undefined && selectedIds.has(id);
24
+ });
25
+ });
26
+
27
+ // Local state for checkbox binding - synced from context
28
+ let localChecked = $state(false);
29
+
30
+ // Sync local state FROM context whenever selection changes
31
+ // This ensures the header checkbox reflects the actual selection state
32
+ $effect(() => {
33
+ localChecked = isAllSelectedInContext;
34
+ });
35
+
36
+ // Handle "select all" checkbox change
37
+ function handleSelectAllChange(data: { isChecked: boolean; value: string }) {
38
+ if (!context) return;
39
+ const currentRows = rows;
40
+ if (!currentRows || currentRows.length === 0) return;
41
+
42
+ // data.isChecked is the NEW desired state from user interaction
43
+ if (data.isChecked) {
44
+ // User wants to select all
45
+ context.selectAll(currentRows);
46
+ } else {
47
+ // User wants to deselect all
48
+ context.deselectAll(currentRows);
49
+ }
50
+ // The effect will sync localChecked from context after this
51
+ }
52
+
53
+ // Handle header cell click - toggle select all when clicking anywhere in the cell
54
+ function handleCellClick(event: MouseEvent) {
55
+ // Don't double-toggle if the click was on the actual input element
56
+ const target = event.target as HTMLElement;
57
+ if (target.tagName === 'INPUT') return;
58
+
59
+ handleSelectAllChange({ isChecked: !localChecked, value: '' });
60
+ }
61
+ </script>
62
+
63
+ {#if selectionMode === 'multi'}
64
+ <th
65
+ class="selection-header"
66
+ style="width: 48px; min-width: 48px; text-align: center;"
67
+ onclick={handleCellClick}
68
+ >
69
+ <div class="header-content">
70
+ <CheckBox
71
+ bind:isChecked={localChecked}
72
+ onChange={handleSelectAllChange}
73
+ inline={true}
74
+ ariaLabel="Select all rows"
75
+ />
76
+ </div>
77
+ </th>
78
+ {:else if selectionMode === 'single'}
79
+ <th class="selection-header" style="width: 48px; min-width: 48px; text-align: center;"></th>
80
+ {/if}
81
+
82
+ <style>.selection-header {
83
+ padding: 0.5rem 0.25rem !important;
84
+ vertical-align: middle;
85
+ text-align: center;
86
+ cursor: pointer;
87
+ }
88
+
89
+ .header-content {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ width: 100%;
94
+ height: 100%;
95
+ }
96
+
97
+ :global(.header-content .checkbox-label) {
98
+ margin: 0;
99
+ cursor: pointer;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ }</style>
@@ -0,0 +1,3 @@
1
+ declare const TableSelectionHeaderCell: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type TableSelectionHeaderCell = ReturnType<typeof TableSelectionHeaderCell>;
3
+ export default TableSelectionHeaderCell;
@@ -6,42 +6,48 @@
6
6
 
7
7
  let {
8
8
  children,
9
+ rows = [],
9
10
  enableSorting = true,
10
- enableSelection = false,
11
- selectionMode = 'multi',
11
+ selectionMode = 'none',
12
12
  rowIdKey = 'id',
13
13
  stickyHeader = false,
14
14
  onSort = undefined,
15
15
  onSelectionChange = undefined
16
16
  }: {
17
17
  children?: Snippet;
18
+ rows?: JsonObject[];
18
19
  enableSorting?: boolean;
19
- enableSelection?: boolean;
20
- selectionMode?: 'single' | 'multi';
20
+ selectionMode?: 'none' | 'single' | 'multi';
21
21
  rowIdKey?: string;
22
22
  stickyHeader?: boolean;
23
23
  onSort?: (column: string, direction: 'asc' | 'desc') => void;
24
- onSelectionChange?: (selectedIds: Set<string | number>) => void;
24
+ onSelectionChange?: (selectedRows: JsonObject[]) => void;
25
25
  } = $props();
26
26
 
27
27
  // Create table context for child components
28
28
  // Using untrack() to indicate we intentionally want non-reactive initial values
29
29
  const config: TableContextConfig<JsonObject> = {
30
30
  enableSorting: untrack(() => enableSorting),
31
- enableSelection: untrack(() => enableSelection),
32
31
  selectionMode: untrack(() => selectionMode),
33
32
  rowIdKey: untrack(() => rowIdKey),
34
33
  onSort: untrack(() => onSort),
35
- onSelectionChange: untrack(() => onSelectionChange)
34
+ onSelectionChange: untrack(() => onSelectionChange),
35
+ rows: untrack(() => rows)
36
36
  };
37
37
 
38
- createTableContext(config);
38
+ const context = createTableContext(config);
39
+
40
+ // Keep context.config.rows updated reactively when rows prop changes
41
+ // This is essential for selection features to work correctly
42
+ $effect(() => {
43
+ context.config.rows = rows;
44
+ });
39
45
  </script>
40
46
 
41
47
  <table
42
48
  class:sticky-header={stickyHeader}
43
49
  role="grid"
44
- aria-rowcount={enableSelection ? undefined : undefined}
50
+ aria-rowcount={undefined}
45
51
  aria-colcount={undefined}
46
52
  >
47
53
  {@render children?.()}
@@ -1,13 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { JsonObject } from '../types/data.js';
2
3
  type $$ComponentProps = {
3
4
  children?: Snippet;
5
+ rows?: JsonObject[];
4
6
  enableSorting?: boolean;
5
- enableSelection?: boolean;
6
- selectionMode?: 'single' | 'multi';
7
+ selectionMode?: 'none' | 'single' | 'multi';
7
8
  rowIdKey?: string;
8
9
  stickyHeader?: boolean;
9
10
  onSort?: (column: string, direction: 'asc' | 'desc') => void;
10
- onSelectionChange?: (selectedIds: Set<string | number>) => void;
11
+ onSelectionChange?: (selectedRows: JsonObject[]) => void;
11
12
  };
12
13
  declare const Table: import("svelte").Component<$$ComponentProps, {}, "">;
13
14
  type Table = ReturnType<typeof Table>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",