@finsweet/webflow-apps-utils 1.0.55 → 1.0.57

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.
@@ -78,7 +78,10 @@ export const RawContentWithComment = {
78
78
  content: '<script>\n // Your custom code here\n console.log("Hello from script!");\n</script>',
79
79
  title: 'Script with Comment',
80
80
  raw: true,
81
- comment: 'Add this script to your site'
81
+ comment: 'Add this script to your site',
82
+ onCopy: (content) => {
83
+ console.log('Copied content:', content);
84
+ }
82
85
  }
83
86
  };
84
87
  export const MultilineContent = {
@@ -84,6 +84,18 @@ declare const meta: {
84
84
  control: string;
85
85
  description: string;
86
86
  };
87
+ onOpen: {
88
+ action: string;
89
+ description: string;
90
+ };
91
+ itemsDisabled: {
92
+ control: string;
93
+ description: string;
94
+ };
95
+ itemsDisabledMessage: {
96
+ control: string;
97
+ description: string;
98
+ };
87
99
  };
88
100
  };
89
101
  export default meta;
@@ -119,4 +131,7 @@ export declare const InvalidState: Story;
119
131
  export declare const InvalidWithAlert: Story;
120
132
  export declare const ValidationStates: Story;
121
133
  export declare const FormValidationExample: Story;
134
+ export declare const ItemsDisabled: Story;
135
+ export declare const ItemsDisabledWithSearch: Story;
136
+ export declare const AsyncRefreshOnOpen: Story;
122
137
  export declare const WithFooter: Story;
@@ -1,5 +1,6 @@
1
1
  import { CheckIcon, UndoIcon } from '../../icons';
2
2
  import Select from './Select.svelte';
3
+ import SelectItemsDisabledStory from './SelectItemsDisabledStory.svelte';
3
4
  import SelectWithFooterStory from './SelectWithFooterStory.svelte';
4
5
  // Mock options for stories
5
6
  const basicOptions = [
@@ -131,6 +132,18 @@ const meta = {
131
132
  closeOnClickOutside: {
132
133
  control: 'boolean',
133
134
  description: 'Whether the dropdown closes when clicking outside the component'
135
+ },
136
+ onOpen: {
137
+ action: 'onOpen',
138
+ description: 'Callback fired when the dropdown opens'
139
+ },
140
+ itemsDisabled: {
141
+ control: 'boolean',
142
+ description: 'Disables all dropdown items and search while the dropdown remains open'
143
+ },
144
+ itemsDisabledMessage: {
145
+ control: 'text',
146
+ description: 'Overlay message shown when itemsDisabled is true'
134
147
  }
135
148
  }
136
149
  };
@@ -619,6 +632,61 @@ export const FormValidationExample = {
619
632
  }
620
633
  }
621
634
  };
635
+ // Items disabled
636
+ export const ItemsDisabled = {
637
+ args: {
638
+ options: basicOptions,
639
+ itemsDisabled: true,
640
+ itemsDisabledMessage: 'Items are currently unavailable',
641
+ defaultText: 'Items disabled'
642
+ },
643
+ parameters: {
644
+ docs: {
645
+ description: {
646
+ story: 'Disables all dropdown items and search input. The dropdown still opens and closes normally, but no selection or filtering can occur.'
647
+ }
648
+ }
649
+ }
650
+ };
651
+ export const ItemsDisabledWithSearch = {
652
+ args: {
653
+ options: basicOptions,
654
+ itemsDisabled: true,
655
+ enableSearch: true,
656
+ defaultText: 'Search disabled too'
657
+ },
658
+ parameters: {
659
+ docs: {
660
+ description: {
661
+ story: 'When itemsDisabled is true, the search input is also disabled along with all items.'
662
+ }
663
+ }
664
+ }
665
+ };
666
+ // Async refresh on open example
667
+ export const AsyncRefreshOnOpen = {
668
+ render: () => ({
669
+ Component: SelectItemsDisabledStory,
670
+ props: {
671
+ options: basicOptions,
672
+ defaultText: 'Open to refresh',
673
+ dropdownWidth: '250px',
674
+ dropdownHeight: '200px',
675
+ enableSearch: true
676
+ }
677
+ }),
678
+ args: {
679
+ options: basicOptions,
680
+ defaultText: 'Open to refresh'
681
+ },
682
+ parameters: {
683
+ docs: {
684
+ description: {
685
+ story: 'Demonstrates the `onOpen` + `itemsDisabled` pattern: opening the dropdown triggers a simulated 2s async refresh. Items and search are disabled during the refresh, then re-enabled with fresh data.'
686
+ }
687
+ }
688
+ }
689
+ };
622
690
  // Footer snippet example
623
691
  const providerOptions = [
624
692
  { label: 'Facebook', value: 'facebook' },
@@ -44,6 +44,9 @@
44
44
  className = '',
45
45
  closeOnEscape = true,
46
46
  closeOnClickOutside = true,
47
+ onOpen,
48
+ itemsDisabled = false,
49
+ itemsDisabledMessage = '',
47
50
  onchange,
48
51
  children,
49
52
  footer
@@ -103,7 +106,6 @@
103
106
  event.preventDefault();
104
107
  event.stopPropagation();
105
108
  closeDropdown();
106
- // Remove focus to prevent focus ring after closing
107
109
  if (document.activeElement instanceof HTMLElement) {
108
110
  document.activeElement.blur();
109
111
  }
@@ -165,7 +167,7 @@
165
167
  * Handles the option selection.
166
168
  */
167
169
  const handleSelect = (value: string, label = defaultText, element: HTMLButtonElement) => {
168
- if (disabled) return;
170
+ if (disabled || itemsDisabled) return;
169
171
  updateActiveElement(element);
170
172
 
171
173
  if (selected === value && !preventNoSelection) {
@@ -257,18 +259,21 @@
257
259
  const handleKeyDown = (event: KeyboardEvent): void => {
258
260
  if (!isOpen || !dropdownItems) return;
259
261
 
260
- const items = Array.from(dropdownItems.querySelectorAll('.dropdown-item'));
262
+ const items = Array.from(dropdownItems.querySelectorAll('.dropdown-item:not(.items-disabled)'));
261
263
  let currentIndex = lastHoveredItem ? items.indexOf(lastHoveredItem) : -1;
262
264
  let newIndex = -1;
263
265
 
264
266
  switch (event.key) {
265
267
  case 'ArrowDown':
268
+ if (itemsDisabled) break;
266
269
  newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
267
270
  break;
268
271
  case 'ArrowUp':
272
+ if (itemsDisabled) break;
269
273
  newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
270
274
  break;
271
275
  case 'Enter': {
276
+ if (itemsDisabled) break;
272
277
  const selectedItem = items[currentIndex] as HTMLButtonElement;
273
278
  selectedItem.click();
274
279
  break;
@@ -387,35 +392,42 @@
387
392
  const showDropdown = () => {
388
393
  if (disabled) return;
389
394
 
395
+ // Show the dropdown FIRST so elements are focusable before calling focus()
390
396
  tooltip.setAttribute('aria-hidden', 'false');
397
+ tooltip.style.display = 'flex';
398
+ isOpen = true;
399
+ isFocused = true;
391
400
  update();
392
401
 
402
+ onOpen?.();
403
+
393
404
  const selectedItemButton = tooltip.querySelector(
394
405
  `.dropdown-item[aria-selected="true"]`
395
406
  ) as HTMLElement;
396
407
  const firstItemButton = tooltip.querySelector('.dropdown-item') as HTMLElement;
397
- const itemToFocus = selectedItemButton || firstItemButton;
398
408
 
399
409
  const searchInput = tooltip?.querySelector<HTMLInputElement>('input[type="text"]');
400
- if (searchInput && enableSearch) {
410
+ if (selectedItemButton) {
411
+ selectedItemButton.focus();
412
+ if (lastHoveredItem) {
413
+ lastHoveredItem.classList.remove('hover-state');
414
+ lastHoveredItem.setAttribute('tabindex', '-1');
415
+ }
416
+ lastHoveredItem = selectedItemButton;
417
+ lastHoveredItem.classList.add('hover-state');
418
+ lastHoveredItem.setAttribute('tabindex', '0');
419
+ } else if (searchInput && enableSearch) {
401
420
  searchInput.focus();
402
- } else {
403
- if (itemToFocus) {
404
- itemToFocus.focus();
405
-
406
- if (lastHoveredItem) {
407
- lastHoveredItem.classList.remove('hover-state');
408
- lastHoveredItem.setAttribute('tabindex', '-1');
409
- }
410
- lastHoveredItem = itemToFocus;
411
- lastHoveredItem.classList.add('hover-state');
412
- lastHoveredItem.setAttribute('tabindex', '0');
421
+ } else if (firstItemButton) {
422
+ firstItemButton.focus();
423
+ if (lastHoveredItem) {
424
+ lastHoveredItem.classList.remove('hover-state');
425
+ lastHoveredItem.setAttribute('tabindex', '-1');
413
426
  }
427
+ lastHoveredItem = firstItemButton;
428
+ lastHoveredItem.classList.add('hover-state');
429
+ lastHoveredItem.setAttribute('tabindex', '0');
414
430
  }
415
-
416
- tooltip.style.display = 'flex';
417
- isOpen = true;
418
- isFocused = true;
419
431
  };
420
432
 
421
433
  const options: EventOption[] = [['click', showDropdown]].filter(Boolean) as EventOption[];
@@ -532,8 +544,20 @@
532
544
  <div
533
545
  class="dropdown-items-scroll"
534
546
  style="max-height:{dropdownHeight};"
547
+ class:disabled={itemsDisabled}
535
548
  onmouseleave={clearHoverState}
536
549
  >
550
+ {#if itemsDisabled && itemsDisabledMessage}
551
+ <div class="items-disabled-overlay" role="status">
552
+ <Text
553
+ loading
554
+ class="items-disabled-text"
555
+ label={itemsDisabledMessage}
556
+ fontSize="normal"
557
+ fontColor="var(--text2)"
558
+ />
559
+ </div>
560
+ {/if}
537
561
  {#if selectedLabel}
538
562
  <div class="selected">
539
563
  <div class="label">
@@ -547,6 +571,7 @@
547
571
  <input
548
572
  type="text"
549
573
  placeholder="Search"
574
+ disabled={itemsDisabled}
550
575
  oninput={(e) => {
551
576
  e.stopPropagation();
552
577
  e.preventDefault();
@@ -565,16 +590,22 @@
565
590
  aria-selected={value === selected && selected?.trim() !== '' ? 'true' : 'false'}
566
591
  id={`${itemId}-list-${indexId}-${id}`}
567
592
  data-value={value}
568
- class="dropdown-item {isDisabled ? 'disabled' : ''} {className}"
593
+ class="dropdown-item {isDisabled || itemsDisabled ? 'disabled' : ''} {itemsDisabled
594
+ ? 'items-disabled'
595
+ : ''} {className}"
569
596
  role="option"
597
+ aria-disabled={isDisabled || itemsDisabled}
570
598
  onclick={(e) => {
571
599
  e.stopPropagation();
572
- if (isDisabled) return;
600
+ if (isDisabled || itemsDisabled) return;
573
601
  handleSelect(value, label, e.currentTarget);
574
602
  }}
575
603
  onkeydown={(e) => {
576
604
  e.stopPropagation();
577
605
  e.preventDefault();
606
+ if (e.key === 'Escape' && closeOnEscape) {
607
+ closeDropdown();
608
+ }
578
609
  }}
579
610
  onmouseenter={handleMouseEnter}
580
611
  aria-hidden={!isOpen}
@@ -649,10 +680,6 @@
649
680
  padding: 0;
650
681
  }
651
682
 
652
- .dropdown-item.disabled {
653
- opacity: 0.75;
654
- cursor: not-allowed;
655
- }
656
683
  .label .label-description-title,
657
684
  .label .label-description {
658
685
  height: max-content;
@@ -742,7 +769,7 @@
742
769
  width: 100%;
743
770
  }
744
771
 
745
- .dropdown-list :global(.dropdown-item.hover-state) {
772
+ .dropdown-list :global(.dropdown-item.hover-state:not(.disabled):not(.items-disabled)) {
746
773
  background: var(--background5);
747
774
  }
748
775
 
@@ -766,6 +793,7 @@
766
793
  }
767
794
 
768
795
  .dropdown-items-scroll {
796
+ position: relative;
769
797
  display: flex;
770
798
  flex-direction: column;
771
799
  align-items: flex-start;
@@ -774,6 +802,18 @@
774
802
  overflow-y: auto;
775
803
  }
776
804
 
805
+ .items-disabled-overlay {
806
+ position: absolute;
807
+ inset: 0;
808
+ display: flex;
809
+ align-items: center;
810
+ justify-content: center;
811
+ background: rgba(0, 0, 0, 0.55);
812
+ z-index: 1;
813
+ padding: 12px;
814
+ text-align: center;
815
+ }
816
+
777
817
  .dropdown-footer {
778
818
  display: flex;
779
819
  align-items: center;
@@ -854,4 +894,9 @@
854
894
  width: 10px;
855
895
  height: 10px;
856
896
  }
897
+
898
+ .dropdown-item.disabled {
899
+ opacity: 0.75;
900
+ cursor: not-allowed;
901
+ }
857
902
  </style>
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ import Select from './Select.svelte';
3
+ import type { SelectOption } from './types.js';
4
+
5
+ interface Props {
6
+ options: SelectOption[];
7
+ defaultText?: string;
8
+ dropdownWidth?: string;
9
+ dropdownHeight?: string;
10
+ enableSearch?: boolean;
11
+ }
12
+
13
+ let {
14
+ options: initialOptions,
15
+ defaultText = 'Select',
16
+ dropdownWidth = '200px',
17
+ dropdownHeight = '200px',
18
+ enableSearch = false
19
+ }: Props = $props();
20
+
21
+ let itemsDisabled = $state(false);
22
+ let currentOptions = $state<SelectOption[]>(initialOptions);
23
+ let selected = $state<string | null>(null);
24
+ let statusText = $state('Idle');
25
+
26
+ const handleOpen = () => {
27
+ itemsDisabled = true;
28
+ statusText = 'Refreshing items...';
29
+
30
+ // Simulate an async refresh (e.g. fetching updated options from an API)
31
+ setTimeout(() => {
32
+ currentOptions = [
33
+ { label: 'Refreshed Option A', value: 'refreshed-a' },
34
+ { label: 'Refreshed Option B', value: 'refreshed-b' },
35
+ { label: 'Refreshed Option C', value: 'refreshed-c' },
36
+ { label: 'Refreshed Option D', value: 'refreshed-d' }
37
+ ];
38
+ itemsDisabled = false;
39
+ statusText = 'Items refreshed!';
40
+ }, 4000);
41
+ };
42
+ </script>
43
+
44
+ <div class="story-container">
45
+ <div class="status">Status: {statusText}</div>
46
+ <Select
47
+ options={currentOptions}
48
+ {defaultText}
49
+ {dropdownWidth}
50
+ {dropdownHeight}
51
+ {enableSearch}
52
+ {itemsDisabled}
53
+ itemsDisabledMessage="Refreshing items..."
54
+ onOpen={handleOpen}
55
+ bind:selected
56
+ />
57
+ <div class="info">
58
+ Open the dropdown to trigger a simulated 2s refresh. Items will be disabled during the refresh.
59
+ </div>
60
+ </div>
61
+
62
+ <style>
63
+ .story-container {
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: 12px;
67
+ align-items: flex-start;
68
+ }
69
+
70
+ .status {
71
+ font-size: 12px;
72
+ color: var(--text2);
73
+ padding: 4px 8px;
74
+ border-radius: 4px;
75
+ background: var(--background3);
76
+ }
77
+
78
+ .info {
79
+ font-size: 11px;
80
+ color: var(--text2);
81
+ max-width: 250px;
82
+ line-height: 1.4;
83
+ }
84
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { SelectOption } from './types.js';
2
+ interface Props {
3
+ options: SelectOption[];
4
+ defaultText?: string;
5
+ dropdownWidth?: string;
6
+ dropdownHeight?: string;
7
+ enableSearch?: boolean;
8
+ }
9
+ declare const SelectItemsDisabledStory: import("svelte").Component<Props, {}, "">;
10
+ type SelectItemsDisabledStory = ReturnType<typeof SelectItemsDisabledStory>;
11
+ export default SelectItemsDisabledStory;
@@ -64,6 +64,20 @@ export interface SelectProps {
64
64
  */
65
65
  invalid?: boolean;
66
66
  className?: string;
67
+ /**
68
+ * Callback fired when the dropdown opens
69
+ */
70
+ onOpen?: () => void;
71
+ /**
72
+ * When true, disables all dropdown items and search input.
73
+ * Useful for showing a loading/refreshing state while items are being updated.
74
+ */
75
+ itemsDisabled?: boolean;
76
+ /**
77
+ * Message displayed as an overlay when itemsDisabled is true.
78
+ * Helps communicate to users why items are unavailable (e.g. "Refreshing...").
79
+ */
80
+ itemsDisabledMessage?: string;
67
81
  onchange?: SelectChangeHandler;
68
82
  children?: Snippet;
69
83
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {
@@ -49,7 +49,7 @@
49
49
  "@storybook/addon-vitest": "9.1.8",
50
50
  "@storybook/sveltekit": "9.1.8",
51
51
  "@sveltejs/adapter-auto": "6.1.0",
52
- "@sveltejs/kit": "2.43.2",
52
+ "@sveltejs/kit": "2.53.1",
53
53
  "@sveltejs/package": "2.5.4",
54
54
  "@sveltejs/vite-plugin-svelte": "5.1.1",
55
55
  "@testing-library/jest-dom": "6.8.0",
@@ -75,11 +75,11 @@
75
75
  "prettier-plugin-svelte": "3.4.0",
76
76
  "publint": "0.3.13",
77
77
  "storybook": "9.1.8",
78
- "svelte": "5.39.5",
78
+ "svelte": "5.53.3",
79
79
  "svelte-check": "4.3.2",
80
80
  "typescript": "5.9.2",
81
81
  "typescript-eslint": "8.44.1",
82
- "vite": "7.1.7",
82
+ "vite": "7.1.11",
83
83
  "vitest": "3.2.4"
84
84
  },
85
85
  "keywords": [
@@ -97,7 +97,6 @@
97
97
  "luxon": "3.7.2",
98
98
  "motion": "10.18.0",
99
99
  "svelte-routing": "2.13.0",
100
- "swiper": "11.2.10",
101
100
  "terser": "5.44.0",
102
101
  "uuid": "11.1.0",
103
102
  "zod": "3.25.76"