Skip to content

Commit

Permalink
next: Controlled State (#676)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Sep 25, 2024
1 parent 9cc6eb7 commit 1f02d0a
Show file tree
Hide file tree
Showing 159 changed files with 1,123 additions and 224 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { RootProps } from "../index.js";
import { mergeProps } from "$lib/internal/mergeProps.js";
import { useId } from "$lib/internal/useId.js";
import { noop } from "$lib/internal/callbacks.js";
let {
disabled = false,
Expand All @@ -13,9 +14,10 @@
value = $bindable(),
ref = $bindable(null),
id = useId(),
onValueChange,
onValueChange = noop,
loop = true,
orientation = "vertical",
controlledValue = false,
...restProps
}: RootProps = $props();
Expand All @@ -26,8 +28,12 @@
value: box.with(
() => value!,
(v) => {
value = v;
onValueChange?.(v as any);
if (controlledValue) {
onValueChange(v as any);
} else {
value = v;
onValueChange(v as any);
}
}
) as WritableBox<string> | WritableBox<string[]>,
id: box.with(() => id),
Expand Down
10 changes: 10 additions & 0 deletions packages/bits-ui/src/lib/bits/accordion/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export type BaseAccordionRootPropsWithoutHTML = {
* @defaultValue "vertical"
*/
orientation?: Orientation;

/**
* Whether the value of the accordion is controlled or not.
* If `true`, the accordion will not update the value internally, instead
* it will call `onValueChange` when it would have otherwise, and it is up to you
* to update the `value` prop.
*
* @defaultValue false
*/
controlledValue?: boolean;
};

export type AccordionRootSinglePropsWithoutHTML = BaseAccordionRootPropsWithoutHTML & {
Expand Down
5 changes: 3 additions & 2 deletions packages/bits-ui/src/lib/bits/button/components/button.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script lang="ts">
import type { Props } from "../index.js";
import type { RootProps } from "../index.js";
let { href, type, children, ...restProps }: Props = $props();
let { href, type, children, ref, ...restProps }: RootProps = $props();
</script>

<svelte:element
this={href ? "a" : "button"}
type={href ? undefined : type}
{href}
tabindex="0"
bind:this={ref}
{...restProps}
>
{@render children?.()}
Expand Down
3 changes: 1 addition & 2 deletions packages/bits-ui/src/lib/bits/button/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { default as Root } from "./components/button.svelte";

export type { ButtonProps as Props } from "./types.js";
export type { ButtonProps as RootProps } from "./types.js";
16 changes: 7 additions & 9 deletions packages/bits-ui/src/lib/bits/button/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import type { WithoutChildren } from "svelte-toolbelt";
import type { WithChildren } from "$lib/shared/index.js";

export type ButtonPropsWithoutHTML = {
export type ButtonPropsWithoutHTML = WithChildren<{
ref?: HTMLElement | null;
};
}>;

type AnchorElement = ButtonPropsWithoutHTML &
HTMLAnchorAttributes & {
href?: HTMLAnchorAttributes["href"];
WithoutChildren<Omit<HTMLAnchorAttributes, "href" | "type">> & {
href: HTMLAnchorAttributes["href"];
type?: never;
};

type ButtonElement = ButtonPropsWithoutHTML &
HTMLButtonAttributes & {
WithoutChildren<Omit<HTMLButtonAttributes, "type" | "href">> & {
type?: HTMLButtonAttributes["type"];
href?: never;
};

export type ButtonProps = AnchorElement | ButtonElement;

export type ButtonEventHandler<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLButtonElement;
};
14 changes: 11 additions & 3 deletions packages/bits-ui/src/lib/bits/calendar/components/calendar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
type,
disableDaysOutsideMonth = true,
initialFocus = false,
controlledValue = false,
controlledPlaceholder = false,
...restProps
}: RootProps = $props();
Expand Down Expand Up @@ -70,7 +72,9 @@
placeholder: box.with(
() => placeholder as DateValue,
(v) => {
if (placeholder !== v) {
if (controlledPlaceholder) {
onPlaceholderChange(v as DateValue);
} else {
placeholder = v;
onPlaceholderChange(v as DateValue);
}
Expand All @@ -80,8 +84,12 @@
value: box.with(
() => value,
(v) => {
value = v;
onValueChange(v as any);
if (controlledValue) {
onValueChange(v as any);
} else {
value = v;
onValueChange(v as any);
}
}
),
type: box.with(() => type),
Expand Down
20 changes: 20 additions & 0 deletions packages/bits-ui/src/lib/bits/calendar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,26 @@ type CalendarBaseRootPropsWithoutHTML = {
* @defaultValue false
*/
disableDaysOutsideMonth?: boolean;

/**
* Whether or not the calendar is controlled or not. If `true`, the calendar will not update
* the value internally, instead it will call `onValueChange` when it would have otherwise,
* and it is up to you to update the `value` prop that is passed to the `Calendar.Root`
* component.
*
* @defaultValue false
*/
controlledValue?: boolean;

/**
* Whether or not the calendar placeholder is controlled or not. If `true`, the calendar will
* not update the placeholder internally, instead it will call `onPlaceholderChange` when it
* would have otherwise, and it is up to you to update the `placeholder` prop that is passed to the
* component.
*
* @defaultValue false
*/
controlledPlaceholder?: boolean;
};

export type CalendarSingleRootPropsWithoutHTML = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
let {
checked = $bindable(false),
ref = $bindable(null),
onCheckedChange,
children,
disabled = false,
required = false,
name = undefined,
value = "on",
id = useId(),
ref = $bindable(null),
controlledChecked = false,
child,
...restProps
}: RootProps = $props();
Expand All @@ -24,7 +25,9 @@
checked: box.with(
() => checked,
(v) => {
if (checked !== v) {
if (controlledChecked) {
onCheckedChange?.(v);
} else {
checked = v;
onCheckedChange?.(v);
}
Expand Down
10 changes: 10 additions & 0 deletions packages/bits-ui/src/lib/bits/checkbox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ export type CheckboxRootPropsWithoutHTML = WithChild<
* A callback function called when the checked state changes.
*/
onCheckedChange?: OnChangeFn<boolean | "indeterminate">;

/**
* Whether or not the checkbox is controlled or not. If `true`, the checkbox will not update
* the checked state internally, instead it will call `onCheckedChange` when it would have
* otherwise, and it is up to you to update the `checked` prop that is passed to the
* component.
*
* @defaultValue false
*/
controlledChecked?: boolean;
},
CheckboxRootSnippetProps
>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useCollapsibleRoot } from "../collapsible.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";
import { useId } from "$lib/internal/useId.js";
import { noop } from "$lib/internal/callbacks.js";
let {
children,
Expand All @@ -12,16 +13,21 @@
ref = $bindable(null),
open = $bindable(false),
disabled = false,
onOpenChange,
controlledOpen = false,
onOpenChange = noop,
...restProps
}: RootProps = $props();
const rootState = useCollapsibleRoot({
open: box.with(
() => open,
(v) => {
open = v;
onOpenChange?.(v);
if (controlledOpen) {
onOpenChange(v);
} else {
open = v;
onOpenChange(v);
}
}
),
disabled: box.with(() => disabled),
Expand Down
9 changes: 9 additions & 0 deletions packages/bits-ui/src/lib/bits/collapsible/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export type CollapsibleRootPropsWithoutHTML = WithChild<{
* A callback function called when the open state changes.
*/
onOpenChange?: OnChangeFn<boolean>;

/**
* Whether or not the collapsible is controlled or not. If `true`, the collapsible will not
* update the open state internally, instead it will call `onOpenChange` when it would have
* otherwise, and it is up to you to update the `value` prop that is passed to the component.
*
* @defaultValue false
*/
controlledOpen?: boolean;
}>;

export type CollapsibleRootProps = CollapsibleRootPropsWithoutHTML &
Expand Down
27 changes: 22 additions & 5 deletions packages/bits-ui/src/lib/bits/combobox/components/combobox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,44 @@
loop = false,
scrollAlignment = "nearest",
required = false,
controlledOpen = false,
controlledValue = false,
children,
}: RootProps = $props();
value === undefined && (value = type === "single" ? "" : []);
if (value === undefined) {
const defaultValue = type === "single" ? "" : [];
if (controlledValue) {
onValueChange(defaultValue as any);
} else {
value = defaultValue;
}
}
useListboxRoot({
type,
value: box.with(
() => value!,
(v) => {
value = v;
onValueChange(v as any);
if (controlledValue) {
onValueChange(v as any);
} else {
value = v;
onValueChange(v as any);
}
}
) as WritableBox<string> | WritableBox<string[]>,
disabled: box.with(() => disabled),
required: box.with(() => required),
open: box.with(
() => open,
(v) => {
open = v;
onOpenChange(v);
if (controlledOpen) {
onOpenChange(v);
} else {
open = v;
onOpenChange(v);
}
}
),
loop: box.with(() => loop),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
label = "",
vimBindings = true,
disablePointerSelection = false,
controlledValue = false,
children,
child,
...restProps
Expand All @@ -35,7 +36,9 @@
value: box.with(
() => value,
(v) => {
if (v !== value) {
if (controlledValue) {
onValueChange(v);
} else {
value = v;
onValueChange(v);
}
Expand Down
11 changes: 10 additions & 1 deletion packages/bits-ui/src/lib/bits/command/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,20 @@ export type CommandRootPropsWithoutHTML = WithChild<{
disablePointerSelection?: boolean;

/**
* Set the `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation.
* Set this prop to `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation.
*
* @defaultValue true
*/
vimBindings?: boolean;

/**
* Whether or not the command is controlled or not. If `true`, the command will not update
* the value state internally, instead it will call `onValueChange` when it would have
* otherwise, and it is up to you to update the `value` prop that is passed to the component.
*
* @defaultValue false
*/
controlledValue?: boolean;
}>;

export type CommandRootProps = CommandRootPropsWithoutHTML &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,32 @@
readonly = false,
readonlySegments = [],
required = false,
controlledPlaceholder = false,
controlledValue = false,
children,
}: RootProps = $props();
if (placeholder === undefined) {
placeholder = getDefaultDate({
const defaultPlaceholder = getDefaultDate({
granularity,
defaultPlaceholder: undefined,
defaultValue: value,
});
if (controlledPlaceholder) {
onPlaceholderChange(defaultPlaceholder);
} else {
placeholder = defaultPlaceholder;
}
}
useDateFieldRoot({
value: box.with(
() => value,
(v) => {
if (value !== v) {
if (controlledValue) {
onValueChange(v);
} else {
value = v;
onValueChange(v);
}
Expand All @@ -46,7 +56,9 @@
placeholder: box.with(
() => placeholder as DateValue,
(v) => {
if (placeholder !== v) {
if (controlledPlaceholder) {
onPlaceholderChange(v);
} else {
placeholder = v;
onPlaceholderChange(v);
}
Expand Down
Loading

0 comments on commit 1f02d0a

Please sign in to comment.