Skip to content

Commit 5fe362f

Browse files
authored
feat: add navLayout prop (#2755)
* Add new `navLayout` prop Signed-off-by: gpbl <[email protected]> * Add `navLayout` support to `DayPicker.tsx` Signed-off-by: gpbl <[email protected]> * Update playground to include navLayout Signed-off-by: gpbl <[email protected]> * Update useAnimation test Signed-off-by: gpbl <[email protected]> * Fix selector for .rdp-month Signed-off-by: gpbl <[email protected]> * Update docs Signed-off-by: gpbl <[email protected]> * Update docs Signed-off-by: gpbl <[email protected]> * Update docs Signed-off-by: gpbl <[email protected]> * Add @SInCE tag to prop jsdoc Signed-off-by: gpbl <[email protected]> * Remove navLayout predef from playground Signed-off-by: gpbl <[email protected]> * Only display `nav` for the last month Signed-off-by: gpbl <[email protected]> * Rearrange conditional Signed-off-by: gpbl <[email protected]> * Move after nav layout out of Month Signed-off-by: gpbl <[email protected]> * Fix for animation Signed-off-by: gpbl <[email protected]> * Use visiblity: hidden Signed-off-by: gpbl <[email protected]> * Cleanup useAnimation test Signed-off-by: gpbl <[email protected]> * Remove unused const Signed-off-by: gpbl <[email protected]> * Move back nav after MonthGrid Signed-off-by: gpbl <[email protected]> * Lint file Signed-off-by: gpbl <[email protected]> * Remove position relative from .rdp-month_caption Signed-off-by: gpbl <[email protected]> * Update docs Signed-off-by: Giampaolo Bellavite <[email protected]> * Fix for “after” layout relative positioning Signed-off-by: Giampaolo Bellavite <[email protected]> * Add tests Signed-off-by: Giampaolo Bellavite <[email protected]> * Update playground Signed-off-by: Giampaolo Bellavite <[email protected]> * Clean up left overs Signed-off-by: Giampaolo Bellavite <[email protected]> --------- Signed-off-by: gpbl <[email protected]> Signed-off-by: Giampaolo Bellavite <[email protected]>
1 parent 7304d3f commit 5fe362f

File tree

11 files changed

+232
-14
lines changed

11 files changed

+232
-14
lines changed

examples/NavLayout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react";
2+
3+
import { DayPicker } from "react-day-picker";
4+
5+
export function NavLayout() {
6+
return <DayPicker navLayout="around" />;
7+
}

examples/NavLayoutAfter.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react";
2+
3+
import { DayPicker } from "react-day-picker";
4+
5+
export function NavLayoutAfter() {
6+
return <DayPicker navLayout="after" />;
7+
}

examples/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export * from "./MultipleMinMax";
5151
export * from "./MultipleRequired";
5252
export * from "./MultipleMonths";
5353
export * from "./MultipleMonthsPaged";
54+
export * from "./NavLayout";
55+
export * from "./NavLayoutAfter";
5456
export * from "./Numerals";
5557
export * from "./OutsideDays";
5658
export * from "./PastDatesDisabled";

src/DayPicker.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
dateButton,
88
grid,
99
nav,
10+
nextButton,
1011
previousButton
1112
} from "@/test/elements";
1213
import { fireEvent, render, screen } from "@/test/render";
@@ -197,3 +198,36 @@ describe("when not interactive", () => {
197198
});
198199
});
199200
});
201+
202+
describe("when navLayout is set", () => {
203+
const today = new Date(2024, 1, 4);
204+
describe("when navLayout is set to 'around'", () => {
205+
beforeEach(() => {
206+
render(
207+
<DayPicker today={today} navLayout="around" data-testid={testId} />
208+
);
209+
});
210+
test("renders navigation layout as 'around'", () => {
211+
expect(dayPicker()).toHaveAttribute("data-nav-layout", "around");
212+
});
213+
test('render the "previous" button before the month caption', () => {
214+
expect(previousButton().nextSibling).toHaveTextContent("February 2024");
215+
});
216+
test('render the "next" button before the month caption', () => {
217+
expect(nextButton().previousSibling).toHaveTextContent("February 2024");
218+
});
219+
});
220+
describe("when navLayout is set to 'aft er'", () => {
221+
beforeEach(() => {
222+
render(
223+
<DayPicker today={today} navLayout="after" data-testid={testId} />
224+
);
225+
});
226+
test("renders navigation layout as 'after'", () => {
227+
expect(dayPicker()).toHaveAttribute("data-nav-layout", "after");
228+
});
229+
test("render the navigation after the month caption", () => {
230+
expect(nav().previousSibling).toHaveTextContent("February 2024");
231+
});
232+
});
233+
});

src/DayPicker.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export function DayPicker(initialProps: DayPickerProps) {
124124
const {
125125
captionLayout,
126126
mode,
127+
navLayout,
128+
numberOfMonths = 1,
127129
onDayBlur,
128130
onDayClick,
129131
onDayFocus,
@@ -180,6 +182,8 @@ export function DayPicker(initialProps: DayPickerProps) {
180182
labelGrid,
181183
labelMonthDropdown,
182184
labelNav,
185+
labelPrevious,
186+
labelNext,
183187
labelWeekday,
184188
labelWeekNumber,
185189
labelWeekNumberHeader,
@@ -343,7 +347,7 @@ export function DayPicker(initialProps: DayPickerProps) {
343347
className={classNames[UI.Months]}
344348
style={styles?.[UI.Months]}
345349
>
346-
{!props.hideNavigation && (
350+
{!props.hideNavigation && !navLayout && (
347351
<components.Nav
348352
data-animated-nav={props.animate ? "true" : undefined}
349353
className={classNames[UI.Nav]}
@@ -379,6 +383,25 @@ export function DayPicker(initialProps: DayPickerProps) {
379383
displayIndex={displayIndex}
380384
calendarMonth={calendarMonth}
381385
>
386+
{navLayout === "around" &&
387+
!props.hideNavigation &&
388+
displayIndex === 0 && (
389+
<components.PreviousMonthButton
390+
type="button"
391+
className={classNames[UI.PreviousMonthButton]}
392+
tabIndex={previousMonth ? undefined : -1}
393+
aria-disabled={previousMonth ? undefined : true}
394+
aria-label={labelPrevious(previousMonth)}
395+
onClick={handlePreviousClick}
396+
data-animated-button={props.animate ? "true" : undefined}
397+
>
398+
<components.Chevron
399+
disabled={previousMonth ? undefined : true}
400+
className={classNames[UI.Chevron]}
401+
orientation={props.dir === "rtl" ? "right" : "left"}
402+
/>
403+
</components.PreviousMonthButton>
404+
)}
382405
<components.MonthCaption
383406
data-animated-caption={props.animate ? "true" : undefined}
384407
className={classNames[UI.MonthCaption]}
@@ -464,6 +487,40 @@ export function DayPicker(initialProps: DayPickerProps) {
464487
</components.CaptionLabel>
465488
)}
466489
</components.MonthCaption>
490+
{navLayout === "around" &&
491+
!props.hideNavigation &&
492+
displayIndex === numberOfMonths - 1 && (
493+
<components.NextMonthButton
494+
type="button"
495+
className={classNames[UI.NextMonthButton]}
496+
tabIndex={nextMonth ? undefined : -1}
497+
aria-disabled={nextMonth ? undefined : true}
498+
aria-label={labelNext(nextMonth)}
499+
onClick={handleNextClick}
500+
data-animated-button={props.animate ? "true" : undefined}
501+
>
502+
<components.Chevron
503+
disabled={nextMonth ? undefined : true}
504+
className={classNames[UI.Chevron]}
505+
orientation={props.dir === "rtl" ? "left" : "right"}
506+
/>
507+
</components.NextMonthButton>
508+
)}
509+
{displayIndex === numberOfMonths - 1 &&
510+
navLayout === "after" &&
511+
!props.hideNavigation && (
512+
<components.Nav
513+
data-animated-nav={props.animate ? "true" : undefined}
514+
className={classNames[UI.Nav]}
515+
style={styles?.[UI.Nav]}
516+
aria-label={labelNav()}
517+
onPreviousClick={handlePreviousClick}
518+
onNextClick={handleNextClick}
519+
previousMonth={previousMonth}
520+
nextMonth={nextMonth}
521+
/>
522+
)}
523+
467524
<components.MonthGrid
468525
role="grid"
469526
aria-multiselectable={mode === "multiple" || mode === "range"}

src/helpers/getDataAttributes.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export function getDataAttributes(
1818
"data-multiple-months":
1919
(props.numberOfMonths && props.numberOfMonths > 1) || undefined,
2020
"data-week-numbers": props.showWeekNumber || undefined,
21-
"data-broadcast-calendar": props.broadcastCalendar || undefined
21+
"data-broadcast-calendar": props.broadcastCalendar || undefined,
22+
"data-nav-layout": props.navLayout || undefined
2223
};
2324
Object.entries(props).forEach(([key, val]) => {
2425
if (key.startsWith("data-")) {

src/style.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,35 @@
198198
font-size: large;
199199
}
200200

201+
.rdp-root[data-nav-layout="around"] .rdp-month,
202+
.rdp-root[data-nav-layout="after"] .rdp-month {
203+
position: relative;
204+
}
205+
206+
.rdp-root[data-nav-layout="around"] .rdp-month_caption {
207+
justify-content: center;
208+
margin-inline-start: var(--rdp-nav_button-width);
209+
margin-inline-end: var(--rdp-nav_button-width);
210+
position: relative;
211+
}
212+
213+
.rdp-root[data-nav-layout="around"] .rdp-button_previous {
214+
position: absolute;
215+
inset-inline-start: 0;
216+
top: 0;
217+
height: var(--rdp-nav-height);
218+
display: inline-flex;
219+
}
220+
221+
.rdp-root[data-nav-layout="around"] .rdp-button_next {
222+
position: absolute;
223+
inset-inline-end: 0;
224+
top: 0;
225+
height: var(--rdp-nav-height);
226+
display: inline-flex;
227+
justify-content: center;
228+
}
229+
201230
.rdp-months {
202231
position: relative;
203232
display: flex;

src/style.module.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,35 @@
198198
font-size: large;
199199
}
200200

201+
.root[data-nav-layout="around"] .month,
202+
.root[data-nav-layout="after"] .month {
203+
position: relative;
204+
}
205+
206+
.root[data-nav-layout="around"] .month_caption {
207+
justify-content: center;
208+
margin-inline-start: var(--rdp-nav_button-width);
209+
margin-inline-end: var(--rdp-nav_button-width);
210+
position: relative;
211+
}
212+
213+
.root[data-nav-layout="around"] .button_previous {
214+
position: absolute;
215+
inset-inline-start: 0;
216+
top: 0;
217+
height: var(--rdp-nav-height);
218+
display: inline-flex;
219+
}
220+
221+
.root[data-nav-layout="around"] .button_next {
222+
position: absolute;
223+
inset-inline-end: 0;
224+
top: 0;
225+
height: var(--rdp-nav-height);
226+
display: inline-flex;
227+
justify-content: center;
228+
}
229+
201230
.months {
202231
position: relative;
203232
display: flex;

src/types/props.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,21 @@ export interface PropsBase {
217217
* @see https://daypicker.dev/docs/customization#caption-layouts
218218
*/
219219
captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years";
220+
221+
/**
222+
* Adjust the positioning of the navigation buttons.
223+
*
224+
* - `around`: Displays the buttons on either side of the caption.
225+
* - `after`: Displays the buttons after the caption. This ensures the tab order
226+
* matches the visual order.
227+
*
228+
* If not set, the buttons default to being displayed after the caption, but
229+
* the tab order may not align with the visual order.
230+
*
231+
* @since 9.7.0
232+
* @see https://daypicker.dev/docs/customization#navigation-layouts
233+
*/
234+
navLayout?: "around" | "after" | undefined;
220235
/**
221236
* Display always 6 weeks per each month, regardless of the month’s number of
222237
* weeks. Weeks will be filled with the days from the next month.

website/docs/docs/customization.mdx

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ sidebar_position: 3
66

77
Use the customization props to tailor the calendar's appearance.
88

9-
| Prop Name | Type | Description |
10-
| ----------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
11-
| `captionLayout` | `"label"`<br/> \| `"dropdown"`<br/> \| `"dropdown-months"`<br/> \| `"dropdown-years"` | Choose the layout of the month caption. Default is `label`. |
12-
| `fixedWeeks` | `boolean` | Display 6 weeks per month. |
13-
| `footer` | `ReactNode` \| `string` | Add a footer to the calendar, acting as a [live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). |
14-
| `hideWeekdays` | `boolean` | Hide the row displaying the weekday names. |
15-
| `numberOfMonths` | `number` | The number of displayed months. Default is `1`. |
16-
| `showOutsideDays` | `boolean` | Display the days falling into other months. |
17-
| `showWeekNumber` | `boolean` | Display the column with the [week numbers](#showweeknumber). |
9+
| Prop Name | Type | Description |
10+
| ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
11+
| `captionLayout` | `"label"`<br/> `"dropdown"`<br/> `"dropdown-months"`<br/> `"dropdown-years"` | Choose the layout of the month caption. Default is `label`. |
12+
| `navLayout` | `around` \| `end` | Adjust the positioning of the navigation buttons. |
13+
| `fixedWeeks` | `boolean` | Display 6 weeks per month. |
14+
| `footer` | `ReactNode` \| `string` | Add a footer to the calendar, acting as a [live region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions). |
15+
| `hideWeekdays` | `boolean` | Hide the row displaying the weekday names. |
16+
| `numberOfMonths` | `number` | The number of displayed months. Default is `1`. |
17+
| `showOutsideDays` | `boolean` | Display the days falling into other months. |
18+
| `showWeekNumber` | `boolean` | Display the column with the [week numbers](#showweeknumber). |
1819

1920
## Caption Layouts
2021

@@ -54,6 +55,42 @@ Without specifying the `startMonth` and `endMonth` properties, the dropdown will
5455

5556
:::
5657

58+
## Navigation Layouts
59+
60+
Use the `navLayout` prop to adjust the positioning of the navigation buttons.
61+
62+
| Navigation Layout | Description |
63+
| ----------------- | ------------------------------------------------------------------------------------------ |
64+
| `"around"` | Buttons are displayed on either side of the caption. |
65+
| `"after"` | Buttons are displayed after the caption, ensuring the tab order matches the visual order. |
66+
| `undefined` | Buttons are displayed after the caption, but the tab order may not match the visual order. |
67+
68+
```tsx
69+
<DayPicker navLayout="around" />
70+
```
71+
72+
<BrowserWindow>
73+
<Examples.NavLayout />
74+
</BrowserWindow>
75+
76+
```tsx
77+
<DayPicker navLayout="after" />
78+
```
79+
80+
<BrowserWindow>
81+
<Examples.NavLayoutAfter />
82+
</BrowserWindow>
83+
84+
See [Navigation](./navigation.mdx) for additional ways to customize the calendar’s navigation.
85+
86+
:::info Tab Order vs. Visual Order
87+
88+
If not set, the navigation buttons default to being displayed after the caption. However, the tab order may not align with the visual order when setting `"dropdown"` as caption layout. To ensure the component [remains accessible](https://www.w3.org/TR/WCAG22/#focus-order), set `navLayout` to `"after"` or to `"around"` instead of leaving it undefined.
89+
90+
In a future version, the default behavior will be changed to `"after"`.
91+
92+
:::
93+
5794
## Outside Days
5895

5996
By default, DayPicker hides the days falling into other months. Use `showOutsideDays` to display them.

0 commit comments

Comments
 (0)