From 57fec63ecc24685e33dec5019fd21a03c03fffb0 Mon Sep 17 00:00:00 2001 From: Giampaolo Bellavite Date: Mon, 22 Jul 2024 07:41:31 -0500 Subject: [PATCH] feat: add excludeDisabled prop for range mode (#2290) * feat: add excludeDisabled for range mode * Update test --- CHANGELOG.md | 2 +- examples/ModifiersDisabled.test.tsx | 13 ++- examples/ModifiersDisabled.tsx | 8 +- examples/RangeExcludeDisabled.tsx | 9 ++ examples/index.ts | 1 + src/selection/useRange.test.tsx | 114 ++++++++++++++++++++++++++ src/selection/useRange.tsx | 7 +- src/types/props.ts | 12 +++ test/render.tsx | 8 +- website/docs/docs/selection-modes.mdx | 27 +++++- 10 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 examples/RangeExcludeDisabled.tsx create mode 100644 src/selection/useRange.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index d80ac78e3..c5fff7dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ npm install react-day-picker@latest - Added support for [UTC dates](https://daypicker.dev/docs/localization#utc-dates) and [Jalali Calendar](https://daypicker.dev/docs/localization#jalali-calendar). - [Enhanced accessibility](https://daypicker.dev/docs/accessibility) to better comply with [WCAG 2.1](https://www.w3.org/TR/WCAG21/) recommendations. - [Simplified styles](https://daypicker.dev/docs/styling) and new CSS variables for easier customization. -- Improved selection logic for [range mode](https://daypicker.dev/docs/selection-modes). +- New `excludeDisabled` prop for [range mode](https://daypicker.dev/docs/selection-modes#exclude-disabled). - New `dropdown-years` and `dropdown-months` caption layouts. - New `hideWeekdayRow` and `hideNavigation` props. - Updated for a complete [custom components](https://daypicker.dev/guides/custom-components) support. diff --git a/examples/ModifiersDisabled.test.tsx b/examples/ModifiersDisabled.test.tsx index ecd5c3705..91bcae04f 100644 --- a/examples/ModifiersDisabled.test.tsx +++ b/examples/ModifiersDisabled.test.tsx @@ -5,13 +5,20 @@ import { render } from "@/test/render"; import { ModifiersDisabled } from "./ModifiersDisabled"; +const today = new Date(2024, 6, 22); + +beforeAll(() => jest.setSystemTime(today)); +afterAll(() => jest.useRealTimers()); + const days = [ - new Date(2024, 5, 2), - new Date(2024, 5, 9), - new Date(2024, 5, 29) + new Date(2024, 6, 6), + new Date(2024, 6, 13), + new Date(2024, 6, 20), + new Date(2024, 6, 27) ]; test.each(days)("the day %s should be disabled", (day) => { render(); + // return all month's expect(dateButton(day)).toBeDisabled(); }); diff --git a/examples/ModifiersDisabled.tsx b/examples/ModifiersDisabled.tsx index ac611821b..b890d4eeb 100644 --- a/examples/ModifiersDisabled.tsx +++ b/examples/ModifiersDisabled.tsx @@ -3,11 +3,5 @@ import React from "react"; import { DayPicker } from "react-day-picker"; export function ModifiersDisabled() { - return ( - - ); + return ; } diff --git a/examples/RangeExcludeDisabled.tsx b/examples/RangeExcludeDisabled.tsx new file mode 100644 index 000000000..fee7588de --- /dev/null +++ b/examples/RangeExcludeDisabled.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +import { DayPicker } from "react-day-picker"; + +export function RangeExcludeDisabled() { + return ( + + ); +} diff --git a/examples/index.ts b/examples/index.ts index 7bb79cad5..ba806e736 100644 --- a/examples/index.ts +++ b/examples/index.ts @@ -43,6 +43,7 @@ export * from "./MultipleMonthsPaged"; export * from "./NumberingSystem"; export * from "./OutsideDays"; export * from "./Range"; +export * from "./RangeExcludeDisabled"; export * from "./RangeMinMax"; export * from "./RangeShiftKey"; export * from "./Rtl"; diff --git a/src/selection/useRange.test.tsx b/src/selection/useRange.test.tsx new file mode 100644 index 000000000..476b20f5a --- /dev/null +++ b/src/selection/useRange.test.tsx @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { act, renderHook } from "@/test/render"; + +import { dateLib } from "../lib"; + +import { useRange } from "./useRange"; + +describe("useRange", () => { + test("initialize with initiallySelected date range", () => { + const initiallySelected = { + from: new Date(2023, 6, 1), + to: new Date(2023, 6, 5) + }; + const { result } = renderHook(() => + useRange( + { mode: "range", selected: initiallySelected, required: false }, + dateLib + ) + ); + + expect(result.current.selected).toEqual(initiallySelected); + }); + + test("update the selected range on select", () => { + const initiallySelected = { + from: new Date(2023, 6, 1), + to: new Date(2023, 6, 5) + }; + const { result } = renderHook(() => + useRange( + { mode: "range", selected: initiallySelected, required: false }, + dateLib + ) + ); + + act(() => { + result.current.select?.(new Date(2023, 6, 10), {}, {} as any); + }); + + expect(result.current.selected).toEqual({ + from: new Date(2023, 6, 1), + to: new Date(2023, 6, 10) + }); + }); + + test("reset range if new range exceeds max days", () => { + const { result } = renderHook(() => + useRange( + { + mode: "range", + selected: undefined, + required: false, + max: 5 + }, + dateLib + ) + ); + + act(() => { + result.current.select?.(new Date(2023, 6, 1), {}, {} as any); + result.current.select?.(new Date(2023, 6, 10), {}, {} as any); + }); + + expect(result.current.selected).toEqual({ + from: new Date(2023, 6, 10), + to: undefined + }); + }); + + test("reset range if new range is less than min days", () => { + const { result } = renderHook(() => + useRange( + { mode: "range", selected: undefined, required: false, min: 5 }, + dateLib + ) + ); + + act(() => { + result.current.select?.(new Date(2023, 6, 1), {}, {} as any); + result.current.select?.(new Date(2023, 6, 3), {}, {} as any); + }); + + expect(result.current.selected).toEqual({ + from: new Date(2023, 6, 3), + to: undefined + }); + }); + + test("exclude disabled dates when selecting range", () => { + const disabled = [{ from: new Date(2023, 6, 5), to: new Date(2023, 6, 7) }]; + const { result } = renderHook(() => + useRange( + { + mode: "range", + selected: undefined, + required: false, + excludeDisabled: true, + disabled + }, + dateLib + ) + ); + + act(() => { + result.current.select?.(new Date(2023, 6, 1), {}, {} as any); + result.current.select?.(new Date(2023, 6, 10), {}, {} as any); + }); + + expect(result.current.selected).toEqual({ + from: new Date(2023, 6, 10), + to: undefined + }); + }); +}); diff --git a/src/selection/useRange.tsx b/src/selection/useRange.tsx index b8f550eba..bb2ea0ed9 100644 --- a/src/selection/useRange.tsx +++ b/src/selection/useRange.tsx @@ -18,6 +18,7 @@ export function useRange( const { mode, disabled, + excludeDisabled, selected: initiallySelected, required, onSelect @@ -74,7 +75,11 @@ export function useRange( let newDate = newRange.from; while (dateLib.differenceInCalendarDays(newRange.to, newDate) > 0) { newDate = dateLib.addDays(newDate, 1); - if (disabled && dateMatchModifiers(newDate, disabled, dateLib)) { + if ( + excludeDisabled && + disabled && + dateMatchModifiers(newDate, disabled, dateLib) + ) { newRange.from = triggerDate; newRange.to = undefined; break; diff --git a/src/types/props.ts b/src/types/props.ts index 0c5d04de2..eb64f468d 100644 --- a/src/types/props.ts +++ b/src/types/props.ts @@ -546,6 +546,12 @@ export interface PropsRangeRequired { mode: "range"; required: true; disabled?: Matcher | Matcher[] | undefined; + /** + * When `true`, the range will reset when including a disabled day. + * + * @since V9.0.2 + */ + excludeDisabled?: boolean | undefined; /** The selected range. */ selected: DateRange; /** Event handler when a range is selected. */ @@ -569,6 +575,12 @@ export interface PropsRange { mode: "range"; required?: false | undefined; disabled?: Matcher | Matcher[] | undefined; + /** + * When `true`, the range will reset when including a disabled day. + * + * @since V9.0.2 + */ + excludeDisabled?: boolean | undefined; /** The selected range. */ selected?: DateRange | undefined; /** Event handler when the selection changes. */ diff --git a/test/render.tsx b/test/render.tsx index de85b0ce1..4ccc6848f 100644 --- a/test/render.tsx +++ b/test/render.tsx @@ -1 +1,7 @@ -export { screen, act, within, render } from "@testing-library/react"; +export { + screen, + act, + within, + render, + renderHook +} from "@testing-library/react"; diff --git a/website/docs/docs/selection-modes.mdx b/website/docs/docs/selection-modes.mdx index 3180208fb..0ba52409e 100644 --- a/website/docs/docs/selection-modes.mdx +++ b/website/docs/docs/selection-modes.mdx @@ -194,17 +194,38 @@ export function RangeMinMax() { ## Disabling Dates -To disable specific days, use the `disabled` prop. The prop accepts a [`Matcher`](../api/type-aliases/Matcher.md) or an array of matchers that can be used to make some days not selectable. +To disable specific days, use the `disabled` prop. Disabled dates cannot be selected. + +The prop accepts a [`Matcher`](../api/type-aliases/Matcher.md) or an array of matchers that can be used to make some days not selectable. + +```tsx +// disable today + + +// disable weekends + +``` + + + + + +### Excluding Disabled Dates from Range {#exclude-disabled} + +When using the `range` mode, disabled dates will be included in the selected range. Use the `excludeDisabled` prop to prevent this behavior. The range will reset when a disabled date is included. ```tsx ``` - + ## Customizing Selections