Skip to content

Commit c7076d3

Browse files
Merge main into feature/popper-target-ref
2 parents 0bc65c7 + 0e13929 commit c7076d3

File tree

12 files changed

+649
-20
lines changed

12 files changed

+649
-20
lines changed

docs-site/src/components/Examples/config.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import PopperTargetRef from "../../examples/ts/popperTargetRef?raw";
1919
import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw";
2020
import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw";
2121
import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw";
22+
import MonthHeaderPosition from "../../examples/ts/monthHeaderPosition?raw";
2223
import RenderCustomDay from "../../examples/ts/renderCustomDay?raw";
2324
import RenderCustomMonth from "../../examples/ts/renderCustomMonth?raw";
2425
import RenderCustomQuarter from "../../examples/ts/renderCustomQuarter?raw";
@@ -196,6 +197,10 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
196197
title: "Custom Day Names",
197198
component: RenderCustomDayName,
198199
},
200+
{
201+
title: "Month header position",
202+
component: MonthHeaderPosition,
203+
},
199204
{
200205
title: "Custom Day",
201206
component: RenderCustomDay,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
type Position = "top" | "middle" | "bottom";
2+
3+
const MonthHeaderPositionExample = () => {
4+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
5+
const [position, setPosition] = useState<Position>("middle");
6+
7+
return (
8+
<>
9+
<div
10+
style={{
11+
marginBottom: "20px",
12+
display: "flex",
13+
flexDirection: "column",
14+
gap: "4px",
15+
}}
16+
>
17+
<label>
18+
<input
19+
type="radio"
20+
value="top"
21+
checked={position === "top"}
22+
onChange={(e) => setPosition(e.target.value as Position)}
23+
/>
24+
Top (default)
25+
</label>
26+
<label>
27+
<input
28+
type="radio"
29+
value="middle"
30+
checked={position === "middle"}
31+
onChange={(e) => setPosition(e.target.value as Position)}
32+
/>
33+
Middle
34+
</label>
35+
<label>
36+
<input
37+
type="radio"
38+
value="bottom"
39+
checked={position === "bottom"}
40+
onChange={(e) => setPosition(e.target.value as Position)}
41+
/>
42+
Bottom
43+
</label>
44+
</div>
45+
<DatePicker
46+
selected={selectedDate}
47+
onChange={setSelectedDate}
48+
monthHeaderPosition={position}
49+
/>
50+
</>
51+
);
52+
};
53+
54+
render(MonthHeaderPositionExample);

docs/month_header_position.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# monthHeaderPosition
2+
3+
## Description
4+
5+
The `monthHeaderPosition` prop allows you to control where the month header (e.g., "December 2025") is displayed in the calendar. By default, it appears in the standard header section above the day names. You can reposition the header to appear between the day names and calendar days ("middle") or at the bottom of the calendar ("bottom").
6+
7+
## Type
8+
9+
```typescript
10+
monthHeaderPosition?: "top" | "middle" | "bottom";
11+
```
12+
13+
## Values
14+
15+
- `"top"` (or undefined) - Month header appears in the standard position at the top of the calendar (default)
16+
- `"middle"` - Month header appears between day names and calendar days
17+
- `"bottom"` - Month header appears at the bottom of the calendar
18+
19+
## Usage
20+
21+
```tsx
22+
import React, { useState } from "react";
23+
import DatePicker from "react-datepicker";
24+
25+
// Example 1: Header in the middle (between day names and days)
26+
const MiddlePositionExample = () => {
27+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
28+
29+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="middle" />;
30+
};
31+
32+
// Example 2: Header at the bottom
33+
const BottomPositionExample = () => {
34+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
35+
36+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="bottom" />;
37+
};
38+
39+
// Example 3: Default position (top)
40+
const DefaultPositionExample = () => {
41+
const [selectedDate, setSelectedDate] = useState<Date | null>(new Date());
42+
43+
return <DatePicker selected={selectedDate} onChange={setSelectedDate} monthHeaderPosition="top" />;
44+
};
45+
```
46+
47+
## Notes
48+
49+
- When `monthHeaderPosition` is set to `"middle"` or `"bottom"`, the month header (including navigation buttons and dropdowns) is removed from the default header section
50+
- Works with multiple months (`monthsShown` prop) - each month's header will be positioned accordingly
51+
- Navigation buttons are included and properly positioned in all three position options

src/calendar.tsx

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ type CalendarProps = React.PropsWithChildren<
206206
renderCustomDayName?: (
207207
props: ReactDatePickerCustomDayNameProps,
208208
) => React.ReactNode;
209+
monthHeaderPosition?: "top" | "middle" | "bottom";
209210
onYearMouseEnter?: YearProps["onYearMouseEnter"];
210211
onYearMouseLeave?: YearProps["onYearMouseLeave"];
211212
monthAriaLabelPrefix?: MonthProps["ariaLabelPrefix"];
@@ -250,6 +251,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
250251
previousMonthButtonLabel: "Previous Month",
251252
nextMonthButtonLabel: "Next Month",
252253
yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
254+
monthHeaderPosition: "top",
253255
};
254256
}
255257

@@ -916,25 +918,44 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
916918
</div>
917919
);
918920

919-
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => (
920-
<div
921-
className={`react-datepicker__header ${
922-
this.props.showTimeSelect
923-
? "react-datepicker__header--has-time-select"
924-
: ""
925-
}`}
926-
>
927-
{this.renderCurrentMonth(monthDate)}
921+
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => {
922+
const headerContent = (
928923
<div
929-
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
930-
onFocus={this.handleDropdownFocus}
924+
className={clsx("react-datepicker__header", {
925+
"react-datepicker__header--has-time-select":
926+
this.props.showTimeSelect,
927+
"react-datepicker__header--middle":
928+
this.props.monthHeaderPosition === "middle",
929+
"react-datepicker__header--bottom":
930+
this.props.monthHeaderPosition === "bottom",
931+
})}
931932
>
932-
{this.renderMonthDropdown(i !== 0)}
933-
{this.renderMonthYearDropdown(i !== 0)}
934-
{this.renderYearDropdown(i !== 0)}
933+
{this.renderCurrentMonth(monthDate)}
934+
<div
935+
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
936+
onFocus={this.handleDropdownFocus}
937+
>
938+
{this.renderMonthDropdown(i !== 0)}
939+
{this.renderMonthYearDropdown(i !== 0)}
940+
{this.renderYearDropdown(i !== 0)}
941+
</div>
935942
</div>
936-
</div>
937-
);
943+
);
944+
945+
// Top position: render header directly in default location
946+
if (this.props.monthHeaderPosition === "top") {
947+
return headerContent;
948+
}
949+
950+
// Middle/bottom positions: wrap with navigation buttons
951+
return (
952+
<div className="react-datepicker__header-wrapper">
953+
{this.renderPreviousButton() || null}
954+
{this.renderNextButton() || null}
955+
{headerContent}
956+
</div>
957+
);
958+
};
938959

939960
renderCustomHeader = (headerArgs: { monthDate: Date; i: number }) => {
940961
const { monthDate, i } = headerArgs;
@@ -1078,7 +1099,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10781099
}}
10791100
className="react-datepicker__month-container"
10801101
>
1081-
{this.renderHeader({ monthDate, i })}
1102+
{this.props.monthHeaderPosition === "top" &&
1103+
this.renderHeader({ monthDate, i })}
10821104
<Month
10831105
{...Calendar.defaultProps}
10841106
{...this.props}
@@ -1095,6 +1117,16 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10951117
monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd}
10961118
monthShowsDuplicateDaysStart={monthShowsDuplicateDaysStart}
10971119
dayNamesHeader={this.renderDayNamesHeader(monthDate, i)}
1120+
monthHeader={
1121+
this.props.monthHeaderPosition === "middle"
1122+
? this.renderHeader({ monthDate, i })
1123+
: undefined
1124+
}
1125+
monthFooter={
1126+
this.props.monthHeaderPosition === "bottom"
1127+
? this.renderHeader({ monthDate, i })
1128+
: undefined
1129+
}
10981130
/>
10991131
</div>,
11001132
);
@@ -1239,8 +1271,10 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
12391271
inline={this.props.inline}
12401272
>
12411273
{this.renderAriaLiveRegion()}
1242-
{this.renderPreviousButton()}
1243-
{this.renderNextButton()}
1274+
{this.props.monthHeaderPosition === "top" &&
1275+
this.renderPreviousButton()}
1276+
{this.props.monthHeaderPosition === "top" &&
1277+
this.renderNextButton()}
12441278
{this.renderMonths()}
12451279
{this.renderYears()}
12461280
{this.renderTodayButton()}

src/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,12 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
726726
return;
727727
}
728728

729+
// Update preSelection to keep calendar viewport consistent when reopening
730+
// Use startDate for preSelection to match calcInitialState behavior
731+
if (startDateNew) {
732+
this.setState({ preSelection: startDateNew });
733+
}
734+
729735
this.props.onChange?.([startDateNew, endDateNew], event);
730736
} else {
731737
// not selectsRange
@@ -1817,6 +1823,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
18171823
popperComponent={calendar}
18181824
popperOnKeyDown={this.onPopperKeyDown}
18191825
showArrow={this.props.showPopperArrow}
1826+
monthHeaderPosition={this.props.monthHeaderPosition}
18201827
/>
18211828
);
18221829
}

src/month.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ interface MonthProps extends Omit<
141141
chooseDayAriaLabelPrefix?: WeekProps["chooseDayAriaLabelPrefix"];
142142
disabledDayAriaLabelPrefix?: WeekProps["disabledDayAriaLabelPrefix"];
143143
dayNamesHeader?: React.ReactNode;
144+
monthHeader?: React.ReactNode;
145+
monthFooter?: React.ReactNode;
144146
}
145147

146148
/**
@@ -1174,6 +1176,9 @@ export default class Month extends Component<MonthProps> {
11741176
{this.props.dayNamesHeader && (
11751177
<div role="rowgroup">{this.props.dayNamesHeader}</div>
11761178
)}
1179+
{this.props.monthHeader && (
1180+
<div role="rowgroup">{this.props.monthHeader}</div>
1181+
)}
11771182
<div
11781183
className={this.getClassNames()}
11791184
onMouseLeave={
@@ -1187,6 +1192,9 @@ export default class Month extends Component<MonthProps> {
11871192
>
11881193
{this.renderWeeks()}
11891194
</div>
1195+
{this.props.monthFooter && (
1196+
<div role="rowgroup">{this.props.monthFooter}</div>
1197+
)}
11901198
</div>
11911199
);
11921200
}

src/popper_component.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface PopperComponentProps
2929
showArrow?: boolean;
3030
portalId?: PortalProps["portalId"];
3131
popperTargetRef?: React.RefObject<HTMLElement | null>;
32+
monthHeaderPosition?: "top" | "middle" | "bottom";
3233
}
3334

3435
// Exported for testing purposes
@@ -46,6 +47,7 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
4647
popperProps,
4748
showArrow,
4849
popperTargetRef,
50+
monthHeaderPosition,
4951
} = props;
5052

5153
// When a custom popperTargetRef is provided, use it as the position reference
@@ -63,6 +65,10 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
6365
const classes = clsx(
6466
"react-datepicker-popper",
6567
!showArrow && "react-datepicker-popper-offset",
68+
monthHeaderPosition === "middle" &&
69+
"react-datepicker-popper--header-middle",
70+
monthHeaderPosition === "bottom" &&
71+
"react-datepicker-popper--header-bottom",
6672
className,
6773
);
6874
popper = (

src/stylesheets/datepicker.scss

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,25 @@
7070
color: #fff;
7171
}
7272
}
73+
74+
&--header-middle,
75+
&--header-bottom {
76+
&[data-placement^="bottom"] {
77+
.react-datepicker__triangle {
78+
fill: #fff;
79+
color: #fff;
80+
}
81+
}
82+
}
83+
84+
&--header-bottom {
85+
&[data-placement^="top"] {
86+
.react-datepicker__triangle {
87+
fill: $datepicker__background-color;
88+
color: $datepicker__background-color;
89+
}
90+
}
91+
}
7392
}
7493

7594
.react-datepicker__header {
@@ -90,9 +109,34 @@
90109
}
91110
}
92111

93-
&:not(&--has-time-select) {
112+
&:not(&--has-time-select, &--middle, &--bottom) {
94113
border-top-right-radius: $datepicker__border-radius;
95114
}
115+
116+
// Header in middle position (between day names and days)
117+
&--middle {
118+
border-top: $datepicker__border;
119+
border-radius: 0;
120+
margin-top: 4px;
121+
}
122+
123+
// Header in bottom position (at calendar bottom)
124+
&--bottom {
125+
border-bottom: none;
126+
border-top: $datepicker__border;
127+
border-radius: 0 0 $datepicker__border-radius $datepicker__border-radius;
128+
}
129+
}
130+
131+
// Wrapper for header in middle/bottom positions
132+
.react-datepicker__header-wrapper {
133+
position: relative;
134+
135+
.react-datepicker__navigation--next--with-time:not(
136+
.react-datepicker__navigation--next--with-today-button
137+
) {
138+
right: 2px;
139+
}
96140
}
97141

98142
.react-datepicker__year-dropdown-container--select,

0 commit comments

Comments
 (0)