Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow RangeCalendar value to be cleared #1075

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-chefs-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix(RangeCalendar): allow `value` to be cleared
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@

<script lang="ts">
let { placeholder, value, ...restProps }: RangeCalendarTestProps = $props();

function clear() {
value = {
start: undefined,
end: undefined,
};
}
</script>

<main>
<div data-testid="start-value">{String(value?.start)}</div>
<div data-testid="end-value">{String(value?.end)}</div>
<button onclick={clear}>clear</button>

<RangeCalendar.Root bind:placeholder bind:value {...restProps} data-testid="calendar">
{#snippet children({ months, weekdays })}
Expand Down
119 changes: 69 additions & 50 deletions packages/tests/src/tests/range-calendar/range-calendar.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -30,52 +30,69 @@ 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<RangeCalendarTestProps> = {}) {
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<HTMLElement>(SELECTED_DAY_SELECTOR);
}

return { ...returned, user, calendar, getSelectedDays };
}

describe("calendar", () => {
it("should have no accessibility violations", async () => {
const { container } = render(RangeCalendarTest);
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<HTMLElement>(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<HTMLElement>(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<HTMLElement>(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 () => {
Expand Down Expand Up @@ -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);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof RangeCalendar.Root> = $props();
</script>

<RangeCalendar.Root
class="mt-6 rounded-15px border border-dark-10 bg-background-alt p-[22px] shadow-card"
weekdayFormat="short"
fixedWeeks={true}
bind:value
>
{#snippet children({ months, weekdays })}
<RangeCalendar.Header class="flex items-center justify-between">
Expand Down
Loading