@invopop/popui 0.1.43 → 0.1.44
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/BaseDropdown.svelte
CHANGED
|
@@ -28,6 +28,12 @@
|
|
|
28
28
|
type DropIndicatorState,
|
|
29
29
|
type DndItem as DndItemType
|
|
30
30
|
} from './drawer-dnd-helpers'
|
|
31
|
+
import {
|
|
32
|
+
getFocusableItems,
|
|
33
|
+
getNextFocusedIndex,
|
|
34
|
+
selectFocusedItem
|
|
35
|
+
} from './drawer-keyboard-helpers'
|
|
36
|
+
import { isInputFocused } from './helpers'
|
|
31
37
|
|
|
32
38
|
const flipDurationMs = 150
|
|
33
39
|
|
|
@@ -81,6 +87,8 @@
|
|
|
81
87
|
let draggedOverGroup = $state<string | null>(null)
|
|
82
88
|
let dropIndicator = $state<DropIndicatorState>(null)
|
|
83
89
|
let cleanupFunctions: (() => void)[] = []
|
|
90
|
+
let focusedIndex = $state<number>(-1)
|
|
91
|
+
let containerRef = $state<HTMLDivElement | null>(null)
|
|
84
92
|
|
|
85
93
|
// Build internal DND items from external items
|
|
86
94
|
function buildListIn() {
|
|
@@ -168,11 +176,14 @@
|
|
|
168
176
|
element: document.documentElement
|
|
169
177
|
})
|
|
170
178
|
cleanupFunctions.push(autoScrollCleanup)
|
|
179
|
+
|
|
180
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
171
181
|
})
|
|
172
182
|
|
|
173
183
|
onDestroy(() => {
|
|
174
184
|
cleanupFunctions.forEach((cleanup) => cleanup())
|
|
175
185
|
cleanupFunctions = []
|
|
186
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
176
187
|
})
|
|
177
188
|
|
|
178
189
|
function emitGroupDistribution() {
|
|
@@ -400,6 +411,40 @@
|
|
|
400
411
|
function toggleGroup(groupSlug: string) {
|
|
401
412
|
openGroups = openGroups[groupSlug] ? {} : { [groupSlug]: true }
|
|
402
413
|
}
|
|
414
|
+
|
|
415
|
+
let focusedItemValue = $derived.by(() => {
|
|
416
|
+
const focusableItems = getFocusableItems(items)
|
|
417
|
+
if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {
|
|
418
|
+
return focusableItems[focusedIndex].value
|
|
419
|
+
}
|
|
420
|
+
return null
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
424
|
+
// Don't handle if any input is focused or container doesn't exist
|
|
425
|
+
if (isInputFocused() || !containerRef || !document.body.contains(containerRef)) return
|
|
426
|
+
|
|
427
|
+
const focusableItems = getFocusableItems(items)
|
|
428
|
+
if (focusableItems.length === 0) return
|
|
429
|
+
|
|
430
|
+
if (event.key === 'ArrowDown') {
|
|
431
|
+
event.preventDefault()
|
|
432
|
+
focusedIndex = getNextFocusedIndex(focusedIndex, 'down', focusableItems.length)
|
|
433
|
+
} else if (event.key === 'ArrowUp') {
|
|
434
|
+
event.preventDefault()
|
|
435
|
+
focusedIndex = getNextFocusedIndex(focusedIndex, 'up', focusableItems.length)
|
|
436
|
+
} else if (event.key === ' ' || event.key === 'Enter') {
|
|
437
|
+
event.preventDefault()
|
|
438
|
+
const result = selectFocusedItem(items, focusedIndex, multiple)
|
|
439
|
+
if (result) {
|
|
440
|
+
if (result.shouldUpdate) {
|
|
441
|
+
updateItem(result.item)
|
|
442
|
+
} else {
|
|
443
|
+
onclick?.(result.item.value)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
403
448
|
</script>
|
|
404
449
|
|
|
405
450
|
{#snippet drawerItem(item: DrawerOption)}
|
|
@@ -407,12 +452,18 @@
|
|
|
407
452
|
<DrawerContextSeparator />
|
|
408
453
|
{:else}
|
|
409
454
|
<div class:px-1={!item.groupBy} class:cursor-grab={draggable && !item.locked}>
|
|
410
|
-
<DrawerContextItem
|
|
455
|
+
<DrawerContextItem
|
|
456
|
+
item={{ ...item, focused: item.value === focusedItemValue }}
|
|
457
|
+
{multiple}
|
|
458
|
+
{onclick}
|
|
459
|
+
onchange={updateItem}
|
|
460
|
+
/>
|
|
411
461
|
</div>
|
|
412
462
|
{/if}
|
|
413
463
|
{/snippet}
|
|
414
464
|
|
|
415
465
|
<div
|
|
466
|
+
bind:this={containerRef}
|
|
416
467
|
class="{widthClass} border border-border rounded-2xl shadow-lg bg-background flex flex-col py-1 max-h-[568px] list-none"
|
|
417
468
|
>
|
|
418
469
|
{@render children?.()}
|
|
@@ -15,19 +15,24 @@
|
|
|
15
15
|
item = $bindable(),
|
|
16
16
|
scrollIfSelected = false,
|
|
17
17
|
onchange,
|
|
18
|
-
onclick
|
|
18
|
+
onclick,
|
|
19
|
+
onfocus
|
|
19
20
|
}: DrawerContextItemProps = $props()
|
|
20
21
|
|
|
21
22
|
let el: HTMLElement | undefined = $state()
|
|
22
23
|
|
|
24
|
+
let shouldShowHoverStyle = $derived.by(() => {
|
|
25
|
+
if (multiple) return true
|
|
26
|
+
if (item?.selected || item?.disabled) return false
|
|
27
|
+
return true
|
|
28
|
+
})
|
|
29
|
+
|
|
23
30
|
let styles = $derived(
|
|
24
31
|
clsx(
|
|
25
32
|
'px-2 py-1.5 space-x-1.5',
|
|
26
33
|
{ 'bg-background-selected': item?.selected && !multiple },
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
(!item?.selected && !item?.disabled) || multiple
|
|
30
|
-
}
|
|
34
|
+
{ 'bg-background-default-secondary': item?.focused && shouldShowHoverStyle },
|
|
35
|
+
{ 'group-hover:bg-background-default-secondary': shouldShowHoverStyle }
|
|
31
36
|
)
|
|
32
37
|
)
|
|
33
38
|
|
|
@@ -67,50 +72,53 @@
|
|
|
67
72
|
>
|
|
68
73
|
<div class="bg-background rounded-md">
|
|
69
74
|
<div class="{styles} rounded-md pr-2 flex items-center justify-start w-full">
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{/if}
|
|
82
|
-
<div class="whitespace-nowrap flex-1 text-left flex items-center space-x-1.5 truncate" {title}>
|
|
83
|
-
{#if item?.color}
|
|
84
|
-
<TagStatus status={item.color} dot />
|
|
75
|
+
{#if item?.useAvatar}
|
|
76
|
+
<ProfileAvatar name={item?.label || ''} picture={item?.picture || ''} variant="sm" />
|
|
77
|
+
{:else if item?.picture}
|
|
78
|
+
<ProfileAvatar name={item?.label || ''} picture={item?.picture} variant="sm" />
|
|
79
|
+
{:else if item?.icon}
|
|
80
|
+
<Icon
|
|
81
|
+
src={item.icon}
|
|
82
|
+
class="w-4 h-4 {item?.destructive
|
|
83
|
+
? 'text-icon-critical'
|
|
84
|
+
: item?.iconClass || 'text-icon'} {item?.locked ? 'opacity-30' : ''}"
|
|
85
|
+
/>
|
|
85
86
|
{/if}
|
|
86
|
-
<
|
|
87
|
+
<div
|
|
88
|
+
class="whitespace-nowrap flex-1 text-left flex items-center space-x-1.5 truncate"
|
|
89
|
+
{title}
|
|
90
|
+
>
|
|
91
|
+
{#if item?.color}
|
|
92
|
+
<TagStatus status={item.color} dot />
|
|
93
|
+
{/if}
|
|
94
|
+
<span class="{labelStyles} text-base font-medium truncate">{item?.label || ''}</span>
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
</div>
|
|
95
|
-
{#if item?.action}
|
|
96
|
-
<div class="no-drag !cursor-default">
|
|
97
|
-
{@render item.action(item)}
|
|
96
|
+
{#if item?.country}
|
|
97
|
+
<BaseFlag country={item.country} />
|
|
98
|
+
<span class="text-xs font-medium text-foreground-default-secondary uppercase">
|
|
99
|
+
{item.country}
|
|
100
|
+
</span>
|
|
101
|
+
{/if}
|
|
98
102
|
</div>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
103
|
+
{#if item?.action}
|
|
104
|
+
<div class="no-drag !cursor-default">
|
|
105
|
+
{@render item.action(item)}
|
|
106
|
+
</div>
|
|
107
|
+
{:else if multiple}
|
|
108
|
+
<InputCheckbox
|
|
109
|
+
checked={item?.selected ?? false}
|
|
110
|
+
onchange={(value) => {
|
|
111
|
+
if (item) {
|
|
112
|
+
item.selected = value
|
|
113
|
+
onchange?.(item)
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
{:else if item?.selected}
|
|
118
|
+
<Icon src={Success} class="size-4 text-icon-selected" />
|
|
119
|
+
{:else if item?.rightIcon}
|
|
120
|
+
<Icon src={item.rightIcon} class="size-4 text-icon-default-secondary" />
|
|
121
|
+
{/if}
|
|
114
122
|
</div>
|
|
115
123
|
</div>
|
|
116
124
|
</button>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrawerOption } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Get all focusable items (non-separator, non-disabled, non-locked)
|
|
4
|
+
*/
|
|
5
|
+
export declare function getFocusableItems(items: DrawerOption[]): DrawerOption[];
|
|
6
|
+
/**
|
|
7
|
+
* Calculate next focused index based on arrow key direction
|
|
8
|
+
*/
|
|
9
|
+
export declare function getNextFocusedIndex(currentIndex: number, direction: 'up' | 'down', itemsCount: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Handle selection of focused item
|
|
12
|
+
*/
|
|
13
|
+
export declare function selectFocusedItem(items: DrawerOption[], focusedIndex: number, multiple: boolean): {
|
|
14
|
+
item: DrawerOption;
|
|
15
|
+
shouldUpdate: boolean;
|
|
16
|
+
} | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get all focusable items (non-separator, non-disabled, non-locked)
|
|
3
|
+
*/
|
|
4
|
+
export function getFocusableItems(items) {
|
|
5
|
+
return items.filter((item) => !item.separator && !item.disabled && !item.locked);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Calculate next focused index based on arrow key direction
|
|
9
|
+
*/
|
|
10
|
+
export function getNextFocusedIndex(currentIndex, direction, itemsCount) {
|
|
11
|
+
if (itemsCount === 0)
|
|
12
|
+
return -1;
|
|
13
|
+
if (direction === 'down') {
|
|
14
|
+
return currentIndex < itemsCount - 1 ? currentIndex + 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
return currentIndex > 0 ? currentIndex - 1 : itemsCount - 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Handle selection of focused item
|
|
22
|
+
*/
|
|
23
|
+
export function selectFocusedItem(items, focusedIndex, multiple) {
|
|
24
|
+
const focusableItems = getFocusableItems(items);
|
|
25
|
+
if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {
|
|
26
|
+
const focusedItem = focusableItems[focusedIndex];
|
|
27
|
+
if (multiple) {
|
|
28
|
+
return {
|
|
29
|
+
item: { ...focusedItem, selected: !focusedItem.selected },
|
|
30
|
+
shouldUpdate: true
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return {
|
|
35
|
+
item: focusedItem,
|
|
36
|
+
shouldUpdate: false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type DrawerOption = SelectOption & {
|
|
|
29
29
|
separator?: boolean;
|
|
30
30
|
destructive?: boolean;
|
|
31
31
|
selected?: boolean;
|
|
32
|
+
focused?: boolean;
|
|
32
33
|
icon?: IconSource | undefined;
|
|
33
34
|
rightIcon?: IconSource | undefined;
|
|
34
35
|
country?: string;
|
|
@@ -349,6 +350,7 @@ export interface DrawerContextItemProps {
|
|
|
349
350
|
scrollIfSelected?: boolean;
|
|
350
351
|
onclick?: (value: AnyProp) => void;
|
|
351
352
|
onchange?: (item: DrawerOption) => void;
|
|
353
|
+
onfocus?: (item: DrawerOption) => void;
|
|
352
354
|
}
|
|
353
355
|
export interface DropdownSelectProps {
|
|
354
356
|
value?: AnyProp;
|