Skip to content

Commit

Permalink
fix: select types (#217)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Dec 6, 2023
1 parent 552a3db commit 4b90f49
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-coins-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix: generic type inference for `Select` component
28 changes: 18 additions & 10 deletions src/lib/bits/select/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,34 @@
import type { CreateSelectProps, SelectOptionProps } from "@melt-ui/svelte";
import type { AsChild, Expand, OmitFloating, OnChangeFn } from "$lib/internal/index.js";
import type { ContentProps, ArrowProps } from "$lib/bits/floating/_types.js";
import type { Selected } from "$lib";

type Items<T> = {
value: T;
label?: string;
}[];
export type WhenTrue<TrueOrFalse, IfTrue, IfFalse, IfNeither = IfTrue | IfFalse> = [
TrueOrFalse
] extends [true]
? IfTrue
: [TrueOrFalse] extends [false]
? IfFalse
: IfNeither;

type SelectValue<T, Multiple extends boolean> = WhenTrue<Multiple, T[] | undefined, T | undefined>;

type Props<T = unknown, Multiple extends boolean = false> = Expand<
OmitFloating<Omit<CreateSelectProps, "selected" | "defaultSelected" | "onSelectedChange">> & {
OmitFloating<
Omit<CreateSelectProps, "selected" | "defaultSelected" | "onSelectedChange" | "multiple">
> & {
/**
* The selected value of the select.
* You can bind this to a value to programmatically control the selected value.
*
* @defaultValue undefined
*/
selected?: CreateSelectProps<T, Multiple>["defaultSelected"] & {};
selected?: SelectValue<Selected<T>, Multiple> | undefined;

/**
* A callback function called when the selected value changes.
*/
onSelectedChange?: OnChangeFn<CreateSelectProps<T, Multiple>["defaultSelected"]>;
onSelectedChange?: OnChangeFn<SelectValue<Selected<T>, Multiple>>;

/**
* The open state of the select menu.
Expand All @@ -46,10 +54,10 @@ type Props<T = unknown, Multiple extends boolean = false> = Expand<
multiple?: Multiple;

/**
* Optional array of items to add type-safety to the
* `onSelectedChange` callback and `selected` prop.
* Optionally provide an array of `Selected<T>` objects to
* type the `selected` and `onSelectedChange` props.
*/
items?: Items<T>;
items?: Selected<T>[];
}
>;

Expand Down
34 changes: 15 additions & 19 deletions src/lib/bits/select/components/select.svelte
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
<script lang="ts">
<script lang="ts" context="module">
type T = unknown;
type Multiple = boolean;
</script>

<script lang="ts" generics="T, Multiple extends boolean = false">
import { derived } from "svelte/store";
import { setCtx } from "../ctx.js";
import type { Props } from "../types.js";
type T = $$Generic<unknown>;
type Multiple = $$Generic<boolean>;
type $$Props = Omit<Props<T, Multiple>, "multiple"> & {
items?: Items<T>;
multiple?: Multiple;
};
type Items<T> = {
value: T;
label?: string;
}[];
type $$Props = Props<T, Multiple>;
export let required: $$Props["required"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
Expand All @@ -29,14 +23,13 @@
export let onSelectedChange: $$Props["onSelectedChange"] = undefined;
export let open: $$Props["open"] = undefined;
export let onOpenChange: $$Props["onOpenChange"] = undefined;
// eslint-disable-next-line svelte/valid-compile
export let items: $$Props["items"] = [];
const {
states: { open: localOpen, selected: localSelected },
updateOption,
ids
} = setCtx({
} = setCtx<T, Multiple>({
required,
disabled,
preventScroll,
Expand All @@ -49,9 +42,10 @@
forceVisible: true,
defaultSelected: Array.isArray(selected)
? ([...selected] as $$Props["selected"])
: selected,
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
(selected as any),
defaultOpen: open,
onSelectedChange: ({ next }) => {
onSelectedChange: (({ next }: { next: $$Props["selected"] }) => {
if (Array.isArray(next)) {
if (JSON.stringify(next) !== JSON.stringify(selected)) {
onSelectedChange?.(next);
Expand All @@ -65,7 +59,8 @@
selected = next;
}
return next;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
onOpenChange: ({ next }) => {
if (open !== next) {
onOpenChange?.(next);
Expand All @@ -90,7 +85,8 @@
localSelected.set(
Array.isArray(selected)
? ([...selected] as $$Props["selected"])
: selected
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
(selected as any)
);
$: updateOption("required", required);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bits/select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
import type { CustomEventHandler } from "$lib/index.js";
import type * as I from "./_types.js";

type Props<T = unknown, Multiple extends boolean = false> = I.Props<T, Multiple>;
type Props<T, Multiple extends boolean = false> = I.Props<T, Multiple>;

type ContentProps<
T extends Transition = Transition,
Expand Down
9 changes: 6 additions & 3 deletions src/tests/select/Select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const testItems: Item[] = [
}
];

function setup(props: Select.Props = {}, items: Item[] = testItems) {
function setup(props: Select.Props<unknown, false> = {}, options: Item[] = testItems) {
const user = userEvent.setup();
const returned = render(SelectTest, { ...props, items });
const returned = render(SelectTest, { ...props, options });
const trigger = returned.getByTestId("trigger");
const input = returned.getByTestId("input");
return {
Expand All @@ -38,7 +38,10 @@ function setup(props: Select.Props = {}, items: Item[] = testItems) {
...returned
};
}
async function open(props: Select.Props = {}, openWith: "click" | (string & {}) = "click") {
async function open(
props: Select.Props<unknown, false> = {},
openWith: "click" | (string & {}) = "click"
) {
const returned = setup(props);
const { trigger, getByTestId, queryByTestId, user } = returned;
expect(queryByTestId("content")).toBeNull();
Expand Down
10 changes: 5 additions & 5 deletions src/tests/select/SelectTest.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" context="module">
export type Item = {
value: string;
value: unknown;
label: string;
disabled?: boolean;
};
Expand All @@ -9,12 +9,12 @@
<script lang="ts">
import { Select } from "$lib";
type $$Props = Select.Props & {
items: Item[];
type $$Props = Select.Props<unknown> & {
options: Item[];
};
export let selected: $$Props["selected"] = undefined;
export let open: $$Props["open"] = false;
export let items: Item[] = [];
export let options: Item[] = [];
export let placeholder = "Select something";
</script>

Expand All @@ -26,7 +26,7 @@
<Select.Content data-testid="content">
<Select.Group data-testid="group">
<Select.Label data-testid="label">Options</Select.Label>
{#each items as { value, label, disabled }}
{#each options as { value, label, disabled }}
<Select.Item data-testid={value} {disabled} {value} {label}>
<Select.ItemIndicator>
<span data-testid="{value}-indicator">x</span>
Expand Down

0 comments on commit 4b90f49

Please sign in to comment.