Skip to content

Commit

Permalink
style: improve selection modes types (#2195)
Browse files Browse the repository at this point in the history
Introduce required type parameter
  • Loading branch information
gpbl authored Jun 6, 2024
1 parent 7e8f3c3 commit 54b7c0d
Show file tree
Hide file tree
Showing 19 changed files with 156 additions and 103 deletions.
4 changes: 2 additions & 2 deletions examples/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useId, useRef, useState } from "react";

import { format, isValid, parse } from "date-fns";
import { DayPicker } from "react-day-picker";
import { DayPicker, type SelectHandler } from "react-day-picker";

export function Dialog() {
const dialogRef = useRef<HTMLDialogElement>(null);
Expand Down Expand Up @@ -45,7 +45,7 @@ export function Dialog() {
* Function to handle the DayPicker select event: update the input value and
* the selected date, and set the month.
*/
const handleDayPickerSelect = (date: Date) => {
const handleDayPickerSelect: SelectHandler<"single"> = (date) => {
if (!date) {
setInputValue("");
setSelectedDate(undefined);
Expand Down
4 changes: 3 additions & 1 deletion examples/RangeShiftKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ function DayWithShiftKey(props: DayProps) {
}

export function RangeShiftKey() {
const [range, setRange] = React.useState<DateRange>({ from: undefined });
const [range, setRange] = React.useState<DateRange | undefined>({
from: undefined
});

let footer = <p>Please pick a day.</p>;

Expand Down
9 changes: 6 additions & 3 deletions src/DayPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import type { DayPickerProps, Mode } from "./types";
* DayPicker is a React component to create date pickers, calendars, and date
* inputs for web applications.
*
* @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
* @template R - Whether the selection is required. Defaults to `false`.
* @group Components
* @see http://daypicker.dev
*/
export function DayPicker<T extends Mode = "default">(
props: DayPickerProps<T>
) {
export function DayPicker<
T extends Mode = "default",
R extends boolean = false
>(props: DayPickerProps<T, R>) {
return (
<ContextProviders {...props}>
<Calendar />
Expand Down
21 changes: 13 additions & 8 deletions src/contexts/props.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ import type {
* Holds the props passed to the DayPicker component, with some optional props
* set to meaningful defaults.
*
* Access the Props context using the {@link useProps} hook.
* Access this context using the {@link useProps} hook.
*
* @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
* @template R - Whether the selection is required. Defaults to `false`.
* @group Contexts
*/
export interface PropsContext<T extends Mode> extends PropsBase {
export interface PropsContext<
T extends Mode = "default",
R extends boolean = false
> extends PropsBase {
classNames: ClassNames;
/** The `data-*` attributes passed to `<DayPicker />`. */
dataAttributes: DataAttributes;
Expand All @@ -46,23 +51,23 @@ export interface PropsContext<T extends Mode> extends PropsBase {
toDate: Date | undefined;
today: Date;
/** The currently selected value. */
selected: Selected<Mode> | undefined;
selected: Selected<Mode, R> | undefined;
/** The default selected value. */
defaultSelected: Selected<Mode> | undefined;
defaultSelected: Selected<Mode, R> | undefined;
/** The function that handles the day selection. */
onSelect: SelectHandler<Mode> | undefined;
onSelect: SelectHandler<Mode, R> | undefined;
}

const propsContext = createContext<PropsContext<Mode> | null>(null);
const propsContext = createContext<PropsContext<Mode, boolean> | null>(null);

/**
* Provide the props to the DayPicker components. Must be used as root of the
* other providers.
*
* @private
*/
export const PropsProvider = <T extends Mode>(
props: PropsWithChildren<DayPickerProps<T>>
export const PropsProvider = <T extends Mode, R extends boolean>(
props: PropsWithChildren<DayPickerProps<T, R>>
) => {
const reactId = useId();
const { children, ...restProps } = props;
Expand Down
4 changes: 2 additions & 2 deletions src/contexts/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import { SelectionProvider } from "./selection";
* @private
*/
export const ContextProviders: FunctionComponent<
PropsWithChildren<DayPickerProps<Mode>>
> = <T extends Mode>(props: PropsWithChildren<DayPickerProps<T>>) => {
PropsWithChildren<DayPickerProps<Mode, boolean>>
> = <T extends Mode>(props: PropsWithChildren<DayPickerProps<T, boolean>>) => {
const { children, ...dayPickerProps } = props;

return (
Expand Down
32 changes: 23 additions & 9 deletions src/contexts/selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import { useProps } from "./props";
*
* Access the selection context using the {@link useSelection} hook.
*
* @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
* @template R - Whether the selection is required. Defaults to `false`.
* @group Contexts
*/
export interface SelectionContext<T extends Mode> {
export interface SelectionContext<
T extends Mode = "default",
R extends boolean = false
> {
/** The currently selected value. */
selected: Selected<T> | undefined;
selected: Selected<T, R>;
/** Set the selected days. */
setSelected: (
date: Date,
Expand All @@ -50,7 +55,7 @@ export interface SelectionContext<T extends Mode> {
isMiddleOfRange: (date: Date) => boolean;
}

const contextValue: SelectionContext<Mode> = {
const contextValue: SelectionContext<Mode, boolean> = {
selected: undefined,
setSelected: () => undefined,
isSelected: () => false,
Expand All @@ -59,7 +64,8 @@ const contextValue: SelectionContext<Mode> = {
isMiddleOfRange: () => false
};

const selectionContext = createContext<SelectionContext<Mode>>(contextValue);
const selectionContext =
createContext<SelectionContext<Mode, boolean>>(contextValue);

/** @private */
export function SelectionProvider(providerProps: PropsWithChildren) {
Expand Down Expand Up @@ -112,11 +118,14 @@ export function SelectionProvider(providerProps: PropsWithChildren) {
selected = value;
} else {
// Remove the date from the selection
selected = value?.filter((day) => !isSameDay(day, date)) || [];
selected =
value?.filter((day: Date | undefined) => {
return Boolean(day && date && !isSameDay(day, date));
}) || ([] as Date[]);
}
} else if (max !== undefined && value?.length === max) {
// Max value reached, reset the selection to date
selected = [date];
selected = date ? [date] : [];
} else {
// Add the date to the selection
selected = [...(value ?? []), date];
Expand All @@ -140,7 +149,7 @@ export function SelectionProvider(providerProps: PropsWithChildren) {
if (value !== undefined && !isDateRange(value)) {
return;
}
const selected = addToRange(date, value);
const selected = date ? addToRange(date, value) : undefined;

if (min) {
if (
Expand Down Expand Up @@ -238,13 +247,18 @@ export function SelectionProvider(providerProps: PropsWithChildren) {
*
* Use this hook from the custom components passed via the `components` prop.
*
* @template T - The {@link Mode | selection mode}. Defaults to `"default"`.
* @template R - Whether the selection is required. Defaults to `false`.
* @group Hooks
* @see https://react-day-picker.js.org/advanced-guides/custom-components
*/
export function useSelection<T extends Mode>() {
export function useSelection<
T extends Mode = "default",
R extends boolean = false
>() {
const context = useContext(selectionContext);
if (!context) {
throw new Error(`useSelection must be used within a SelectionProvider.`);
}
return context as SelectionContext<T>;
return context as SelectionContext<T, R>;
}
2 changes: 1 addition & 1 deletion src/helpers/getDates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function getDates(
displayMonths: Date[],
maxDate?: Date | undefined,
options?: Pick<
DayPickerProps<Mode>,
DayPickerProps<Mode, boolean>,
"ISOWeek" | "fixedWeeks" | "locale" | "weekStartsOn"
>
): Date[] {
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getDropdownMonths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Formatters, Mode } from "../types";

/** Return the months to show in the dropdown. */
export function getDropdownMonths(
props: Pick<PropsContext<Mode>, "fromDate" | "toDate" | "locale"> & {
props: Pick<PropsContext<Mode, boolean>, "fromDate" | "toDate" | "locale"> & {
formatters: Pick<Formatters, "formatMonthDropdown">;
}
): DropdownOption[] | undefined {
Expand Down
15 changes: 6 additions & 9 deletions src/helpers/getDropdownYears.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,19 @@ import type { Formatters, Mode } from "../types";

/** Return the years to show in the dropdown. */
export function getDropdownYears(
dayPicker: Pick<PropsContext<Mode>, "fromDate" | "toDate"> & {
props: Pick<PropsContext<Mode>, "fromDate" | "toDate"> & {
formatters: Pick<Formatters, "formatYearDropdown">;
}
): DropdownOption[] | undefined {
if (!dayPicker.fromDate) return undefined;
if (!dayPicker.toDate) return undefined;
const firstNavYear = startOfYear(dayPicker.fromDate);
const lastNavYear = endOfYear(dayPicker.toDate);
if (!props.fromDate) return undefined;
if (!props.toDate) return undefined;
const firstNavYear = startOfYear(props.fromDate);
const lastNavYear = endOfYear(props.toDate);
const years: number[] = [];
let year = firstNavYear;
while (isBefore(year, lastNavYear) || isSameYear(year, lastNavYear)) {
years.push(year.getFullYear());
year = addYears(year, 1);
}
return years.map((year) => [
year,
dayPicker.formatters.formatYearDropdown(year)
]);
return years.map((year) => [year, props.formatters.formatYearDropdown(year)]);
}
4 changes: 2 additions & 2 deletions src/helpers/getFromToDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { DayPickerProps, Mode } from "../types";
*/
export function getFromToDate(
props: Pick<
DayPickerProps<Mode>,
DayPickerProps<Mode, boolean>,
| "fromYear"
| "toYear"
| "fromDate"
Expand All @@ -23,7 +23,7 @@ export function getFromToDate(
| "today"
| "captionLayout"
>
): Pick<DayPickerProps<Mode>, "fromDate" | "toDate"> {
): Pick<DayPickerProps<Mode, boolean>, "fromDate" | "toDate"> {
const { fromYear, toYear, fromMonth, toMonth } = props;
let { fromDate, toDate } = props;
const hasDropdowns = props.captionLayout?.startsWith("dropdown");
Expand Down
4 changes: 3 additions & 1 deletion src/helpers/getInitialMonth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type { PropsContext } from "../contexts/props";
import type { Mode } from "../types";

/** Return the initial month according to the given options. */
export function getInitialMonth(context: Partial<PropsContext<Mode>>): Date {
export function getInitialMonth(
context: Partial<PropsContext<Mode, boolean>>
): Date {
const { month, defaultMonth, today } = context;
let initialMonth = month || defaultMonth || today || new Date();

Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getMonths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function getMonths(
/** The dates to display in the calendar. */
dates: Date[],
options: Pick<
DayPickerProps<Mode>,
DayPickerProps<Mode, boolean>,
| "fixedWeeks"
| "ISOWeek"
| "locale"
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getNextFocus.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { Mode } from "../types";
import { getNextFocus } from "./getNextFocus";

const defaultDayPicker: Pick<
PropsContext<Mode>,
PropsContext<Mode, boolean>,
"disabled" | "hidden" | "fromDate" | "toDate"
> = {
disabled: [],
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/getNextFocus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { dateMatchModifiers } from "../utils/dateMatchModifiers";
import { getPossibleFocusDate } from "./getPossibleFocusDate";

export type Options = Pick<
PropsContext<Mode>,
PropsContext<Mode, boolean>,
"modifiers" | "locale" | "ISOWeek" | "weekStartsOn" | "fromDate" | "toDate"
>;

Expand All @@ -19,7 +19,7 @@ export function getNextFocus(
/** The date that is currently focused. */
focused: CalendarDay,
options: Pick<
PropsContext<Mode>,
PropsContext<Mode, boolean>,
| "disabled"
| "hidden"
| "modifiers"
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getPossibleFocusDate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getPossibleFocusDate } from "./getPossibleFocusDate";

const baseDate = new Date(2023, 0, 1); // Jan 1, 2023
const options: Pick<
PropsContext<Mode>,
PropsContext<Mode, boolean>,
"locale" | "ISOWeek" | "weekStartsOn" | "fromDate" | "toDate"
> = {
locale: undefined,
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getPossibleFocusDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function getPossibleFocusDate(
moveDir: MoveFocusDir,
focusedDate: Date,
options: Pick<
PropsContext<Mode>,
PropsContext<Mode, boolean>,
"locale" | "ISOWeek" | "weekStartsOn" | "fromDate" | "toDate"
>
): Date {
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/useControlledValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type DispatchStateAction<T> = React.Dispatch<React.SetStateAction<T>>;
*
* If the value is controlled, pass the controlled value as the second argument,
* which will always be returned as `value`.
*
* @template T - The type of the value.
*/
export function useControlledValue<T>(
defaultValue: T,
Expand Down
14 changes: 7 additions & 7 deletions src/types-deprecated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,19 @@ export type RowProps = WeekProps;
* @deprecated This type has been renamed. Use `PropsSingle` instead.
* @protected
*/
export type DayPickerSingleProps = PropsSingle;
export type DayPickerSingleProps = PropsSingle<boolean>;

/**
* @deprecated This type has been renamed. Use `PropsMulti` instead.
* @protected
*/
export type DayPickerMultipleProps = PropsMulti;
export type DayPickerMultipleProps = PropsMulti<boolean>;

/**
* @deprecated This type has been renamed. Use `PropsRange` instead.
* @protected
*/
export type DayPickerRangeProps = PropsRange;
export type DayPickerRangeProps = PropsRange<boolean>;

/**
* @deprecated This type will be removed.
Expand All @@ -112,20 +112,20 @@ export type Modifier = string;
* @deprecated This type will be removed. Use `SelectHandler<"single">` instead.
* @protected
*/
export type SelectSingleEventHandler = SelectHandler<"single">;
export type SelectSingleEventHandler = SelectHandler<"single", false>;

/**
* @deprecated This type will be removed. Use `SelectHandler<"multiple">`
* instead.
* @protected
*/
export type SelectMultipleEventHandler = SelectHandler<"multiple">;
export type SelectMultipleEventHandler = SelectHandler<"multiple", false>;

/**
* @deprecated This type will be removed. Use `SelectHandler<"range">` instead.
* @protected
*/
export type SelectRangeEventHandler = SelectHandler<"range">;
export type SelectRangeEventHandler = SelectHandler<"range", false>;

/**
* @deprecated This type is not used anymore.
Expand Down Expand Up @@ -236,4 +236,4 @@ export type DayTouchEventHandler = DayEventHandler<React.TouchEvent>;
* `PropsContext` instead.
* @protected
*/
export type DayPickerContext = PropsContext<Mode>;
export type DayPickerContext = PropsContext;
Loading

0 comments on commit 54b7c0d

Please sign in to comment.