Skip to content
Draft
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-ranges-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-day-picker": minor
---

Add `defaultSelected` to initialize uncontrolled single, multiple, and range selections.
17 changes: 10 additions & 7 deletions apps/website/docs/selections/multiple-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ Set the `mode` prop to `"multiple"` to enable the selection of multiple dates in

## Multiple Mode Props

| Prop Name | Type | Description |
| ---------- | -------------------------------------------------------------------------------- | ------------------------------------------------- |
| `selected` | `Date[] \| undefined` | The selected dates. |
| `onSelect` | [`OnSelectHandler<Date[] \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `min` | `number` | The minimum number of dates that can be selected. |
| `max` | `number` | The maximum number of dates that can be selected. |
| `required` | `boolean` | Make the selection required. |
| Prop Name | Type | Description |
| ----------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `defaultSelected` | `Date[] \| undefined` | The initially selected dates for uncontrolled selection. |
| `selected` | `Date[] \| undefined` | The selected dates. |
| `onSelect` | [`OnSelectHandler<Date[] \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `min` | `number` | The minimum number of dates that can be selected. |
| `max` | `number` | The maximum number of dates that can be selected. |
| `required` | `boolean` | Make the selection required. |

Use `defaultSelected` to set the initial value for an uncontrolled selection. Use `selected` with `onSelect` to control the selected dates. For a read-only controlled selection, pass a no-op handler such as `onSelect={() => undefined}`.

Use the `selected` and `onSelect` props to manage the selected dates:

Expand Down
21 changes: 12 additions & 9 deletions apps/website/docs/selections/range-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@ Set the `mode` prop to `"range"` to enable the selection of a continuous range o

## Range Mode Props

| Prop Name | Type | Description |
| ----------------- | ----------------------------------------------------------------------------------- | ------------------------------------------ |
| `selected` | [`DateRange`](../api/react/type-aliases/DateRange.md) | The selected range. |
| `onSelect` | [`OnSelectHandler<DateRange \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `required` | `boolean` | Make the selection required. |
| `resetOnSelect` | `boolean` | Start a new range after a completed one. |
| `min` | `number` | The minimum number of nights in the range. |
| `max` | `number` | The maximum number of nights in the range. |
| `excludeDisabled` | `boolean` | Exclude disabled dates from the range. |
| Prop Name | Type | Description |
| ----------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `defaultSelected` | [`DateRange`](../api/react/type-aliases/DateRange.md) | The initially selected range for uncontrolled selection. |
| `selected` | [`DateRange`](../api/react/type-aliases/DateRange.md) | The selected range. |
| `onSelect` | [`OnSelectHandler<DateRange \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `required` | `boolean` | Make the selection required. |
| `resetOnSelect` | `boolean` | Start a new range after a completed one. |
| `min` | `number` | The minimum number of nights in the range. |
| `max` | `number` | The maximum number of nights in the range. |
| `excludeDisabled` | `boolean` | Exclude disabled dates from the range. |

Use `defaultSelected` to set the initial value for an uncontrolled selection. Use `selected` with `onSelect` to control the selected range. For a read-only controlled selection, pass a no-op handler such as `onSelect={() => undefined}`.

## Min and Max Dates

Expand Down
17 changes: 10 additions & 7 deletions apps/website/docs/selections/selection-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ DayPicker offers predefined rules for date selection.
- [Multiple mode](./multiple-mode.mdx): Allows the selection of multiple individual dates.
- [Range mode](./range-mode.mdx): Allows the selection of a continuous range of dates.

| Prop Name | Type | Description |
| ---------- | ------------------------------------------------------------------------------------ | --------------------------------------- |
| `mode` | `"single"` \| `"multiple"` \| `"range"` | Enter a selection mode. |
| `disabled` | [`Matcher`](../api/react/type-aliases/Matcher.md) \| `Matcher[]` | Disabled dates that cannot be selected. |
| `selected` | `Date` \| `Date[]` \| [`DateRange`](../api/react/type-aliases/DateRange.md) \| `undefined` | The selected date(s). |
| `required` | `boolean` | When `true`, the selection is required. |
| `onSelect` | [`OnSelectHandler`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| Prop Name | Type | Description |
| ----------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| `mode` | `"single"` \| `"multiple"` \| `"range"` | Enter a selection mode. |
| `disabled` | [`Matcher`](../api/react/type-aliases/Matcher.md) \| `Matcher[]` | Disabled dates that cannot be selected. |
| `defaultSelected` | `Date` \| `Date[]` \| [`DateRange`](../api/react/type-aliases/DateRange.md) \| `undefined` | The initially selected date(s) for uncontrolled selection. |
| `selected` | `Date` \| `Date[]` \| [`DateRange`](../api/react/type-aliases/DateRange.md) \| `undefined` | The selected date(s). |
| `required` | `boolean` | When `true`, the selection is required. |
| `onSelect` | [`OnSelectHandler`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |

Use `defaultSelected` to set the initial value for an uncontrolled selection. Use `selected` with `onSelect` to control the selection from your state. For a read-only controlled selection, pass a no-op handler such as `onSelect={() => undefined}`.

## Customizing Selections

Expand Down
13 changes: 8 additions & 5 deletions apps/website/docs/selections/single-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ When the `mode` prop is set to `"single"`, only one date can be selected at a ti

## Single Mode Props

| Prop Name | Type | Description |
| ---------- | ------------------------------------------------------------------------------ | --------------------------------------- |
| `selected` | `Date \| undefined` | The selected date. |
| `onSelect` | [`OnSelectHandler<Date \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `required` | `boolean` | Make the selection required. |
| Prop Name | Type | Description |
| ----------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `defaultSelected` | `Date \| undefined` | The initially selected date for uncontrolled selection. |
| `selected` | `Date \| undefined` | The selected date. |
| `onSelect` | [`OnSelectHandler<Date \| undefined>`](../api/react/type-aliases/OnSelectHandler.md) | Event callback when a date is selected. |
| `required` | `boolean` | Make the selection required. |

Use `defaultSelected` to set the initial value for an uncontrolled selection. Use `selected` with `onSelect` to control the selected date. For a read-only controlled selection, pass a no-op handler such as `onSelect={() => undefined}`.

## Controlled Selections

Expand Down
18 changes: 18 additions & 0 deletions packages/react-day-picker/src/DayPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
activeElement,
dateButton,
grid,
gridcell,
nav,
nextButton,
previousButton,
Expand Down Expand Up @@ -257,6 +258,23 @@ test("calls selection and day event callbacks with Date instances", async () =>
expect(handleDayClick.mock.calls[0][0]).toBeInstanceOf(Date);
});

test("renders defaultSelected as the initial selection", () => {
const defaultSelected = new Date(2024, 0, 15);

render(
<DayPicker
defaultMonth={defaultSelected}
defaultSelected={defaultSelected}
mode="single"
/>,
);

expect(gridcell(defaultSelected, true)).toHaveAttribute(
"aria-selected",
"true",
);
});

describe("when navigating with month callbacks", () => {
const handleMonthChange = jest.fn();
const handleNextClick = jest.fn();
Expand Down
54 changes: 39 additions & 15 deletions packages/react-day-picker/src/DayPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,45 @@ export function DayPicker(initialProps: DayPickerProps) {
if (props.endMonth) {
props.endMonth = toTimeZone(props.endMonth, timeZone);
}
if (props.mode === "single" && props.selected) {
props.selected = toTimeZone(props.selected, timeZone);
} else if (props.mode === "multiple" && props.selected) {
props.selected = props.selected?.map((date) =>
toTimeZone(date, timeZone),
);
} else if (props.mode === "range" && props.selected) {
props.selected = {
from: props.selected.from
? toTimeZone(props.selected.from, timeZone)
: props.selected.from,
to: props.selected.to
? toTimeZone(props.selected.to, timeZone)
: props.selected.to,
};
if (props.mode === "single") {
if (props.selected) {
props.selected = toTimeZone(props.selected, timeZone);
}
if (props.defaultSelected) {
props.defaultSelected = toTimeZone(props.defaultSelected, timeZone);
}
} else if (props.mode === "multiple") {
if (props.selected) {
props.selected = props.selected.map((date) =>
toTimeZone(date, timeZone),
);
}
if (props.defaultSelected) {
props.defaultSelected = props.defaultSelected.map((date) =>
toTimeZone(date, timeZone),
);
}
} else if (props.mode === "range") {
if (props.selected) {
props.selected = {
from: props.selected.from
? toTimeZone(props.selected.from, timeZone)
: props.selected.from,
to: props.selected.to
? toTimeZone(props.selected.to, timeZone)
: props.selected.to,
};
}
if (props.defaultSelected) {
props.defaultSelected = {
from: props.defaultSelected.from
? toTimeZone(props.defaultSelected.from, timeZone)
: props.defaultSelected.from,
to: props.defaultSelected.to
? toTimeZone(props.defaultSelected.to, timeZone)
: props.defaultSelected.to,
};
}
}
if (props.disabled !== undefined) {
props.disabled = convertMatchersToTimeZone(props.disabled, timeZone);
Expand Down
12 changes: 12 additions & 0 deletions packages/react-day-picker/src/selection/useMulti.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ describe("useMulti", () => {
expect(result.current.selected).toBe(selectedDates);
});

test("uses defaultSelected for uncontrolled selection", () => {
const defaultSelected = [new Date(2023, 9, 1), new Date(2023, 9, 2)];
const props: DayPickerProps = {
mode: "multiple",
defaultSelected,
};

const { result } = renderHook(() => useMulti(props, defaultDateLib));

expect(result.current.selected).toBe(defaultSelected);
});

test("uses the internally selected value when onSelect is not provided", () => {
const initialSelectedDates = [new Date(2023, 9, 1), new Date(2023, 9, 2)];
const props: DayPickerProps = {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-day-picker/src/selection/useMulti.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ export function useMulti<T extends DayPickerProps>(
dateLib: DateLib,
): Selection<T> {
const {
defaultSelected,
selected: initiallySelected,
required,
onSelect,
} = props as PropsMulti;

const [internallySelected, setSelected] = useControlledValue(
initiallySelected,
defaultSelected ?? initiallySelected,
onSelect ? initiallySelected : undefined,
);

Expand Down
15 changes: 15 additions & 0 deletions packages/react-day-picker/src/selection/useRange.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,21 @@ describe("useRange", () => {
expect(result.current.selected).toBe(selectedRange);
});

test("uses defaultSelected for uncontrolled selection", () => {
const defaultSelected = {
from: new Date(2023, 9, 1),
to: new Date(2023, 9, 5),
};
const props: DayPickerProps = {
mode: "range",
defaultSelected,
};

const { result } = renderHook(() => useRange(props, defaultDateLib));

expect(result.current.selected).toBe(defaultSelected);
});

test("uses the internally selected value when onSelect is not provided", () => {
const initialSelectedRange = {
from: new Date(2023, 9, 1),
Expand Down
3 changes: 2 additions & 1 deletion packages/react-day-picker/src/selection/useRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function useRange<T extends DayPickerProps>(
dateLib: DateLib,
): Selection<T> {
const {
defaultSelected,
disabled,
excludeDisabled,
resetOnSelect,
Expand All @@ -34,7 +35,7 @@ export function useRange<T extends DayPickerProps>(
} = props as PropsRange;

const [internallySelected, setSelected] = useControlledValue(
initiallySelected,
defaultSelected ?? initiallySelected,
onSelect ? initiallySelected : undefined,
);

Expand Down
12 changes: 12 additions & 0 deletions packages/react-day-picker/src/selection/useSingle.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ describe("useSingle", () => {
expect(result.current.selected).toBe(selectedDate);
});

test("uses defaultSelected for uncontrolled selection", () => {
const defaultSelected = new Date(2023, 9, 1);
const props: DayPickerProps = {
mode: "single",
defaultSelected,
};

const { result } = renderHook(() => useSingle(props, defaultDateLib));

expect(result.current.selected).toBe(defaultSelected);
});

test("uses the internally selected value when onSelect is not provided", () => {
const initialSelectedDate = new Date(2023, 9, 1);
const props: DayPickerProps = {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-day-picker/src/selection/useSingle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ export function useSingle<T extends DayPickerProps>(
dateLib: DateLib,
): Selection<T> {
const {
defaultSelected,
selected: initiallySelected,
required,
onSelect,
} = props as PropsSingle;

const [internallySelected, setSelected] = useControlledValue(
initiallySelected,
defaultSelected ?? initiallySelected,
onSelect ? initiallySelected : undefined,
);

Expand Down
9 changes: 9 additions & 0 deletions packages/react-day-picker/src/types/props.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const Test = () => {
<DayPicker />
<DayPicker {...dateShapedProps} />
<DayPicker mode="single" />
<DayPicker mode="single" defaultSelected={new Date()} />
<DayPicker
mode="single"
selected={undefined}
Expand Down Expand Up @@ -139,13 +140,15 @@ const Test = () => {
{/** @ts-expect-error Wrong selected prop */}
<DayPicker mode="multiple" selected={new Date()} />
<DayPicker mode="multiple" onSelect={(_date: Date[] | undefined) => {}} />
<DayPicker mode="multiple" defaultSelected={[new Date()]} />
<DayPicker
mode="multiple"
required
selected={[]}
onSelect={(_date: Date[]) => {}}
/>
<DayPicker mode="single" selected={new Date()} />
<DayPicker mode="range" defaultSelected={{ from: month, to: endMonth }} />
<DayPicker
mode="range"
selected={{ from: month, to: endMonth }}
Expand Down Expand Up @@ -185,10 +188,16 @@ const Test = () => {
<DayPicker endMonth={plainDateLike} />
{/* @ts-expect-error single selection is Date-shaped */}
<DayPicker mode="single" selected={plainDateLike} />
{/* @ts-expect-error single default selection is Date-shaped */}
<DayPicker mode="single" defaultSelected={plainDateLike} />
{/* @ts-expect-error multiple selection contains Date values */}
<DayPicker mode="multiple" selected={[plainDateLike]} />
{/* @ts-expect-error multiple default selection contains Date values */}
<DayPicker mode="multiple" defaultSelected={[plainDateLike]} />
{/* @ts-expect-error range endpoints must be Date values */}
<DayPicker mode="range" selected={{ from: plainDateLike }} />
{/* @ts-expect-error range default selection endpoints must be Date values */}
<DayPicker mode="range" defaultSelected={{ from: plainDateLike }} />
{/* @ts-expect-error matchers must use Date values */}
<DayPicker disabled={plainDateLike} />
{/* @ts-expect-error matcher arrays must use Date values */}
Expand Down
Loading
Loading