@juspay/svelte-ui-components 2.76.0 → 2.77.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.
@@ -132,7 +132,7 @@
132
132
  --input-focus-border: none;
133
133
  --input-box-shadow: none;
134
134
  --input-margin: none;
135
- --input-width: fit-content;
135
+ --input-width: 100%;
136
136
  font-size: var(--input-font-size, 16px) !important;
137
137
  font-weight: var(--input-button-font-weight, 500);
138
138
  margin: var(--input-button-margin);
@@ -15,6 +15,8 @@
15
15
  disabled = false,
16
16
  bottomContent,
17
17
  optionIndicator,
18
+ showSelectAll = false,
19
+ selectAllLabel = 'Select all',
18
20
  triggerSummary,
19
21
  testId,
20
22
  itemTestId,
@@ -53,6 +55,31 @@
53
55
  : items
54
56
  );
55
57
 
58
+ type SelectRow = { kind: 'select-all' } | { kind: 'item'; item: SelectItem };
59
+
60
+ let selectAllVisible: boolean = $derived(multiple && showSelectAll && filteredItems.length > 0);
61
+
62
+ let selectedFilteredCount: number = $derived(
63
+ filteredItems.filter((item) => value.includes(item.id)).length
64
+ );
65
+ let allFilteredSelected: boolean = $derived(
66
+ filteredItems.length > 0 && selectedFilteredCount === filteredItems.length
67
+ );
68
+ let selectAllIndeterminate: boolean = $derived(
69
+ selectedFilteredCount > 0 && selectedFilteredCount < filteredItems.length
70
+ );
71
+
72
+ // Unified, keyboard-navigable row list: the optional "select all" row shares the same
73
+ // highlightedIndex space as the options, so arrow-key navigation needs no special casing.
74
+ let optionRows: SelectRow[] = $derived(
75
+ selectAllVisible
76
+ ? [
77
+ { kind: 'select-all' },
78
+ ...filteredItems.map((item): SelectRow => ({ kind: 'item', item }))
79
+ ]
80
+ : filteredItems.map((item): SelectRow => ({ kind: 'item', item }))
81
+ );
82
+
56
83
  let displayText = $derived.by(() => {
57
84
  const firstId = value.at(0);
58
85
  if (typeof firstId !== 'string') {
@@ -111,19 +138,36 @@
111
138
  onchange?.(value);
112
139
  }
113
140
 
141
+ function toggleSelectAll(): void {
142
+ if (disabled) {
143
+ return;
144
+ }
145
+ if (allFilteredSelected) {
146
+ value = value.filter((id) => !filteredItems.some((item) => item.id === id));
147
+ } else {
148
+ value = [...new Set([...value, ...filteredItems.map((item) => item.id)])];
149
+ }
150
+ onchange?.(value);
151
+ }
152
+
114
153
  function selectHighlighted(): void {
115
- if (highlightedIndex < 0 || highlightedIndex >= filteredItems.length) {
154
+ if (highlightedIndex < 0 || highlightedIndex >= optionRows.length) {
116
155
  return;
117
156
  }
118
- const item = filteredItems.at(highlightedIndex);
119
- if (typeof item === 'object' && item !== null) {
120
- selectItem(item.id);
157
+ const row = optionRows.at(highlightedIndex);
158
+ if (typeof row !== 'object' || row === null) {
159
+ return;
160
+ }
161
+ if (row.kind === 'select-all') {
162
+ toggleSelectAll();
163
+ } else {
164
+ selectItem(row.item.id);
121
165
  }
122
166
  }
123
167
 
124
168
  async function moveHighlight(delta: number): Promise<void> {
125
169
  const next = highlightedIndex + delta;
126
- if (next < 0 || next >= filteredItems.length) {
170
+ if (next < 0 || next >= optionRows.length) {
127
171
  return;
128
172
  }
129
173
  highlightedIndex = next;
@@ -355,44 +399,88 @@
355
399
  {#if filteredItems.length === 0}
356
400
  <div class="select-empty">No results</div>
357
401
  {:else}
358
- {#each filteredItems as item, index (item.id)}
359
- <div
360
- class="select-option"
361
- class:multi={multiple}
362
- class:selected={value.includes(item.id)}
363
- class:highlighted={index === highlightedIndex}
364
- role="option"
365
- id={`${listboxId}-option-${index}`}
366
- aria-selected={value.includes(item.id)}
367
- tabindex="-1"
368
- {...typeof item.testId === 'string'
369
- ? { 'data-pw': item.testId }
370
- : typeof itemTestId === 'string'
371
- ? { 'data-pw': `${itemTestId}-${item.id}` }
372
- : typeof testId === 'string'
373
- ? { 'data-pw': `${testId}-${item.id}` }
374
- : {}}
375
- onclick={() => selectItem(item.id)}
376
- onmouseenter={() => (highlightedIndex = index)}
377
- >
378
- {#if multiple}
402
+ {#each optionRows as row, index (row.kind === 'select-all' ? 'select-all' : row.item.id)}
403
+ {#if row.kind === 'select-all'}
404
+ <div
405
+ class="select-option select-all"
406
+ class:multi={multiple}
407
+ class:selected={allFilteredSelected}
408
+ class:highlighted={index === highlightedIndex}
409
+ role="option"
410
+ id={`${listboxId}-option-${index}`}
411
+ aria-selected={allFilteredSelected}
412
+ aria-label={selectAllIndeterminate
413
+ ? `${selectAllLabel}, ${selectedFilteredCount} of ${filteredItems.length} selected`
414
+ : selectAllLabel}
415
+ tabindex="-1"
416
+ {...typeof testId === 'string' ? { 'data-pw': `${testId}-select-all` } : {}}
417
+ onclick={toggleSelectAll}
418
+ onmouseenter={() => (highlightedIndex = index)}
419
+ >
379
420
  {#if typeof optionIndicator === 'function'}
380
- {@render optionIndicator({ checked: value.includes(item.id) })}
421
+ {@render optionIndicator({
422
+ checked: allFilteredSelected,
423
+ indeterminate: selectAllIndeterminate
424
+ })}
381
425
  {:else}
382
426
  <span
383
427
  class="select-option-indicator"
384
- class:checked={value.includes(item.id)}
428
+ class:checked={allFilteredSelected}
429
+ class:indeterminate={selectAllIndeterminate}
385
430
  aria-hidden="true"
386
431
  >
387
- {#if value.includes(item.id)}
432
+ {#if allFilteredSelected}
388
433
  <!-- eslint-disable-next-line svelte/no-at-html-tags -->
389
434
  <span class="select-option-check">{@html checkmarkSvg}</span>
435
+ {:else if selectAllIndeterminate}
436
+ <span class="select-option-dash"></span>
390
437
  {/if}
391
438
  </span>
392
439
  {/if}
393
- {/if}
394
- {item.label}
395
- </div>
440
+ {selectAllLabel}
441
+ </div>
442
+ {:else}
443
+ <div
444
+ class="select-option"
445
+ class:multi={multiple}
446
+ class:selected={value.includes(row.item.id)}
447
+ class:highlighted={index === highlightedIndex}
448
+ role="option"
449
+ id={`${listboxId}-option-${index}`}
450
+ aria-selected={value.includes(row.item.id)}
451
+ tabindex="-1"
452
+ {...typeof row.item.testId === 'string'
453
+ ? { 'data-pw': row.item.testId }
454
+ : typeof itemTestId === 'string'
455
+ ? { 'data-pw': `${itemTestId}-${row.item.id}` }
456
+ : typeof testId === 'string'
457
+ ? { 'data-pw': `${testId}-${row.item.id}` }
458
+ : {}}
459
+ onclick={() => selectItem(row.item.id)}
460
+ onmouseenter={() => (highlightedIndex = index)}
461
+ >
462
+ {#if multiple}
463
+ {#if typeof optionIndicator === 'function'}
464
+ {@render optionIndicator({
465
+ checked: value.includes(row.item.id),
466
+ indeterminate: false
467
+ })}
468
+ {:else}
469
+ <span
470
+ class="select-option-indicator"
471
+ class:checked={value.includes(row.item.id)}
472
+ aria-hidden="true"
473
+ >
474
+ {#if value.includes(row.item.id)}
475
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
476
+ <span class="select-option-check">{@html checkmarkSvg}</span>
477
+ {/if}
478
+ </span>
479
+ {/if}
480
+ {/if}
481
+ {row.item.label}
482
+ </div>
483
+ {/if}
396
484
  {/each}
397
485
  {/if}
398
486
  {#if typeof bottomContent === 'function'}
@@ -598,6 +686,23 @@
598
686
  height: 100%;
599
687
  }
600
688
 
689
+ .select-option-indicator.indeterminate {
690
+ background-color: var(--select-option-indicator-checked-background, #2196f3);
691
+ border-color: var(--select-option-indicator-checked-border-color, #2196f3);
692
+ }
693
+
694
+ .select-option-dash {
695
+ width: var(--select-option-indicator-dash-size, 10px);
696
+ height: var(--select-option-indicator-dash-thickness, 2px);
697
+ border-radius: 1px;
698
+ background-color: var(--select-option-indicator-dash-color, #ffffff);
699
+ }
700
+
701
+ .select-option.select-all {
702
+ border-bottom: var(--select-all-border, none);
703
+ font-weight: var(--select-all-font-weight, inherit);
704
+ }
705
+
601
706
  .select-empty {
602
707
  padding: var(--select-empty-padding, 8px 12px);
603
708
  color: var(--select-empty-color, #999999);
@@ -18,16 +18,28 @@ export type OptionalSelectProperties = {
18
18
  disabled?: boolean;
19
19
  bottomContent?: Snippet;
20
20
  /**
21
- * Snippet for rendering a custom multi-select option indicator, receiving `{ checked }`.
22
- * When omitted, multiple-mode options render a design-system checkbox box (bordered square
23
- * that fills and shows a checkmark when selected), themeable via the `--select-option-indicator-*`
24
- * CSS variables: `-size` (18px), `-border`, `-border-radius`, `-background`,
25
- * `-checked-background`, `-checked-border-color`, `-check-size`, `-check-color`. Provide this
26
- * snippet to fully replace the indicator (e.g. to restore the legacy ☑/☐ glyph).
21
+ * Snippet for rendering a custom multi-select option indicator, receiving `{ checked, indeterminate }`.
22
+ * `indeterminate` is `true` only for the `showSelectAll` row when some — but not all — listed options
23
+ * are selected; it is always `false` for individual option rows. When omitted, multiple-mode options
24
+ * render a design-system checkbox box (bordered
25
+ * square that fills and shows a checkmark when selected, or a centred dash when indeterminate),
26
+ * themeable via the `--select-option-indicator-*` CSS variables: `-size` (18px), `-border`,
27
+ * `-border-radius`, `-background`, `-checked-background`, `-checked-border-color`, `-check-size`,
28
+ * `-check-color`, `-dash-size`, `-dash-thickness`, `-dash-color`. Provide this snippet to fully
29
+ * replace the indicator (e.g. to restore the legacy ☑/☐ glyph).
27
30
  */
28
31
  optionIndicator?: Snippet<[{
29
32
  checked: boolean;
33
+ indeterminate?: boolean;
30
34
  }]>;
35
+ /**
36
+ * Multiple mode only. When `true`, renders a "Select all" row at the top of the dropdown that
37
+ * toggles every currently-listed (search-filtered) option. The row shows an indeterminate
38
+ * indicator when only some of the listed options are selected. No effect outside `multiple` mode.
39
+ */
40
+ showSelectAll?: boolean;
41
+ /** Label for the `showSelectAll` row. Defaults to `'Select all'`. */
42
+ selectAllLabel?: string;
31
43
  testId?: string;
32
44
  /** Fallback per-option test id prefix. Each option emits `data-pw="{itemTestId}-{id}"` when its own `item.testId` is not set. */
33
45
  itemTestId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.76.0",
3
+ "version": "2.77.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",