@juspay/svelte-ui-components 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,41 +1,63 @@
1
1
  <script lang="ts">
2
2
  import type { TableProperties } from './properties';
3
+ import type { JSONValue } from 'type-decoder';
4
+ import Button from '../Button/Button.svelte';
5
+ import chevronUpSvg from '../assets/chevron-up.svg?raw';
6
+ import chevronDownSvg from '../assets/chevron-down.svg?raw';
7
+ import sortDefaultSvg from '../assets/sort-default.svg?raw';
3
8
 
4
9
  let {
5
10
  tableTitle = '',
6
11
  tableHeaders = [],
7
12
  tableData = [],
13
+ sortable = true,
14
+ sortableColumns,
15
+ stickyHeader = false,
8
16
  isTableScrollable = false,
9
17
  isContentScrollable = false,
18
+ testId,
19
+ caption,
20
+ sortAscIcon,
21
+ sortDescIcon,
22
+ sortDefaultIcon,
23
+ cell,
24
+ empty,
25
+ onRowClick,
26
+ onSort,
10
27
  classes
11
28
  }: TableProperties = $props();
12
29
 
13
- let sortOrders = $state<{ [key: string]: 'asc' | 'desc' }>({});
30
+ let sortColumn = $state<number | null>(null);
31
+ let sortDirection = $state<'asc' | 'desc'>('asc');
14
32
 
15
- let sortedTableData = $derived.by(() => {
16
- const columns = Object.keys(sortOrders);
17
- if (columns.length === 0) {
18
- return [...tableData];
33
+ function isColumnSortable(colIndex: number): boolean {
34
+ if (!sortable) {
35
+ return false;
36
+ }
37
+ if (sortableColumns) {
38
+ return sortableColumns.includes(colIndex);
19
39
  }
40
+ return true;
41
+ }
20
42
 
21
- // Sort by the last clicked column
22
- const column = columns.at(-1);
23
- if (!column) {
43
+ let sortedTableData = $derived.by(() => {
44
+ if (sortColumn === null) {
24
45
  return [...tableData];
25
46
  }
26
- const order = sortOrders[column];
47
+
48
+ const colIndex = sortColumn;
49
+ const direction = sortDirection;
27
50
 
28
51
  return [...tableData].sort((a, b) => {
29
- const colIndex = tableHeaders.indexOf(column);
30
52
  const valueA = a[colIndex];
31
53
  const valueB = b[colIndex];
32
54
 
33
55
  if (typeof valueA === 'number' && typeof valueB === 'number') {
34
- return order === 'asc' ? valueA - valueB : valueB - valueA;
56
+ return direction === 'asc' ? valueA - valueB : valueB - valueA;
35
57
  } else if (typeof valueA === 'string' && typeof valueB === 'string') {
36
- return order === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
58
+ return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
37
59
  } else if (typeof valueA === 'boolean' && typeof valueB === 'boolean') {
38
- return order === 'asc'
60
+ return direction === 'asc'
39
61
  ? valueA === valueB
40
62
  ? 0
41
63
  : valueA
@@ -46,26 +68,38 @@
46
68
  : valueA
47
69
  ? 1
48
70
  : -1;
49
- } else {
50
- return 0;
51
71
  }
72
+ return 0;
52
73
  });
53
74
  });
54
75
 
55
- function sortTableData(column: string) {
56
- if (typeof sortOrders[column] === 'undefined') {
57
- sortOrders[column] = 'asc';
76
+ function handleSort(colIndex: number) {
77
+ if (!isColumnSortable(colIndex)) {
78
+ return;
79
+ }
80
+
81
+ if (sortColumn === colIndex) {
82
+ sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
58
83
  } else {
59
- sortOrders[column] = sortOrders[column] === 'asc' ? 'desc' : 'asc';
84
+ sortColumn = colIndex;
85
+ sortDirection = 'asc';
60
86
  }
87
+ onSort?.(colIndex, sortDirection);
61
88
  }
62
89
 
63
- function handleKeydown(event: KeyboardEvent, header: string) {
90
+ function handleRowClick(rowIndex: number, rowData: JSONValue[]) {
91
+ onRowClick?.(rowIndex, rowData);
92
+ }
93
+
94
+ function handleRowKeydown(event: KeyboardEvent, rowIndex: number, rowData: JSONValue[]) {
64
95
  if (event.key === 'Enter' || event.key === ' ') {
65
96
  event.preventDefault();
66
- sortTableData(header);
97
+ onRowClick?.(rowIndex, rowData);
67
98
  }
68
99
  }
100
+
101
+ let isRowClickable = $derived(typeof onRowClick === 'function');
102
+ let isStickyHeader = $derived(stickyHeader || isTableScrollable);
69
103
  </script>
70
104
 
71
105
  {#if typeof tableTitle === 'string' && tableTitle.length > 0}
@@ -75,48 +109,86 @@
75
109
  {/if}
76
110
  {#if tableHeaders.length !== 0 || tableData.length !== 0}
77
111
  <div
78
- class="table-container {isTableScrollable ? 'scrollable-table' : ' '} {classes ?? ''}"
79
- role="grid"
112
+ class="table-container {isTableScrollable ? 'scrollable-table' : ''} {classes ?? ''}"
113
+ data-pw={testId}
80
114
  >
81
115
  <table>
116
+ {#if caption}
117
+ <caption class="sr-only">{caption}</caption>
118
+ {/if}
82
119
  <thead>
83
120
  <tr>
84
- {#each tableHeaders as header (header)}
85
- <th class="table-header {isTableScrollable ? 'table-header-sticky' : ' '}">
86
- {header}
87
- {#if sortOrders[header] === 'asc'}
88
- <span
89
- class="sort-arrow"
90
- onclick={() => sortTableData(header)}
91
- onkeydown={(e) => handleKeydown(e, header)}
92
- role="button"
93
- tabindex="0">▼</span
94
- >
95
- {:else}
96
- <span
97
- class="sort-arrow"
98
- onclick={() => sortTableData(header)}
99
- onkeydown={(e) => handleKeydown(e, header)}
100
- role="button"
101
- tabindex="0">▲</span
102
- >
103
- {/if}
121
+ {#each tableHeaders as header, colIndex (colIndex)}
122
+ <th class="table-header" class:table-header-sticky={isStickyHeader}>
123
+ <span class="table-header-content">
124
+ {header}
125
+ {#if isColumnSortable(colIndex)}
126
+ <div class="sort-button">
127
+ <Button onclick={() => handleSort(colIndex)} ariaLabel="Sort by {header}">
128
+ {#if sortColumn === colIndex && sortDirection === 'asc'}
129
+ {#if typeof sortAscIcon === 'function'}
130
+ {@render sortAscIcon()}
131
+ {:else}
132
+ <span class="sort-icon">
133
+ <!-- eslint-disable svelte/no-at-html-tags -->
134
+ {@html chevronUpSvg}
135
+ </span>
136
+ {/if}
137
+ {:else if sortColumn === colIndex && sortDirection === 'desc'}
138
+ {#if typeof sortDescIcon === 'function'}
139
+ {@render sortDescIcon()}
140
+ {:else}
141
+ <span class="sort-icon">
142
+ <!-- eslint-disable svelte/no-at-html-tags -->
143
+ {@html chevronDownSvg}
144
+ </span>
145
+ {/if}
146
+ {:else if typeof sortDefaultIcon === 'function'}
147
+ {@render sortDefaultIcon()}
148
+ {:else}
149
+ <span class="sort-icon sort-icon-idle">
150
+ <!-- eslint-disable svelte/no-at-html-tags -->
151
+ {@html sortDefaultSvg}
152
+ </span>
153
+ {/if}
154
+ </Button>
155
+ </div>
156
+ {/if}
157
+ </span>
104
158
  </th>
105
159
  {/each}
106
160
  </tr>
107
161
  </thead>
108
162
  <tbody>
109
- {#each sortedTableData as row, rowIndex (rowIndex)}
163
+ {#if sortedTableData.length === 0 && typeof empty === 'function'}
110
164
  <tr>
111
- {#each row as cell, cellIndex (cellIndex)}
112
- <td class="table-content">
113
- <div class={isContentScrollable ? 'scrollable-content' : ' '}>
114
- {cell}
115
- </div>
116
- </td>
117
- {/each}
165
+ <td class="table-empty" colspan={tableHeaders.length}>
166
+ {@render empty()}
167
+ </td>
118
168
  </tr>
119
- {/each}
169
+ {:else}
170
+ {#each sortedTableData as row, rowIndex (rowIndex)}
171
+ <tr
172
+ class="table-row"
173
+ class:table-row-clickable={isRowClickable}
174
+ onclick={isRowClickable ? () => handleRowClick(rowIndex, row) : null}
175
+ onkeydown={isRowClickable ? (e) => handleRowKeydown(e, rowIndex, row) : null}
176
+ tabindex={isRowClickable ? 0 : null}
177
+ >
178
+ {#each row as cellValue, colIndex (colIndex)}
179
+ <td class="table-content">
180
+ <div class={isContentScrollable ? 'scrollable-content' : ''}>
181
+ {#if typeof cell === 'function'}
182
+ {@render cell(cellValue, rowIndex, colIndex)}
183
+ {:else}
184
+ {cellValue}
185
+ {/if}
186
+ </div>
187
+ </td>
188
+ {/each}
189
+ </tr>
190
+ {/each}
191
+ {/if}
120
192
  </tbody>
121
193
  </table>
122
194
  </div>
@@ -124,57 +196,160 @@
124
196
 
125
197
  <style>
126
198
  .table-title {
127
- margin: var(--table-title-margin, 0px 0px 10px 0px);
128
- font-size: var(--table-tile-font-size, 25px);
199
+ margin: var(--table-title-margin, 0px 0px 12px 0px);
200
+ font-size: var(--table-title-font-size, var(--table-tile-font-size, 18px));
201
+ font-weight: var(--table-title-font-weight, 600);
202
+ color: var(--table-title-color, #111827);
129
203
  font-family: var(--table-title-font-family);
130
204
  padding: var(--table-title-padding);
131
205
  }
206
+
132
207
  .table-container {
133
- border-top: var(--table-border, 0.5px solid #ccc);
134
- width: var(--table-container-width, 400px);
208
+ border: var(--table-border, 1px solid #e5e7eb);
209
+ border-radius: var(--table-border-radius, 8px);
210
+ width: var(--table-container-width, 100%);
211
+ overflow: hidden;
135
212
  }
136
213
 
137
214
  .scrollable-table {
138
215
  height: var(--table-container-height, 143px);
139
216
  overflow-y: auto;
140
217
  }
218
+
141
219
  table {
142
- width: var(--table-width, 400px);
220
+ width: var(--table-width, 100%);
143
221
  border-collapse: var(--table-border-collapse, collapse);
144
222
  }
223
+
145
224
  .table-header,
146
225
  .table-content {
147
- border: var(--table-inner-border, 1px solid #ccc);
148
- padding: var(--table-padding, 8px);
226
+ border: var(--table-inner-border, none);
227
+ padding: var(--table-padding, 12px 16px);
149
228
  text-align: var(--table-text-align, left);
150
- width: var(--table-column-width, 100px);
229
+ width: var(--table-column-width);
151
230
  word-break: break-all;
152
231
  }
232
+
153
233
  .scrollable-content {
154
234
  overflow-y: auto;
155
235
  height: var(--scrollable-column-height, 20px);
156
236
  }
157
237
 
158
238
  .table-header {
159
- background-color: var(--table-header-border-bgcolor, beige);
160
- font-size: var(--table-header-font-size);
239
+ background-color: var(--table-header-background, var(--table-header-border-bgcolor, #f9fafb));
240
+ font-size: var(--table-header-font-size, 13px);
161
241
  font-family: var(--table-header-font-family);
162
- color: var(--table-header-font-color);
242
+ font-weight: var(--table-header-font-weight, 600);
243
+ letter-spacing: var(--table-header-letter-spacing, 0.02em);
244
+ text-transform: var(--table-header-text-transform);
245
+ color: var(--table-header-color, var(--table-header-font-color, #6b7280));
246
+ }
247
+
248
+ .table-header-content {
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 4px;
163
252
  }
164
253
 
165
254
  .table-header-sticky {
166
255
  position: sticky;
167
- top: -1px;
256
+ top: var(--table-header-sticky-top, 0);
257
+ z-index: 1;
168
258
  }
169
259
 
170
260
  .table-content {
171
- background-color: var(--table-content-border-bgcolor);
172
- font-size: var(--table-content-font-size);
261
+ background-color: var(--table-content-background, var(--table-content-border-bgcolor));
262
+ font-size: var(--table-content-font-size, 14px);
173
263
  font-family: var(--table-content-font-family);
174
- color: var(--table-content-font-color);
264
+ color: var(--table-content-color, var(--table-content-font-color, #111827));
265
+ }
266
+
267
+ .table-row {
268
+ border-bottom: var(--table-row-border, 1px solid #f3f4f6);
269
+ background-color: var(--table-row-background);
270
+ }
271
+
272
+ .table-row:last-child {
273
+ border-bottom: var(--table-row-last-border, none);
274
+ }
275
+
276
+ .table-row:nth-child(even) {
277
+ background-color: var(--table-row-alt-background, var(--table-row-background));
278
+ }
279
+
280
+ .table-row:hover {
281
+ background-color: var(--table-row-hover-background);
282
+ }
283
+
284
+ .table-row:hover > .table-content {
285
+ background-color: var(
286
+ --table-row-hover-background,
287
+ var(--table-content-background, var(--table-content-border-bgcolor))
288
+ );
289
+ }
290
+
291
+ .table-row-clickable {
292
+ cursor: pointer;
293
+ }
294
+
295
+ .table-row-clickable:focus-visible {
296
+ outline: 2px solid var(--table-focus-outline-color, #3b82f6);
297
+ outline-offset: -2px;
298
+ }
299
+
300
+ .sort-button {
301
+ --button-color: transparent;
302
+ --button-border: none;
303
+ --button-padding: 2px;
304
+ --button-margin: 0;
305
+ --button-text-color: var(--table-sort-button-color, inherit);
306
+ --button-border-radius: 4px;
307
+ --button-width: fit-content;
308
+ --button-height: fit-content;
309
+ --button-hover-color: var(--table-sort-button-hover-background, rgba(0, 0, 0, 0.05));
310
+ --button-hover-text-color: var(
311
+ --table-sort-button-hover-color,
312
+ var(--table-sort-button-color, inherit)
313
+ );
314
+ display: inline-flex;
315
+ align-items: center;
316
+ line-height: 1;
317
+ }
318
+
319
+ .sort-button :global(.sort-icon) {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ }
323
+
324
+ .sort-button :global(.sort-icon svg) {
325
+ width: var(--table-sort-icon-size, 14px);
326
+ height: var(--table-sort-icon-size, 14px);
327
+ display: block;
328
+ }
329
+
330
+ .sort-button :global(.sort-icon-idle) {
331
+ opacity: var(--table-sort-idle-opacity, 0.3);
332
+ }
333
+
334
+ .sort-button:hover :global(.sort-icon-idle) {
335
+ opacity: 0.6;
336
+ }
337
+
338
+ .table-empty {
339
+ padding: var(--table-empty-padding, 32px 24px);
340
+ text-align: center;
341
+ color: var(--table-empty-color, #9ca3af);
175
342
  }
176
343
 
177
- .sort-arrow {
178
- font-size: 10px;
344
+ .sr-only {
345
+ position: absolute;
346
+ width: 1px;
347
+ height: 1px;
348
+ padding: 0;
349
+ overflow: hidden;
350
+ clip: rect(0, 0, 0, 0);
351
+ clip-path: inset(50%);
352
+ white-space: nowrap;
353
+ border-width: 0;
179
354
  }
180
355
  </style>
@@ -1,9 +1,26 @@
1
1
  import type { JSONValue } from 'type-decoder';
2
- export type TableProperties = {
2
+ import type { Snippet } from 'svelte';
3
+ export type SortDirection = 'asc' | 'desc';
4
+ export type TableProperties = OptionalTableProperties & TableEventProperties;
5
+ export type OptionalTableProperties = {
3
6
  tableTitle?: string | null;
4
7
  tableHeaders?: string[];
5
8
  tableData?: Array<JSONValue[]>;
9
+ sortable?: boolean;
10
+ sortableColumns?: number[];
11
+ stickyHeader?: boolean;
6
12
  isTableScrollable?: boolean;
7
13
  isContentScrollable?: boolean;
14
+ testId?: string;
15
+ caption?: string;
16
+ sortAscIcon?: Snippet;
17
+ sortDescIcon?: Snippet;
18
+ sortDefaultIcon?: Snippet;
19
+ cell?: Snippet<[JSONValue, number, number]>;
20
+ empty?: Snippet;
8
21
  classes?: string;
9
22
  };
23
+ export type TableEventProperties = {
24
+ onRowClick?: (rowIndex: number, rowData: JSONValue[]) => void;
25
+ onSort?: (columnIndex: number, direction: SortDirection) => void;
26
+ };
@@ -0,0 +1,4 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <polyline points="18 15 12 9 6 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
3
+ <polyline points="6 17 12 23 18 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/>
4
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",