@finsweet/webflow-apps-utils 1.0.52 → 1.0.54
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 +51 -2
- package/dist/ui/components/select/types.d.ts +10 -0
- package/dist/ui/components/tags/TagsInput.stories.d.ts +11 -0
- package/dist/ui/components/tags/TagsInput.stories.js +55 -0
- package/dist/ui/components/tags/TagsInput.svelte +91 -3
- package/dist/ui/components/tags/types.d.ts +12 -0
- package/dist/ui/providers/GlobalProvider.svelte +0 -4
- 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,28 @@
|
|
|
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
|
+
closeDropdown();
|
|
105
|
+
// Remove focus to prevent focus ring after closing
|
|
106
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
107
|
+
document.activeElement.blur();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (isOpen) {
|
|
113
|
+
document?.addEventListener('keydown', handleGlobalKeyDown);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
document?.removeEventListener('keydown', handleGlobalKeyDown);
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
97
121
|
// Computed states
|
|
98
122
|
let hasAlert = $derived(alert?.message);
|
|
99
123
|
|
|
@@ -159,6 +183,8 @@
|
|
|
159
183
|
* Dismiss dropdown when clicking outside of it.
|
|
160
184
|
*/
|
|
161
185
|
const dismissTooltip = (event: Event): void => {
|
|
186
|
+
if (!closeOnClickOutside) return;
|
|
187
|
+
|
|
162
188
|
const isClickInside = dropdownWrapper?.contains(event.target as Node);
|
|
163
189
|
|
|
164
190
|
if (!isClickInside) {
|
|
@@ -247,7 +273,13 @@
|
|
|
247
273
|
break;
|
|
248
274
|
}
|
|
249
275
|
case 'Escape':
|
|
250
|
-
|
|
276
|
+
if (closeOnEscape) {
|
|
277
|
+
closeDropdown();
|
|
278
|
+
// Remove focus to prevent focus ring after closing
|
|
279
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
280
|
+
document.activeElement.blur();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
251
283
|
break;
|
|
252
284
|
}
|
|
253
285
|
|
|
@@ -301,7 +333,9 @@
|
|
|
301
333
|
|
|
302
334
|
if (!dropdownItems || !target) return instances;
|
|
303
335
|
|
|
304
|
-
|
|
336
|
+
if (closeOnClickOutside) {
|
|
337
|
+
document?.addEventListener('click', dismissTooltip);
|
|
338
|
+
}
|
|
305
339
|
instances.push(setupDropdown(target, dropdownItems));
|
|
306
340
|
|
|
307
341
|
return instances;
|
|
@@ -312,6 +346,9 @@
|
|
|
312
346
|
*/
|
|
313
347
|
const cleanupDropdownInstances = (instances: DropdownInstance[]) => {
|
|
314
348
|
instances.forEach((instance) => instance.cleanup());
|
|
349
|
+
if (closeOnClickOutside) {
|
|
350
|
+
document?.removeEventListener('click', dismissTooltip);
|
|
351
|
+
}
|
|
315
352
|
};
|
|
316
353
|
|
|
317
354
|
/**
|
|
@@ -679,6 +716,10 @@
|
|
|
679
716
|
display: flex;
|
|
680
717
|
align-items: center;
|
|
681
718
|
gap: 4px;
|
|
719
|
+
outline: none; /* Remove default focus outline since we have custom focus styling */
|
|
720
|
+
}
|
|
721
|
+
.dropdown:focus-visible {
|
|
722
|
+
outline: none; /* Prevent browser's default focus ring */
|
|
682
723
|
}
|
|
683
724
|
.dropdown.disabled {
|
|
684
725
|
cursor: not-allowed !important;
|
|
@@ -712,6 +753,10 @@
|
|
|
712
753
|
background: var(--background3);
|
|
713
754
|
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.15);
|
|
714
755
|
z-index: 99999;
|
|
756
|
+
outline: none; /* Remove focus outline */
|
|
757
|
+
}
|
|
758
|
+
.dropdown-list:focus-visible {
|
|
759
|
+
outline: none; /* Prevent browser's default focus ring */
|
|
715
760
|
}
|
|
716
761
|
|
|
717
762
|
.dropdown-items-scroll {
|
|
@@ -778,6 +823,10 @@
|
|
|
778
823
|
font-weight: 400;
|
|
779
824
|
line-height: 16px;
|
|
780
825
|
border: 1px solid transparent;
|
|
826
|
+
outline: none; /* Remove focus outline */
|
|
827
|
+
}
|
|
828
|
+
.dropdown-item:focus-visible {
|
|
829
|
+
outline: none; /* Prevent browser's default focus ring */
|
|
781
830
|
}
|
|
782
831
|
.dropdown-item .icon,
|
|
783
832
|
.dropdown-list .selected .icon {
|
|
@@ -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
|
*/
|
|
@@ -60,6 +60,14 @@ declare const meta: {
|
|
|
60
60
|
control: "boolean";
|
|
61
61
|
description: string;
|
|
62
62
|
};
|
|
63
|
+
showRemoveIcon: {
|
|
64
|
+
control: "boolean";
|
|
65
|
+
description: string;
|
|
66
|
+
};
|
|
67
|
+
expandOnClick: {
|
|
68
|
+
control: "boolean";
|
|
69
|
+
description: string;
|
|
70
|
+
};
|
|
63
71
|
width: {
|
|
64
72
|
control: "text";
|
|
65
73
|
description: string;
|
|
@@ -99,5 +107,8 @@ export declare const KeywordsTags: Story;
|
|
|
99
107
|
export declare const FormIntegration: Story;
|
|
100
108
|
export declare const ManyTags: Story;
|
|
101
109
|
export declare const LongTags: Story;
|
|
110
|
+
export declare const ShowRemoveIcon: Story;
|
|
111
|
+
export declare const ExpandOnClick: Story;
|
|
112
|
+
export declare const ShowRemoveIconAndExpandOnClick: Story;
|
|
102
113
|
export declare const SpecialCharacters: Story;
|
|
103
114
|
export declare const UnicodeSupport: Story;
|
|
@@ -60,6 +60,14 @@ const meta = {
|
|
|
60
60
|
control: 'boolean',
|
|
61
61
|
description: 'Whether to trim whitespace from tags'
|
|
62
62
|
},
|
|
63
|
+
showRemoveIcon: {
|
|
64
|
+
control: 'boolean',
|
|
65
|
+
description: 'Whether to always show the remove icon on tags'
|
|
66
|
+
},
|
|
67
|
+
expandOnClick: {
|
|
68
|
+
control: 'boolean',
|
|
69
|
+
description: 'Whether clicking a tag expands it to show full content'
|
|
70
|
+
},
|
|
63
71
|
width: {
|
|
64
72
|
control: 'text',
|
|
65
73
|
description: 'Custom width for the component'
|
|
@@ -407,6 +415,53 @@ export const LongTags = {
|
|
|
407
415
|
}
|
|
408
416
|
}
|
|
409
417
|
};
|
|
418
|
+
export const ShowRemoveIcon = {
|
|
419
|
+
args: {
|
|
420
|
+
value: ['JavaScript', 'TypeScript', 'Svelte'],
|
|
421
|
+
showRemoveIcon: true,
|
|
422
|
+
placeholder: 'Remove icon always visible...'
|
|
423
|
+
},
|
|
424
|
+
parameters: {
|
|
425
|
+
docs: {
|
|
426
|
+
description: {
|
|
427
|
+
story: 'When showRemoveIcon is true, the X button is always visible inline with 4px gap.'
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
export const ExpandOnClick = {
|
|
433
|
+
args: {
|
|
434
|
+
value: [
|
|
435
|
+
'This is a very long tag that will be truncated',
|
|
436
|
+
'Another long tag content here',
|
|
437
|
+
'Short'
|
|
438
|
+
],
|
|
439
|
+
expandOnClick: true,
|
|
440
|
+
placeholder: 'Click tags to expand...'
|
|
441
|
+
},
|
|
442
|
+
parameters: {
|
|
443
|
+
docs: {
|
|
444
|
+
description: {
|
|
445
|
+
story: 'When expandOnClick is true, clicking a tag expands it to show full content.'
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
export const ShowRemoveIconAndExpandOnClick = {
|
|
451
|
+
args: {
|
|
452
|
+
value: ['This is a very long tag name', 'TypeScript', 'Click me to expand'],
|
|
453
|
+
showRemoveIcon: true,
|
|
454
|
+
expandOnClick: true,
|
|
455
|
+
placeholder: 'Both features enabled...'
|
|
456
|
+
},
|
|
457
|
+
parameters: {
|
|
458
|
+
docs: {
|
|
459
|
+
description: {
|
|
460
|
+
story: 'Both showRemoveIcon and expandOnClick enabled together.'
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
410
465
|
export const SpecialCharacters = {
|
|
411
466
|
args: {
|
|
412
467
|
value: ['C++', 'C#', '.NET', '@angular', '#svelte'],
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
|
|
2
4
|
import { TimesIcon } from '../../icons';
|
|
3
5
|
|
|
4
6
|
import Loader from '../Loader.svelte';
|
|
@@ -21,6 +23,8 @@
|
|
|
21
23
|
allowDuplicates = false,
|
|
22
24
|
validateTag,
|
|
23
25
|
trimTags = true,
|
|
26
|
+
showRemoveIcon = false,
|
|
27
|
+
expandOnClick = false,
|
|
24
28
|
width = '100%',
|
|
25
29
|
height = 'auto',
|
|
26
30
|
class: className = '',
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
let inputElement: HTMLInputElement | undefined = $state();
|
|
40
44
|
let inputValue = $state('');
|
|
41
45
|
let isFocused = $state(false);
|
|
46
|
+
let expandedTags = new SvelteSet<number>();
|
|
42
47
|
|
|
43
48
|
// Derived states
|
|
44
49
|
let isDisabled = $derived(disabled || loading);
|
|
@@ -67,12 +72,40 @@
|
|
|
67
72
|
${isSuccessAlert ? 'success' : ''}
|
|
68
73
|
${isFocused ? 'focused' : ''}
|
|
69
74
|
${loading ? 'loading' : ''}
|
|
75
|
+
${showRemoveIcon ? 'show-remove-icon' : ''}
|
|
70
76
|
${className}
|
|
71
77
|
`
|
|
72
78
|
.trim()
|
|
73
79
|
.replace(/\s+/g, ' ')
|
|
74
80
|
);
|
|
75
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Collapses all expanded tags
|
|
84
|
+
*/
|
|
85
|
+
const collapseAllTags = () => {
|
|
86
|
+
expandedTags.clear();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Toggles the expanded state of a tag
|
|
91
|
+
*/
|
|
92
|
+
const toggleTagExpand = (index: number) => {
|
|
93
|
+
if (!expandOnClick) return;
|
|
94
|
+
|
|
95
|
+
if (expandedTags.has(index)) {
|
|
96
|
+
expandedTags.delete(index);
|
|
97
|
+
} else {
|
|
98
|
+
expandedTags.add(index);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if a tag is expanded
|
|
104
|
+
*/
|
|
105
|
+
const isTagExpanded = (index: number): boolean => {
|
|
106
|
+
return expandedTags.has(index);
|
|
107
|
+
};
|
|
108
|
+
|
|
76
109
|
/**
|
|
77
110
|
* Focus the input element
|
|
78
111
|
*/
|
|
@@ -232,9 +265,13 @@
|
|
|
232
265
|
};
|
|
233
266
|
|
|
234
267
|
/**
|
|
235
|
-
* Handles wrapper click to focus input
|
|
268
|
+
* Handles wrapper click to focus input and collapse expanded tags
|
|
236
269
|
*/
|
|
237
270
|
const handleWrapperClick = () => {
|
|
271
|
+
// Collapse all expanded tags when clicking on the wrapper area
|
|
272
|
+
if (expandOnClick && expandedTags.size > 0) {
|
|
273
|
+
collapseAllTags();
|
|
274
|
+
}
|
|
238
275
|
focusInput();
|
|
239
276
|
};
|
|
240
277
|
|
|
@@ -268,7 +305,24 @@
|
|
|
268
305
|
>
|
|
269
306
|
<div class="tags-input-content">
|
|
270
307
|
{#each value as tag, index (`${index}-${tag}`)}
|
|
271
|
-
|
|
308
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
309
|
+
<span
|
|
310
|
+
class="tag {expandOnClick ? 'expandable' : ''} {isTagExpanded(index) ? 'expanded' : ''}"
|
|
311
|
+
role="listitem"
|
|
312
|
+
onclick={(e) => {
|
|
313
|
+
if (expandOnClick) {
|
|
314
|
+
e.stopPropagation();
|
|
315
|
+
toggleTagExpand(index);
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
onkeydown={(e) => {
|
|
319
|
+
if (expandOnClick && e.key === 'Enter') {
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
toggleTagExpand(index);
|
|
322
|
+
}
|
|
323
|
+
}}
|
|
324
|
+
tabindex={expandOnClick ? 0 : undefined}
|
|
325
|
+
>
|
|
272
326
|
<span class="tag-text">{tag}</span>
|
|
273
327
|
{#if !readonly && !isDisabled}
|
|
274
328
|
<button
|
|
@@ -391,10 +445,14 @@
|
|
|
391
445
|
flex-wrap: wrap;
|
|
392
446
|
align-items: flex-start;
|
|
393
447
|
align-content: flex-start;
|
|
394
|
-
gap:
|
|
448
|
+
gap: 0;
|
|
395
449
|
width: 100%;
|
|
396
450
|
}
|
|
397
451
|
|
|
452
|
+
.tags-input-wrapper.show-remove-icon .tags-input-content {
|
|
453
|
+
gap: 4px;
|
|
454
|
+
}
|
|
455
|
+
|
|
398
456
|
.tag {
|
|
399
457
|
position: relative;
|
|
400
458
|
display: flex;
|
|
@@ -414,6 +472,22 @@
|
|
|
414
472
|
user-select: none;
|
|
415
473
|
max-width: 100%;
|
|
416
474
|
min-width: 0;
|
|
475
|
+
margin: 2px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.tags-input-wrapper.show-remove-icon .tag {
|
|
479
|
+
margin: 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.tag.expandable {
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.tag.expanded .tag-text {
|
|
487
|
+
overflow: visible;
|
|
488
|
+
text-overflow: unset;
|
|
489
|
+
white-space: normal;
|
|
490
|
+
word-break: break-word;
|
|
417
491
|
}
|
|
418
492
|
|
|
419
493
|
.tag-text {
|
|
@@ -447,6 +521,20 @@
|
|
|
447
521
|
pointer-events: auto;
|
|
448
522
|
}
|
|
449
523
|
|
|
524
|
+
/* Show remove icon mode - inline button */
|
|
525
|
+
.tags-input-wrapper.show-remove-icon .tag-remove {
|
|
526
|
+
position: relative;
|
|
527
|
+
top: auto;
|
|
528
|
+
right: auto;
|
|
529
|
+
bottom: auto;
|
|
530
|
+
padding: 0;
|
|
531
|
+
margin-left: 4px;
|
|
532
|
+
background: transparent;
|
|
533
|
+
opacity: 1;
|
|
534
|
+
pointer-events: auto;
|
|
535
|
+
border-radius: 0;
|
|
536
|
+
}
|
|
537
|
+
|
|
450
538
|
.tag-remove:not(:disabled) {
|
|
451
539
|
color: var(--text1);
|
|
452
540
|
}
|
|
@@ -62,6 +62,18 @@ export interface TagsInputProps {
|
|
|
62
62
|
* Whether to trim whitespace from tags (default: true)
|
|
63
63
|
*/
|
|
64
64
|
trimTags?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Whether to always show the remove icon on tags (default: false)
|
|
67
|
+
* When true: remove button is inline, 4px gap, no padding on close button
|
|
68
|
+
* When false: remove button is absolute positioned, appears on hover
|
|
69
|
+
*/
|
|
70
|
+
showRemoveIcon?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Whether clicking a tag expands it to show full content (default: false)
|
|
73
|
+
* When true: clicking a tag removes ellipsis and shows full text
|
|
74
|
+
* When false: long tags are always truncated with ellipsis
|
|
75
|
+
*/
|
|
76
|
+
expandOnClick?: boolean;
|
|
65
77
|
/**
|
|
66
78
|
* Custom width for the component
|
|
67
79
|
*/
|