diff --git a/src/calendar.tsx b/src/calendar.tsx index 4bedbdc06..6e6664348 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -167,6 +167,7 @@ type CalendarProps = React.PropsWithChildren< showQuarterYearPicker?: boolean; showTimeSelect?: boolean; showTimeInput?: boolean; + todayButton?: React.ReactNode; showYearDropdown?: boolean; showMonthDropdown?: boolean; yearItemNumber?: number; @@ -239,6 +240,7 @@ interface CalendarState extends Pick, Pick { date: Required["date"]; monthContainer: TimeProps["monthRef"]; + todayButtonContainer: TimeProps["todayButtonRef"]; isRenderAriaLiveMessage: boolean; } @@ -267,18 +269,22 @@ export default class Calendar extends Component { date: this.getDateInView(), selectingDate: undefined, monthContainer: undefined, + todayButtonContainer: undefined, isRenderAriaLiveMessage: false, }; } componentDidMount() { - // monthContainer height is needed in time component + // monthContainer/todayButtonContainer height is needed in time component // to determine the height for the ul in the time component // setState here so height is given after final component // layout is rendered if (this.props.showTimeSelect) { this.assignMonthContainer = ((): void => { - this.setState({ monthContainer: this.monthContainer }); + this.setState({ + monthContainer: this.monthContainer, + todayButtonContainer: this.todayButtonContainer, + }); })(); } } @@ -314,6 +320,8 @@ export default class Calendar extends Component { monthContainer: CalendarState["monthContainer"] = undefined; + todayButtonContainer: CalendarState["todayButtonContainer"] = undefined; + assignMonthContainer: void | undefined; handleClickOutside = (event: MouseEvent): void => { @@ -773,9 +781,6 @@ export default class Calendar extends Component { if (this.props.showTimeSelect) { classes.push("react-datepicker__navigation--next--with-time"); } - if (this.props.todayButton) { - classes.push("react-datepicker__navigation--next--with-today-button"); - } let clickHandler: React.MouseEventHandler | undefined = this.increaseMonth; @@ -907,7 +912,13 @@ export default class Calendar extends Component { } return (
{ + this.todayButtonContainer = div ?? undefined; + }} + className={clsx("react-datepicker__today-button", { + "react-datepicker__today-button--with-time-select": + this.props.showTimeSelect, + })} onClick={this.handleTodayButtonClick} > {this.props.todayButton} @@ -1174,6 +1185,7 @@ export default class Calendar extends Component { format={this.props.timeFormat} intervals={this.props.timeIntervals} monthRef={this.state.monthContainer} + todayButtonRef={this.state.todayButtonContainer} /> ); } @@ -1327,8 +1339,8 @@ export default class Calendar extends Component { this.renderNextButton()} {this.renderMonths()} {this.renderYears()} - {this.renderTodayButton()} {this.renderTimeSection()} + {this.renderTodayButton()} {this.renderInputTimeSection()} {this.renderChildren()} diff --git a/src/stylesheets/datepicker.scss b/src/stylesheets/datepicker.scss index 57b8445f9..fcdb6799f 100644 --- a/src/stylesheets/datepicker.scss +++ b/src/stylesheets/datepicker.scss @@ -132,9 +132,7 @@ .react-datepicker__header-wrapper { position: relative; - .react-datepicker__navigation--next--with-time:not( - .react-datepicker__navigation--next--with-today-button - ) { + .react-datepicker__navigation--next--with-time { right: 2px; } } @@ -211,8 +209,8 @@ h2.react-datepicker__current-month { &--next { right: 2px; - &--with-time:not(&--with-today-button) { - right: 85px; + &--with-time { + right: $datepicker__time-container-width; } } @@ -344,16 +342,7 @@ h2.react-datepicker__current-month { .react-datepicker__time-container { float: right; border-left: $datepicker__border; - width: 85px; - - &--with-today-button { - display: inline; - border: 1px solid #aeaeae; - border-radius: 0.375em; - position: absolute; - right: -87px; - top: 0; - } + width: $datepicker__time-container-width; .react-datepicker__time { position: relative; @@ -361,7 +350,7 @@ h2.react-datepicker__current-month { border-bottom-right-radius: 0.375em; .react-datepicker__time-box { - width: 85px; + width: $datepicker__time-container-width; overflow-x: hidden; margin: 0 auto; text-align: center; @@ -770,6 +759,13 @@ h2.react-datepicker__current-month { font-weight: bold; padding: 5px 0; clear: left; + + // Leave room for the time panel so it isn't painted over by the + // time-container float, which stretches to cover this row's height. + // The extra 1px accounts for the time-container's border-left. + &--with-time-select { + width: calc(100% - #{$datepicker__time-container-width} - 1px); + } } .react-datepicker__portal { diff --git a/src/stylesheets/variables.scss b/src/stylesheets/variables.scss index 53866477f..a65b932c9 100644 --- a/src/stylesheets/variables.scss +++ b/src/stylesheets/variables.scss @@ -23,3 +23,4 @@ $datepicker__font-family: $datepicker__item-size: 2.125em !default; $datepicker__margin: 0.5em !default; $datepicker__navigation-button-size: 32px !default; +$datepicker__time-container-width: 85px !default; diff --git a/src/test/calendar_test.test.tsx b/src/test/calendar_test.test.tsx index 721ee0523..5522c3181 100644 --- a/src/test/calendar_test.test.tsx +++ b/src/test/calendar_test.test.tsx @@ -1226,6 +1226,50 @@ describe("Calendar", () => { expect(isSameDay(instance?.state.date, newDate())).toBeTruthy(); }); + it("should render the time container before the today button so the time panel stays in normal flow next to the month view", () => { + const { calendar } = getCalendar({ + todayButton: "Vandaag", + showTimeSelect: true, + }); + const todayButton = safeQuerySelector( + calendar, + ".react-datepicker__today-button", + ); + const timeContainer = safeQuerySelector( + calendar, + ".react-datepicker__time-container", + ); + expect( + todayButton.compareDocumentPosition(timeContainer) & + Node.DOCUMENT_POSITION_PRECEDING, + ).toBeTruthy(); + }); + + it("should narrow the today button's width to leave room for the time panel when showTimeSelect is enabled", () => { + const { calendar } = getCalendar({ + todayButton: "Vandaag", + showTimeSelect: true, + }); + const todayButton = safeQuerySelector( + calendar, + ".react-datepicker__today-button", + ); + expect(todayButton.classList).toContain( + "react-datepicker__today-button--with-time-select", + ); + }); + + it("should not narrow the today button's width when showTimeSelect is disabled", () => { + const { calendar } = getCalendar({ todayButton: "Vandaag" }); + const todayButton = safeQuerySelector( + calendar, + ".react-datepicker__today-button", + ); + expect(todayButton.classList).not.toContain( + "react-datepicker__today-button--with-time-select", + ); + }); + it("should use a hash for week label if weekLabel is NOT provided", () => { const { calendar } = getCalendar({ showWeekNumbers: true }); const weekLabel = calendar.querySelectorAll( diff --git a/src/test/timepicker_test.test.tsx b/src/test/timepicker_test.test.tsx index 5cb56f36d..0b82843f7 100644 --- a/src/test/timepicker_test.test.tsx +++ b/src/test/timepicker_test.test.tsx @@ -1,5 +1,5 @@ import { render, fireEvent, waitFor } from "@testing-library/react"; -import React from "react"; +import React, { act } from "react"; import { formatDate, KeyType, newDate } from "../date_utils"; import DatePicker from "../index"; @@ -118,6 +118,53 @@ describe("TimePicker", () => { // The fix ensures setState is only called when height actually changes expect(timeList.style.height).toBe(initialHeight); }); + + it("should grow the time list to also cover the today button's height", async () => { + const { container } = render( + , + ); + + await waitFor(() => { + expect(mockObserve).toHaveBeenCalledTimes(2); + }); + + const monthContainer = safeQuerySelector( + container, + ".react-datepicker__month-container", + ); + const timeHeader = safeQuerySelector( + container, + ".react-datepicker__header--time", + ); + const todayButton = safeQuerySelector( + container, + ".react-datepicker__today-button", + ); + const timeList = safeQuerySelector( + container, + ".react-datepicker__time-list", + ); + + Object.defineProperty(monthContainer, "clientHeight", { value: 300 }); + Object.defineProperty(timeHeader, "clientHeight", { value: 30 }); + Object.defineProperty(todayButton, "clientHeight", { value: 40 }); + + const resizeObserverCallback = getResizeObserverCallback(); + expect(typeof resizeObserverCallback).toBe("function"); + act(() => { + resizeObserverCallback?.([], mockObserve.mock.calls[0][0]); + }); + + await waitFor(() => { + expect(timeList.style.height).toBe("310px"); + }); + }); }); it("should update on input time change", () => { diff --git a/src/time.tsx b/src/time.tsx index c02dc2369..25b0f0a9b 100644 --- a/src/time.tsx +++ b/src/time.tsx @@ -29,8 +29,8 @@ interface TimeProps extends Pick< openToDate?: Date; onChange?: (time: Date) => void; timeClassName?: (time: Date) => string; - todayButton?: React.ReactNode; monthRef?: HTMLDivElement; + todayButtonRef?: HTMLDivElement; timeCaption?: string; injectTimes?: Date[]; handleOnKeyDown?: React.KeyboardEventHandler; @@ -47,7 +47,6 @@ export default class Time extends Component { static get defaultProps() { return { intervals: 30, - todayButton: null, timeCaption: "Time", showTimeCaption: true, }; @@ -84,7 +83,7 @@ export default class Time extends Component { private centerLi?: HTMLLIElement; private observeDatePickerHeightChanges(): void { - const { monthRef } = this.props; + const { monthRef, todayButtonRef } = this.props; this.updateContainerHeight(); if (monthRef) { @@ -93,13 +92,24 @@ export default class Time extends Component { }); this.resizeObserver.observe(monthRef); + if (todayButtonRef) { + this.resizeObserver.observe(todayButtonRef); + } } } + // Height contributed by the today-button, so the time list can extend + // to cover the full height of the month view plus the button below it. + private getTodayButtonHeight(): number { + return this.props.todayButtonRef?.clientHeight ?? 0; + } + private updateContainerHeight(): void { if (this.props.monthRef && this.header) { const newHeight = - this.props.monthRef.clientHeight - this.header.clientHeight; + this.props.monthRef.clientHeight - + this.header.clientHeight + + this.getTodayButtonHeight(); // Only update state if height actually changed to prevent infinite resize loops if (this.state.height !== newHeight) { this.setState({ @@ -118,7 +128,8 @@ export default class Time extends Component { Time.calcCenterPosition( this.props.monthRef ? this.props.monthRef.clientHeight - - (this.header?.clientHeight ?? 0) + (this.header?.clientHeight ?? 0) + + this.getTodayButtonHeight() : this.list.clientHeight, this.centerLi, )) ?? @@ -311,13 +322,7 @@ export default class Time extends Component { const { height } = this.state; return ( -
+
{this.renderTimeCaption()}