Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ type CalendarProps = React.PropsWithChildren<
showQuarterYearPicker?: boolean;
showTimeSelect?: boolean;
showTimeInput?: boolean;
todayButton?: React.ReactNode;
showYearDropdown?: boolean;
showMonthDropdown?: boolean;
yearItemNumber?: number;
Expand Down Expand Up @@ -239,6 +240,7 @@ interface CalendarState
extends Pick<YearProps, "selectingDate">, Pick<MonthProps, "selectingDate"> {
date: Required<YearProps>["date"];
monthContainer: TimeProps["monthRef"];
todayButtonContainer: TimeProps["todayButtonRef"];
isRenderAriaLiveMessage: boolean;
}

Expand Down Expand Up @@ -267,18 +269,22 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
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,
});
})();
}
}
Expand Down Expand Up @@ -314,6 +320,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {

monthContainer: CalendarState["monthContainer"] = undefined;

todayButtonContainer: CalendarState["todayButtonContainer"] = undefined;

assignMonthContainer: void | undefined;

handleClickOutside = (event: MouseEvent): void => {
Expand Down Expand Up @@ -773,9 +781,6 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
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<HTMLButtonElement> | undefined =
this.increaseMonth;
Expand Down Expand Up @@ -907,7 +912,13 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
}
return (
<div
className="react-datepicker__today-button"
ref={(div) => {
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}
Expand Down Expand Up @@ -1174,6 +1185,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
format={this.props.timeFormat}
intervals={this.props.timeIntervals}
monthRef={this.state.monthContainer}
todayButtonRef={this.state.todayButtonContainer}
/>
);
}
Expand Down Expand Up @@ -1327,8 +1339,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
this.renderNextButton()}
{this.renderMonths()}
{this.renderYears()}
{this.renderTodayButton()}
{this.renderTimeSection()}
{this.renderTodayButton()}
{this.renderInputTimeSection()}
{this.renderChildren()}
</Container>
Expand Down
28 changes: 12 additions & 16 deletions src/stylesheets/datepicker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -344,24 +342,15 @@ 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;
background: white;
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;
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/stylesheets/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
44 changes: 44 additions & 0 deletions src/test/calendar_test.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
49 changes: 48 additions & 1 deletion src/test/timepicker_test.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(
<DatePicker
inline
selected={new Date()}
showTimeSelect
todayButton="Today"
timeIntervals={15}
/>,
);

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<HTMLElement>(
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", () => {
Expand Down
29 changes: 17 additions & 12 deletions src/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLLIElement>;
Expand All @@ -47,7 +47,6 @@ export default class Time extends Component<TimeProps, TimeState> {
static get defaultProps() {
return {
intervals: 30,
todayButton: null,
timeCaption: "Time",
showTimeCaption: true,
};
Expand Down Expand Up @@ -84,7 +83,7 @@ export default class Time extends Component<TimeProps, TimeState> {
private centerLi?: HTMLLIElement;

private observeDatePickerHeightChanges(): void {
const { monthRef } = this.props;
const { monthRef, todayButtonRef } = this.props;
this.updateContainerHeight();

if (monthRef) {
Expand All @@ -93,13 +92,24 @@ export default class Time extends Component<TimeProps, TimeState> {
});

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({
Expand All @@ -118,7 +128,8 @@ export default class Time extends Component<TimeProps, TimeState> {
Time.calcCenterPosition(
this.props.monthRef
? this.props.monthRef.clientHeight -
(this.header?.clientHeight ?? 0)
(this.header?.clientHeight ?? 0) +
this.getTodayButtonHeight()
: this.list.clientHeight,
this.centerLi,
)) ??
Expand Down Expand Up @@ -311,13 +322,7 @@ export default class Time extends Component<TimeProps, TimeState> {
const { height } = this.state;

return (
<div
className={`react-datepicker__time-container ${
(this.props.todayButton ?? Time.defaultProps.todayButton)
? "react-datepicker__time-container--with-today-button"
: ""
}`}
>
<div className="react-datepicker__time-container">
{this.renderTimeCaption()}
<div className="react-datepicker__time">
<div className="react-datepicker__time-box">
Expand Down
Loading