diff --git a/.changeset/gentle-chefs-wait.md b/.changeset/gentle-chefs-wait.md new file mode 100644 index 000000000..116de5f4c --- /dev/null +++ b/.changeset/gentle-chefs-wait.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix(RangeCalendar): allow `value` to be cleared diff --git a/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts b/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts index 0d6b8d8b1..8c4ca4580 100644 --- a/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts +++ b/packages/bits-ui/src/lib/bits/range-calendar/range-calendar.svelte.ts @@ -5,9 +5,8 @@ import { isSameMonth, isToday, } from "@internationalized/date"; -import { untrack } from "svelte"; import { useRefById } from "svelte-toolbelt"; -import { Context } from "runed"; +import { Context, watch } from "runed"; import { CalendarRootContext } from "../calendar/calendar.svelte.js"; import type { DateRange, Month } from "$lib/shared/index.js"; import type { ReadableBoxedValues, WritableBoxedValues } from "$lib/internal/box.svelte.js"; @@ -198,45 +197,51 @@ export class RangeCalendarRootState { * Synchronize the start and end values with the `value` in case * it is updated externally. */ - $effect(() => { - const value = this.value.current; - untrack(() => { + watch( + () => this.value.current, + (value) => { if (value.start && value.end) { this.startValue.current = value.start; this.endValue.current = value.end; } else if (value.start) { this.startValue.current = value.start; this.endValue.current = undefined; + } else if (value.start === undefined && value.end === undefined) { + this.startValue.current = undefined; + this.endValue.current = undefined; } - }); - }); + } + ); /** * Synchronize the placeholder value with the current start value */ - $effect(() => { - this.value.current; - untrack(() => { - const startValue = this.value.current.start; + watch( + () => this.value.current, + (value) => { + const startValue = value.start; if (startValue && this.placeholder.current !== startValue) { this.placeholder.current = startValue; } - }); - }); - - $effect(() => { - const startValue = this.startValue.current; - const endValue = this.endValue.current; + } + ); - untrack(() => { - const value = this.value.current; - if (value && value.start === startValue && value.end === endValue) { + watch( + [() => this.startValue.current, () => this.endValue.current], + ([startValue, endValue]) => { + if ( + this.value.current && + this.value.current.start === startValue && + this.value.current.end === endValue + ) { return; } if (startValue && endValue) { this.#updateValue((prev) => { - if (prev.start === startValue && prev.end === endValue) return prev; + if (prev.start === startValue && prev.end === endValue) { + return prev; + } if (isBefore(endValue, startValue)) { const start = startValue; const end = endValue; @@ -250,14 +255,16 @@ export class RangeCalendarRootState { }; } }); - } else if (value && value.start && value.end) { - this.value.current = { - start: undefined, - end: undefined, - }; + } else if ( + this.value.current && + this.value.current.start && + this.value.current.end + ) { + this.value.current.start = undefined; + this.value.current.end = undefined; } - }); - }); + } + ); this.shiftFocus = this.shiftFocus.bind(this); this.handleCellClick = this.handleCellClick.bind(this); diff --git a/packages/tests/src/tests/range-calendar/range-calendar-test.svelte b/packages/tests/src/tests/range-calendar/range-calendar-test.svelte index c2426cdaa..ddb3174f7 100644 --- a/packages/tests/src/tests/range-calendar/range-calendar-test.svelte +++ b/packages/tests/src/tests/range-calendar/range-calendar-test.svelte @@ -6,11 +6,19 @@
{String(value?.start)}
{String(value?.end)}
+ {#snippet children({ months, weekdays })} diff --git a/packages/tests/src/tests/range-calendar/range-calendar.test.ts b/packages/tests/src/tests/range-calendar/range-calendar.test.ts index e73ab492b..1b15b3f8a 100644 --- a/packages/tests/src/tests/range-calendar/range-calendar.test.ts +++ b/packages/tests/src/tests/range-calendar/range-calendar.test.ts @@ -1,4 +1,4 @@ -import { render } from "@testing-library/svelte/svelte5"; +import { fireEvent, render } from "@testing-library/svelte/svelte5"; import { userEvent } from "@testing-library/user-event"; import { axe } from "jest-axe"; import { describe, it } from "vitest"; @@ -30,17 +30,21 @@ const shortWeekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const longWeekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; // prettier-ignore const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October" ,"November", "December"]; +const SELECTED_DAY_SELECTOR = "[data-bits-day][data-selected]"; +const SELECTED_ATTR = "data-selected"; function setup(props: Partial = {}) { const user = userEvent.setup(); const returned = render(RangeCalendarTest, { ...props }); const calendar = returned.getByTestId("calendar"); expect(calendar).toBeVisible(); - return { ...returned, user, calendar }; -} -const SELECTED_DAY_SELECTOR = "[data-bits-day][data-selected]"; -const SELECTED_ATTR = "data-selected"; + function getSelectedDays() { + return calendar.querySelectorAll(SELECTED_DAY_SELECTOR); + } + + return { ...returned, user, calendar, getSelectedDays }; +} describe("calendar", () => { it("should have no accessibility violations", async () => { @@ -48,34 +52,47 @@ describe("calendar", () => { expect(await axe(container)).toHaveNoViolations(); }); - it("should respect a default value if provided - `CalendarDate`", async () => { - const { calendar, getByTestId } = setup({ value: calendarDateRange }); + describe("respects default value if provided", () => { + it("CalendarDate", async () => { + const { getSelectedDays, getByTestId } = setup({ value: calendarDateRange }); - const selectedDays = calendar.querySelectorAll(SELECTED_DAY_SELECTOR); - expect(selectedDays).toHaveLength(6); + expect(getSelectedDays()).toHaveLength(6); - const heading = getByTestId("heading"); - expect(heading).toHaveTextContent("January 1980"); - }); + const heading = getByTestId("heading"); + expect(heading).toHaveTextContent("January 1980"); + }); - it("should respect a default value if provided - `CalendarDateTime`", async () => { - const { calendar, getByTestId } = setup({ value: calendarDateTimeRange }); + it("CalendarDateTime", async () => { + const { getSelectedDays, getByTestId } = setup({ value: calendarDateTimeRange }); - const selectedDays = calendar.querySelectorAll(SELECTED_DAY_SELECTOR); - expect(selectedDays).toHaveLength(6); + expect(getSelectedDays()).toHaveLength(6); - const heading = getByTestId("heading"); - expect(heading).toHaveTextContent("January 1980"); + const heading = getByTestId("heading"); + expect(heading).toHaveTextContent("January 1980"); + }); + + it("ZonedDateTime", async () => { + const { getSelectedDays, getByTestId } = setup({ value: zonedDateTimeRange }); + + expect(getSelectedDays()).toHaveLength(6); + + const heading = getByTestId("heading"); + expect(heading).toHaveTextContent("January 1980"); + }); }); - it("should respect a default value if provided - `ZonedDateTime`", async () => { - const { calendar, getByTestId } = setup({ value: zonedDateTimeRange }); + it("should allow clearing the selected range", async () => { + const { getByText, getSelectedDays } = setup({ + value: calendarDateRange, + }); + + expect(getSelectedDays()).toHaveLength(6); - const selectedDays = calendar.querySelectorAll(SELECTED_DAY_SELECTOR); - expect(selectedDays).toHaveLength(6); + const clearButton = getByText("clear"); - const heading = getByTestId("heading"); - expect(heading).toHaveTextContent("January 1980"); + await fireEvent.click(clearButton); + + expect(getSelectedDays()).toHaveLength(0); }); it("should reset range on select when a range is already selected", async () => { @@ -296,36 +313,38 @@ describe("calendar", () => { expect(thirdDayInMonth).not.toHaveAttribute(SELECTED_ATTR); }); - it("should format the weekday labels correctly - `'narrow'`", async () => { - const { getByTestId } = setup({ - placeholder: calendarDateRange.start, - weekdayFormat: "narrow", + describe("correct weekday label formatting", () => { + it("narrow", async () => { + const { getByTestId } = setup({ + placeholder: calendarDateRange.start, + weekdayFormat: "narrow", + }); + for (const [i, weekday] of narrowWeekdays.entries()) { + const weekdayEl = getByTestId(`weekday-1-${i}`); + expect(weekdayEl).toHaveTextContent(weekday); + } }); - for (const [i, weekday] of narrowWeekdays.entries()) { - const weekdayEl = getByTestId(`weekday-1-${i}`); - expect(weekdayEl).toHaveTextContent(weekday); - } - }); - it("should format the weekday labels correctly - `'short'`", async () => { - const { getByTestId } = setup({ - placeholder: calendarDateRange.start, - weekdayFormat: "short", + it("short", async () => { + const { getByTestId } = setup({ + placeholder: calendarDateRange.start, + weekdayFormat: "short", + }); + for (const [i, weekday] of shortWeekdays.entries()) { + const weekdayEl = getByTestId(`weekday-1-${i}`); + expect(weekdayEl).toHaveTextContent(weekday); + } }); - for (const [i, weekday] of shortWeekdays.entries()) { - const weekdayEl = getByTestId(`weekday-1-${i}`); - expect(weekdayEl).toHaveTextContent(weekday); - } - }); - it("should format the weekday labels correctly - `'long'`", async () => { - const { getByTestId } = setup({ - placeholder: calendarDateRange.start, - weekdayFormat: "long", + it("long`", async () => { + const { getByTestId } = setup({ + placeholder: calendarDateRange.start, + weekdayFormat: "long", + }); + for (const [i, weekday] of longWeekdays.entries()) { + const weekdayEl = getByTestId(`weekday-1-${i}`); + expect(weekdayEl).toHaveTextContent(weekday); + } }); - for (const [i, weekday] of longWeekdays.entries()) { - const weekdayEl = getByTestId(`weekday-1-${i}`); - expect(weekdayEl).toHaveTextContent(weekday); - } }); }); diff --git a/sites/docs/src/lib/components/demos/range-calendar-demo.svelte b/sites/docs/src/lib/components/demos/range-calendar-demo.svelte index b6b9163a2..d4e2d39d4 100644 --- a/sites/docs/src/lib/components/demos/range-calendar-demo.svelte +++ b/sites/docs/src/lib/components/demos/range-calendar-demo.svelte @@ -3,12 +3,16 @@ import CaretLeft from "phosphor-svelte/lib/CaretLeft"; import CaretRight from "phosphor-svelte/lib/CaretRight"; import { cn } from "$lib/utils/index.js"; + import type { ComponentProps } from "svelte"; + + let { value = $bindable() }: ComponentProps = $props(); {#snippet children({ months, weekdays })} diff --git a/sites/docs/src/routes/(main)/sink/+page.svelte b/sites/docs/src/routes/(main)/sink/+page.svelte index 6d467d75a..5e410109b 100644 --- a/sites/docs/src/routes/(main)/sink/+page.svelte +++ b/sites/docs/src/routes/(main)/sink/+page.svelte @@ -1,70 +1,29 @@ - - - {#snippet child({ props })} - - {/snippet} - - - Edit profile - - - {#snippet child({ props })} - - {/snippet} - - Testing - - - +
+ + +
+ +
+
diff --git a/sites/docs/src/routes/(main)/sink/range-cal.svelte b/sites/docs/src/routes/(main)/sink/range-cal.svelte new file mode 100644 index 000000000..d0392600c --- /dev/null +++ b/sites/docs/src/routes/(main)/sink/range-cal.svelte @@ -0,0 +1,74 @@ + + + + {#snippet children({ months, weekdays })} + + + ← + + + + → + + +
+ {#each months as month} + + + + {#each weekdays as day} + +
{day.slice(0, 2)}
+
+ {/each} +
+
+ + {#each month.weeks as weekDates} + + {#each weekDates as date} + + + + {date.day} + + + {/each} + + {/each} + +
+ {/each} +
+ {/snippet} +