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

style: improve selection modes types #2195

Merged
merged 2 commits into from
Jun 6, 2024
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
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
Loading