From 91ede5e2668e3523886f4b41da80982a81cd9513 Mon Sep 17 00:00:00 2001 From: roshidhmohammed Date: Tue, 24 Mar 2026 00:55:40 +0530 Subject: [PATCH] feat: added darkTheme prop for dark mode support Adds built-in dark theme support to DatePicker components using the darkTheme prop. - When darkTheme={true}, the DatePicker renders with dark theme styles - When darkTheme={false} or not provided, defaults to existing light theme (ensures backward compatibility) - Applies to all DatePicker subcomponents (time, day, month, year, etc.) - No changes required to existing implementations - Enables easy theme switching without modifying styles - Default behaviour remains unchanged (no breaking changes) - Provides optional dark theme support across the entire DatePicker UI --- docs-site/src/components/Examples/config.tsx | 6 + docs-site/src/examples/ts/applyDarkTheme.tsx | 16 ++ src/apply_dark_theme.tsx | 20 +++ src/index.tsx | 6 + src/month_dropdown.tsx | 2 +- src/stylesheets/datepicker.scss | 102 +++++++++++-- src/stylesheets/variables.scss | 7 + src/test/apply_dark_theme.test.tsx | 147 +++++++++++++++++++ src/year_dropdown.tsx | 2 +- 9 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 docs-site/src/examples/ts/applyDarkTheme.tsx create mode 100644 src/apply_dark_theme.tsx create mode 100644 src/test/apply_dark_theme.test.tsx diff --git a/docs-site/src/components/Examples/config.tsx b/docs-site/src/components/Examples/config.tsx index 4baac1b7f..62751de1c 100644 --- a/docs-site/src/components/Examples/config.tsx +++ b/docs-site/src/components/Examples/config.tsx @@ -113,6 +113,7 @@ import WeekPicker from "../../examples/ts/weekPicker?raw"; import ExcludeWeeks from "../../examples/ts/excludeWeeks?raw"; import ExternalForm from "../../examples/ts/externalForm?raw"; import Timezone from "../../examples/ts/timezone?raw"; +import ApplyDarkTheme from "../../examples/ts/applyDarkTheme?raw" export const EXAMPLE_CONFIG: IExampleConfig[] = [ { @@ -588,4 +589,9 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [ "Display and handle dates in a specific timezone using the timeZone prop. Requires date-fns-tz as a peer dependency.", component: Timezone, }, + { + title: "Apply Dark Theme", + description: "Apply the dark theme without modifying existing styles by using the darkTheme prop. Set darkTheme={true} or simply darkTheme to enable it.", + component: ApplyDarkTheme, + }, ]; diff --git a/docs-site/src/examples/ts/applyDarkTheme.tsx b/docs-site/src/examples/ts/applyDarkTheme.tsx new file mode 100644 index 000000000..1d5037658 --- /dev/null +++ b/docs-site/src/examples/ts/applyDarkTheme.tsx @@ -0,0 +1,16 @@ +const ApplyDarkTheme = () => { + const [selectedDate, setSelectedDate] = useState(new Date()); + + return ( + + ); +}; + +render(ApplyDarkTheme); diff --git a/src/apply_dark_theme.tsx b/src/apply_dark_theme.tsx new file mode 100644 index 000000000..168bd9a00 --- /dev/null +++ b/src/apply_dark_theme.tsx @@ -0,0 +1,20 @@ +export const applyDarkTheme = (isDarkTheme: boolean) => { + const root = document.documentElement; + + const variables = { + "--datepicker__container-background-color": isDarkTheme ? "#000000" : "#ffffff", + "--datepicker__header-background-color": isDarkTheme ? "#333" : "#f0f0f0", + "--datepicker__text-color": isDarkTheme ? "#ffffff" : "#000", + "--datepicker__header-color": isDarkTheme ? "#ffffff" : "#000", + "--datepicker__hover-text-color": "#000", + "--datepicker__background-color": isDarkTheme ? "#000000" : "#f0f0f0", + "--datepicker__time-container-background-color": isDarkTheme ? "#7e7b7b" : "#f0f0f0", + "--datepicker__today-button-background-color": isDarkTheme ? "#333" : "#f0f0f0", + "--datepicker__today-button-text-color": isDarkTheme ? "#ffffff" : "#000", + "--datepicker-time-icon-filter": isDarkTheme ? "invert(1)" : "none" + }; + + Object.entries(variables).forEach(([key, value]) => + root.style.setProperty(key, value) + ); +}; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 05fde7b81..dd814ed83 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -64,6 +64,7 @@ import Portal from "./portal"; import TabLoop from "./tab_loop"; import type { ClickOutsideHandler } from "./click_outside_wrapper"; +import { applyDarkTheme } from "./apply_dark_theme"; export { default as CalendarContainer } from "./calendar_container"; @@ -162,6 +163,7 @@ export type DatePickerProps = OmitUnion< onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; onClickOutside?: ClickOutsideHandler; + darkTheme?: boolean; onInputClick?: VoidFunction; preventOpenOnFocus?: boolean; closeOnScroll?: boolean | ((event: Event) => boolean); @@ -380,6 +382,8 @@ export class DatePicker extends Component { "visibilitychange", this.setHiddenStateOnVisibilityHidden, ); + + applyDarkTheme(!!this.props.darkTheme); } componentDidUpdate( @@ -438,6 +442,8 @@ export class DatePicker extends Component { this.props.onCalendarClose?.(); } } + + applyDarkTheme(!!this.props.darkTheme); } componentWillUnmount(): void { diff --git a/src/month_dropdown.tsx b/src/month_dropdown.tsx index 9efd3c727..86467c113 100644 --- a/src/month_dropdown.tsx +++ b/src/month_dropdown.tsx @@ -36,7 +36,7 @@ export default class MonthDropdown extends Component< renderSelectOptions = (monthNames: string[]): React.ReactElement[] => monthNames.map( (m: string, i: number): React.ReactElement => ( - ), diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index 57b8445f9..1a913351a 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -24,8 +24,8 @@ .react-datepicker { font-family: $datepicker__font-family; font-size: $datepicker__font-size; - background-color: #fff; - color: $datepicker__text-color; + background-color: var(--datepicker__container-background-color); + color: var(--datepicker__text-color); border: $datepicker__border; border-radius: $datepicker__border-radius; display: inline-block; @@ -55,19 +55,21 @@ .react-datepicker__triangle { stroke: $datepicker__border-color; + color: var(--datepicker__background-color); + fill: var(--datepicker__background-color); } &[data-placement^="bottom"] { .react-datepicker__triangle { - fill: $datepicker__background-color; - color: $datepicker__background-color; + fill: var(--datepicker__background-color); + color: var(--datepicker__background-color); } } &[data-placement^="top"] { .react-datepicker__triangle { - fill: #fff; - color: #fff; + fill: var(--datepicker__background-color); + color: var(--datepicker__background-color); } } @@ -75,8 +77,8 @@ &--header-bottom { &[data-placement^="bottom"] { .react-datepicker__triangle { - fill: #fff; - color: #fff; + fill: var(--datepicker__background-color); + color: var(--datepicker__background-color); } } } @@ -93,7 +95,7 @@ .react-datepicker__header { text-align: center; - background-color: $datepicker__background-color; + background-color: var(--datepicker__header-background-color); border-bottom: $datepicker__border; border-top-left-radius: $datepicker__border-radius; padding: 8px 0; @@ -165,15 +167,21 @@ &:focus-visible { outline: auto 1px; } + + &--options { + color: var(--datepicker__text-color); + background: var(--datepicker__container-background-color); + } } .react-datepicker__current-month, .react-datepicker-time__header, .react-datepicker-year-header { margin-top: 0; - color: $datepicker__header-color; + color: var(--datepicker__header-color); font-weight: bold; font-size: $datepicker__font-size * 1.18; + border-bottom-width: 0; } h2.react-datepicker__current-month { @@ -286,6 +294,10 @@ h2.react-datepicker__current-month { display: inline-block; width: 5em; margin: 2px; + + &:hover { + color: var(--datepicker__hover-text-color); + } } } @@ -318,6 +330,8 @@ h2.react-datepicker__current-month { .react-datepicker-time__input { display: inline-block; margin-left: 10px; + background: var(--datepicker__container-background-color); + color: var(--datepicker__text-color); input { width: auto; @@ -332,6 +346,17 @@ h2.react-datepicker__current-month { input[type="time"] { -moz-appearance: textfield; } + + input[type="time"] { + background: var(--datepicker__container-background-color); + color: var(--datepicker__text-color); + + /* Chrome / Edge / Safari */ + &::-webkit-calendar-picker-indicator { + filter: var(--datepicker-time-icon-filter); + cursor: pointer; + } + } } .react-datepicker-time__delimiter { @@ -345,6 +370,8 @@ h2.react-datepicker__current-month { float: right; border-left: $datepicker__border; width: 85px; + background: var(--datepicker__container-background-color); + color: var(--datepicker__text-color); &--with-today-button { display: inline; @@ -357,7 +384,7 @@ h2.react-datepicker__current-month { .react-datepicker__time { position: relative; - background: white; + background: var(--datepicker__container-background-color); border-bottom-right-radius: 0.375em; .react-datepicker__time-box { @@ -377,11 +404,34 @@ h2.react-datepicker__current-month { width: 100%; box-sizing: content-box; + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: var(--datepicker__time-container-background-color); + } + + &::-webkit-scrollbar-thumb { + background: $datepicker__muted-color; + border-radius: 4px; + } + + li.react-datepicker__time-list-item { height: 30px; padding: 5px 10px; white-space: nowrap; + &:not(&--disabled):hover { + cursor: pointer; + } + + &:not(&--selected):not(&--disabled):hover { + color: #000; + } + + &:hover { cursor: pointer; background-color: $datepicker__background-color; @@ -455,7 +505,7 @@ h2.react-datepicker__current-month { .react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name { - color: $datepicker__text-color; + color: var(--datepicker__text-color); display: inline-block; width: $datepicker__item-size; line-height: $datepicker__item-size; @@ -465,6 +515,10 @@ h2.react-datepicker__current-month { &--disabled { cursor: default; color: $datepicker__muted-color; + + &:hover { + color: $datepicker__muted-color; + } } } @@ -477,6 +531,7 @@ h2.react-datepicker__current-month { &:not([aria-disabled="true"]):hover { border-radius: $datepicker__border-radius; background-color: $datepicker__background-color; + color: var(--datepicker__hover-text-color); } &--today { @@ -493,14 +548,24 @@ h2.react-datepicker__current-month { $datepicker__highlighted-color, $lightness: -5% ); + color: #fff; } &-custom-1 { color: magenta; + + &:not([aria-disabled="true"]):hover { + color: magenta + } + } &-custom-2 { color: green; + + &:not([aria-disabled="true"]):hover { + color: green + } } } @@ -532,6 +597,7 @@ h2.react-datepicker__current-month { $datepicker__holidays-color, $lightness: -10% ); + color: #ffff; } &:hover .overlay { @@ -548,6 +614,7 @@ h2.react-datepicker__current-month { color: #fff; &:not([aria-disabled="true"]):hover { + color: #fff; background-color: color.adjust( $datepicker__selected-color, $lightness: -5% @@ -580,7 +647,7 @@ h2.react-datepicker__current-month { .react-datepicker__month--selecting-range &, .react-datepicker__year--selecting-range & { background-color: $datepicker__background-color; - color: $datepicker__text-color; + color: var(--datepicker__hover-text-color); } } @@ -631,6 +698,8 @@ h2.react-datepicker__current-month { border: 1px solid transparent; border-radius: $datepicker__border-radius; position: relative; + background: var(--datepicker__container-background-color); + color: var(--datepicker__text-color); &:hover { cursor: pointer; @@ -656,7 +725,7 @@ h2.react-datepicker__current-month { .react-datepicker__year-dropdown, .react-datepicker__month-dropdown, .react-datepicker__month-year-dropdown { - background-color: $datepicker__background-color; + background-color: var(--datepicker__container-background-color); position: absolute; width: 50%; left: 25%; @@ -665,6 +734,7 @@ h2.react-datepicker__current-month { text-align: center; border-radius: $datepicker__border-radius; border: $datepicker__border; + color: var(--datepicker__text-color); &:hover { cursor: pointer; @@ -701,6 +771,7 @@ h2.react-datepicker__current-month { &:hover { background-color: $datepicker__muted-color; + color: var(--datepicker__hover-text-color); .react-datepicker__navigation--years-upcoming { border-bottom-color: color.adjust( @@ -763,7 +834,8 @@ h2.react-datepicker__current-month { } .react-datepicker__today-button { - background: $datepicker__background-color; + background: var(--datepicker__today-button-background-color); + color: var(--datepicker__today-button-text-color); border-top: $datepicker__border; cursor: pointer; text-align: center; diff --git a/src/stylesheets/variables.scss b/src/stylesheets/variables.scss index 53866477f..c3b5b4c2f 100644 --- a/src/stylesheets/variables.scss +++ b/src/stylesheets/variables.scss @@ -1,6 +1,12 @@ @use "sass:color"; $datepicker__background-color: #f0f0f0 !default; +$datepicker__header-background-color: #f0f0f0 !default; +$datepicker__container-background-color: #fff !default; +$datepicker__time-container-background-color: #f0f0f0 !default; +$datepicker__today-button-background-color: #f0f0f0 !default; +$datepicker-time-icon-filter: none !default; +$datepicker__today-button-text-color: #000 !default; $datepicker__border-color: #aeaeae !default; $datepicker__highlighted-color: #3dcc4a !default; $datepicker__holidays-color: #ff6803 !default; @@ -8,6 +14,7 @@ $datepicker__muted-color: #ccc !default; $datepicker__selected-color: #216ba5 !default; $datepicker__selected-color--disabled: rgba($datepicker__selected-color, 0.5); $datepicker__text-color: #000 !default; +$datepicker__hover-text-color: #000 !default; $datepicker__header-color: #000 !default; $datepicker__navigation-disabled-color: color.adjust( $datepicker__muted-color, diff --git a/src/test/apply_dark_theme.test.tsx b/src/test/apply_dark_theme.test.tsx new file mode 100644 index 000000000..84ad31564 --- /dev/null +++ b/src/test/apply_dark_theme.test.tsx @@ -0,0 +1,147 @@ +import { applyDarkTheme } from "../apply_dark_theme"; + +describe("applyDarkTheme", () => { + let setPropertySpy: jest.SpyInstance; + + beforeEach(() => { + setPropertySpy = jest.spyOn( + document.documentElement.style, + "setProperty" + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("Dark Theme", () => { + it("should apply all dark theme CSS variables", () => { + applyDarkTheme(true); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__container-background-color", + "#000000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__header-background-color", + "#333" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__text-color", + "#ffffff" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__header-color", + "#ffffff" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__hover-text-color", + "#000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__background-color", + "#000000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__time-container-background-color", + "#7e7b7b" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__today-button-background-color", + "#333" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__today-button-text-color", + "#ffffff" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker-time-icon-filter", + "invert(1)" + ); + }); + }); + + describe("Light Theme", () => { + it("should apply all light theme CSS variables", () => { + applyDarkTheme(false); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__container-background-color", + "#ffffff" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__header-background-color", + "#f0f0f0" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__text-color", + "#000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__header-color", + "#000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__hover-text-color", + "#000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__background-color", + "#f0f0f0" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__time-container-background-color", + "#f0f0f0" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__today-button-background-color", + "#f0f0f0" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker__today-button-text-color", + "#000" + ); + + expect(setPropertySpy).toHaveBeenCalledWith( + "--datepicker-time-icon-filter", + "none" + ); + }); + }); + + describe("Edge Cases", () => { + it("should call setProperty exactly 10 times", () => { + applyDarkTheme(true); + + expect(setPropertySpy).toHaveBeenCalledTimes(10); + }); + + it("should not throw when executed", () => { + expect(() => applyDarkTheme(true)).not.toThrow(); + expect(() => applyDarkTheme(false)).not.toThrow(); + }); + + it("should handle unexpected runtime values gracefully", () => { + applyDarkTheme(undefined as unknown as boolean); + + expect(setPropertySpy).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/src/year_dropdown.tsx b/src/year_dropdown.tsx index c3bd83169..3bb18c88e 100644 --- a/src/year_dropdown.tsx +++ b/src/year_dropdown.tsx @@ -42,7 +42,7 @@ export default class YearDropdown extends Component< const options: React.ReactElement[] = []; for (let i = minYear; i <= maxYear; i++) { options.push( - , );