@finsweet/webflow-apps-utils 1.0.53 → 1.0.55

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.
@@ -76,6 +76,14 @@ declare const meta: {
76
76
  control: string;
77
77
  description: string;
78
78
  };
79
+ closeOnEscape: {
80
+ control: string;
81
+ description: string;
82
+ };
83
+ closeOnClickOutside: {
84
+ control: string;
85
+ description: string;
86
+ };
79
87
  };
80
88
  };
81
89
  export default meta;
@@ -90,6 +98,9 @@ export declare const CustomDimensions: Story;
90
98
  export declare const CompactSelect: Story;
91
99
  export declare const WideSelect: Story;
92
100
  export declare const PreventDeselection: Story;
101
+ export declare const DisableEscapeClose: Story;
102
+ export declare const DisableClickOutsideClose: Story;
103
+ export declare const DisableAllCloseBehaviors: Story;
93
104
  export declare const MixedOptions: Story;
94
105
  export declare const TopPlacement: Story;
95
106
  export declare const LeftPlacement: Story;
@@ -123,6 +123,14 @@ const meta = {
123
123
  invalid: {
124
124
  control: 'boolean',
125
125
  description: 'Whether the select is in an invalid state'
126
+ },
127
+ closeOnEscape: {
128
+ control: 'boolean',
129
+ description: 'Whether the dropdown closes when pressing the Escape key'
130
+ },
131
+ closeOnClickOutside: {
132
+ control: 'boolean',
133
+ description: 'Whether the dropdown closes when clicking outside the component'
126
134
  }
127
135
  }
128
136
  };
@@ -241,6 +249,49 @@ export const PreventDeselection = {
241
249
  }
242
250
  }
243
251
  };
252
+ export const DisableEscapeClose = {
253
+ args: {
254
+ options: basicOptions,
255
+ defaultText: 'Press Escape (disabled)',
256
+ closeOnEscape: false
257
+ },
258
+ parameters: {
259
+ docs: {
260
+ description: {
261
+ story: 'Prevents the dropdown from closing when the Escape key is pressed. Users must select an option or click the button again to close.'
262
+ }
263
+ }
264
+ }
265
+ };
266
+ export const DisableClickOutsideClose = {
267
+ args: {
268
+ options: basicOptions,
269
+ defaultText: 'Click outside (disabled)',
270
+ closeOnClickOutside: false
271
+ },
272
+ parameters: {
273
+ docs: {
274
+ description: {
275
+ story: 'Prevents the dropdown from closing when clicking outside the component. Users must select an option or click the button again to close.'
276
+ }
277
+ }
278
+ }
279
+ };
280
+ export const DisableAllCloseBehaviors = {
281
+ args: {
282
+ options: basicOptions,
283
+ defaultText: 'Must select to close',
284
+ closeOnEscape: false,
285
+ closeOnClickOutside: false
286
+ },
287
+ parameters: {
288
+ docs: {
289
+ description: {
290
+ story: 'Disables both Escape key and click outside behaviors. The dropdown can only be closed by selecting an option or clicking the select button. Useful for modal-like contexts where you want to force a selection.'
291
+ }
292
+ }
293
+ }
294
+ };
244
295
  export const MixedOptions = {
245
296
  args: {
246
297
  options: mixedOptions,
@@ -42,6 +42,8 @@
42
42
  alert = null,
43
43
  invalid = false,
44
44
  className = '',
45
+ closeOnEscape = true,
46
+ closeOnClickOutside = true,
45
47
  onchange,
46
48
  children,
47
49
  footer
@@ -94,6 +96,29 @@
94
96
  }
95
97
  });
96
98
 
99
+ // Handle global Escape key when dropdown is open
100
+ $effect(() => {
101
+ const handleGlobalKeyDown = (event: KeyboardEvent) => {
102
+ if (event.key === 'Escape' && closeOnEscape && isOpen) {
103
+ event.preventDefault();
104
+ event.stopPropagation();
105
+ closeDropdown();
106
+ // Remove focus to prevent focus ring after closing
107
+ if (document.activeElement instanceof HTMLElement) {
108
+ document.activeElement.blur();
109
+ }
110
+ }
111
+ };
112
+
113
+ if (isOpen) {
114
+ document?.addEventListener('keydown', handleGlobalKeyDown);
115
+ }
116
+
117
+ return () => {
118
+ document?.removeEventListener('keydown', handleGlobalKeyDown);
119
+ };
120
+ });
121
+
97
122
  // Computed states
98
123
  let hasAlert = $derived(alert?.message);
99
124
 
@@ -159,6 +184,8 @@
159
184
  * Dismiss dropdown when clicking outside of it.
160
185
  */
161
186
  const dismissTooltip = (event: Event): void => {
187
+ if (!closeOnClickOutside) return;
188
+
162
189
  const isClickInside = dropdownWrapper?.contains(event.target as Node);
163
190
 
164
191
  if (!isClickInside) {
@@ -247,7 +274,13 @@
247
274
  break;
248
275
  }
249
276
  case 'Escape':
250
- closeDropdown();
277
+ if (closeOnEscape) {
278
+ closeDropdown();
279
+ // Remove focus to prevent focus ring after closing
280
+ if (document.activeElement instanceof HTMLElement) {
281
+ document.activeElement.blur();
282
+ }
283
+ }
251
284
  break;
252
285
  }
253
286
 
@@ -301,7 +334,9 @@
301
334
 
302
335
  if (!dropdownItems || !target) return instances;
303
336
 
304
- document?.addEventListener('click', dismissTooltip);
337
+ if (closeOnClickOutside) {
338
+ document?.addEventListener('click', dismissTooltip, true);
339
+ }
305
340
  instances.push(setupDropdown(target, dropdownItems));
306
341
 
307
342
  return instances;
@@ -312,6 +347,9 @@
312
347
  */
313
348
  const cleanupDropdownInstances = (instances: DropdownInstance[]) => {
314
349
  instances.forEach((instance) => instance.cleanup());
350
+ if (closeOnClickOutside) {
351
+ document?.removeEventListener('click', dismissTooltip, true);
352
+ }
315
353
  };
316
354
 
317
355
  /**
@@ -440,10 +478,13 @@
440
478
  </script>
441
479
 
442
480
  {#snippet selectWrapper()}
481
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
482
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
443
483
  <div
444
484
  class="dropdown-wrapper {className}"
445
485
  bind:this={dropdownWrapper}
446
486
  style="{hide ? 'display:none;' : ''} width: {width};"
487
+ onclick={(e) => e.stopPropagation()}
447
488
  >
448
489
  <div
449
490
  class="dropdown"
@@ -472,12 +513,14 @@
472
513
  </div>
473
514
  </div>
474
515
 
516
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
475
517
  <div
476
518
  tabindex={disabled || isOpen ? -1 : 0}
477
519
  class="dropdown-list"
478
520
  class:has-footer={footer}
479
521
  role="listbox"
480
522
  style="width:{dropdownWidth};"
523
+ onclick={(e) => e.stopPropagation()}
481
524
  onkeydown={(e) => {
482
525
  e.stopPropagation();
483
526
  e.preventDefault();
@@ -679,6 +722,10 @@
679
722
  display: flex;
680
723
  align-items: center;
681
724
  gap: 4px;
725
+ outline: none; /* Remove default focus outline since we have custom focus styling */
726
+ }
727
+ .dropdown:focus-visible {
728
+ outline: none; /* Prevent browser's default focus ring */
682
729
  }
683
730
  .dropdown.disabled {
684
731
  cursor: not-allowed !important;
@@ -712,6 +759,10 @@
712
759
  background: var(--background3);
713
760
  box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.15);
714
761
  z-index: 99999;
762
+ outline: none; /* Remove focus outline */
763
+ }
764
+ .dropdown-list:focus-visible {
765
+ outline: none; /* Prevent browser's default focus ring */
715
766
  }
716
767
 
717
768
  .dropdown-items-scroll {
@@ -778,6 +829,10 @@
778
829
  font-weight: 400;
779
830
  line-height: 16px;
780
831
  border: 1px solid transparent;
832
+ outline: none; /* Remove focus outline */
833
+ }
834
+ .dropdown-item:focus-visible {
835
+ outline: none; /* Prevent browser's default focus ring */
781
836
  }
782
837
  .dropdown-item .icon,
783
838
  .dropdown-list .selected .icon {
@@ -0,0 +1,18 @@
1
+ import SelectInModalStory from './SelectInModalStory.svelte';
2
+ const meta = {
3
+ title: 'Ui/Select/InModal',
4
+ component: SelectInModalStory,
5
+ parameters: {
6
+ layout: 'centered',
7
+ docs: {
8
+ description: {
9
+ component: 'Testing Modal with Select component to ensure proper event handling'
10
+ }
11
+ }
12
+ },
13
+ tags: ['autodocs']
14
+ };
15
+ export default meta;
16
+ export const Default = {
17
+ args: {}
18
+ };
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import { Button } from '../button';
3
+ import Modal from '../modal/Modal.svelte';
4
+ import Select from './Select.svelte';
5
+ import type { SelectOption } from './types';
6
+
7
+ interface Props {
8
+ initialOpen?: boolean;
9
+ }
10
+
11
+ let { initialOpen = false }: Props = $props();
12
+
13
+ let modalOpen = $state(initialOpen);
14
+ let selectedValue = $state<string | null>(null);
15
+
16
+ const options: SelectOption[] = [
17
+ { label: 'Option 1', value: 'option1' },
18
+ { label: 'Option 2', value: 'option2' },
19
+ { label: 'Option 3', value: 'option3' },
20
+ { label: 'Option 4', value: 'option4' },
21
+ { label: 'Option 5', value: 'option5' }
22
+ ];
23
+
24
+ const handleOpenModal = () => {
25
+ modalOpen = true;
26
+ };
27
+
28
+ const handleCloseModal = () => {
29
+ console.log('Modal closing');
30
+ modalOpen = false;
31
+ };
32
+ </script>
33
+
34
+ <div style="padding: 20px;">
35
+ <Button onclick={handleOpenModal}>Open Modal with Select</Button>
36
+
37
+ <Modal
38
+ open={modalOpen}
39
+ onOpenChange={(open) => {
40
+ console.log('Modal onOpenChange:', open);
41
+ modalOpen = open;
42
+ }}
43
+ title="Select Example Inside Modal"
44
+ showFooter={false}
45
+ width="400px"
46
+ closeOnOverlayClick={false}
47
+ closeOnEscape={false}
48
+ >
49
+ <div style="padding: 20px; display: flex; flex-direction: column; gap: 16px;">
50
+ <p style="color: var(--text1); margin: 0;">Test the following behaviors:</p>
51
+ <ul style="color: var(--text2); margin: 0; padding-left: 20px;">
52
+ <li>Click outside the modal (overlay) - modal stays open (disabled)</li>
53
+ <li>Press Escape with select closed - modal stays open (disabled)</li>
54
+ <li>Press Escape with select open - should close select only</li>
55
+ <li>Click outside select but inside modal - should close select only</li>
56
+ <li>Use "Close Modal" button to close the modal</li>
57
+ </ul>
58
+
59
+ <Select
60
+ {options}
61
+ bind:selected={selectedValue}
62
+ defaultText="Choose an option"
63
+ width="100%"
64
+ dropdownWidth="100%"
65
+ />
66
+
67
+ {#if selectedValue}
68
+ <p style="color: var(--text1); margin: 0;">
69
+ Selected: {selectedValue}
70
+ </p>
71
+ {/if}
72
+
73
+ <Button onclick={handleCloseModal} variant="secondary">Close Modal</Button>
74
+ </div>
75
+ </Modal>
76
+ </div>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ initialOpen?: boolean;
3
+ }
4
+ declare const SelectInModalStory: import("svelte").Component<Props, {}, "">;
5
+ type SelectInModalStory = ReturnType<typeof SelectInModalStory>;
6
+ export default SelectInModalStory;
@@ -49,6 +49,16 @@ export interface SelectProps {
49
49
  * Alert configuration for showing validation messages
50
50
  */
51
51
  alert?: AlertConfig | null;
52
+ /**
53
+ * If true, the dropdown will close when the Escape key is pressed
54
+ * @default true
55
+ */
56
+ closeOnEscape?: boolean;
57
+ /**
58
+ * If true, the dropdown will close when clicking outside the component
59
+ * @default true
60
+ */
61
+ closeOnClickOutside?: boolean;
52
62
  /**
53
63
  * If true, the select will be invalid
54
64
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finsweet/webflow-apps-utils",
3
- "version": "1.0.53",
3
+ "version": "1.0.55",
4
4
  "description": "Shared utilities for Webflow apps",
5
5
  "homepage": "https://github.com/finsweet/webflow-apps-utils",
6
6
  "repository": {