Skip to content

Commit 370c6d3

Browse files
committed
docs: expand custom selections guide with runnable examples
1 parent 55f7180 commit 370c6d3

File tree

4 files changed

+276
-19
lines changed

4 files changed

+276
-19
lines changed

examples/CustomMonthSelection.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { endOfMonth, startOfMonth } from "date-fns";
2+
import React, { useState } from "react";
3+
4+
import { type DateRange, DayPicker } from "react-day-picker";
5+
6+
/** Toggle selection of an entire month. */
7+
export function CustomMonthSelection() {
8+
const [monthRange, setMonthRange] = useState<DateRange | undefined>();
9+
10+
const toMonthRange = (day: Date): DateRange => ({
11+
from: startOfMonth(day),
12+
to: endOfMonth(day),
13+
});
14+
15+
const isInRange = (day: Date) =>
16+
monthRange?.from && monthRange?.to
17+
? day >= monthRange.from && day <= monthRange.to
18+
: false;
19+
20+
return (
21+
<DayPicker
22+
mode="default"
23+
showOutsideDays
24+
modifiers={{
25+
selected: monthRange,
26+
range_start: monthRange?.from,
27+
range_end: monthRange?.to,
28+
range_middle: monthRange,
29+
}}
30+
onDayClick={(day, modifiers) => {
31+
if (modifiers.disabled || modifiers.hidden) return;
32+
setMonthRange(isInRange(day) ? undefined : toMonthRange(day));
33+
}}
34+
/>
35+
);
36+
}

examples/CustomRollingWindow.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { addDays } from "date-fns";
2+
import React, { useState } from "react";
3+
4+
import { type DateRange, DayPicker } from "react-day-picker";
5+
6+
/** Select a fixed-length range starting from the clicked day. */
7+
export function CustomRollingWindow() {
8+
const [range, setRange] = useState<DateRange | undefined>();
9+
const windowLength = 7;
10+
11+
const applyRange = (start: Date): DateRange => ({
12+
from: start,
13+
to: addDays(start, windowLength - 1),
14+
});
15+
16+
return (
17+
<DayPicker
18+
mode="default"
19+
modifiers={{
20+
selected: range,
21+
range_start: range?.from,
22+
range_end: range?.to,
23+
range_middle: range,
24+
}}
25+
onDayClick={(day, modifiers) => {
26+
if (modifiers.disabled || modifiers.hidden) return;
27+
setRange(modifiers.selected ? undefined : applyRange(day));
28+
}}
29+
onDayKeyDown={(day, modifiers, e) => {
30+
if (e.key === " " || e.key === "Enter") {
31+
e.preventDefault();
32+
if (modifiers.disabled || modifiers.hidden) return;
33+
setRange(modifiers.selected ? undefined : applyRange(day));
34+
}
35+
}}
36+
/>
37+
);
38+
}

examples/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export * from "./CustomDropdown";
1818
export * from "./CustomMultiple";
1919
export * from "./CustomSingle";
2020
export * from "./CustomWeek";
21+
export * from "./CustomRollingWindow";
22+
export * from "./CustomMonthSelection";
2123
export * from "./DefaultMonth";
2224
export * from "./Dialog";
2325
export * from "./Disabled";

website/docs/guides/custom-selections.mdx

Lines changed: 200 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ sidebar_position: 2
44

55
# Custom Selections
66

7-
:::info Draft
8-
9-
This documentation is still a work in progress. If you have any questions, please visit the [discussions](https://github.com/gpbl/react-day-picker/discussions) page on GitHub.
10-
11-
:::
12-
137
If the built-in [selection modes](../selections/selection-modes.mdx) do not satisfy your app’s requirements, you can control the selection behavior using the [`onDayClick`](../api/interfaces/PropsBase.md#ondayclick) and [`modifiers`](../api/type-aliases/Modifiers.md) props.
148

159
| Prop | Type | Description |
@@ -19,6 +13,13 @@ If the built-in [selection modes](../selections/selection-modes.mdx) do not sati
1913

2014
The `onDayClick` prop is a function that receives the clicked day and its modifiers as arguments.
2115

16+
## How custom selection works
17+
18+
- Use `mode="default"` (or omit `mode`) to disable built-in selection logic.
19+
- Keep your own selection state, derive a `selected` modifier (and any `range_*` modifiers) from it, and pass them to `modifiers` so styling/ARIA stay aligned.
20+
- Ignore clicks on `modifiers.disabled` or `modifiers.hidden` to preserve accessibility.
21+
- For keyboard support, handle `onDayKeyDown` (Space/Enter) in the same way as `onDayClick`.
22+
2223
## Examples
2324

2425
### Week Selection
@@ -31,35 +32,53 @@ Note the use of the `startOfWeek` and `endOfWeek` functions from [date-fns](http
3132
import React, { useState } from "react";
3233

3334
import { endOfWeek, startOfWeek } from "date-fns";
34-
import { DateRange, DayPicker, isDateInRange } from "react-day-picker";
35+
import {
36+
type DateRange,
37+
DayPicker,
38+
rangeIncludesDate,
39+
} from "react-day-picker";
3540

3641
/** Select the whole week when the day is clicked. */
3742
export function CustomWeek() {
3843
const [selectedWeek, setSelectedWeek] = useState<DateRange | undefined>();
3944

4045
return (
4146
<DayPicker
47+
mode="default"
4248
showWeekNumber
4349
showOutsideDays
4450
modifiers={{
4551
selected: selectedWeek,
4652
range_start: selectedWeek?.from,
4753
range_end: selectedWeek?.to,
4854
range_middle: (date: Date) =>
49-
selectedWeek
50-
? isDateInRange(date, selectedWeek, { excludeEnds: true })
51-
: false,
55+
selectedWeek ? rangeIncludesDate(selectedWeek, date, true) : false,
5256
}}
5357
onDayClick={(day, modifiers) => {
58+
if (modifiers.disabled || modifiers.hidden) return;
5459
if (modifiers.selected) {
55-
setSelectedWeek(undefined); // Clear the selection if the day is already selected
60+
setSelectedWeek(undefined);
5661
return;
5762
}
5863
setSelectedWeek({
5964
from: startOfWeek(day),
6065
to: endOfWeek(day),
6166
});
6267
}}
68+
onDayKeyDown={(day, modifiers, e) => {
69+
if (e.key === " " || e.key === "Enter") {
70+
e.preventDefault();
71+
if (modifiers.disabled || modifiers.hidden) return;
72+
if (modifiers.selected) {
73+
setSelectedWeek(undefined);
74+
return;
75+
}
76+
setSelectedWeek({
77+
from: startOfWeek(day),
78+
to: endOfWeek(day),
79+
});
80+
}
81+
}}
6382
footer={
6483
selectedWeek &&
6584
`Week from ${selectedWeek?.from?.toLocaleDateString()} to ${selectedWeek?.to?.toLocaleDateString()}`
@@ -91,12 +110,17 @@ export function CustomSingle() {
91110
}
92111
return (
93112
<DayPicker
113+
mode="default"
94114
modifiers={modifiers}
95115
onDayClick={(day, modifiers) => {
96-
if (modifiers.selected) {
97-
setSelectedDate(undefined);
98-
} else {
99-
setSelectedDate(day);
116+
if (modifiers.disabled || modifiers.hidden) return;
117+
setSelectedDate(modifiers.selected ? undefined : day);
118+
}}
119+
onDayKeyDown={(day, modifiers, e) => {
120+
if (e.key === " " || e.key === "Enter") {
121+
e.preventDefault();
122+
if (modifiers.disabled || modifiers.hidden) return;
123+
setSelectedDate(modifiers.selected ? undefined : day);
100124
}
101125
}}
102126
footer={selectedDate && `You selected ${selectedDate.toDateString()}`}
@@ -117,18 +141,24 @@ Selecting multiple days is a bit more complex as it involves handling an array o
117141
import { useState } from "react";
118142

119143
import { isSameDay } from "date-fns";
120-
import { DayMouseEventHandler, DayPicker } from "react-day-picker";
144+
import { DayEventHandler, DayPicker } from "react-day-picker";
121145

122146
export function CustomMultiple() {
123147
const [value, setValue] = useState<Date[]>([]);
124148

125-
const handleDayClick: DayMouseEventHandler = (day, modifiers) => {
149+
const handleDayClick: DayEventHandler<React.MouseEvent> = (
150+
day,
151+
modifiers,
152+
) => {
153+
if (modifiers.disabled || modifiers.hidden) return;
126154
const newValue = [...value];
127155
if (modifiers.selected) {
128156
const index = value.findIndex((d) => isSameDay(day, d));
129157
newValue.splice(index, 1);
130158
} else {
131-
newValue.push(day);
159+
if (!value.some((d) => isSameDay(d, day))) {
160+
newValue.push(day);
161+
}
132162
}
133163
setValue(newValue);
134164
};
@@ -141,13 +171,22 @@ export function CustomMultiple() {
141171
footer = (
142172
<>
143173
You selected {value.length} days.{" "}
144-
<button onClick={handleResetClick}>Reset</button>
174+
<button type="button" onClick={handleResetClick}>
175+
Reset
176+
</button>
145177
</>
146178
);
147179

148180
return (
149181
<DayPicker
182+
mode="default"
150183
onDayClick={handleDayClick}
184+
onDayKeyDown={(day, modifiers, e) => {
185+
if (e.key === " " || e.key === "Enter") {
186+
e.preventDefault();
187+
handleDayClick(day, modifiers, e as any);
188+
}
189+
}}
151190
modifiers={{ selected: value }}
152191
footer={footer}
153192
/>
@@ -158,3 +197,145 @@ export function CustomMultiple() {
158197
<BrowserWindow>
159198
<Examples.CustomMultiple />
160199
</BrowserWindow>
200+
201+
### Rolling N-day window
202+
203+
Create a fixed-length range (for example, 7 days) starting from the clicked day.
204+
205+
```tsx
206+
import { addDays } from "date-fns";
207+
import { useState } from "react";
208+
209+
import { type DateRange, DayPicker } from "react-day-picker";
210+
211+
export function CustomRollingWindow() {
212+
const [range, setRange] = useState<DateRange | undefined>();
213+
const windowLength = 7;
214+
215+
const applyRange = (start: Date): DateRange => ({
216+
from: start,
217+
to: addDays(start, windowLength - 1),
218+
});
219+
220+
return (
221+
<DayPicker
222+
mode="default"
223+
modifiers={{
224+
selected: range,
225+
range_start: range?.from,
226+
range_end: range?.to,
227+
range_middle: range,
228+
}}
229+
onDayClick={(day, modifiers) => {
230+
if (modifiers.disabled || modifiers.hidden) return;
231+
setRange(modifiers.selected ? undefined : applyRange(day));
232+
}}
233+
onDayKeyDown={(day, modifiers, e) => {
234+
if (e.key === " " || e.key === "Enter") {
235+
e.preventDefault();
236+
if (modifiers.disabled || modifiers.hidden) return;
237+
setRange(modifiers.selected ? undefined : applyRange(day));
238+
}
239+
}}
240+
/>
241+
);
242+
}
243+
```
244+
245+
<BrowserWindow>
246+
<Examples.CustomRollingWindow />
247+
</BrowserWindow>
248+
249+
### Selecting full months
250+
251+
Toggle an entire month at a time.
252+
253+
```tsx
254+
import { endOfMonth, startOfMonth } from "date-fns";
255+
import { useState } from "react";
256+
257+
import { type DateRange, DayPicker } from "react-day-picker";
258+
259+
export function CustomMonthSelection() {
260+
const [monthRange, setMonthRange] = useState<DateRange | undefined>();
261+
262+
const toMonthRange = (day: Date): DateRange => ({
263+
from: startOfMonth(day),
264+
to: endOfMonth(day),
265+
});
266+
267+
const isInRange = (day: Date) =>
268+
monthRange?.from && monthRange?.to
269+
? day >= monthRange.from && day <= monthRange.to
270+
: false;
271+
272+
return (
273+
<DayPicker
274+
mode="default"
275+
showOutsideDays
276+
modifiers={{
277+
selected: monthRange,
278+
range_start: monthRange?.from,
279+
range_end: monthRange?.to,
280+
range_middle: monthRange,
281+
}}
282+
onDayClick={(day, modifiers) => {
283+
if (modifiers.disabled || modifiers.hidden) return;
284+
setMonthRange(isInRange(day) ? undefined : toMonthRange(day));
285+
}}
286+
/>
287+
);
288+
}
289+
```
290+
291+
<BrowserWindow>
292+
<Examples.CustomMonthSelection />
293+
</BrowserWindow>
294+
295+
## Style custom modifiers
296+
297+
Use `modifiersClassNames` or `modifiersStyles` to surface your custom state (for example, `range_middle`, `rolling`, `blocked`).
298+
299+
```tsx
300+
<DayPicker
301+
mode="default"
302+
modifiers={{ selected: value, rolling: rollingRange }}
303+
modifiersClassNames={{ rolling: "my-rolling" }}
304+
modifiersStyles={{ rolling: { background: "var(--rdp-accent-color)" } }}
305+
/>
306+
```
307+
308+
## Week numbers and starts
309+
310+
For week-based selections, set `weekStartsOn` (locale-aware) and consider handling `onWeekNumberClick` to select a week when its number is clicked. Combine it with `showWeekNumber`.
311+
312+
```tsx
313+
<DayPicker
314+
mode="default"
315+
showWeekNumber
316+
weekStartsOn={1}
317+
onWeekNumberClick={(weekNumber, week, e) => {
318+
e.preventDefault();
319+
setSelectedWeek({ from: week.from, to: week.to });
320+
}}
321+
modifiers={{ selected: selectedWeek }}
322+
/>
323+
```
324+
325+
## Persisting custom selections
326+
327+
Serialize your custom selection state before storing or sending it.
328+
329+
```ts
330+
// Single date
331+
const payload = selectedDate ? selectedDate.toISOString() : null;
332+
333+
// Multiple dates
334+
const payload = value.map((date) => date.toISOString());
335+
336+
// Range
337+
const payload =
338+
range?.from && range?.to
339+
? { from: range.from.toISOString(), to: range.to.toISOString() }
340+
: null;
341+
```

0 commit comments

Comments
 (0)