@finsweet/webflow-apps-utils 1.0.51 → 1.0.53
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 +1 -0
- package/dist/ui/components/select/Select.stories.js +35 -0
- package/dist/ui/components/select/Select.svelte +129 -82
- package/dist/ui/components/select/SelectWithFooterStory.svelte +54 -0
- package/dist/ui/components/select/SelectWithFooterStory.svelte.d.ts +11 -0
- package/dist/ui/components/select/index.d.ts +1 -1
- package/dist/ui/components/select/types.d.ts +8 -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 +129 -27
- package/dist/ui/components/tags/types.d.ts +12 -0
- package/dist/ui/icons/CheckCircleIcon.svelte +1 -1
- package/dist/ui/icons/CheckCircleOutlinedIcon.svelte +1 -1
- package/dist/ui/icons/CloseCircleIcon.svelte +1 -1
- package/dist/ui/providers/GlobalProvider.svelte +0 -4
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CheckIcon, UndoIcon } from '../../icons';
|
|
2
2
|
import Select from './Select.svelte';
|
|
3
|
+
import SelectWithFooterStory from './SelectWithFooterStory.svelte';
|
|
3
4
|
// Mock options for stories
|
|
4
5
|
const basicOptions = [
|
|
5
6
|
{ label: 'Option 1', value: 'option1' },
|
|
@@ -567,3 +568,37 @@ export const FormValidationExample = {
|
|
|
567
568
|
}
|
|
568
569
|
}
|
|
569
570
|
};
|
|
571
|
+
// Footer snippet example
|
|
572
|
+
const providerOptions = [
|
|
573
|
+
{ label: 'Facebook', value: 'facebook' },
|
|
574
|
+
{ label: 'Google', value: 'google' },
|
|
575
|
+
{ label: 'Cloudflare', value: 'cloudflare' },
|
|
576
|
+
{ label: 'Youtube', value: 'youtube' },
|
|
577
|
+
{ label: 'Swiper', value: 'swiper' },
|
|
578
|
+
{ label: 'GSAP', value: 'gsap' }
|
|
579
|
+
];
|
|
580
|
+
export const WithFooter = {
|
|
581
|
+
render: () => ({
|
|
582
|
+
Component: SelectWithFooterStory,
|
|
583
|
+
props: {
|
|
584
|
+
options: providerOptions,
|
|
585
|
+
defaultText: 'Providers',
|
|
586
|
+
dropdownWidth: '250px',
|
|
587
|
+
dropdownHeight: '200px',
|
|
588
|
+
selected: 'facebook'
|
|
589
|
+
}
|
|
590
|
+
}),
|
|
591
|
+
args: {
|
|
592
|
+
options: providerOptions,
|
|
593
|
+
defaultText: 'Providers',
|
|
594
|
+
dropdownWidth: '250px',
|
|
595
|
+
dropdownHeight: '200px'
|
|
596
|
+
},
|
|
597
|
+
parameters: {
|
|
598
|
+
docs: {
|
|
599
|
+
description: {
|
|
600
|
+
story: 'Select with a sticky footer action. The footer stays visible while scrolling through options. Click the footer to trigger a custom action and close the dropdown.'
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
import { Tooltip } from '..';
|
|
16
16
|
import { Text } from '../text';
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
DropdownInstance,
|
|
19
|
+
SelectFooterProps,
|
|
20
|
+
SelectInstanceManager,
|
|
21
|
+
SelectProps
|
|
22
|
+
} from './types.js';
|
|
18
23
|
|
|
19
24
|
let {
|
|
20
25
|
id = uuidv4(),
|
|
@@ -38,7 +43,8 @@
|
|
|
38
43
|
invalid = false,
|
|
39
44
|
className = '',
|
|
40
45
|
onchange,
|
|
41
|
-
children
|
|
46
|
+
children,
|
|
47
|
+
footer
|
|
42
48
|
}: SelectProps = $props();
|
|
43
49
|
|
|
44
50
|
// State variables
|
|
@@ -274,6 +280,17 @@
|
|
|
274
280
|
lastHoveredItem = target;
|
|
275
281
|
};
|
|
276
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Clears the hover state when mouse leaves the items area.
|
|
285
|
+
*/
|
|
286
|
+
const clearHoverState = (): void => {
|
|
287
|
+
if (lastHoveredItem) {
|
|
288
|
+
lastHoveredItem.classList.remove('hover-state');
|
|
289
|
+
lastHoveredItem.setAttribute('tabindex', '-1');
|
|
290
|
+
lastHoveredItem = null;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
277
294
|
type EventOption = [string, () => void];
|
|
278
295
|
|
|
279
296
|
/**
|
|
@@ -403,14 +420,14 @@
|
|
|
403
420
|
const getTooltipColor = (alertType: string) => {
|
|
404
421
|
switch (alertType) {
|
|
405
422
|
case 'error':
|
|
406
|
-
return 'var(--redBackground
|
|
423
|
+
return 'var(--redBackground)';
|
|
407
424
|
case 'warning':
|
|
408
|
-
return 'var(--orangeBackground
|
|
425
|
+
return 'var(--orangeBackground)';
|
|
409
426
|
case 'success':
|
|
410
|
-
return 'var(--greenBackground
|
|
427
|
+
return 'var(--greenBackground)';
|
|
411
428
|
case 'info':
|
|
412
429
|
default:
|
|
413
|
-
return 'var(--
|
|
430
|
+
return 'var(--actionPrimaryBackground)';
|
|
414
431
|
}
|
|
415
432
|
};
|
|
416
433
|
|
|
@@ -458,8 +475,9 @@
|
|
|
458
475
|
<div
|
|
459
476
|
tabindex={disabled || isOpen ? -1 : 0}
|
|
460
477
|
class="dropdown-list"
|
|
478
|
+
class:has-footer={footer}
|
|
461
479
|
role="listbox"
|
|
462
|
-
style="width:{dropdownWidth};
|
|
480
|
+
style="width:{dropdownWidth};"
|
|
463
481
|
onkeydown={(e) => {
|
|
464
482
|
e.stopPropagation();
|
|
465
483
|
e.preventDefault();
|
|
@@ -467,85 +485,98 @@
|
|
|
467
485
|
}}
|
|
468
486
|
bind:this={dropdownItems}
|
|
469
487
|
>
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
488
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
489
|
+
<div
|
|
490
|
+
class="dropdown-items-scroll"
|
|
491
|
+
style="max-height:{dropdownHeight};"
|
|
492
|
+
onmouseleave={clearHoverState}
|
|
493
|
+
>
|
|
494
|
+
{#if selectedLabel}
|
|
495
|
+
<div class="selected">
|
|
496
|
+
<div class="label">
|
|
497
|
+
<Text label={selectedLabel} fontSize="normal" fontColor="var(--text1)" />
|
|
498
|
+
</div>
|
|
474
499
|
</div>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
500
|
+
{/if}
|
|
501
|
+
|
|
502
|
+
{#if enableSearch}
|
|
503
|
+
<div class="search-container">
|
|
504
|
+
<input
|
|
505
|
+
type="text"
|
|
506
|
+
placeholder="Search"
|
|
507
|
+
oninput={(e) => {
|
|
508
|
+
e.stopPropagation();
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
handleSearch(e);
|
|
511
|
+
}}
|
|
512
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
{/if}
|
|
516
|
+
|
|
517
|
+
{#each optionsStore?.length > 0 ? optionsStore : options as { label, value, className = null, description = null, labelIcon = null, descriptionTitle = null, isDisabled = false }, index (index)}
|
|
518
|
+
{@const indexId = index + 1}
|
|
519
|
+
{@const itemId = ref ? ref.replace(' ', '-') : 'dropdown'}
|
|
520
|
+
<button
|
|
521
|
+
aria-posinset={indexId}
|
|
522
|
+
aria-selected={value === selected && selected?.trim() !== '' ? 'true' : 'false'}
|
|
523
|
+
id={`${itemId}-list-${indexId}-${id}`}
|
|
524
|
+
data-value={value}
|
|
525
|
+
class="dropdown-item {isDisabled ? 'disabled' : ''} {className}"
|
|
526
|
+
role="option"
|
|
527
|
+
onclick={(e) => {
|
|
528
|
+
e.stopPropagation();
|
|
529
|
+
if (isDisabled) return;
|
|
530
|
+
handleSelect(value, label, e.currentTarget);
|
|
531
|
+
}}
|
|
532
|
+
onkeydown={(e) => {
|
|
484
533
|
e.stopPropagation();
|
|
485
534
|
e.preventDefault();
|
|
486
|
-
handleSearch(e);
|
|
487
535
|
}}
|
|
488
|
-
|
|
489
|
-
|
|
536
|
+
onmouseenter={handleMouseEnter}
|
|
537
|
+
aria-hidden={!isOpen}
|
|
538
|
+
tabindex={value === selected ? 0 : -1}
|
|
539
|
+
style={description ? 'align-items:start;' : ''}
|
|
540
|
+
>
|
|
541
|
+
<div class="icon" aria-label={label}>
|
|
542
|
+
{#if value === selected && selected?.trim() !== ''}
|
|
543
|
+
<CheckIcon />
|
|
544
|
+
{/if}
|
|
545
|
+
</div>
|
|
546
|
+
<div class="label">
|
|
547
|
+
{#if description || descriptionTitle || labelIcon}
|
|
548
|
+
<div class="label-content">
|
|
549
|
+
<div class="label-name">
|
|
550
|
+
<Text {label} />
|
|
551
|
+
{#if labelIcon}
|
|
552
|
+
{@const IconComponent = labelIcon}
|
|
553
|
+
<IconComponent />
|
|
554
|
+
{/if}
|
|
555
|
+
</div>
|
|
556
|
+
<div class="label-description-title">
|
|
557
|
+
<Text
|
|
558
|
+
label={descriptionTitle || ''}
|
|
559
|
+
fontColor="var(--greenText)"
|
|
560
|
+
fontSize="10px"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="label-description">
|
|
564
|
+
<Text label={description || ''} fontColor="var(--text2)" fontSize="10px" />
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
{:else}
|
|
568
|
+
<Text {label} fontSize="normal" />
|
|
569
|
+
{/if}
|
|
570
|
+
</div>
|
|
571
|
+
</button>
|
|
572
|
+
{/each}
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
{#if footer}
|
|
576
|
+
<div class="dropdown-footer">
|
|
577
|
+
{@render footer({ close: closeDropdown })}
|
|
490
578
|
</div>
|
|
491
579
|
{/if}
|
|
492
|
-
|
|
493
|
-
{#each optionsStore?.length > 0 ? optionsStore : options as { label, value, className = null, description = null, labelIcon = null, descriptionTitle = null, isDisabled = false }, index (index)}
|
|
494
|
-
{@const indexId = index + 1}
|
|
495
|
-
{@const itemId = ref ? ref.replace(' ', '-') : 'dropdown'}
|
|
496
|
-
<button
|
|
497
|
-
aria-posinset={indexId}
|
|
498
|
-
aria-selected={value === selected && selected?.trim() !== '' ? 'true' : 'false'}
|
|
499
|
-
id={`${itemId}-list-${indexId}-${id}`}
|
|
500
|
-
data-value={value}
|
|
501
|
-
class="dropdown-item {isDisabled ? 'disabled' : ''} {className}"
|
|
502
|
-
role="option"
|
|
503
|
-
onclick={(e) => {
|
|
504
|
-
e.stopPropagation();
|
|
505
|
-
if (isDisabled) return;
|
|
506
|
-
handleSelect(value, label, e.currentTarget);
|
|
507
|
-
}}
|
|
508
|
-
onkeydown={(e) => {
|
|
509
|
-
e.stopPropagation();
|
|
510
|
-
e.preventDefault();
|
|
511
|
-
}}
|
|
512
|
-
onmouseenter={handleMouseEnter}
|
|
513
|
-
aria-hidden={!isOpen}
|
|
514
|
-
tabindex={value === selected ? 0 : -1}
|
|
515
|
-
style={description ? 'align-items:start;' : ''}
|
|
516
|
-
>
|
|
517
|
-
<div class="icon" aria-label={label}>
|
|
518
|
-
{#if value === selected && selected?.trim() !== ''}
|
|
519
|
-
<CheckIcon />
|
|
520
|
-
{/if}
|
|
521
|
-
</div>
|
|
522
|
-
<div class="label">
|
|
523
|
-
{#if description || descriptionTitle || labelIcon}
|
|
524
|
-
<div class="label-content">
|
|
525
|
-
<div class="label-name">
|
|
526
|
-
<Text {label} />
|
|
527
|
-
{#if labelIcon}
|
|
528
|
-
{@const IconComponent = labelIcon}
|
|
529
|
-
<IconComponent />
|
|
530
|
-
{/if}
|
|
531
|
-
</div>
|
|
532
|
-
<div class="label-description-title">
|
|
533
|
-
<Text
|
|
534
|
-
label={descriptionTitle || ''}
|
|
535
|
-
fontColor="var(--greenText)"
|
|
536
|
-
fontSize="10px"
|
|
537
|
-
/>
|
|
538
|
-
</div>
|
|
539
|
-
<div class="label-description">
|
|
540
|
-
<Text label={description || ''} fontColor="var(--text2)" fontSize="10px" />
|
|
541
|
-
</div>
|
|
542
|
-
</div>
|
|
543
|
-
{:else}
|
|
544
|
-
<Text {label} fontSize="normal" />
|
|
545
|
-
{/if}
|
|
546
|
-
</div>
|
|
547
|
-
</button>
|
|
548
|
-
{/each}
|
|
549
580
|
</div>
|
|
550
581
|
</div>
|
|
551
582
|
</div>
|
|
@@ -675,14 +706,30 @@
|
|
|
675
706
|
position: absolute;
|
|
676
707
|
flex-direction: column;
|
|
677
708
|
align-items: flex-start;
|
|
678
|
-
gap:
|
|
709
|
+
gap: 0;
|
|
679
710
|
border-radius: 4px;
|
|
680
711
|
border: 1px solid var(--border1);
|
|
681
712
|
background: var(--background3);
|
|
682
713
|
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.15);
|
|
714
|
+
z-index: 99999;
|
|
715
|
+
}
|
|
683
716
|
|
|
717
|
+
.dropdown-items-scroll {
|
|
718
|
+
display: flex;
|
|
719
|
+
flex-direction: column;
|
|
720
|
+
align-items: flex-start;
|
|
721
|
+
gap: 4px;
|
|
722
|
+
width: 100%;
|
|
684
723
|
overflow-y: auto;
|
|
685
|
-
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.dropdown-footer {
|
|
727
|
+
display: flex;
|
|
728
|
+
align-items: center;
|
|
729
|
+
width: 100%;
|
|
730
|
+
border-top: 1px solid var(--border1);
|
|
731
|
+
background: var(--background3);
|
|
732
|
+
flex-shrink: 0;
|
|
686
733
|
}
|
|
687
734
|
.dropdown-list .selected {
|
|
688
735
|
display: flex;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
selected?: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
options,
|
|
15
|
+
defaultText = 'Select',
|
|
16
|
+
dropdownWidth = '200px',
|
|
17
|
+
dropdownHeight = '200px',
|
|
18
|
+
selected = $bindable(null)
|
|
19
|
+
}: Props = $props();
|
|
20
|
+
|
|
21
|
+
const handleFooterClick = (close: () => void) => {
|
|
22
|
+
console.log('Footer action clicked - adding a new provider manually');
|
|
23
|
+
close();
|
|
24
|
+
};
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<Select {options} {defaultText} {dropdownWidth} {dropdownHeight} bind:selected>
|
|
28
|
+
{#snippet footer({ close })}
|
|
29
|
+
<button type="button" class="footer-action" onclick={() => handleFooterClick(close)}>
|
|
30
|
+
+ Add manually a provider
|
|
31
|
+
</button>
|
|
32
|
+
{/snippet}
|
|
33
|
+
</Select>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.footer-action {
|
|
37
|
+
all: unset;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
padding: 8px;
|
|
41
|
+
width: 100%;
|
|
42
|
+
color: var(--blueText);
|
|
43
|
+
font-size: 11.5px;
|
|
44
|
+
font-weight: 400;
|
|
45
|
+
line-height: 16px;
|
|
46
|
+
letter-spacing: -0.115px;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
box-sizing: border-box;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.footer-action:hover {
|
|
52
|
+
background: var(--background5);
|
|
53
|
+
}
|
|
54
|
+
</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
|
+
selected?: string | null;
|
|
8
|
+
}
|
|
9
|
+
declare const SelectWithFooterStory: import("svelte").Component<Props, {}, "selected">;
|
|
10
|
+
type SelectWithFooterStory = ReturnType<typeof SelectWithFooterStory>;
|
|
11
|
+
export default SelectWithFooterStory;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Select } from './Select.svelte';
|
|
2
|
-
export type { SelectOption, SelectProps, SelectChangeEvent, SelectChangeHandler, SelectState, DropdownInstance, DropdownConfig, SelectStyles, NavigationKey, KeyboardNavigationEvent, SearchConfig, FilterFunction, SelectElementRefs, SelectInstanceManager } from './types.js';
|
|
2
|
+
export type { SelectOption, SelectProps, SelectFooterProps, SelectChangeEvent, SelectChangeHandler, SelectState, DropdownInstance, DropdownConfig, SelectStyles, NavigationKey, KeyboardNavigationEvent, SearchConfig, FilterFunction, SelectElementRefs, SelectInstanceManager } from './types.js';
|
|
@@ -24,6 +24,9 @@ export interface SelectChangeEvent {
|
|
|
24
24
|
value: string | null;
|
|
25
25
|
}
|
|
26
26
|
export type SelectChangeHandler = (event: SelectChangeEvent) => void;
|
|
27
|
+
export interface SelectFooterProps {
|
|
28
|
+
close: () => void;
|
|
29
|
+
}
|
|
27
30
|
export interface SelectProps {
|
|
28
31
|
id?: string;
|
|
29
32
|
defaultText?: string;
|
|
@@ -53,6 +56,11 @@ export interface SelectProps {
|
|
|
53
56
|
className?: string;
|
|
54
57
|
onchange?: SelectChangeHandler;
|
|
55
58
|
children?: Snippet;
|
|
59
|
+
/**
|
|
60
|
+
* Footer snippet for custom actions at the bottom of the dropdown.
|
|
61
|
+
* Receives { close } function to close the dropdown.
|
|
62
|
+
*/
|
|
63
|
+
footer?: Snippet<[SelectFooterProps]>;
|
|
56
64
|
}
|
|
57
65
|
export interface SelectState {
|
|
58
66
|
isOpen: boolean;
|
|
@@ -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);
|
|
@@ -52,20 +57,55 @@
|
|
|
52
57
|
return value.length < minTags;
|
|
53
58
|
});
|
|
54
59
|
|
|
60
|
+
// Derived alert state for styling
|
|
61
|
+
let alertType = $derived(alert?.type || null);
|
|
62
|
+
let isErrorAlert = $derived(alertType === 'error' || alertType === 'warning');
|
|
63
|
+
let isSuccessAlert = $derived(alertType === 'success');
|
|
64
|
+
|
|
55
65
|
// CSS classes
|
|
56
66
|
let wrapperClasses = $derived(
|
|
57
67
|
`
|
|
58
68
|
tags-input-wrapper
|
|
59
69
|
${isDisabled ? 'disabled' : ''}
|
|
60
|
-
${
|
|
70
|
+
${readonly ? 'readonly' : ''}
|
|
71
|
+
${invalid || isErrorAlert || isMinTagsInvalid ? 'invalid' : ''}
|
|
72
|
+
${isSuccessAlert ? 'success' : ''}
|
|
61
73
|
${isFocused ? 'focused' : ''}
|
|
62
74
|
${loading ? 'loading' : ''}
|
|
75
|
+
${showRemoveIcon ? 'show-remove-icon' : ''}
|
|
63
76
|
${className}
|
|
64
77
|
`
|
|
65
78
|
.trim()
|
|
66
79
|
.replace(/\s+/g, ' ')
|
|
67
80
|
);
|
|
68
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
|
+
|
|
69
109
|
/**
|
|
70
110
|
* Focus the input element
|
|
71
111
|
*/
|
|
@@ -225,9 +265,13 @@
|
|
|
225
265
|
};
|
|
226
266
|
|
|
227
267
|
/**
|
|
228
|
-
* Handles wrapper click to focus input
|
|
268
|
+
* Handles wrapper click to focus input and collapse expanded tags
|
|
229
269
|
*/
|
|
230
270
|
const handleWrapperClick = () => {
|
|
271
|
+
// Collapse all expanded tags when clicking on the wrapper area
|
|
272
|
+
if (expandOnClick && expandedTags.size > 0) {
|
|
273
|
+
collapseAllTags();
|
|
274
|
+
}
|
|
231
275
|
focusInput();
|
|
232
276
|
};
|
|
233
277
|
|
|
@@ -237,14 +281,14 @@
|
|
|
237
281
|
const getTooltipColor = (alertType: string) => {
|
|
238
282
|
switch (alertType) {
|
|
239
283
|
case 'error':
|
|
240
|
-
return 'var(--redBackground
|
|
284
|
+
return 'var(--redBackground)';
|
|
241
285
|
case 'warning':
|
|
242
|
-
return 'var(--orangeBackground
|
|
286
|
+
return 'var(--orangeBackground)';
|
|
243
287
|
case 'success':
|
|
244
|
-
return 'var(--greenBackground
|
|
288
|
+
return 'var(--greenBackground)';
|
|
245
289
|
case 'info':
|
|
246
290
|
default:
|
|
247
|
-
return 'var(--actionPrimaryBackground
|
|
291
|
+
return 'var(--actionPrimaryBackground)';
|
|
248
292
|
}
|
|
249
293
|
};
|
|
250
294
|
</script>
|
|
@@ -260,8 +304,25 @@
|
|
|
260
304
|
onkeydown={(e) => e.key === 'Enter' && handleWrapperClick()}
|
|
261
305
|
>
|
|
262
306
|
<div class="tags-input-content">
|
|
263
|
-
{#each value as tag, index (tag)}
|
|
264
|
-
|
|
307
|
+
{#each value as tag, index (`${index}-${tag}`)}
|
|
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
|
+
>
|
|
265
326
|
<span class="tag-text">{tag}</span>
|
|
266
327
|
{#if !readonly && !isDisabled}
|
|
267
328
|
<button
|
|
@@ -337,14 +398,14 @@
|
|
|
337
398
|
|
|
338
399
|
.tags-input-wrapper {
|
|
339
400
|
position: relative;
|
|
340
|
-
border: 1px solid var(--border3
|
|
341
|
-
border-radius: var(--border-radius
|
|
401
|
+
border: 1px solid var(--border3);
|
|
402
|
+
border-radius: var(--border-radius);
|
|
342
403
|
padding: 4px;
|
|
343
404
|
display: flex;
|
|
344
405
|
flex-wrap: wrap;
|
|
345
406
|
align-items: flex-start;
|
|
346
407
|
align-content: flex-start;
|
|
347
|
-
background: var(--background1
|
|
408
|
+
background: var(--background1);
|
|
348
409
|
min-height: 32px;
|
|
349
410
|
box-shadow:
|
|
350
411
|
0px 16px 16px -16px rgba(0, 0, 0, 0.13) inset,
|
|
@@ -357,17 +418,22 @@
|
|
|
357
418
|
}
|
|
358
419
|
|
|
359
420
|
.tags-input-wrapper.focused {
|
|
360
|
-
border-color: var(--blueBorder
|
|
421
|
+
border-color: var(--blueBorder);
|
|
361
422
|
}
|
|
362
423
|
|
|
363
424
|
.tags-input-wrapper.invalid {
|
|
364
|
-
border-color: var(--redBorder
|
|
425
|
+
border-color: var(--redBorder);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.tags-input-wrapper.success {
|
|
429
|
+
border-color: var(--greenBorder);
|
|
365
430
|
}
|
|
366
431
|
|
|
367
|
-
.tags-input-wrapper.disabled
|
|
432
|
+
.tags-input-wrapper.disabled,
|
|
433
|
+
.tags-input-wrapper.readonly {
|
|
368
434
|
cursor: not-allowed;
|
|
369
|
-
opacity: 0.
|
|
370
|
-
border-color: var(--border1
|
|
435
|
+
opacity: 0.7;
|
|
436
|
+
border-color: var(--border1);
|
|
371
437
|
}
|
|
372
438
|
|
|
373
439
|
.tags-input-wrapper.loading {
|
|
@@ -379,34 +445,56 @@
|
|
|
379
445
|
flex-wrap: wrap;
|
|
380
446
|
align-items: flex-start;
|
|
381
447
|
align-content: flex-start;
|
|
382
|
-
gap:
|
|
448
|
+
gap: 0;
|
|
383
449
|
width: 100%;
|
|
384
450
|
}
|
|
385
451
|
|
|
452
|
+
.tags-input-wrapper.show-remove-icon .tags-input-content {
|
|
453
|
+
gap: 4px;
|
|
454
|
+
}
|
|
455
|
+
|
|
386
456
|
.tag {
|
|
387
457
|
position: relative;
|
|
388
458
|
display: flex;
|
|
389
459
|
padding: 4px 8px;
|
|
390
460
|
justify-content: center;
|
|
391
461
|
align-items: center;
|
|
392
|
-
border-radius: var(--border-radius
|
|
462
|
+
border-radius: var(--border-radius);
|
|
393
463
|
background: var(--actionSecondaryBackground);
|
|
394
|
-
color: var(--text1
|
|
395
|
-
font-size: var(--font-size-small
|
|
396
|
-
font-weight: var(--font-weight-normal
|
|
464
|
+
color: var(--text1);
|
|
465
|
+
font-size: var(--font-size-small);
|
|
466
|
+
font-weight: var(--font-weight-normal);
|
|
397
467
|
line-height: 16px;
|
|
398
468
|
letter-spacing: -0.115px;
|
|
399
469
|
box-shadow:
|
|
400
470
|
0 0.5px 1px 0 #000,
|
|
401
471
|
0 0.5px 0.5px 0 rgba(255, 255, 255, 0.12) inset;
|
|
402
472
|
user-select: none;
|
|
473
|
+
max-width: 100%;
|
|
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;
|
|
403
491
|
}
|
|
404
492
|
|
|
405
493
|
.tag-text {
|
|
406
|
-
max-width: 150px;
|
|
407
494
|
overflow: hidden;
|
|
408
495
|
text-overflow: ellipsis;
|
|
409
496
|
white-space: nowrap;
|
|
497
|
+
min-width: 0;
|
|
410
498
|
}
|
|
411
499
|
|
|
412
500
|
.tag-remove {
|
|
@@ -423,7 +511,7 @@
|
|
|
423
511
|
background: #464646;
|
|
424
512
|
color: var(--text2);
|
|
425
513
|
cursor: pointer;
|
|
426
|
-
border-radius: 0 var(--border-radius
|
|
514
|
+
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
|
427
515
|
opacity: 0;
|
|
428
516
|
pointer-events: none;
|
|
429
517
|
}
|
|
@@ -433,8 +521,22 @@
|
|
|
433
521
|
pointer-events: auto;
|
|
434
522
|
}
|
|
435
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
|
+
|
|
436
538
|
.tag-remove:not(:disabled) {
|
|
437
|
-
color: var(--text1
|
|
539
|
+
color: var(--text1);
|
|
438
540
|
}
|
|
439
541
|
|
|
440
542
|
.tag-remove:disabled {
|
|
@@ -453,8 +555,8 @@
|
|
|
453
555
|
padding: 4px 8px;
|
|
454
556
|
border: none;
|
|
455
557
|
background: transparent;
|
|
456
|
-
color: var(--text1
|
|
457
|
-
font-size: var(--font-size-small
|
|
558
|
+
color: var(--text1);
|
|
559
|
+
font-size: var(--font-size-small);
|
|
458
560
|
font-family: inherit;
|
|
459
561
|
line-height: 16px;
|
|
460
562
|
letter-spacing: -0.115px;
|
|
@@ -462,7 +564,7 @@
|
|
|
462
564
|
}
|
|
463
565
|
|
|
464
566
|
.tags-input-field::placeholder {
|
|
465
|
-
color: var(--text3
|
|
567
|
+
color: var(--text3);
|
|
466
568
|
}
|
|
467
569
|
|
|
468
570
|
.tags-input-field:disabled {
|
|
@@ -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
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16" fill="none">
|
|
2
2
|
<path
|
|
3
3
|
d="M8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0ZM11.53 10.47L10.47 11.53L8 9.06L5.53 11.53L4.47 10.47L6.94 8L4.47 5.53L5.53 4.47L8 6.94L10.47 4.47L11.53 5.53L9.06 8L11.53 10.47Z"
|
|
4
4
|
fill="currentColor"
|