Skip to content

Commit

Permalink
feat: add excludeDisabled prop for range mode (#2290)
Browse files Browse the repository at this point in the history
* feat: add excludeDisabled for range mode

* Update test
  • Loading branch information
gpbl committed Jul 22, 2024
1 parent ad8ec93 commit 57fec63
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 16 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 10 additions & 3 deletions examples/ModifiersDisabled.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ModifiersDisabled />);
// return all month's
expect(dateButton(day)).toBeDisabled();
});
8 changes: 1 addition & 7 deletions examples/ModifiersDisabled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,5 @@ import React from "react";
import { DayPicker } from "react-day-picker";

export function ModifiersDisabled() {
return (
<DayPicker
defaultMonth={new Date(2024, 5, 10)}
mode="range"
disabled={{ dayOfWeek: [0, 6] }}
/>
);
return <DayPicker mode="range" disabled={{ dayOfWeek: [0, 6] }} />;
}
9 changes: 9 additions & 0 deletions examples/RangeExcludeDisabled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";

import { DayPicker } from "react-day-picker";

export function RangeExcludeDisabled() {
return (
<DayPicker mode="range" disabled={{ dayOfWeek: [0, 6] }} excludeDisabled />
);
}
1 change: 1 addition & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
114 changes: 114 additions & 0 deletions src/selection/useRange.test.tsx
Original file line number Diff line number Diff line change
@@ -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
});
});
});
7 changes: 6 additions & 1 deletion src/selection/useRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function useRange<T extends DayPickerProps>(
const {
mode,
disabled,
excludeDisabled,
selected: initiallySelected,
required,
onSelect
Expand Down Expand Up @@ -74,7 +75,11 @@ export function useRange<T extends DayPickerProps>(
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;
Expand Down
12 changes: 12 additions & 0 deletions src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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. */
Expand Down
8 changes: 7 additions & 1 deletion test/render.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { screen, act, within, render } from "@testing-library/react";
export {
screen,
act,
within,
render,
renderHook
} from "@testing-library/react";
27 changes: 24 additions & 3 deletions website/docs/docs/selection-modes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<DayPicker mode="single" disabled={ new Date() } />

// disable weekends
<DayPicker mode="range" disabled={{ dayOfWeek: [0, 6] }} />
```

<BrowserWindow>
<Examples.ModifiersDisabled />
</BrowserWindow>

### 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
<DayPicker
mode="range"
disabled={{ dayOfWeek: [0, 6] }} // Disable including Sundays and Saturdays in range
// Disable weekends
disabled={{ dayOfWeek: [0, 6] }}
// Reset range when a disabled date is included
excludeDisabled
/>
```

<BrowserWindow>
<Examples.ModifiersDisabled />
<Examples.RangeExcludeDisabled />
</BrowserWindow>

## Customizing Selections
Expand Down

0 comments on commit 57fec63

Please sign in to comment.