@ibobbyts/svelte-ui-utils 0.4.2 → 0.4.3

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.
package/README.md CHANGED
@@ -511,7 +511,12 @@ relational pseudo-class.
511
511
  The original flat `options` API remains available. New callers may opt into
512
512
  grouped options with `optionGroups`; each group can have an
513
513
  optional accessible label and its options keep the same keyboard and disabled
514
- semantics. `width`, `minWidth`, `maxWidth`, and `className` are optional styling
514
+ semantics. Set `groupsCollapsedByDefault` to `"true"` to initially collapse
515
+ all groups, or to `"auto"` to initially expand only groups containing a
516
+ selected option; `"false"` is the default. The initial state is evaluated each
517
+ time the menu opens, and selecting options while it remains open does not
518
+ change the current collapsed state. Group headings can always be toggled
519
+ manually. `width`, `minWidth`, `maxWidth`, and `className` are optional styling
515
520
  hooks. `onTriggerClick` runs before the dropdown toggles, so a nested control
516
521
  can stop event propagation without preventing the dropdown from opening.
517
522
 
@@ -21,6 +21,7 @@
21
21
  export let multiselect = false;
22
22
  export let options: DropdownOption[] = [];
23
23
  export let optionGroups: DropdownOptionGroup[] | undefined = undefined;
24
+ export let groupsCollapsedByDefault: 'true' | 'false' | 'auto' = 'false';
24
25
  export let ariaLabel: string | undefined = undefined;
25
26
  export let placement: DropdownPlacement = 'auto';
26
27
  export let menuAlign: DropdownMenuAlign = 'left';
@@ -56,11 +57,15 @@
56
57
  let typeaheadBuffer = '';
57
58
  let typeaheadTimer: ReturnType<typeof setTimeout> | undefined;
58
59
  let typeaheadGeneration = 0;
60
+ let collapsedGroupIndexes = new Set<number>();
59
61
  // Keep the buffer long enough for users to type multi-character aliases
60
62
  // while the menu is rendering a large option list.
61
63
  const typeaheadTimeoutMs = 1000;
62
64
 
63
65
  $: resolvedOptions = optionGroups === undefined ? options : optionGroups.flatMap((group) => group.options);
66
+ $: visibleOptions = optionGroups === undefined
67
+ ? options
68
+ : optionGroups.flatMap((group, groupIndex) => group.label && collapsedGroupIndexes.has(groupIndex) ? [] : group.options);
64
69
  $: selectedValues = normalizeSelectedValues(multiselect && Array.isArray(value) ? value : []);
65
70
  $: selectedOption = Array.isArray(value) ? undefined : resolvedOptions.find((option) => option.value === value);
66
71
  $: selectedText = multiselect
@@ -89,7 +94,7 @@
89
94
  }
90
95
 
91
96
  function firstEnabledOption(): DropdownOption | undefined {
92
- return resolvedOptions.find((option) => !option.disabled);
97
+ return visibleOptions.find((option) => !option.disabled);
93
98
  }
94
99
 
95
100
  function normalizeSelectedValues(nextValues: DropdownMultiValue): DropdownMultiValue {
@@ -104,7 +109,7 @@
104
109
  }
105
110
 
106
111
  function initialActiveValue(): DropdownValue {
107
- const firstSelectedEnabled = resolvedOptions.find(
112
+ const firstSelectedEnabled = visibleOptions.find(
108
113
  (option) => selectedValues.includes(option.value) && !option.disabled
109
114
  );
110
115
  return firstSelectedEnabled?.value ?? selectedOption?.value ?? firstEnabledOption()?.value ?? '';
@@ -115,21 +120,21 @@
115
120
  }
116
121
 
117
122
  function activeOptionIndex(nextActiveValue: DropdownValue): number {
118
- const index = resolvedOptions.findIndex((option) => option.value === nextActiveValue && !option.disabled);
123
+ const index = visibleOptions.findIndex((option) => option.value === nextActiveValue && !option.disabled);
119
124
  if (index >= 0) {
120
125
  return index;
121
126
  }
122
- const fallbackIndex = resolvedOptions.findIndex(
127
+ const fallbackIndex = visibleOptions.findIndex(
123
128
  (option) => isOptionSelected(option) && !option.disabled
124
129
  );
125
130
  if (fallbackIndex >= 0) {
126
131
  return fallbackIndex;
127
132
  }
128
- return resolvedOptions.findIndex((option) => !option.disabled);
133
+ return visibleOptions.findIndex((option) => !option.disabled);
129
134
  }
130
135
 
131
136
  function moveActiveOption(offset: number) {
132
- const enabledOptions = resolvedOptions.filter((option) => !option.disabled);
137
+ const enabledOptions = visibleOptions.filter((option) => !option.disabled);
133
138
  if (enabledOptions.length === 0) {
134
139
  return;
135
140
  }
@@ -180,7 +185,7 @@
180
185
  : key.toLocaleLowerCase();
181
186
  const nextBuffer = `${typeaheadBuffer}${normalizedKey}`;
182
187
  const findMatch = (prefix: string) =>
183
- resolvedOptions.find(
188
+ visibleOptions.find(
184
189
  (option) =>
185
190
  !option.disabled &&
186
191
  [option.searchText, option.label]
@@ -233,12 +238,42 @@
233
238
  }
234
239
  if (!open) {
235
240
  updateResolvedPlacement();
241
+ initializeCollapsedGroups();
236
242
  }
237
243
  clearTypeaheadBuffer();
238
244
  open = !open;
239
245
  activeValue = initialActiveValue();
240
246
  }
241
247
 
248
+ function isGroupCollapsed(groupIndex: number): boolean {
249
+ return collapsedGroupIndexes.has(groupIndex);
250
+ }
251
+
252
+ function initializeCollapsedGroups() {
253
+ if (optionGroups === undefined || groupsCollapsedByDefault === 'false') {
254
+ collapsedGroupIndexes = new Set();
255
+ return;
256
+ }
257
+ if (groupsCollapsedByDefault === 'true') {
258
+ collapsedGroupIndexes = new Set(
259
+ optionGroups.flatMap((group, index) => group.label ? [index] : [])
260
+ );
261
+ return;
262
+ }
263
+ collapsedGroupIndexes = new Set(
264
+ optionGroups.flatMap((group, index) =>
265
+ group.label && group.options.some((option) => isOptionSelected(option)) ? [] : group.label ? [index] : []
266
+ )
267
+ );
268
+ }
269
+
270
+ function toggleGroup(groupIndex: number) {
271
+ const next = new Set(collapsedGroupIndexes);
272
+ if (next.has(groupIndex)) next.delete(groupIndex);
273
+ else next.add(groupIndex);
274
+ collapsedGroupIndexes = next;
275
+ }
276
+
242
277
  function handleTriggerClick(event: MouseEvent) {
243
278
  onTriggerClick?.(event);
244
279
  toggleOpen();
@@ -284,7 +319,7 @@
284
319
  if ((event.key === 'Enter' || event.key === ' ') && open) {
285
320
  event.preventDefault();
286
321
  const index = activeOptionIndex(activeValue);
287
- const option = index >= 0 ? resolvedOptions[index] : undefined;
322
+ const option = index >= 0 ? visibleOptions[index] : undefined;
288
323
  if (option) {
289
324
  selectOption(option);
290
325
  }
@@ -558,12 +593,21 @@
558
593
  </button>
559
594
  {/each}
560
595
  {:else}
561
- {#each optionGroups as group}
596
+ {#each optionGroups as group, groupIndex}
562
597
  <div class="suu-dropdown__group" role="group" aria-label={group.label}>
563
598
  {#if group.label}
564
- <div class="suu-dropdown__group-label">{group.label}</div>
599
+ <button
600
+ type="button"
601
+ class="suu-dropdown__group-label"
602
+ aria-expanded={!collapsedGroupIndexes.has(groupIndex)}
603
+ on:click|stopPropagation={() => toggleGroup(groupIndex)}
604
+ >
605
+ <span>{group.label}</span>
606
+ <span class="suu-dropdown__group-chevron" aria-hidden="true"></span>
607
+ </button>
565
608
  {/if}
566
- {#each group.options as option, optionIndex}
609
+ {#if !group.label || !collapsedGroupIndexes.has(groupIndex)}
610
+ {#each group.options as option, optionIndex}
567
611
  <button
568
612
  type="button"
569
613
  class="suu-dropdown__option"
@@ -592,7 +636,8 @@
592
636
  {/if}
593
637
  <span class="suu-dropdown__label">{option.label}</span>
594
638
  </button>
595
- {/each}
639
+ {/each}
640
+ {/if}
596
641
  </div>
597
642
  {/each}
598
643
  {/if}
@@ -18,6 +18,7 @@ declare const Dropdown: $$__sveltets_2_IsomorphicComponent<{
18
18
  multiselect?: boolean;
19
19
  options?: DropdownOption[];
20
20
  optionGroups?: DropdownOptionGroup[] | undefined;
21
+ groupsCollapsedByDefault?: "true" | "false" | "auto";
21
22
  ariaLabel?: string | undefined;
22
23
  placement?: DropdownPlacement;
23
24
  menuAlign?: DropdownMenuAlign;
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAER,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AA+ftB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAVqb,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUz/B,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/Dropdown.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAER,iBAAiB,EAGjB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,8BAA8B,EAC9B,iBAAiB,EACjB,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AAuiBtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,QAAQ;SAV0e,MAAM,GAAG,SAAS;YAAU,iBAAiB;;cAA8C,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;WAAuG,MAAM,GAAG,SAAS;;YAAsC,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,8BAA8B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUpmC,CAAC;AAC5E,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;AAChD,eAAe,QAAQ,CAAC"}
@@ -16,6 +16,7 @@
16
16
  export let value: DropdownMultiValue = [];
17
17
  export let options: DropdownOption[] = [];
18
18
  export let optionGroups: DropdownOptionGroup[] | undefined = undefined;
19
+ export let groupsCollapsedByDefault: 'true' | 'false' | 'auto' = 'false';
19
20
  export let ariaLabel: string | undefined = undefined;
20
21
  export let placement: DropdownPlacement = 'auto';
21
22
  export let menuAlign: DropdownMenuAlign = 'left';
@@ -37,6 +38,7 @@
37
38
  multiselect={true}
38
39
  {options}
39
40
  {optionGroups}
41
+ {groupsCollapsedByDefault}
40
42
  {ariaLabel}
41
43
  {placement}
42
44
  {menuAlign}
@@ -17,6 +17,7 @@ declare const DropdownMultiSelect: $$__sveltets_2_IsomorphicComponent<{
17
17
  value?: DropdownMultiValue;
18
18
  options?: DropdownOption[];
19
19
  optionGroups?: DropdownOptionGroup[] | undefined;
20
+ groupsCollapsedByDefault?: "true" | "false" | "auto";
20
21
  ariaLabel?: string | undefined;
21
22
  placement?: DropdownPlacement;
22
23
  menuAlign?: DropdownMenuAlign;
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/DropdownMultiSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,YAAY,CAAC;AA+BtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,mBAAmB;SAV6W,MAAM,GAAG,SAAS;YAAU,kBAAkB;cAAY,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;YAAwG,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,0BAA0B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUr1B,CAAC;AACvF,KAAK,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"DropdownMultiSelect.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/dropdown/DropdownMultiSelect.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACR,iBAAiB,EACjB,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,YAAY,CAAC;AAgCtB,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IAC9G,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,mBAAmB;SAVka,MAAM,GAAG,SAAS;YAAU,kBAAkB;cAAY,cAAc,EAAE;mBAAiB,mBAAmB,EAAE,GAAG,SAAS;+BAA6B,MAAM,GAAG,OAAO,GAAG,MAAM;gBAAc,MAAM,GAAG,SAAS;gBAAc,iBAAiB;gBAAc,iBAAiB;;;;YAAwG,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;;eAAqC,0BAA0B,GAAG,SAAS;qBAAmB,2BAA2B,GAAG,SAAS;;;kBAUh8B,CAAC;AACvF,KAAK,mBAAmB,GAAG,YAAY,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,eAAe,mBAAmB,CAAC"}
package/dist/style.css CHANGED
@@ -1293,12 +1293,33 @@
1293
1293
  }
1294
1294
 
1295
1295
  .suu-dropdown__group-label {
1296
+ display: flex;
1297
+ width: 100%;
1298
+ border: 0;
1296
1299
  padding: 8px 14px 6px;
1297
1300
  background: var(--suu-color-surface);
1298
1301
  color: var(--suu-color-muted);
1302
+ cursor: pointer;
1299
1303
  font-size: 0.75em;
1300
1304
  font-weight: 700;
1301
1305
  letter-spacing: 0.02em;
1306
+ font: inherit;
1307
+ text-align: left;
1308
+ align-items: center;
1309
+ justify-content: space-between;
1310
+ }
1311
+
1312
+ .suu-dropdown__group-chevron {
1313
+ width: 7px;
1314
+ height: 7px;
1315
+ border-right: 2px solid currentColor;
1316
+ border-bottom: 2px solid currentColor;
1317
+ transform: rotate(225deg);
1318
+ transition: transform 120ms ease;
1319
+ }
1320
+
1321
+ .suu-dropdown__group-label[aria-expanded='true'] .suu-dropdown__group-chevron {
1322
+ transform: rotate(45deg);
1302
1323
  }
1303
1324
 
1304
1325
  .suu-dropdown__option {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibobbyts/svelte-ui-utils",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Reusable Svelte 5 UI utilities for dialogs, toast notifications, dropdown search, and data tables.",
5
5
  "type": "module",
6
6
  "license": "MIT",