UNPKG

22.7 kBTypeScriptView Raw
1import React, { useCallback, useMemo } from "react";
2import type {
3 ChangeEventHandler,
4 MouseEvent,
5 FocusEvent,
6 KeyboardEvent
7} from "react";
8
9import { UI, DayFlag, SelectionState } from "./UI.js";
10import type { CalendarDay } from "./classes/CalendarDay.js";
11import { getClassNamesForModifiers } from "./helpers/getClassNamesForModifiers.js";
12import { getComponents } from "./helpers/getComponents.js";
13import { getDataAttributes } from "./helpers/getDataAttributes.js";
14import { getDateLib } from "./helpers/getDateLib.js";
15import { getDefaultClassNames } from "./helpers/getDefaultClassNames.js";
16import { getFormatters } from "./helpers/getFormatters.js";
17import { getMonthOptions } from "./helpers/getMonthOptions.js";
18import { getStyleForModifiers } from "./helpers/getStyleForModifiers.js";
19import { getWeekdays } from "./helpers/getWeekdays.js";
20import { getYearOptions } from "./helpers/getYearOptions.js";
21import * as defaultLabels from "./labels/index.js";
22import type { FormatOptions, LabelOptions } from "./lib/dateLib.js";
23import { enUS } from "./lib/locales.js";
24import type {
25 DayPickerProps,
26 Modifiers,
27 MoveFocusBy,
28 MoveFocusDir,
29 SelectedValue,
30 SelectHandler
31} from "./types/index.js";
32import { useCalendar } from "./useCalendar.js";
33import { type DayPickerContext, dayPickerContext } from "./useDayPicker.js";
34import { useFocus } from "./useFocus.js";
35import { useGetModifiers } from "./useGetModifiers.js";
36import { useSelection } from "./useSelection.js";
37import { rangeIncludesDate } from "./utils/rangeIncludesDate.js";
38import { isDateRange } from "./utils/typeguards.js";
39
40/**
41 * Render the date picker calendar.
42 *
43 * @group DayPicker
44 * @see https://daypicker.dev
45 */
46export function DayPicker(props: DayPickerProps) {
47 const { components, formatters, labels, dateLib, locale, classNames } =
48 useMemo(
49 () => ({
50 dateLib: getDateLib(props.dateLib),
51 components: getComponents(props.components),
52 formatters: getFormatters(props.formatters),
53 labels: { ...defaultLabels, ...props.labels },
54 locale: { ...enUS, ...props.locale },
55 classNames: { ...getDefaultClassNames(), ...props.classNames }
56 }),
57 [
58 props.classNames,
59 props.components,
60 props.dateLib,
61 props.formatters,
62 props.labels,
63 props.locale
64 ]
65 );
66
67 const {
68 captionLayout,
69 firstWeekContainsDate,
70 mode,
71 onDayBlur,
72 onDayClick,
73 onDayFocus,
74 onDayKeyDown,
75 onDayMouseEnter,
76 onDayMouseLeave,
77 onNextClick,
78 onPrevClick,
79 showWeekNumber,
80 styles,
81 useAdditionalDayOfYearTokens,
82 useAdditionalWeekYearTokens,
83 weekStartsOn
84 } = props;
85
86 const formatOptions: FormatOptions = {
87 locale,
88 weekStartsOn,
89 firstWeekContainsDate,
90 useAdditionalWeekYearTokens,
91 useAdditionalDayOfYearTokens
92 };
93
94 const labelOptions: LabelOptions = formatOptions;
95
96 const {
97 formatCaption,
98 formatDay,
99 formatMonthDropdown,
100 formatWeekNumber,
101 formatWeekNumberHeader,
102 formatWeekdayName,
103 formatYearDropdown
104 } = formatters;
105
106 const calendar = useCalendar(props, dateLib);
107
108 const {
109 days,
110 months,
111 navStart,
112 navEnd,
113 previousMonth,
114 nextMonth,
115 goToMonth
116 } = calendar;
117
118 const getModifiers = useGetModifiers(days, props, dateLib);
119
120 const {
121 isSelected,
122 select,
123 selected: selectedValue
124 } = useSelection(props, dateLib) ?? {};
125
126 const { blur, focused, isFocusTarget, moveFocus, setFocused } = useFocus(
127 props,
128 calendar,
129 getModifiers,
130 isSelected ?? (() => false),
131 dateLib
132 );
133
134 const {
135 labelDayButton,
136 labelGridcell,
137 labelGrid,
138 labelMonthDropdown,
139 labelNav,
140 labelNext,
141 labelPrevious,
142 labelWeekday,
143 labelWeekNumber,
144 labelWeekNumberHeader,
145 labelYearDropdown
146 } = labels;
147
148 const weekdays = useMemo(
149 () => getWeekdays(locale, props.weekStartsOn, props.ISOWeek, dateLib),
150 [dateLib, locale, props.ISOWeek, props.weekStartsOn]
151 );
152
153 const isInteractive = mode !== undefined || onDayClick !== undefined;
154
155 const handlePreviousClick = useCallback(() => {
156 if (!previousMonth) return;
157 goToMonth(previousMonth);
158 onPrevClick?.(previousMonth);
159 }, [previousMonth, goToMonth, onPrevClick]);
160
161 const handleNextClick = useCallback(() => {
162 if (!nextMonth) return;
163 goToMonth(nextMonth);
164 onNextClick?.(nextMonth);
165 }, [goToMonth, nextMonth, onNextClick]);
166
167 const handleDayClick = useCallback(
168 (day: CalendarDay, m: Modifiers) => (e: MouseEvent) => {
169 e.preventDefault();
170 e.stopPropagation();
171 setFocused(day);
172 select?.(day.date, m, e);
173 onDayClick?.(day.date, m, e);
174 },
175 [select, onDayClick, setFocused]
176 );
177
178 const handleDayFocus = useCallback(
179 (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => {
180 setFocused(day);
181 onDayFocus?.(day.date, m, e);
182 },
183 [onDayFocus, setFocused]
184 );
185
186 const handleDayBlur = useCallback(
187 (day: CalendarDay, m: Modifiers) => (e: FocusEvent) => {
188 blur();
189 onDayBlur?.(day.date, m, e);
190 },
191 [blur, onDayBlur]
192 );
193
194 const handleDayKeyDown = useCallback(
195 (day: CalendarDay, modifiers: Modifiers) => (e: KeyboardEvent) => {
196 const keyMap: Record<string, [MoveFocusBy, MoveFocusDir]> = {
197 ArrowLeft: ["day", props.dir === "rtl" ? "after" : "before"],
198 ArrowRight: ["day", props.dir === "rtl" ? "before" : "after"],
199 ArrowDown: ["week", "after"],
200 ArrowUp: ["week", "before"],
201 PageUp: [e.shiftKey ? "year" : "month", "before"],
202 PageDown: [e.shiftKey ? "year" : "month", "after"],
203 Home: ["startOfWeek", "before"],
204 End: ["endOfWeek", "after"]
205 };
206 if (keyMap[e.key]) {
207 e.preventDefault();
208 e.stopPropagation();
209 const [moveBy, moveDir] = keyMap[e.key];
210 moveFocus(moveBy, moveDir);
211 }
212 onDayKeyDown?.(day.date, modifiers, e);
213 },
214 [moveFocus, onDayKeyDown, props.dir]
215 );
216
217 const handleDayMouseEnter = useCallback(
218 (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => {
219 onDayMouseEnter?.(day.date, modifiers, e);
220 },
221 [onDayMouseEnter]
222 );
223
224 const handleDayMouseLeave = useCallback(
225 (day: CalendarDay, modifiers: Modifiers) => (e: MouseEvent) => {
226 onDayMouseLeave?.(day.date, modifiers, e);
227 },
228 [onDayMouseLeave]
229 );
230
231 const { className, style } = useMemo(
232 () => ({
233 className: [classNames[UI.Root], props.className]
234 .filter(Boolean)
235 .join(" "),
236 style: { ...styles?.[UI.Root], ...props.style }
237 }),
238 [classNames, props.className, props.style, styles]
239 );
240
241 const dataAttributes = getDataAttributes(props);
242
243 const contextValue: DayPickerContext<DayPickerProps> = {
244 selected: selectedValue as SelectedValue<DayPickerProps>,
245 select: select as SelectHandler<DayPickerProps>,
246 isSelected,
247 months,
248 nextMonth,
249 previousMonth,
250 goToMonth,
251 getModifiers
252 };
253
254 return (
255 <dayPickerContext.Provider value={contextValue}>
256 <components.Root
257 className={className}
258 style={style}
259 dir={props.dir}
260 id={props.id}
261 lang={props.lang}
262 nonce={props.nonce}
263 title={props.title}
264 {...dataAttributes}
265 >
266 <components.Months
267 className={classNames[UI.Months]}
268 style={styles?.[UI.Months]}
269 >
270 {!props.hideNavigation && (
271 <components.Nav
272 role="navigation"
273 className={classNames[UI.Nav]}
274 style={styles?.[UI.Nav]}
275 aria-label={labelNav()}
276 >
277 <components.Button
278 type="button"
279 className={classNames[UI.ButtonPrevious]}
280 tabIndex={previousMonth ? undefined : -1}
281 disabled={previousMonth ? undefined : true}
282 aria-label={labelPrevious(previousMonth, labelOptions)}
283 onClick={handlePreviousClick}
284 >
285 <components.Chevron
286 disabled={previousMonth ? undefined : true}
287 className={classNames[UI.Chevron]}
288 orientation="left"
289 />
290 </components.Button>
291 <components.Button
292 type="button"
293 className={classNames[UI.ButtonNext]}
294 tabIndex={nextMonth ? undefined : -1}
295 disabled={nextMonth ? undefined : true}
296 aria-label={labelNext(nextMonth, labelOptions)}
297 onClick={handleNextClick}
298 >
299 <components.Chevron
300 disabled={previousMonth ? undefined : true}
301 orientation="right"
302 className={classNames[UI.Chevron]}
303 />
304 </components.Button>
305 </components.Nav>
306 )}
307 {months.map((calendarMonth, displayIndex) => {
308 const handleMonthChange: ChangeEventHandler<HTMLSelectElement> = (
309 e
310 ) => {
311 const selectedMonth = Number(
312 (e.target as HTMLSelectElement).value
313 );
314 const month = dateLib.setMonth(
315 dateLib.startOfMonth(calendarMonth.date),
316 selectedMonth
317 );
318 goToMonth(month);
319 };
320
321 const handleYearChange: ChangeEventHandler<HTMLSelectElement> = (
322 e
323 ) => {
324 const month = dateLib.setYear(
325 dateLib.startOfMonth(calendarMonth.date),
326 Number(e.target.value)
327 );
328 goToMonth(month);
329 };
330
331 const dropdownMonths = getMonthOptions(
332 calendarMonth.date,
333 navStart,
334 navEnd,
335 formatters,
336 locale,
337 dateLib
338 );
339
340 const dropdownYears = getYearOptions(
341 months[0].date,
342 navStart,
343 navEnd,
344 formatters,
345 dateLib
346 );
347
348 return (
349 <components.Month
350 className={classNames[UI.Month]}
351 style={styles?.[UI.Month]}
352 key={displayIndex}
353 displayIndex={displayIndex}
354 calendarMonth={calendarMonth}
355 >
356 <components.MonthCaption
357 className={classNames[UI.MonthCaption]}
358 style={styles?.[UI.MonthCaption]}
359 calendarMonth={calendarMonth}
360 displayIndex={displayIndex}
361 >
362 {captionLayout?.startsWith("dropdown") ? (
363 <components.DropdownNav
364 className={classNames[UI.Dropdowns]}
365 style={styles?.[UI.Dropdowns]}
366 >
367 {captionLayout === "dropdown" ||
368 captionLayout === "dropdown-months" ? (
369 <components.Dropdown
370 aria-label={labelMonthDropdown()}
371 classNames={classNames}
372 components={components}
373 disabled={Boolean(props.disableNavigation)}
374 onChange={handleMonthChange}
375 options={dropdownMonths}
376 style={styles?.[UI.Dropdown]}
377 value={calendarMonth.date.getMonth()}
378 />
379 ) : (
380 <span role="status" aria-live="polite">
381 {formatMonthDropdown(calendarMonth.date.getMonth())}
382 </span>
383 )}
384 {captionLayout === "dropdown" ||
385 captionLayout === "dropdown-years" ? (
386 <components.Dropdown
387 aria-label={labelYearDropdown(labelOptions)}
388 classNames={classNames}
389 components={components}
390 disabled={Boolean(props.disableNavigation)}
391 onChange={handleYearChange}
392 options={dropdownYears}
393 style={styles?.[UI.Dropdown]}
394 value={calendarMonth.date.getFullYear()}
395 />
396 ) : (
397 <span role="status" aria-live="polite">
398 {formatYearDropdown(calendarMonth.date.getFullYear())}
399 </span>
400 )}
401 </components.DropdownNav>
402 ) : (
403 <components.CaptionLabel
404 className={classNames[UI.CaptionLabel]}
405 role="status"
406 aria-live="polite"
407 >
408 {formatCaption(
409 calendarMonth.date,
410 formatOptions,
411 dateLib
412 )}
413 </components.CaptionLabel>
414 )}
415 </components.MonthCaption>
416 <components.MonthGrid
417 role="grid"
418 aria-multiselectable={mode === "multiple" || mode === "range"}
419 aria-label={
420 labelGrid(calendarMonth.date, labelOptions, dateLib) ||
421 undefined
422 }
423 className={classNames[UI.MonthGrid]}
424 style={styles?.[UI.MonthGrid]}
425 >
426 {!props.hideWeekdays && (
427 <components.Weekdays
428 className={classNames[UI.Weekdays]}
429 role="row"
430 style={styles?.[UI.Weekdays]}
431 >
432 {showWeekNumber && (
433 <components.WeekNumberHeader
434 aria-label={labelWeekNumberHeader(labelOptions)}
435 className={classNames[UI.WeekNumberHeader]}
436 role="columnheader"
437 style={styles?.[UI.WeekNumberHeader]}
438 >
439 {formatWeekNumberHeader()}
440 </components.WeekNumberHeader>
441 )}
442 {weekdays.map((weekday, i) => (
443 <components.Weekday
444 aria-label={labelWeekday(
445 weekday,
446 labelOptions,
447 dateLib
448 )}
449 className={classNames[UI.Weekday]}
450 key={i}
451 role="columnheader"
452 style={styles?.[UI.Weekday]}
453 >
454 {formatWeekdayName(weekday, formatOptions, dateLib)}
455 </components.Weekday>
456 ))}
457 </components.Weekdays>
458 )}
459 <components.Weeks
460 className={classNames[UI.Weeks]}
461 role="rowgroup"
462 style={styles?.[UI.Weeks]}
463 >
464 {calendarMonth.weeks.map((week, weekIndex) => {
465 return (
466 <components.Week
467 className={classNames[UI.Week]}
468 key={week.weekNumber}
469 role="row"
470 style={styles?.[UI.Week]}
471 week={week}
472 >
473 {showWeekNumber && (
474 <components.WeekNumber
475 week={week}
476 role="rowheader"
477 style={styles?.[UI.WeekNumber]}
478 aria-label={labelWeekNumber(week.weekNumber, {
479 locale
480 })}
481 className={classNames[UI.WeekNumber]}
482 >
483 {formatWeekNumber(week.weekNumber)}
484 </components.WeekNumber>
485 )}
486 {week.days.map((day: CalendarDay) => {
487 const { date } = day;
488 const modifiers = getModifiers(day);
489
490 modifiers[DayFlag.focused] =
491 !modifiers.hidden &&
492 Boolean(focused?.isEqualTo(day));
493
494 modifiers[SelectionState.selected] =
495 !modifiers.disabled &&
496 (isSelected?.(date) || modifiers.selected);
497
498 if (isDateRange(selectedValue)) {
499 // add range modifiers
500 const { from, to } = selectedValue;
501 modifiers[SelectionState.range_start] = Boolean(
502 from && to && dateLib.isSameDay(date, from)
503 );
504 modifiers[SelectionState.range_end] = Boolean(
505 from && to && dateLib.isSameDay(date, to)
506 );
507 modifiers[SelectionState.range_middle] =
508 rangeIncludesDate(
509 selectedValue,
510 date,
511 true,
512 dateLib
513 );
514 }
515
516 const style = getStyleForModifiers(
517 modifiers,
518 styles,
519 props.modifiersStyles
520 );
521
522 const className = getClassNamesForModifiers(
523 modifiers,
524 classNames,
525 props.modifiersClassNames
526 );
527
528 const ariaLabel = !isInteractive
529 ? labelGridcell(
530 date,
531 modifiers,
532 labelOptions,
533 dateLib
534 )
535 : undefined;
536
537 return (
538 <components.Day
539 key={`${dateLib.format(date, "yyyy-MM-dd")}_${dateLib.format(day.displayMonth, "yyyy-MM")}`}
540 day={day}
541 modifiers={modifiers}
542 role="gridcell"
543 className={className.join(" ")}
544 style={style}
545 aria-hidden={modifiers.hidden || undefined}
546 aria-selected={modifiers.selected || undefined}
547 aria-label={ariaLabel}
548 data-day={dateLib.format(date, "yyyy-MM-dd")}
549 data-month={
550 day.outside
551 ? dateLib.format(date, "yyyy-MM")
552 : undefined
553 }
554 data-selected={modifiers.selected || undefined}
555 data-disabled={modifiers.disabled || undefined}
556 data-hidden={modifiers.hidden || undefined}
557 data-outside={day.outside || undefined}
558 data-focused={modifiers.focused || undefined}
559 data-today={modifiers.today || undefined}
560 >
561 {isInteractive ? (
562 <components.DayButton
563 className={classNames[UI.DayButton]}
564 style={styles?.[UI.DayButton]}
565 day={day}
566 modifiers={modifiers}
567 disabled={modifiers.disabled || undefined}
568 tabIndex={isFocusTarget(day) ? 0 : -1}
569 aria-label={labelDayButton(
570 date,
571 modifiers,
572 labelOptions,
573 dateLib
574 )}
575 onClick={handleDayClick(day, modifiers)}
576 onBlur={handleDayBlur(day, modifiers)}
577 onFocus={handleDayFocus(day, modifiers)}
578 onKeyDown={handleDayKeyDown(day, modifiers)}
579 onMouseEnter={handleDayMouseEnter(
580 day,
581 modifiers
582 )}
583 onMouseLeave={handleDayMouseLeave(
584 day,
585 modifiers
586 )}
587 >
588 {formatDay(date, formatOptions, dateLib)}
589 </components.DayButton>
590 ) : (
591 formatDay(day.date, formatOptions, dateLib)
592 )}
593 </components.Day>
594 );
595 })}
596 </components.Week>
597 );
598 })}
599 </components.Weeks>
600 </components.MonthGrid>
601 </components.Month>
602 );
603 })}
604 </components.Months>
605 {props.footer && (
606 <components.Footer
607 className={classNames[UI.Footer]}
608 style={styles?.[UI.Footer]}
609 role="status"
610 aria-live="polite"
611 >
612 {props.footer}
613 </components.Footer>
614 )}
615 </components.Root>
616 </dayPickerContext.Provider>
617 );
618}
619
\No newline at end of file