@invopop/popui 0.1.74 → 0.1.76

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.
@@ -34,6 +34,7 @@
34
34
  </script>
35
35
 
36
36
  <DrawerContext
37
+ autofocus
37
38
  {items}
38
39
  onclick={(e) => {
39
40
  if (e === 'hide') {
@@ -9,6 +9,7 @@
9
9
  import { slide } from 'svelte/transition'
10
10
  import { flip } from 'svelte/animate'
11
11
  import clsx from 'clsx'
12
+ import { cn } from './utils.js'
12
13
  import {
13
14
  draggable as makeDraggable,
14
15
  dropTargetForElements
@@ -22,6 +23,7 @@
22
23
  type Edge
23
24
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
24
25
  import { onMount, onDestroy, untrack } from 'svelte'
26
+ import { Command } from 'bits-ui'
25
27
  import {
26
28
  shouldShowDropIndicator,
27
29
  reorderItems,
@@ -29,11 +31,6 @@
29
31
  type DropIndicatorState,
30
32
  type DndItem as DndItemType
31
33
  } from './drawer-dnd-helpers'
32
- import {
33
- getFocusableItems,
34
- getNextFocusedIndex,
35
- selectFocusedItem
36
- } from './drawer-keyboard-helpers'
37
34
 
38
35
  const flipDurationMs = 150
39
36
 
@@ -44,24 +41,25 @@
44
41
  widthClass = 'w-60',
45
42
  collapsibleGroups = true,
46
43
  flagPosition = 'after',
44
+ autofocus = false,
47
45
  onclick,
48
46
  onselect,
49
47
  onreorder,
50
48
  ondropitem,
51
49
  children,
52
50
  groups,
51
+ class: className,
53
52
  ...rest
54
53
  }: DrawerContextProps = $props()
55
54
 
56
55
  type DndItem = DrawerOption & { id: string }
57
56
 
58
57
  let containerClasses = $derived(
59
- clsx(
58
+ cn(
60
59
  widthClass,
61
- 'border border-border rounded-2xl shadow-lg bg-background flex flex-col py-1 max-h-[568px] overflow-y-auto list-none',
62
- {
63
- '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden': draggable
64
- }
60
+ 'border border-border rounded-2xl shadow-lg bg-background flex flex-col py-1 max-h-[568px] overflow-y-auto list-none outline-none',
61
+ clsx({ '[scrollbar-width:none] [&::-webkit-scrollbar]:hidden': draggable }),
62
+ className
65
63
  )
66
64
  )
67
65
 
@@ -88,31 +86,11 @@
88
86
  return { groupedItems: grouped, ungroupedItems: ungrouped }
89
87
  })
90
88
 
91
- // Items in display order (matches visual rendering order)
92
- let itemsInDisplayOrder = $derived.by(() => {
93
- const displayOrder: DrawerOption[] = []
94
-
95
- if (hasGroups && groups) {
96
- // Add grouped items in group order, only if group is open (when collapsible)
97
- groups.forEach((group) => {
98
- const isOpen = collapsibleGroups ? openGroups[group.slug] : true
99
- if (isOpen) {
100
- const groupItems = groupedItems.get(group.slug) || []
101
- displayOrder.push(...groupItems)
102
- }
103
- })
104
- }
105
-
106
- // Add ungrouped items
107
- displayOrder.push(...ungroupedItems)
108
-
109
- return displayOrder
110
- })
111
-
112
89
  let openGroups = $state<Record<string, boolean>>({})
113
90
  let groupDndItems = $state<Record<string, DndItem[]>>({})
114
91
  let ungroupedDndItems = $state<DndItem[]>([])
115
92
  let mounted = $state(false)
93
+ let commandRootEl = $state<HTMLElement | null>(null)
116
94
  let itemsCache = $state<DrawerOption[]>([])
117
95
  let isDragging = $state(false)
118
96
  let draggedItemId = $state<string | null>(null)
@@ -120,8 +98,6 @@
120
98
  let draggedOverGroup = $state<string | null>(null)
121
99
  let dropIndicator = $state<DropIndicatorState>(null)
122
100
  let cleanupFunctions: (() => void)[] = []
123
- let focusedIndex = $state<number>(-1)
124
- let containerRef = $state<HTMLDivElement | null>(null)
125
101
 
126
102
  // Build internal DND items from external items
127
103
  function buildListIn() {
@@ -204,19 +180,21 @@
204
180
  buildListIn()
205
181
  mounted = true
206
182
 
207
- // Set up auto-scroll
183
+ if (autofocus) {
184
+ setTimeout(() => {
185
+ commandRootEl?.focus()
186
+ }, 10)
187
+ }
188
+
208
189
  const autoScrollCleanup = autoScrollForElements({
209
190
  element: document.documentElement
210
191
  })
211
192
  cleanupFunctions.push(autoScrollCleanup)
212
-
213
- window.addEventListener('keydown', handleKeyDown)
214
193
  })
215
194
 
216
195
  onDestroy(() => {
217
196
  cleanupFunctions.forEach((cleanup) => cleanup())
218
197
  cleanupFunctions = []
219
- window.removeEventListener('keydown', handleKeyDown)
220
198
  })
221
199
 
222
200
  function emitGroupDistribution() {
@@ -445,37 +423,11 @@
445
423
  openGroups = openGroups[groupSlug] ? {} : { [groupSlug]: true }
446
424
  }
447
425
 
448
- let focusedItemValue = $derived.by(() => {
449
- const focusableItems = getFocusableItems(itemsInDisplayOrder)
450
- if (focusedIndex >= 0 && focusedIndex < focusableItems.length) {
451
- return focusableItems[focusedIndex].value
452
- }
453
- return null
454
- })
455
-
456
- function handleKeyDown(event: KeyboardEvent) {
457
- // Don't handle if container doesn't exist
458
- if (!containerRef || !document.body.contains(containerRef)) return
459
-
460
- const focusableItems = getFocusableItems(itemsInDisplayOrder)
461
- if (focusableItems.length === 0) return
462
-
463
- if (event.key === 'ArrowDown') {
464
- event.preventDefault()
465
- focusedIndex = getNextFocusedIndex(focusedIndex, 'down', focusableItems.length)
466
- } else if (event.key === 'ArrowUp') {
467
- event.preventDefault()
468
- focusedIndex = getNextFocusedIndex(focusedIndex, 'up', focusableItems.length)
469
- } else if (event.key === ' ' || event.key === 'Enter') {
470
- event.preventDefault()
471
- const result = selectFocusedItem(itemsInDisplayOrder, focusedIndex, multiple)
472
- if (result) {
473
- if (result.shouldUpdate) {
474
- updateItem(result.item)
475
- } else {
476
- onclick?.(result.item.value)
477
- }
478
- }
426
+ function handleItemSelect(item: DrawerOption) {
427
+ if (multiple) {
428
+ updateItem({ ...item, selected: !item.selected })
429
+ } else {
430
+ onclick?.(item.value)
479
431
  }
480
432
  }
481
433
  </script>
@@ -487,21 +439,28 @@
487
439
  {#snippet itemChildren()}
488
440
  {@render item.content?.(item)}
489
441
  {/snippet}
490
- <div class:px-1={!item.groupBy} class:cursor-grab={draggable && !item.locked}>
442
+ <Command.Item
443
+ value={String(item.value)}
444
+ disabled={item.disabled || item.locked}
445
+ onSelect={() => handleItemSelect(item)}
446
+ class={clsx('group outline-none', { 'px-1': !item.groupBy, 'cursor-grab': draggable && !item.locked })}
447
+ >
491
448
  <DrawerContextItem
492
- item={{ ...item, focused: item.value === focusedItemValue, flagPosition: item.flagPosition || flagPosition }}
449
+ item={{ ...item, flagPosition: item.flagPosition || flagPosition }}
493
450
  {multiple}
494
451
  {onclick}
495
452
  onchange={updateItem}
496
453
  children={item.content ? itemChildren : undefined}
497
454
  />
498
- </div>
455
+ </Command.Item>
499
456
  {/if}
500
457
  {/snippet}
501
458
 
502
- <div
503
- bind:this={containerRef}
459
+ <Command.Root
460
+ shouldFilter={false}
461
+ loop
504
462
  class={containerClasses}
463
+ bind:ref={commandRootEl}
505
464
  {...rest}
506
465
  >
507
466
  {@render children?.()}
@@ -644,4 +603,4 @@
644
603
  </div>
645
604
  {/if}
646
605
  {/if}
647
- </div>
606
+ </Command.Root>
@@ -31,8 +31,10 @@
31
31
  clsx(
32
32
  'px-2 py-1.5 space-x-1.5',
33
33
  { 'bg-background-selected': item?.selected && !multiple },
34
- { 'bg-background-default-secondary': item?.focused && shouldShowHoverStyle },
35
- { 'group-hover:bg-background-default-secondary': shouldShowHoverStyle }
34
+ {
35
+ 'group-hover:bg-background-default-secondary group-data-[selected]:bg-background-default-secondary':
36
+ shouldShowHoverStyle
37
+ }
36
38
  )
37
39
  )
38
40
 
@@ -66,7 +68,7 @@
66
68
 
67
69
  <button
68
70
  bind:this={el}
69
- class="cursor-pointer w-full disabled:opacity-30 group"
71
+ class="cursor-pointer w-full disabled:opacity-30 group focus:outline-none"
70
72
  disabled={item?.disabled}
71
73
  onclick={handleClick}
72
74
  >
@@ -170,6 +170,10 @@
170
170
  <BaseFlag country={selectedItem.country} />
171
171
  {@render label()}
172
172
  </div>
173
+ {:else if !multiple && selectedItem?.content}
174
+ <div class="flex items-center gap-1 flex-1 min-w-0">
175
+ {@render selectedItem.content(selectedItem)}
176
+ </div>
173
177
  {:else if selectedIcon || resolvedIcon}
174
178
  <div class="flex items-center gap-1 flex-1 min-w-0">
175
179
  {#if typeof selectedIcon === 'string'}
@@ -189,6 +193,7 @@
189
193
  <DrawerContext
190
194
  data-dropdown-select-content
191
195
  widthClass="min-w-[256px]"
196
+ autofocus
192
197
  {multiple}
193
198
  {items}
194
199
  {flagPosition}
@@ -99,8 +99,13 @@ export function handleScrollEvent(event, lastScrollLeft, columnDropdowns) {
99
99
  }
100
100
  // Keyboard navigation helpers
101
101
  export function shouldIgnoreKeyEvent(event) {
102
- const targetTag = event.target.tagName;
103
- return targetTag === 'INPUT' || targetTag === 'TEXTAREA';
102
+ const target = event.target;
103
+ const targetTag = target.tagName;
104
+ if (targetTag === 'INPUT' || targetTag === 'TEXTAREA')
105
+ return true;
106
+ if (target.closest('[data-command-root]'))
107
+ return true;
108
+ return false;
104
109
  }
105
110
  export function handleArrowDown(currentIndex, rows, shiftKey, enableSelection, onScroll) {
106
111
  if (currentIndex === -1 && rows.length > 0) {
package/dist/types.d.ts CHANGED
@@ -29,7 +29,6 @@ export type DrawerOption = SelectOption & {
29
29
  separator?: boolean;
30
30
  destructive?: boolean;
31
31
  selected?: boolean;
32
- focused?: boolean;
33
32
  icon?: IconSource | string | undefined;
34
33
  rightIcon?: IconSource | undefined;
35
34
  country?: string;
@@ -346,12 +345,14 @@ export interface DrawerContextProps {
346
345
  widthClass?: string;
347
346
  collapsibleGroups?: boolean;
348
347
  flagPosition?: 'before' | 'after';
348
+ autofocus?: boolean;
349
349
  onclick?: (value: AnyProp) => void;
350
350
  onselect?: (selected: DrawerOption[]) => void;
351
351
  onreorder?: (items: DrawerOption[]) => void;
352
352
  ondropitem?: (groups: Record<string, DrawerOption[]>) => void;
353
353
  children?: Snippet;
354
354
  groups?: DrawerGroup[];
355
+ class?: string;
355
356
  [key: string]: unknown;
356
357
  }
357
358
  export interface DrawerContextItemProps {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@invopop/popui",
3
3
  "license": "MIT",
4
- "version": "0.1.74",
4
+ "version": "0.1.76",
5
5
  "repository": {
6
6
  "url": "https://github.com/invopop/popui"
7
7
  },
@@ -1,16 +0,0 @@
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;
@@ -1,41 +0,0 @@
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
- }