@invopop/popui 0.1.43 → 0.1.45
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,11 @@
|
|
|
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'
|
|
31
36
|
|
|
32
37
|
const flipDurationMs = 150
|
|
33
38
|
|
|
@@ -70,6 +75,27 @@
|
|
|
70
75
|
return { groupedItems: grouped, ungroupedItems: ungrouped }
|
|
71
76
|
})
|
|
72
77
|
|
|
78
|
+
// Items in display order (matches visual rendering order)
|
|
79
|
+
let itemsInDisplayOrder = $derived.by(() => {
|
|
80
|
+
const displayOrder: DrawerOption[] = []
|
|
81
|
+
|
|
82
|
+
if (hasGroups && groups) {
|
|
83
|
+
// Add grouped items in group order, only if group is open (when collapsible)
|
|
84
|
+
groups.forEach((group) => {
|
|
85
|
+
const isOpen = collapsibleGroups ? openGroups[group.slug] : true
|
|
86
|
+
if (isOpen) {
|
|
87
|
+
const groupItems = groupedItems.get(group.slug) || []
|
|
88
|
+
displayOrder.push(...groupItems)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add ungrouped items
|
|
94
|
+
displayOrder.push(...ungroupedItems)
|
|
95
|
+
|
|
96
|
+
return displayOrder
|
|
97
|
+
})
|
|
98
|
+
|
|
73
99
|
let openGroups = $state<Record<string, boolean>>({})
|
|
74
100
|
let groupDndItems = $state<Record<string, DndItem[]>>({})
|
|
75
101
|
let ungroupedDndItems = $state<DndItem[]>([])
|
|
@@ -81,6 +107,8 @@
|
|
|
81
107
|
let draggedOverGroup = $state<string | null>(null)
|
|
82
108
|
let dropIndicator = $state<DropIndicatorState>(null)
|
|
83
109
|
let cleanupFunctions: (() => void)[] = []
|
|
110
|
+
let focusedIndex = $state<number>(-1)
|
|
111
|
+
let containerRef = $state<HTMLDivElement | null>(null)
|
|
84
112
|
|
|
85
113
|
// Build internal DND items from external items
|
|
86
114
|
function buildListIn() {
|
|
@@ -168,11 +196,14 @@
|
|
|
168
196
|
element: document.documentElement
|
|
169
197
|
})
|
|
170
198
|
cleanupFunctions.push(autoScrollCleanup)
|
|
199
|
+
|
|
200
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
171
201
|
})
|
|
172
202
|
|
|
173
203
|
onDestroy(() => {
|
|
174
204
|
cleanupFunctions.forEach((cleanup) => cleanup())
|
|
175
205
|
cleanupFunctions = []
|
|
206
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
176
207
|
})
|
|
177
208
|
|
|
178
209
|
function emitGroupDistribution() {
|
|
@@ -400,6 +431,40 @@
|
|
|
400
431
|
function toggleGroup(groupSlug: string) {
|
|
401
432
|
openGroups = openGroups[groupSlug] ? {} : { [groupSlug]: true }
|
|
402
433
|
}
|
|
434
|
+
|
|
435
|
+
let focusedItemValue = $derived.by(() => {
|
|
436
|
+
const focusableItems = getFocusableItems(itemsInDisplayOrder)
|
|
437
|
+
if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {
|
|
438
|
+
return focusableItems[focusedIndex].value
|
|
439
|
+
}
|
|
440
|
+
return null
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
444
|
+
// Don't handle if container doesn't exist
|
|
445
|
+
if (!containerRef || !document.body.contains(containerRef)) return
|
|
446
|
+
|
|
447
|
+
const focusableItems = getFocusableItems(itemsInDisplayOrder)
|
|
448
|
+
if (focusableItems.length === 0) return
|
|
449
|
+
|
|
450
|
+
if (event.key === 'ArrowDown') {
|
|
451
|
+
event.preventDefault()
|
|
452
|
+
focusedIndex = getNextFocusedIndex(focusedIndex, 'down', focusableItems.length)
|
|
453
|
+
} else if (event.key === 'ArrowUp') {
|
|
454
|
+
event.preventDefault()
|
|
455
|
+
focusedIndex = getNextFocusedIndex(focusedIndex, 'up', focusableItems.length)
|
|
456
|
+
} else if (event.key === ' ' || event.key === 'Enter') {
|
|
457
|
+
event.preventDefault()
|
|
458
|
+
const result = selectFocusedItem(itemsInDisplayOrder, focusedIndex, multiple)
|
|
459
|
+
if (result) {
|
|
460
|
+
if (result.shouldUpdate) {
|
|
461
|
+
updateItem(result.item)
|
|
462
|
+
} else {
|
|
463
|
+
onclick?.(result.item.value)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
403
468
|
</script>
|
|
404
469
|
|
|
405
470
|
{#snippet drawerItem(item: DrawerOption)}
|
|
@@ -407,12 +472,18 @@
|
|
|
407
472
|
<DrawerContextSeparator />
|
|
408
473
|
{:else}
|
|
409
474
|
<div class:px-1={!item.groupBy} class:cursor-grab={draggable && !item.locked}>
|
|
410
|
-
<DrawerContextItem
|
|
475
|
+
<DrawerContextItem
|
|
476
|
+
item={{ ...item, focused: item.value === focusedItemValue }}
|
|
477
|
+
{multiple}
|
|
478
|
+
{onclick}
|
|
479
|
+
onchange={updateItem}
|
|
480
|
+
/>
|
|
411
481
|
</div>
|
|
412
482
|
{/if}
|
|
413
483
|
{/snippet}
|
|
414
484
|
|
|
415
485
|
<div
|
|
486
|
+
bind:this={containerRef}
|
|
416
487
|
class="{widthClass} border border-border rounded-2xl shadow-lg bg-background flex flex-col py-1 max-h-[568px] list-none"
|
|
417
488
|
>
|
|
418
489
|
{@render children?.()}
|
|
@@ -433,7 +504,7 @@
|
|
|
433
504
|
>
|
|
434
505
|
{#if collapsibleGroups}
|
|
435
506
|
<button
|
|
436
|
-
class="cursor-pointer flex items-center justify-between h-8 pl-2.5 pr-2.5 py-2.5 text-base font-medium text-foreground-default-secondary w-full hover:bg-background-default-secondary rounded-lg overflow-clip flex-shrink-0"
|
|
507
|
+
class="cursor-pointer flex items-center justify-between h-8 pl-2.5 pr-2.5 py-2.5 text-base font-medium text-foreground-default-secondary w-full hover:bg-background-default-secondary rounded-lg overflow-clip flex-shrink-0 outline-none"
|
|
437
508
|
onclick={() => toggleGroup(group.slug)}
|
|
438
509
|
>
|
|
439
510
|
<div class="flex items-center gap-1.5">
|
|
@@ -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;
|