@human-kit/svelte-components 1.0.0-alpha.10 → 1.0.0-alpha.11

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.
@@ -66,6 +66,7 @@
66
66
 
67
67
  // Virtual focus from ComboBox context
68
68
  const isFocused = $derived(ctx.focusedItemId === id);
69
+ const isFocusVisible = $derived(isFocused && ctx.isFocusVisible);
69
70
 
70
71
  // Generate unique ID using instanceId
71
72
  const uniqueId = $derived(`combobox-item-${ctx.instanceId}-${id}`);
@@ -120,6 +121,7 @@
120
121
  customId={uniqueId}
121
122
  disableFocusHandling={true}
122
123
  isFocusedOverride={isFocused}
124
+ isFocusVisibleOverride={isFocusVisible}
123
125
  onItemSelect={handleSelect}
124
126
  onResolvedTextValue={handleResolvedTextValue}
125
127
  scrollOnFocus={true}
@@ -7,7 +7,21 @@
7
7
  Name: `ComboBox.Popover`
8
8
  Description: Floating container for combobox options. Internally composes `Popover.Root` and `Popover.Content` in non-modal mode.
9
9
 
10
- | Prop | Type | Default | Description |
11
- | ---------- | --------- | ----------- | ------------------------------------------- |
12
- | `class` | `string` | `''` | CSS class names for the floating panel. |
13
- | `children` | `Snippet` | `undefined` | Popover content, typically `ComboBox.List`. |
10
+ | Prop | Type | Default | Description |
11
+ | ------------------------------ | ---------------------------------- | ---------------- | -------------------------------------------------------------------------- |
12
+ | `offset` | `number` | `8` | Main-axis offset from the combobox trigger. |
13
+ | `placement` | `ExtendedPlacement` | `'bottom-start'` | Preferred floating placement. |
14
+ | `shouldFlip` | `boolean` | `true` | Enables automatic fallback placement when space is limited. |
15
+ | `boundaryElement` | `Element \| null` | `null` | Optional boundary element for positioning constraints. |
16
+ | `class` | `string` | `''` | CSS class names for the floating panel. |
17
+ | `children` | `Snippet` | `undefined` | Popover content, typically `ComboBox.List`. |
18
+ | `isNonModal` | `boolean` | `true` | Controls whether the popover behaves as a non-modal overlay. |
19
+ | `shouldCloseOnInteractOutside` | `boolean` | `true` | Closes when interacting outside the panel. |
20
+ | `shouldCloseOnEscape` | `boolean` | `true` | Closes on Escape key press. |
21
+ | `shouldCloseOnBlur` | `boolean` | `true` | Closes on focus leaving trigger/content in the combobox interaction model. |
22
+ | `initialFocus` | `FocusTrapOptions['initialFocus']` | `undefined` | Initial focus target when modal focus trapping is enabled. |
23
+
24
+ ## Notes
25
+
26
+ - `ComboBox.Popover` forwards all `Popover.Content` configuration props except the controlled open-state wiring (`open`, `triggerRef`, and `onOpenChange`).
27
+ - The default placement is `bottom-start` to match the combobox input.
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from 'svelte';
3
+ import ComboBox from '../index';
4
+ import type { PopoverContent } from '../../popover';
5
+
6
+ type Props = {
7
+ offset?: number;
8
+ placement?: ComponentProps<typeof PopoverContent>['placement'];
9
+ shouldFlip?: boolean;
10
+ shouldCloseOnEscape?: boolean;
11
+ };
12
+
13
+ let {
14
+ offset = 8,
15
+ placement = 'bottom-start',
16
+ shouldFlip = true,
17
+ shouldCloseOnEscape = true
18
+ }: Props = $props();
19
+
20
+ const countries = [
21
+ { id: 'ar', name: 'Argentina' },
22
+ { id: 'br', name: 'Brazil' },
23
+ { id: 'ca', name: 'Canada' }
24
+ ];
25
+ </script>
26
+
27
+ <ComboBox.Root trigger="press">
28
+ <ComboBox.Input placeholder="Search countries..." />
29
+ <ComboBox.Trigger />
30
+
31
+ <ComboBox.Popover {offset} {placement} {shouldFlip} {shouldCloseOnEscape}>
32
+ <ComboBox.List emptyPlaceholder="No countries found">
33
+ {#each countries as country (country.id)}
34
+ <ComboBox.Item id={country.id} textValue={country.name}>{country.name}</ComboBox.Item>
35
+ {/each}
36
+ </ComboBox.List>
37
+ </ComboBox.Popover>
38
+ </ComboBox.Root>
@@ -0,0 +1,11 @@
1
+ import type { ComponentProps } from 'svelte';
2
+ import type { PopoverContent } from '../../popover';
3
+ type Props = {
4
+ offset?: number;
5
+ placement?: ComponentProps<typeof PopoverContent>['placement'];
6
+ shouldFlip?: boolean;
7
+ shouldCloseOnEscape?: boolean;
8
+ };
9
+ declare const ComboboxPopoverPropsTest: import("svelte").Component<Props, {}, "">;
10
+ type ComboboxPopoverPropsTest = ReturnType<typeof ComboboxPopoverPropsTest>;
11
+ export default ComboboxPopoverPropsTest;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from 'svelte';
2
+ import type { ComponentProps, Snippet } from 'svelte';
3
3
  import { useComboBoxContext } from '../root/context';
4
4
  import { Popover } from '../../popover';
5
5
  import { focusWithModality, type InputModality } from '../../primitives/input-modality';
@@ -9,12 +9,22 @@
9
9
  * ComboBox.Popover - Just the floating container wrapper.
10
10
  * Should contain ComboBox.ListBox as a child.
11
11
  */
12
- type ComboBoxPopoverProps = {
13
- class?: string;
12
+ type ComboBoxPopoverProps = Omit<
13
+ ComponentProps<typeof Popover.Content>,
14
+ 'open' | 'triggerRef' | 'onOpenChange' | 'children'
15
+ > & {
14
16
  children?: Snippet;
15
17
  };
16
18
 
17
- let { class: className = '', children }: ComboBoxPopoverProps = $props();
19
+ let {
20
+ class: className = '',
21
+ children,
22
+ placement = 'bottom-start',
23
+ isNonModal = true,
24
+ shouldCloseOnEscape = true,
25
+ shouldCloseOnBlur = true,
26
+ ...contentProps
27
+ }: ComboBoxPopoverProps = $props();
18
28
 
19
29
  const ctx = useComboBoxContext();
20
30
  let restoreListboxMaxHeight: (() => void) | undefined;
@@ -168,15 +178,32 @@
168
178
  ctx.inputRef?.focus();
169
179
  }
170
180
  });
181
+
182
+ $effect(() => {
183
+ ctx.setShouldCloseOnEscape(shouldCloseOnEscape);
184
+ return () => {
185
+ ctx.setShouldCloseOnEscape(true);
186
+ };
187
+ });
188
+
189
+ $effect(() => {
190
+ ctx.setShouldCloseOnBlur(shouldCloseOnBlur);
191
+ return () => {
192
+ ctx.setShouldCloseOnBlur(true);
193
+ };
194
+ });
171
195
  </script>
172
196
 
173
197
  <Popover.Root open={ctx.isOpen} triggerRef={ctx.triggerRef} onOpenChange={handleOpenChange}>
174
198
  <Popover.Content
175
- isNonModal={true}
176
- placement="bottom-start"
199
+ {isNonModal}
200
+ {placement}
201
+ {shouldCloseOnEscape}
202
+ {shouldCloseOnBlur}
177
203
  class={className}
178
204
  onmousedown={handleMouseDown}
179
205
  onwheel={handleWheel}
206
+ {...contentProps}
180
207
  >
181
208
  {#if children}
182
209
  {@render children()}
@@ -1,10 +1,10 @@
1
- import type { Snippet } from 'svelte';
1
+ import type { ComponentProps, Snippet } from 'svelte';
2
+ import { Popover } from '../../popover';
2
3
  /**
3
4
  * ComboBox.Popover - Just the floating container wrapper.
4
5
  * Should contain ComboBox.ListBox as a child.
5
6
  */
6
- type ComboBoxPopoverProps = {
7
- class?: string;
7
+ type ComboBoxPopoverProps = Omit<ComponentProps<typeof Popover.Content>, 'open' | 'triggerRef' | 'onOpenChange' | 'children'> & {
8
8
  children?: Snippet;
9
9
  };
10
10
  declare const ComboboxPopover: import("svelte").Component<ComboBoxPopoverProps, {}, "">;
@@ -107,6 +107,8 @@
107
107
  let focusWithin = $state(false);
108
108
  let focusVisible = $state(false);
109
109
  let popoverPointerDownPending = $state(false);
110
+ let shouldCloseOnEscapeState = $state(true);
111
+ let shouldCloseOnBlurState = $state(true);
110
112
 
111
113
  // Flag to control whether inputValue should be used for filtering
112
114
  // When false, all items are shown regardless of inputValue
@@ -407,6 +409,9 @@
407
409
  return;
408
410
  }
409
411
 
412
+ if (!shouldCloseOnBlurState) {
413
+ return;
414
+ }
410
415
  // Close popover first to prevent flash of options when clearing input
411
416
  closePopover();
412
417
 
@@ -611,16 +616,16 @@
611
616
  }
612
617
  break;
613
618
  case 'Escape':
614
- if (currentIsOpen) {
619
+ if (currentIsOpen && shouldCloseOnEscapeState) {
615
620
  closePopover(true); // Keep focus on input after Escape
616
621
  // Stop propagation so parent dialogs don't also close
617
622
  event.stopPropagation();
618
623
  event.stopImmediatePropagation();
624
+ handleInputBlur();
625
+ // Escape is a keyboard-only path, so focus-visible remains enabled for the input.
626
+ focusVisible = true;
627
+ event.preventDefault();
619
628
  }
620
- handleInputBlur();
621
- // Escape is a keyboard-only path, so focus-visible remains enabled for the input.
622
- focusVisible = true;
623
- event.preventDefault();
624
629
  break;
625
630
  case 'Backspace':
626
631
  // In multiple mode, remove last tag when input is empty
@@ -668,6 +673,15 @@
668
673
  get isPending() {
669
674
  return isPending;
670
675
  },
676
+ get isFocusVisible() {
677
+ return focusVisible;
678
+ },
679
+ get shouldCloseOnEscape() {
680
+ return shouldCloseOnEscapeState;
681
+ },
682
+ get shouldCloseOnBlur() {
683
+ return shouldCloseOnBlurState;
684
+ },
671
685
  get isReadOnly() {
672
686
  return isReadOnly;
673
687
  },
@@ -742,7 +756,13 @@
742
756
  markPopoverPointerDown: () => {
743
757
  popoverPointerDownPending = true;
744
758
  },
745
- consumePopoverPointerDown
759
+ consumePopoverPointerDown,
760
+ setShouldCloseOnEscape: (value: boolean) => {
761
+ shouldCloseOnEscapeState = value;
762
+ },
763
+ setShouldCloseOnBlur: (value: boolean) => {
764
+ shouldCloseOnBlurState = value;
765
+ }
746
766
  };
747
767
 
748
768
  setComboBoxContext(ctx);
@@ -22,6 +22,8 @@ export type ComboBoxContext<T extends object = object> = {
22
22
  isDisabled: boolean;
23
23
  /** Whether the combobox is pending async work */
24
24
  isPending: boolean;
25
+ /** Whether focus should currently be presented as keyboard-visible */
26
+ isFocusVisible: boolean;
25
27
  /** Whether the combobox is read-only */
26
28
  isReadOnly: boolean;
27
29
  /** Selection mode */
@@ -92,6 +94,14 @@ export type ComboBoxContext<T extends object = object> = {
92
94
  markPopoverPointerDown: () => void;
93
95
  /** Consume the pending popover-pointer marker. */
94
96
  consumePopoverPointerDown: () => boolean;
97
+ /** Whether Escape should close the popover. */
98
+ shouldCloseOnEscape: boolean;
99
+ /** Whether blur should close the popover. */
100
+ shouldCloseOnBlur: boolean;
101
+ /** Update whether Escape should close the popover. */
102
+ setShouldCloseOnEscape: (value: boolean) => void;
103
+ /** Update whether blur should close the popover. */
104
+ setShouldCloseOnBlur: (value: boolean) => void;
95
105
  };
96
106
  export declare function setComboBoxContext<T extends object = object>(ctx: ComboBoxContext<T>): void;
97
107
  export declare function getComboBoxContext<T extends object = object>(): ComboBoxContext<T> | undefined;
@@ -30,6 +30,8 @@
30
30
  disableFocusHandling?: boolean;
31
31
  /** Override the focused state. When provided, this value is used instead of internal focus tracking. */
32
32
  isFocusedOverride?: boolean;
33
+ /** Override the focus-visible presentation state. */
34
+ isFocusVisibleOverride?: boolean;
33
35
  /** Override the select behavior. When provided, called instead of default listbox selection. */
34
36
  onItemSelect?: (id: string | number, label: string) => void;
35
37
  /** Callback with resolved text value when mounted (from prop or rendered content). */
@@ -52,6 +54,7 @@
52
54
  customId,
53
55
  disableFocusHandling = false,
54
56
  isFocusedOverride,
57
+ isFocusVisibleOverride,
55
58
  onItemSelect,
56
59
  onResolvedTextValue,
57
60
  scrollOnFocus = false,
@@ -82,6 +85,9 @@
82
85
  ? Boolean(pressedOverride) && !isDisabledComputed
83
86
  : isPressed && !isDisabledComputed
84
87
  );
88
+ const isFocusVisibleComputed = $derived(
89
+ isFocusVisibleOverride !== undefined ? isFocusVisibleOverride : isFocusVisible
90
+ );
85
91
 
86
92
  // ID: use custom if provided, otherwise generate
87
93
  const uniqueId = $derived(customId ?? `listbox-item-${id}`);
@@ -311,7 +317,7 @@
311
317
  data-selected={isSelected || undefined}
312
318
  data-disabled={isDisabledComputed || undefined}
313
319
  data-focused={isFocusedComputed || undefined}
314
- data-focus-visible={isFocusVisible || undefined}
320
+ data-focus-visible={isFocusVisibleComputed || undefined}
315
321
  data-hovered={isHovered || undefined}
316
322
  data-pressed={isPressedComputed || undefined}
317
323
  onpointerdown={handlePointerDown}
@@ -20,6 +20,8 @@ type ListBoxItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'children'>
20
20
  disableFocusHandling?: boolean;
21
21
  /** Override the focused state. When provided, this value is used instead of internal focus tracking. */
22
22
  isFocusedOverride?: boolean;
23
+ /** Override the focus-visible presentation state. */
24
+ isFocusVisibleOverride?: boolean;
23
25
  /** Override the select behavior. When provided, called instead of default listbox selection. */
24
26
  onItemSelect?: (id: string | number, label: string) => void;
25
27
  /** Callback with resolved text value when mounted (from prop or rendered content). */
@@ -57,7 +57,7 @@
57
57
  } & Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'>;
58
58
 
59
59
  let {
60
- offset = 8,
60
+ offset = 4,
61
61
  placement = 'bottom',
62
62
  shouldFlip = true,
63
63
  boundaryElement = null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-kit/svelte-components",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",