Skip to content

Commit 452bef6

Browse files
committed
feat: add monthHeaderPosition prop to control month header placement
Add new monthHeaderPosition prop that allows positioning the month header in three locations: top (default), middle (between day names and calendar), or bottom (at calendar bottom). This feature provides greater flexibility for calendar layout customization. Changes: - Add monthHeaderPosition prop to DatePicker, PopperComponent, and Calendar components - Implement conditional rendering logic in Calendar.renderDefaultHeader() - Add monthHeader and monthFooter props to Month component for content injection - Add CSS classes for middle/bottom header positions with proper triangle styling - Add .react-datepicker__header-wrapper for navigation button positioning - Include comprehensive test coverage for new functionality - Add documentation and live example to docs-site Technical implementation: - PropperComponent applies CSS classes based on header position - Calendar wraps header with navigation buttons for middle/bottom positions - Month component renders header/footer content at appropriate locations - SCSS handles triangle colors and navigation button positioning per position Test coverage includes unit tests for all three positions across components: calendar_test.test.tsx, month_header_position.test.tsx, and popper_component.test.tsx. All 167 tests passing with maintained coverage.
1 parent b198c49 commit 452bef6

File tree

12 files changed

+565
-20
lines changed

12 files changed

+565
-20
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ es
9898
tmp
9999

100100
.vscode
101+
.history
101102
*.iml
102103
.idea
103104

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ConfigureFloatingUI from "../../examples/ts/configureFloatingUI?raw";
1717
import CustomInput from "../../examples/ts/customInput?raw";
1818
import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw";
1919
import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw";
20+
import MonthHeaderPosition from "../../examples/ts/monthHeaderPosition?raw";
2021
import RenderCustomDay from "../../examples/ts/renderCustomDay?raw";
2122
import RenderCustomMonth from "../../examples/ts/renderCustomMonth?raw";
2223
import RenderCustomQuarter from "../../examples/ts/renderCustomQuarter?raw";
@@ -184,6 +185,10 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [
184185
title: "Custom header with two months displayed",
185186
component: RenderCustomHeaderTwoMonths,
186187
},
188+
{
189+
title: "Month header position",
190+
component: MonthHeaderPosition,
191+
},
187192
{
188193
title: "Custom Day",
189194
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
@@ -195,6 +195,7 @@ type CalendarProps = React.PropsWithChildren<
195195
renderCustomHeader?: (
196196
props: ReactDatePickerCustomHeaderProps,
197197
) => React.ReactElement;
198+
monthHeaderPosition?: "top" | "middle" | "bottom";
198199
onYearMouseEnter?: YearProps["onYearMouseEnter"];
199200
onYearMouseLeave?: YearProps["onYearMouseLeave"];
200201
monthAriaLabelPrefix?: MonthProps["ariaLabelPrefix"];
@@ -239,6 +240,7 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
239240
previousMonthButtonLabel: "Previous Month",
240241
nextMonthButtonLabel: "Next Month",
241242
yearItemNumber: DEFAULT_YEAR_ITEM_NUMBER,
243+
monthHeaderPosition: "top",
242244
};
243245
}
244246

@@ -877,25 +879,44 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
877879
</div>
878880
);
879881

880-
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => (
881-
<div
882-
className={`react-datepicker__header ${
883-
this.props.showTimeSelect
884-
? "react-datepicker__header--has-time-select"
885-
: ""
886-
}`}
887-
>
888-
{this.renderCurrentMonth(monthDate)}
882+
renderDefaultHeader = ({ monthDate, i }: { monthDate: Date; i: number }) => {
883+
const headerContent = (
889884
<div
890-
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
891-
onFocus={this.handleDropdownFocus}
885+
className={clsx("react-datepicker__header", {
886+
"react-datepicker__header--has-time-select":
887+
this.props.showTimeSelect,
888+
"react-datepicker__header--middle":
889+
this.props.monthHeaderPosition === "middle",
890+
"react-datepicker__header--bottom":
891+
this.props.monthHeaderPosition === "bottom",
892+
})}
892893
>
893-
{this.renderMonthDropdown(i !== 0)}
894-
{this.renderMonthYearDropdown(i !== 0)}
895-
{this.renderYearDropdown(i !== 0)}
894+
{this.renderCurrentMonth(monthDate)}
895+
<div
896+
className={`react-datepicker__header__dropdown react-datepicker__header__dropdown--${this.props.dropdownMode}`}
897+
onFocus={this.handleDropdownFocus}
898+
>
899+
{this.renderMonthDropdown(i !== 0)}
900+
{this.renderMonthYearDropdown(i !== 0)}
901+
{this.renderYearDropdown(i !== 0)}
902+
</div>
896903
</div>
897-
</div>
898-
);
904+
);
905+
906+
// Top position: render header directly in default location
907+
if (this.props.monthHeaderPosition === "top") {
908+
return headerContent;
909+
}
910+
911+
// Middle/bottom positions: wrap with navigation buttons
912+
return (
913+
<div className="react-datepicker__header-wrapper">
914+
{this.renderPreviousButton() || null}
915+
{this.renderNextButton() || null}
916+
{headerContent}
917+
</div>
918+
);
919+
};
899920

900921
renderCustomHeader = (headerArgs: { monthDate: Date; i: number }) => {
901922
const { monthDate, i } = headerArgs;
@@ -1039,7 +1060,8 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10391060
}}
10401061
className="react-datepicker__month-container"
10411062
>
1042-
{this.renderHeader({ monthDate, i })}
1063+
{this.props.monthHeaderPosition === "top" &&
1064+
this.renderHeader({ monthDate, i })}
10431065
<Month
10441066
{...Calendar.defaultProps}
10451067
{...this.props}
@@ -1056,6 +1078,16 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
10561078
monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd}
10571079
monthShowsDuplicateDaysStart={monthShowsDuplicateDaysStart}
10581080
dayNamesHeader={this.renderDayNamesHeader(monthDate)}
1081+
monthHeader={
1082+
this.props.monthHeaderPosition === "middle"
1083+
? this.renderHeader({ monthDate, i })
1084+
: undefined
1085+
}
1086+
monthFooter={
1087+
this.props.monthHeaderPosition === "bottom"
1088+
? this.renderHeader({ monthDate, i })
1089+
: undefined
1090+
}
10591091
/>
10601092
</div>,
10611093
);
@@ -1200,8 +1232,10 @@ export default class Calendar extends Component<CalendarProps, CalendarState> {
12001232
inline={this.props.inline}
12011233
>
12021234
{this.renderAriaLiveRegion()}
1203-
{this.renderPreviousButton()}
1204-
{this.renderNextButton()}
1235+
{this.props.monthHeaderPosition === "top" &&
1236+
this.renderPreviousButton()}
1237+
{this.props.monthHeaderPosition === "top" &&
1238+
this.renderNextButton()}
12051239
{this.renderMonths()}
12061240
{this.renderYears()}
12071241
{this.renderTodayButton()}

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,7 @@ export class DatePicker extends Component<DatePickerProps, DatePickerState> {
18131813
popperComponent={calendar}
18141814
popperOnKeyDown={this.onPopperKeyDown}
18151815
showArrow={this.props.showPopperArrow}
1816+
monthHeaderPosition={this.props.monthHeaderPosition}
18161817
/>
18171818
);
18181819
}

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
@@ -28,6 +28,7 @@ interface PopperComponentProps
2828
popperOnKeyDown: React.KeyboardEventHandler<HTMLDivElement>;
2929
showArrow?: boolean;
3030
portalId?: PortalProps["portalId"];
31+
monthHeaderPosition?: "top" | "middle" | "bottom";
3132
}
3233

3334
// Exported for testing purposes
@@ -44,6 +45,7 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
4445
portalHost,
4546
popperProps,
4647
showArrow,
48+
monthHeaderPosition,
4749
} = props;
4850

4951
let popper: React.ReactElement | undefined = undefined;
@@ -52,6 +54,10 @@ export const PopperComponent: React.FC<PopperComponentProps> = (props) => {
5254
const classes = clsx(
5355
"react-datepicker-popper",
5456
!showArrow && "react-datepicker-popper-offset",
57+
monthHeaderPosition === "middle" &&
58+
"react-datepicker-popper--header-middle",
59+
monthHeaderPosition === "bottom" &&
60+
"react-datepicker-popper--header-bottom",
5561
className,
5662
);
5763
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)