Skip to content

Commit 58f5968

Browse files
committed
init
1 parent d7b5534 commit 58f5968

4 files changed

Lines changed: 175 additions & 61 deletions

File tree

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,97 @@
11
<script lang="ts">
2-
import { Tooltip, Dialog } from "bits-ui";
2+
import { Combobox } from "bits-ui";
3+
import CaretUpDown from "phosphor-svelte/lib/CaretUpDown";
4+
import Check from "phosphor-svelte/lib/Check";
5+
import CaretDoubleUp from "phosphor-svelte/lib/CaretDoubleUp";
6+
import CaretDoubleDown from "phosphor-svelte/lib/CaretDoubleDown";
7+
import DemoContainer from "$lib/components/demo-container.svelte";
38
4-
let open = $state(false);
9+
const fruits = [
10+
{ value: "mango", label: "Mango" },
11+
{ value: "watermelon", label: "Watermelon" },
12+
{ value: "apple", label: "Apple" },
13+
{ value: "pineapple", label: "Pineapple" },
14+
{ value: "orange", label: "Orange" },
15+
{ value: "grape", label: "Grape" },
16+
{ value: "strawberry", label: "Strawberry" },
17+
{ value: "banana", label: "Banana" },
18+
{ value: "kiwi", label: "Kiwi" },
19+
{ value: "peach", label: "Peach" },
20+
{ value: "cherry", label: "Cherry" },
21+
{ value: "blueberry", label: "Blueberry" },
22+
{ value: "raspberry", label: "Raspberry" },
23+
{ value: "blackberry", label: "Blackberry" },
24+
{ value: "plum", label: "Plum" },
25+
{ value: "apricot", label: "Apricot" },
26+
{ value: "pear", label: "Pear" },
27+
{ value: "grapefruit", label: "Grapefruit" },
28+
];
29+
30+
let searchValue = $state("");
31+
32+
const filteredFruits = $derived(
33+
searchValue === ""
34+
? fruits
35+
: fruits.filter((fruit) =>
36+
fruit.label.toLowerCase().includes(searchValue.toLowerCase())
37+
)
38+
);
539
</script>
640

7-
<Tooltip.Provider>
8-
<Tooltip.Root delayDuration={200} disableCloseOnTriggerClick={true}>
9-
<Tooltip.Trigger
10-
class="inline-flex size-fit items-center justify-center"
11-
onclick={() => (open = true)}
41+
<DemoContainer>
42+
<Combobox.Root
43+
type="multiple"
44+
name="favoriteFruit"
45+
onOpenChangeComplete={(o) => {
46+
if (!o) searchValue = "";
47+
}}
48+
>
49+
<Combobox.Trigger
50+
class="bg-background h-input pointer-events-auto flex w-[200px] items-center justify-between rounded-[9px] border px-3 py-2"
1251
>
13-
Hover Me & Then Click
14-
</Tooltip.Trigger>
15-
<Tooltip.Content sideOffset={8} side="bottom">
16-
<div
17-
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 flex items-center justify-center border p-3 text-sm font-medium"
18-
>
19-
Tooltip Content
20-
</div>
21-
</Tooltip.Content>
22-
</Tooltip.Root>
23-
24-
<Dialog.Root bind:open>
25-
<Dialog.Portal>
26-
<Dialog.Content
27-
class="rounded-input border-dark-10 bg-background shadow-popover outline-hidden z-0 border p-3 text-sm font-medium"
52+
Select a fruit
53+
<CaretUpDown class="text-muted-foreground size-6" />
54+
</Combobox.Trigger>
55+
<Combobox.Portal>
56+
<Combobox.Content
57+
class="focus-override border-muted bg-background shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden z-50 h-96 max-h-[var(--bits-combobox-content-available-height)] w-[var(--bits-combobox-anchor-width)] min-w-[var(--bits-combobox-anchor-width)] select-none rounded-xl border px-1 py-3 data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1"
58+
sideOffset={10}
2859
>
29-
<p>Dialog Content</p>
30-
<p>
31-
Click "Close" to close dialog and hover tooltip again. The tooltip will not
32-
appear.
33-
</p>
34-
<Dialog.Close class="block">Close</Dialog.Close>
35-
</Dialog.Content>
36-
</Dialog.Portal>
37-
</Dialog.Root>
38-
</Tooltip.Provider>
60+
<!-- <Combobox.Input
61+
oninput={(e) => (searchValue = e.currentTarget.value)}
62+
class="h-input rounded-9px border-border-input bg-background placeholder:text-foreground-alt/50 focus:ring-foreground focus:ring-offset-background focus:outline-hidden inline-flex w-[296px] touch-none truncate border px-11 text-base transition-colors focus:ring-2 focus:ring-offset-2 sm:text-sm"
63+
placeholder="Search a fruit"
64+
aria-label="Search a fruit"
65+
/> -->
66+
<Combobox.ScrollUpButton class="flex w-full items-center justify-center py-1">
67+
<CaretDoubleUp class="size-3" />
68+
</Combobox.ScrollUpButton>
69+
<Combobox.Viewport class="p-1">
70+
{#each filteredFruits as fruit, i (i + fruit.value)}
71+
<Combobox.Item
72+
class="rounded-button data-highlighted:bg-muted outline-hidden flex h-10 w-full select-none items-center py-3 pl-5 pr-1.5 text-sm capitalize"
73+
value={fruit.value}
74+
label={fruit.label}
75+
>
76+
{#snippet children({ selected })}
77+
{fruit.label}
78+
{#if selected}
79+
<div class="ml-auto">
80+
<Check />
81+
</div>
82+
{/if}
83+
{/snippet}
84+
</Combobox.Item>
85+
{:else}
86+
<span class="block px-5 py-2 text-sm text-muted-foreground">
87+
No results found, try again.
88+
</span>
89+
{/each}
90+
</Combobox.Viewport>
91+
<Combobox.ScrollDownButton class="flex w-full items-center justify-center py-1">
92+
<CaretDoubleDown class="size-3" />
93+
</Combobox.ScrollDownButton>
94+
</Combobox.Content>
95+
</Combobox.Portal>
96+
</Combobox.Root>
97+
</DemoContainer>

packages/bits-ui/src/lib/bits/combobox/components/combobox-input.svelte

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { ComboboxInputProps } from "../types.js";
44
import { useId } from "$lib/internal/use-id.js";
55
import { FloatingLayer } from "$lib/bits/utilities/floating-layer/index.js";
6-
import { SelectInputState } from "$lib/bits/select/select.svelte.js";
6+
import { ComboboxInputState, SelectContentContext } from "$lib/bits/select/select.svelte.js";
77
88
let {
99
id = useId(),
@@ -14,14 +14,19 @@
1414
...restProps
1515
}: ComboboxInputProps = $props();
1616
17-
const inputState = SelectInputState.create({
18-
id: boxWith(() => id),
19-
ref: boxWith(
20-
() => ref,
21-
(v) => (ref = v)
22-
),
23-
clearOnDeselect: boxWith(() => clearOnDeselect),
24-
});
17+
const contentState = SelectContentContext.getOr(null);
18+
19+
const inputState = ComboboxInputState.create(
20+
{
21+
id: boxWith(() => id),
22+
ref: boxWith(
23+
() => ref,
24+
(v) => (ref = v)
25+
),
26+
clearOnDeselect: boxWith(() => clearOnDeselect),
27+
},
28+
contentState
29+
);
2530
2631
if (defaultValue) {
2732
inputState.root.opts.inputValue.current = defaultValue;
@@ -32,10 +37,18 @@
3237
);
3338
</script>
3439

35-
<FloatingLayer.Anchor {id} ref={inputState.opts.ref}>
40+
{#if contentState}
3641
{#if child}
3742
{@render child({ props: mergedProps })}
3843
{:else}
39-
<input {...mergedProps} />
44+
<input bind:this={inputState.opts.ref.current} {...mergedProps} />
4045
{/if}
41-
</FloatingLayer.Anchor>
46+
{:else}
47+
<FloatingLayer.Anchor {id} ref={inputState.opts.ref}>
48+
{#if child}
49+
{@render child({ props: mergedProps })}
50+
{:else}
51+
<input {...mergedProps} />
52+
{/if}
53+
</FloatingLayer.Anchor>
54+
{/if}

packages/bits-ui/src/lib/bits/combobox/components/combobox-trigger.svelte

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { boxWith, mergeProps } from "svelte-toolbelt";
33
import type { ComboboxTriggerProps } from "../types.js";
44
import { useId } from "$lib/internal/use-id.js";
5-
import { SelectComboTriggerState } from "$lib/bits/select/select.svelte.js";
5+
import { ComboboxTriggerState } from "$lib/bits/select/select.svelte.js";
6+
import FloatingLayerAnchor from "$lib/bits/utilities/floating-layer/components/floating-layer-anchor.svelte";
67
78
let {
89
id = useId(),
@@ -13,7 +14,7 @@
1314
...restProps
1415
}: ComboboxTriggerProps = $props();
1516
16-
const triggerState = SelectComboTriggerState.create({
17+
const triggerState = ComboboxTriggerState.create({
1718
id: boxWith(() => id),
1819
ref: boxWith(
1920
() => ref,
@@ -22,9 +23,29 @@
2223
});
2324
2425
const mergedProps = $derived(mergeProps(restProps, triggerState.props, { type }));
26+
const isFloatingAnchor = $derived.by(() => {
27+
if (triggerState.root.inputNode && !triggerState.root.hasInputInContent) {
28+
return false;
29+
}
30+
31+
if (!triggerState.root.inputNode) {
32+
return true;
33+
}
34+
return false;
35+
});
2536
</script>
2637

27-
{#if child}
38+
{#if isFloatingAnchor}
39+
<FloatingLayerAnchor {id} ref={triggerState.opts.ref}>
40+
{#if child}
41+
{@render child({ props: mergedProps })}
42+
{:else}
43+
<button {...mergedProps}>
44+
{@render children?.()}
45+
</button>
46+
{/if}
47+
</FloatingLayerAnchor>
48+
{:else if child}
2849
{@render child({ props: mergedProps })}
2950
{:else}
3051
<button {...mergedProps}>

packages/bits-ui/src/lib/bits/select/select.svelte.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ const selectAttrs = createBitsAttrs({
7070

7171
const SelectRootContext = new Context<SelectRoot>("Select.Root | Combobox.Root");
7272
const SelectGroupContext = new Context<SelectGroupState>("Select.Group | Combobox.Group");
73-
const SelectContentContext = new Context<SelectContentState>("Select.Content | Combobox.Content");
73+
export const SelectContentContext = new Context<SelectContentState>(
74+
"Select.Content | Combobox.Content"
75+
);
7476

7577
interface SelectBaseRootStateOpts
7678
extends ReadableBoxedValues<{
@@ -113,6 +115,7 @@ abstract class SelectBaseRootState {
113115
isUsingKeyboard = false;
114116
isCombobox = false;
115117
domContext = new DOMContext(() => null);
118+
hasInputInContent = $state(false);
116119

117120
constructor(opts: SelectBaseRootStateOpts) {
118121
this.opts = opts;
@@ -370,21 +373,24 @@ export class SelectRootState {
370373

371374
type SelectRoot = SelectSingleRootState | SelectMultipleRootState;
372375

373-
interface SelectInputStateOpts
376+
interface ComboboxInputStateOpts
374377
extends WithRefOpts,
375378
ReadableBoxedValues<{
376379
clearOnDeselect: boolean;
377380
}> {}
378381

379-
export class SelectInputState {
380-
static create(opts: SelectInputStateOpts) {
381-
return new SelectInputState(opts, SelectRootContext.get());
382+
export class ComboboxInputState {
383+
static create(opts: ComboboxInputStateOpts, contentState: SelectContentState | null = null) {
384+
if (contentState) {
385+
contentState.registerInput();
386+
}
387+
return new ComboboxInputState(opts, SelectRootContext.get());
382388
}
383-
readonly opts: SelectInputStateOpts;
389+
readonly opts: ComboboxInputStateOpts;
384390
readonly root: SelectRoot;
385391
readonly attachment: RefAttachment;
386392

387-
constructor(opts: SelectInputStateOpts, root: SelectRoot) {
393+
constructor(opts: ComboboxInputStateOpts, root: SelectRoot) {
388394
this.opts = opts;
389395
this.root = root;
390396
this.attachment = attachRef(opts.ref, (v) => (this.root.inputNode = v));
@@ -533,17 +539,17 @@ export class SelectInputState {
533539
);
534540
}
535541

536-
interface SelectComboTriggerStateOpts extends WithRefOpts {}
542+
interface ComboboxTriggerStateOpts extends WithRefOpts {}
537543

538-
export class SelectComboTriggerState {
539-
static create(opts: SelectComboTriggerStateOpts) {
540-
return new SelectComboTriggerState(opts, SelectRootContext.get());
544+
export class ComboboxTriggerState {
545+
static create(opts: ComboboxTriggerStateOpts) {
546+
return new ComboboxTriggerState(opts, SelectRootContext.get());
541547
}
542-
readonly opts: SelectComboTriggerStateOpts;
548+
readonly opts: ComboboxTriggerStateOpts;
543549
readonly root: SelectBaseRootState;
544550
readonly attachment: RefAttachment;
545551

546-
constructor(opts: SelectComboTriggerStateOpts, root: SelectBaseRootState) {
552+
constructor(opts: ComboboxTriggerStateOpts, root: SelectBaseRootState) {
547553
this.opts = opts;
548554
this.root = root;
549555
this.attachment = attachRef(opts.ref);
@@ -569,7 +575,11 @@ export class SelectComboTriggerState {
569575
onpointerdown(e: BitsPointerEvent) {
570576
if (this.root.opts.disabled.current || !this.root.domContext) return;
571577
e.preventDefault();
572-
if (this.root.domContext.getActiveElement() !== this.root.inputNode) {
578+
579+
if (
580+
this.root.inputNode &&
581+
this.root.domContext.getActiveElement() !== this.root.inputNode
582+
) {
573583
this.root.inputNode?.focus();
574584
}
575585
this.root.toggleMenu();
@@ -872,6 +882,7 @@ export class SelectContentState {
872882
viewportNode = $state<HTMLElement | null>(null);
873883
isPositioned = $state(false);
874884
domContext: DOMContext;
885+
containsInput = $state(false);
875886

876887
constructor(opts: SelectContentStateOpts, root: SelectRoot) {
877888
this.opts = opts;
@@ -903,6 +914,16 @@ export class SelectContentState {
903914
this.root.isUsingKeyboard = false;
904915
}
905916

917+
registerInput() {
918+
this.containsInput = true;
919+
this.root.hasInputInContent = true;
920+
921+
return () => {
922+
this.containsInput = false;
923+
this.root.hasInputInContent = false;
924+
};
925+
}
926+
906927
readonly #styles = $derived.by(() => {
907928
return getFloatingContentCSSVars(this.root.isCombobox ? "combobox" : "select");
908929
});

0 commit comments

Comments
 (0)