@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.
- package/dist/ui/components/select/Select.stories.d.ts +11 -0
- package/dist/ui/components/select/Select.stories.js +51 -0
- package/dist/ui/components/select/Select.svelte +57 -2
- package/dist/ui/components/select/SelectInModal.stories.js +18 -0
- package/dist/ui/components/select/SelectInModalStory.svelte +76 -0
- package/dist/ui/components/select/SelectInModalStory.svelte.d.ts +6 -0
- package/dist/ui/components/select/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
*/
|