From 496cb6ef9eb0ba921b68f1920c4abbba3e08fe55 Mon Sep 17 00:00:00 2001 From: balajis-qb Date: Mon, 29 Sep 2025 17:19:59 +0530 Subject: [PATCH] Enhance inline DatePicker component with disabled state support - Added a `disabled` prop to the DatePicker, affecting all date selection components (Day, Month, Year, etc.) to visually indicate disabled states. - Updated styles to reflect disabled states, including cursor and color changes. - Modified utility functions to check for disabled dates. - Ensured that all date-related components respect the disabled state, preventing user interaction when disabled. - Updated tests to verify the disabled functionality across various scenarios. This change improves accessibility and user experience by clearly indicating when the DatePicker is not interactive. Closes #5620 --- docs-site/src/components/Examples/index.jsx | 5 ++ docs-site/src/examples/disabledInline.js | 11 ++++ src/calendar.tsx | 19 +++++- src/date_utils.ts | 39 +++++++++-- src/day.tsx | 6 +- src/month.tsx | 20 +++++- src/stylesheets/datepicker.scss | 5 ++ src/test/datepicker_test.test.tsx | 72 +++++++++++++++++++++ src/year.tsx | 19 ++++-- 9 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 docs-site/src/examples/disabledInline.js diff --git a/docs-site/src/components/Examples/index.jsx b/docs-site/src/components/Examples/index.jsx index 1e1f51cfc..9fb8a6c4c 100644 --- a/docs-site/src/components/Examples/index.jsx +++ b/docs-site/src/components/Examples/index.jsx @@ -61,6 +61,7 @@ import MonthDropdownShort from "../../examples/monthDropdownShort?raw"; import MonthYearDropdown from "../../examples/monthYearDropdown?raw"; import YearSelectDropdown from "../../examples/yearSelectDropdown?raw"; import Inline from "../../examples/inline?raw"; +import InlineDisabled from "../../examples/disabledInline?raw"; import InlineVisible from "../../examples/inlineVisible?raw"; import OpenToDate from "../../examples/openToDate?raw"; import FixedCalendar from "../../examples/fixedCalendar?raw"; @@ -360,6 +361,10 @@ export default class exampleComponents extends React.Component { title: "Inline version", component: Inline, }, + { + title: "Disabled Inline version", + component: InlineDisabled, + }, { title: "Button to show Inline version", component: InlineVisible, diff --git a/docs-site/src/examples/disabledInline.js b/docs-site/src/examples/disabledInline.js new file mode 100644 index 000000000..d8a236eb0 --- /dev/null +++ b/docs-site/src/examples/disabledInline.js @@ -0,0 +1,11 @@ +() => { + const [selectedDate, setSelectedDate] = useState(new Date()); + return ( + setSelectedDate(date)} + inline + disabled + /> + ); +}; diff --git a/src/calendar.tsx b/src/calendar.tsx index 9b00b22ef..e04158010 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -468,6 +468,7 @@ export default class Calendar extends Component { }; header = (date: Date = this.state.date): React.ReactElement[] => { + const disabled = this.props.disabled; const startOfWeek = getStartOfWeek( date, this.props.locale, @@ -477,7 +478,11 @@ export default class Calendar extends Component { const dayNames: React.ReactElement[] = []; if (this.props.showWeekNumbers) { dayNames.push( -
+
Week number
, @@ -496,7 +501,11 @@ export default class Calendar extends Component {
{formatDate(day, "EEEE", this.props.locale)} @@ -551,6 +560,9 @@ export default class Calendar extends Component { let allPrevDaysDisabled; switch (true) { + case this.props.disabled: + allPrevDaysDisabled = true; + break; case this.props.showMonthYearPicker: allPrevDaysDisabled = yearDisabledBefore(this.state.date, this.props); break; @@ -662,6 +674,9 @@ export default class Calendar extends Component { let allNextDaysDisabled: boolean; switch (true) { + case this.props.disabled: + allNextDaysDisabled = true; + break; case this.props.showMonthYearPicker: allNextDaysDisabled = yearDisabledAfter(this.state.date, this.props); break; diff --git a/src/date_utils.ts b/src/date_utils.ts index 12600b89b..eee05b9b0 100644 --- a/src/date_utils.ts +++ b/src/date_utils.ts @@ -728,6 +728,10 @@ export interface DateFilterOptions { yearItemNumber?: number; } +export type DateFilterOptionsWithDisabled = DateFilterOptions & { + disabled?: boolean; +}; + /** * Checks if a day is disabled. * @@ -745,8 +749,13 @@ export function isDayDisabled( includeDates, includeDateIntervals, filterDate, - }: DateFilterOptions = {}, + disabled, + }: DateFilterOptionsWithDisabled = {}, ): boolean { + if (disabled) { + return true; + } + return ( isOutOfBounds(day, { minDate, maxDate }) || (excludeDates && @@ -898,11 +907,21 @@ export function isQuarterDisabled( excludeDates, includeDates, filterDate, + disabled, }: Pick< - DateFilterOptions, - "minDate" | "maxDate" | "excludeDates" | "includeDates" | "filterDate" + DateFilterOptionsWithDisabled, + | "minDate" + | "maxDate" + | "excludeDates" + | "includeDates" + | "filterDate" + | "disabled" > = {}, ): boolean { + if (disabled) { + return true; + } + return ( isOutOfBounds(quarter, { minDate, maxDate }) || excludeDates?.some((excludeDate) => @@ -941,11 +960,21 @@ export function isYearDisabled( excludeDates, includeDates, filterDate, + disabled, }: Pick< - DateFilterOptions, - "minDate" | "maxDate" | "excludeDates" | "includeDates" | "filterDate" + DateFilterOptionsWithDisabled, + | "minDate" + | "maxDate" + | "excludeDates" + | "includeDates" + | "filterDate" + | "disabled" > = {}, ): boolean { + if (disabled) { + return true; + } + const date = new Date(year, 0, 1); return ( isOutOfBounds(date, { diff --git a/src/day.tsx b/src/day.tsx index 4413c8873..42291bda3 100644 --- a/src/day.tsx +++ b/src/day.tsx @@ -16,7 +16,7 @@ import { getDayOfWeekCode, getStartOfWeek, formatDate, - type DateFilterOptions, + type DateFilterOptionsWithDisabled, type DateNumberType, type Locale, type HolidaysMap, @@ -25,7 +25,7 @@ import { interface DayProps extends Pick< - DateFilterOptions, + DateFilterOptionsWithDisabled, | "minDate" | "maxDate" | "excludeDates" @@ -33,6 +33,7 @@ interface DayProps | "includeDateIntervals" | "includeDates" | "filterDate" + | "disabled" > { ariaLabelPrefixWhenEnabled?: string; ariaLabelPrefixWhenDisabled?: string; @@ -210,6 +211,7 @@ export default class Day extends Component { includeDateIntervals: this.props.includeDateIntervals, includeDates: this.props.includeDates, filterDate: this.props.filterDate, + disabled: this.props.disabled, }); isExcluded = () => diff --git a/src/month.tsx b/src/month.tsx index c39054d23..e8681d04d 100644 --- a/src/month.tsx +++ b/src/month.tsx @@ -250,6 +250,7 @@ export default class Month extends Component { includeDateIntervals: this.props.includeDateIntervals, includeDates: this.props.includeDates, filterDate: this.props.filterDate, + disabled: this.props.disabled, }); isExcluded = (day: Date) => @@ -783,8 +784,17 @@ export default class Month extends Component { isDisabled: boolean; labelDate: Date; } => { - const { day, minDate, maxDate, excludeDates, includeDates } = this.props; + const { day, disabled, minDate, maxDate, excludeDates, includeDates } = + this.props; const labelDate = setMonth(day, month); + + if (disabled) { + return { + isDisabled: true, + labelDate: setMonth(day, month), + }; + } + return { isDisabled: ((minDate || maxDate || excludeDates || includeDates) && @@ -919,10 +929,16 @@ export default class Month extends Component { filterDate, preSelection, disabledKeyboardNavigation, + disabled, } = this.props; const isDisabled = - (minDate || maxDate || excludeDates || includeDates || filterDate) && + (minDate || + maxDate || + excludeDates || + includeDates || + filterDate || + disabled) && isQuarterDisabled(setQuarter(day, q), this.props); return clsx( diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index fc6652de6..687ec019f 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -399,6 +399,11 @@ h2.react-datepicker__current-month { line-height: $datepicker__item-size; text-align: center; margin: $datepicker__day-margin; + + &--disabled { + cursor: default; + color: $datepicker__muted-color; + } } .react-datepicker__day, diff --git a/src/test/datepicker_test.test.tsx b/src/test/datepicker_test.test.tsx index c856bf109..658d57a35 100644 --- a/src/test/datepicker_test.test.tsx +++ b/src/test/datepicker_test.test.tsx @@ -4883,4 +4883,76 @@ describe("DatePicker", () => { expect(eventObject.target?.value).toBe(inputValue); }); }); + + describe("disabled", () => { + const validateAllDisabled = (container: HTMLElement, className: string) => { + const allDays = Array.from(container.querySelectorAll(`.${className}`)); + expect(allDays.length).toBeGreaterThan(0); + expect( + allDays.every((day) => + day.classList.contains(`${className}--disabled`), + ), + ).toBe(true); + }; + + const validateNonExistence = ( + container: HTMLElement, + querySelector: string, + ) => { + const element = container.querySelector(querySelector); + expect(element).toBeFalsy(); + }; + + it("should disable all days and headers in DatePicker when disabled prop is true", () => { + const { container } = render(); + + validateAllDisabled(container, "react-datepicker__day"); + validateAllDisabled(container, "react-datepicker__day-name"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + + it("should disable all days and headers in DatePicker Range Selector when disabled prop is true", () => { + const { container } = render(); + + validateAllDisabled(container, "react-datepicker__day"); + validateAllDisabled(container, "react-datepicker__day-name"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + + it("should disable all days and headers in MonthPicker when disabled prop is true", () => { + const { container } = render( + , + ); + + validateAllDisabled(container, "react-datepicker__month-text"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + + it("should disable all days and headers in YearPicker when disabled prop is true", () => { + const { container } = render( + , + ); + + validateAllDisabled(container, "react-datepicker__year-text"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + + it("should disable all days and headers in WeekPicker when disabled prop is true", () => { + const { container } = render( + , + ); + + validateAllDisabled(container, "react-datepicker__day"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + + it("should disable all days and headers in QuarterPicker when disabled prop is true", () => { + const { container } = render( + , + ); + + validateAllDisabled(container, "react-datepicker__quarter-text"); + validateNonExistence(container, "react-datepicker__navigation"); + }); + }); }); diff --git a/src/year.tsx b/src/year.tsx index 271c3f18b..eb7fe419c 100644 --- a/src/year.tsx +++ b/src/year.tsx @@ -2,7 +2,7 @@ import { clsx } from "clsx"; import React, { Component, createRef } from "react"; import { - type DateFilterOptions, + type DateFilterOptionsWithDisabled, addYears, getStartOfYear, getYear, @@ -24,8 +24,13 @@ const VERTICAL_NAVIGATION_OFFSET = 3; interface YearProps extends Pick< - DateFilterOptions, - "minDate" | "maxDate" | "excludeDates" | "includeDates" | "filterDate" + DateFilterOptionsWithDisabled, + | "minDate" + | "maxDate" + | "excludeDates" + | "includeDates" + | "filterDate" + | "disabled" > { clearSelectingDate?: VoidFunction; date?: Date; @@ -381,6 +386,7 @@ export default class Year extends Component { getYearClassNames = (y: number) => { const { date, + disabled, minDate, maxDate, excludeDates, @@ -396,7 +402,12 @@ export default class Year extends Component { { "react-datepicker__year-text--selected": this.isSelectedYear(y), "react-datepicker__year-text--disabled": - (minDate || maxDate || excludeDates || includeDates || filterDate) && + (minDate || + maxDate || + excludeDates || + includeDates || + filterDate || + disabled) && isYearDisabled(y, this.props), "react-datepicker__year-text--keyboard-selected": this.isKeyboardSelected(y),