Skip to content

Commit

Permalink
fix: DateRangePicker not updating value under certain conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed Feb 17, 2025
1 parent 0e15d8f commit a1c2fba
Show file tree
Hide file tree
Showing 14 changed files with 1,763 additions and 1,053 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-lamps-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix: `DateRangePicker` not allowing range selection under certain value conditions
3 changes: 0 additions & 3 deletions docs/src/lib/components/demos/date-range-picker-demo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
import CaretLeft from "phosphor-svelte/lib/CaretLeft";
import CaretRight from "phosphor-svelte/lib/CaretRight";
import { cn } from "$lib/utils/index.js";
let value: DateRange = $state({ start: undefined, end: undefined });
</script>

<DateRangePicker.Root
bind:value
weekdayFormat="short"
fixedWeeks={true}
class="flex w-full max-w-[340px] flex-col gap-1.5"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { DateValue } from "@internationalized/date";
import { untrack } from "svelte";
import { box, onDestroyEffect, useRefById } from "svelte-toolbelt";
import { Context } from "runed";
import { Context, watch } from "runed";
import type { DateFieldRootState } from "../date-field/date-field.svelte.js";
import { DateFieldInputState, useDateFieldRoot } from "../date-field/date-field.svelte.js";
import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js";
Expand Down Expand Up @@ -58,22 +57,6 @@ export class DateRangeFieldRootState {
startValueComplete = $derived.by(() => this.opts.startValue.current !== undefined);
endValueComplete = $derived.by(() => this.opts.endValue.current !== undefined);
rangeComplete = $derived(this.startValueComplete && this.endValueComplete);
mergedValues = $derived.by(() => {
if (
this.opts.startValue.current === undefined ||
this.opts.endValue.current === undefined
) {
return {
start: undefined,
end: undefined,
};
} else {
return {
start: this.opts.startValue.current,
end: this.opts.endValue.current,
};
}
});

constructor(readonly opts: DateRangeFieldRootStateProps) {
this.formatter = createFormatter(this.opts.locale.current);
Expand All @@ -94,53 +77,78 @@ export class DateRangeFieldRootState {
this.formatter.setLocale(this.opts.locale.current);
});

$effect(() => {
const startValue = this.opts.value.current.start;
untrack(() => {
if (startValue) this.opts.placeholder.current = startValue;
});
});

$effect(() => {
const endValue = this.opts.value.current.end;
untrack(() => {
if (endValue) this.opts.placeholder.current = endValue;
});
});
/**
* Synchronize the start and end values with the `value` in case
* it is updated externally.
*/
watch(
() => this.opts.value.current,
(value) => {
if (value.start && value.end) {
this.opts.startValue.current = value.start;
this.opts.endValue.current = value.end;
} else if (value.start) {
this.opts.startValue.current = value.start;
this.opts.endValue.current = undefined;
} else if (value.start === undefined && value.end === undefined) {
this.opts.startValue.current = undefined;
this.opts.endValue.current = undefined;
}
}
);

/**
* Sync values set programatically with the `startValue` and `endValue`
* Synchronize the placeholder value with the current start value
*/
$effect(() => {
const value = this.opts.value.current;
untrack(() => {
if (value.start !== undefined && value.start !== this.opts.startValue.current) {
this.#setStartValue(value.start);
watch(
() => this.opts.value.current,
(value) => {
const startValue = value.start;
if (startValue && this.opts.placeholder.current !== startValue) {
this.opts.placeholder.current = startValue;
}
if (value.end !== undefined && value.end !== this.opts.endValue.current) {
this.#setEndValue(value.end);
}
);

watch(
[() => this.opts.startValue.current, () => this.opts.endValue.current],
([startValue, endValue]) => {
if (
this.opts.value.current &&
this.opts.value.current.start === startValue &&
this.opts.value.current.end === endValue
) {
return;
}
});
});

// TODO: Handle description element

$effect(() => {
const placeholder = untrack(() => this.opts.placeholder.current);
const startValue = untrack(() => this.opts.startValue.current);

if (this.startValueComplete && placeholder !== startValue) {
untrack(() => {
if (startValue) {
this.opts.placeholder.current = startValue;
}
});
if (startValue && endValue) {
this.#updateValue((prev) => {
if (prev.start === startValue && prev.end === endValue) {
return prev;
}
if (isBefore(endValue, startValue)) {
const start = startValue;
const end = endValue;
this.#setStartValue(end);
this.#setEndValue(start);
return { start: endValue, end: startValue };
} else {
return {
start: startValue,
end: endValue,
};
}
});
} else if (
this.opts.value.current &&
this.opts.value.current.start &&
this.opts.value.current.end
) {
this.opts.value.current.start = undefined;
this.opts.value.current.end = undefined;
}
}
});

$effect(() => {
this.opts.value.current = this.mergedValues;
});
);
}

validationStatus = $derived.by(() => {
Expand Down Expand Up @@ -186,6 +194,12 @@ export class DateRangeFieldRootState {
return true;
});

#updateValue(cb: (value: DateRange) => DateRange) {
const value = this.opts.value.current;
const newValue = cb(value);
this.opts.value.current = newValue;
}

#setStartValue(value: DateValue | undefined) {
this.opts.startValue.current = value;
}
Expand Down
111 changes: 111 additions & 0 deletions tests/src/tests/date-range-picker/date-range-picker-test.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<script lang="ts" module>
import {
DateRangePicker,
type DateRangePickerInputProps,
type WithoutChildrenOrChild,
} from "bits-ui";
export type DateRangePickerTestProps = WithoutChildrenOrChild<DateRangePicker.RootProps> & {
startProps?: Omit<DateRangePickerInputProps, "type">;
endProps?: Omit<DateRangePickerInputProps, "type">;
};
</script>

<script lang="ts">
let {
placeholder,
value,
open = false,
startProps,
endProps,
...restProps
}: DateRangePickerTestProps = $props();
function clear() {
value = {
start: undefined,
end: undefined,
};
}
</script>

<main>
<div data-testid="value">{value}</div>
<div data-testid="open">{open}</div>
<div data-testid="start-value">{String(value?.start)}</div>
<div data-testid="end-value">{String(value?.end)}</div>
<button onclick={clear}>clear</button>
<button onclick={() => (open = !open)}>toggle open</button>
<DateRangePicker.Root bind:value bind:placeholder bind:open {...restProps}>
<DateRangePicker.Label data-testid="label">Rental Days</DateRangePicker.Label>
{#each ["start", "end"] as const as type}
{@const inputProps = type === "start" ? startProps : endProps}
<DateRangePicker.Input {type} data-testid="{type}-input" {...inputProps}>
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangePicker.Segment
{part}
data-testid={part === "literal" ? undefined : `${type}-${part}`}
>
{value}
</DateRangePicker.Segment>
{/each}
{/snippet}
</DateRangePicker.Input>
{/each}

<DateRangePicker.Trigger data-testid="trigger">Open</DateRangePicker.Trigger>
<DateRangePicker.Content data-testid="content">
<DateRangePicker.Calendar data-testid="calendar">
{#snippet children({ months, weekdays })}
<DateRangePicker.Header data-testid="header">
<DateRangePicker.PrevButton data-testid="prev-button"
>Prev</DateRangePicker.PrevButton
>
<DateRangePicker.Heading data-testid="heading" />
<DateRangePicker.NextButton data-testid="next-button"
>Next</DateRangePicker.NextButton
>
</DateRangePicker.Header>
<div>
{#each months as month}
{@const m = month.value.month}
<DateRangePicker.Grid data-testid="grid-{m}">
<DateRangePicker.GridHead data-testid="grid-head-{m}">
<DateRangePicker.GridRow data-testid="grid-row-{m}">
{#each weekdays as day, i}
<DateRangePicker.HeadCell data-testid="weekday-{m}-{i}">
{day}
</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody data-testid="grid-body-{m}">
{#each month.weeks as weekDates, i}
<DateRangePicker.GridRow
data-testid="grid-row-{m}-{i}"
data-week
>
{#each weekDates as date, d}
<DateRangePicker.Cell
{date}
month={month.value}
data-testid="cell-{date.month}-{d}"
>
<DateRangePicker.Day
data-testid="date-{date.month}-{date.day}"
>
{date.day}
</DateRangePicker.Day>
</DateRangePicker.Cell>
{/each}
</DateRangePicker.GridRow>
{/each}
</DateRangePicker.GridBody>
</DateRangePicker.Grid>
{/each}
</div>
{/snippet}
</DateRangePicker.Calendar>
</DateRangePicker.Content>
</DateRangePicker.Root>
</main>
Loading

0 comments on commit a1c2fba

Please sign in to comment.