Skip to content

feat: add navLayout prop #2755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions examples/NavLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from "react";

import { DayPicker } from "react-day-picker";

export function NavLayout() {
return <DayPicker navLayout="around" />;
}
1 change: 1 addition & 0 deletions examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from "./MultipleMinMax";
export * from "./MultipleRequired";
export * from "./MultipleMonths";
export * from "./MultipleMonthsPaged";
export * from "./NavLayout";
export * from "./Numerals";
export * from "./OutsideDays";
export * from "./PastDatesDisabled";
Expand Down
59 changes: 58 additions & 1 deletion src/DayPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export function DayPicker(initialProps: DayPickerProps) {
const {
captionLayout,
mode,
navLayout,
numberOfMonths = 1,
onDayBlur,
onDayClick,
onDayFocus,
Expand Down Expand Up @@ -178,6 +180,8 @@ export function DayPicker(initialProps: DayPickerProps) {
labelGrid,
labelMonthDropdown,
labelNav,
labelPrevious,
labelNext,
labelWeekday,
labelWeekNumber,
labelWeekNumberHeader,
Expand Down Expand Up @@ -341,7 +345,7 @@ export function DayPicker(initialProps: DayPickerProps) {
className={classNames[UI.Months]}
style={styles?.[UI.Months]}
>
{!props.hideNavigation && (
{!props.hideNavigation && !navLayout && (
<components.Nav
data-animated-nav={props.animate ? "true" : undefined}
className={classNames[UI.Nav]}
Expand Down Expand Up @@ -377,6 +381,25 @@ export function DayPicker(initialProps: DayPickerProps) {
displayIndex={displayIndex}
calendarMonth={calendarMonth}
>
{navLayout === "around" &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could move this out of the months.map scope, as it is absolute positioned, I expect we could mount one button before the map and another one after.
If you tried this already, lemme know if there are any issues.

This comment also applies to the after navLayout, which is mounted within the map scope.

Perhaps doing that fixes the issue that happens during the animation.

!props.hideNavigation &&
displayIndex === 0 && (
<components.PreviousMonthButton
type="button"
className={classNames[UI.PreviousMonthButton]}
tabIndex={previousMonth ? undefined : -1}
aria-disabled={previousMonth ? undefined : true}
aria-label={labelPrevious(previousMonth)}
onClick={handlePreviousClick}
data-animated-button={props.animate ? "true" : undefined}
>
<components.Chevron
disabled={previousMonth ? undefined : true}
className={classNames[UI.Chevron]}
orientation={props.dir === "rtl" ? "right" : "left"}
/>
</components.PreviousMonthButton>
)}
<components.MonthCaption
data-animated-caption={props.animate ? "true" : undefined}
className={classNames[UI.MonthCaption]}
Expand Down Expand Up @@ -462,6 +485,40 @@ export function DayPicker(initialProps: DayPickerProps) {
</components.CaptionLabel>
)}
</components.MonthCaption>
{navLayout === "around" &&
!props.hideNavigation &&
displayIndex === numberOfMonths - 1 && (
<components.NextMonthButton
type="button"
className={classNames[UI.NextMonthButton]}
tabIndex={nextMonth ? undefined : -1}
aria-disabled={nextMonth ? undefined : true}
aria-label={labelNext(nextMonth)}
onClick={handleNextClick}
data-animated-button={props.animate ? "true" : undefined}
>
<components.Chevron
disabled={nextMonth ? undefined : true}
className={classNames[UI.Chevron]}
orientation={props.dir === "rtl" ? "left" : "right"}
/>
</components.NextMonthButton>
)}
{displayIndex === numberOfMonths - 1 &&
navLayout === "after" &&
!props.hideNavigation && (
<components.Nav
data-animated-nav={props.animate ? "true" : undefined}
className={classNames[UI.Nav]}
style={styles?.[UI.Nav]}
aria-label={labelNav()}
onPreviousClick={handlePreviousClick}
onNextClick={handleNextClick}
previousMonth={previousMonth}
nextMonth={nextMonth}
/>
)}

<components.MonthGrid
role="grid"
aria-multiselectable={mode === "multiple" || mode === "range"}
Expand Down
1 change: 1 addition & 0 deletions src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function Nav(
onNextClick?: MouseEventHandler<HTMLButtonElement>;
previousMonth?: Date | undefined;
nextMonth?: Date | undefined;
/** The component to render the previous month button. */
} & HTMLAttributes<HTMLElement>
) {
const {
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/getDataAttributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export function getDataAttributes(
"data-multiple-months":
(props.numberOfMonths && props.numberOfMonths > 1) || undefined,
"data-week-numbers": props.showWeekNumber || undefined,
"data-broadcast-calendar": props.broadcastCalendar || undefined
"data-broadcast-calendar": props.broadcastCalendar || undefined,
"data-nav-layout": props.navLayout || undefined
};
Object.entries(props).forEach(([key, val]) => {
if (key.startsWith("data-")) {
Expand Down
32 changes: 32 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,38 @@
font-size: large;
}

.rdp-root[data-nav-layout="around"] .rdp-month {
position: relative;
}

.rdp-root[data-nav-layout="around"] .rdp-month_caption {
justify-content: center;
margin-inline-start: var(--rdp-nav_button-width);
margin-inline-end: var(--rdp-nav_button-width);
position: relative;
}

.rdp-root[data-nav-layout="around"] .rdp-button_previous {
position: absolute;
inset-inline-start: 0;
top: 0;
height: var(--rdp-nav-height);
display: inline-flex;
}

.rdp-root[data-nav-layout="around"] .rdp-button_next {
position: absolute;
inset-inline-end: 0;
top: 0;
height: var(--rdp-nav-height);
display: inline-flex;
justify-content: center;
}

.rdp-root[data-nav-layout="after"] .rdp-month_caption {
margin-inline-end: calc(var(--rdp-nav_button-width) * 2);
}

.rdp-months {
position: relative;
display: flex;
Expand Down
32 changes: 32 additions & 0 deletions src/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,38 @@
font-size: large;
}

.root[data-nav-layout="around"] .month {
position: relative;
}

.root[data-nav-layout="around"] .month_caption {
justify-content: center;
margin-inline-start: var(--rdp-nav_button-width);
margin-inline-end: var(--rdp-nav_button-width);
position: relative;
}

.root[data-nav-layout="around"] .button_previous {
position: absolute;
inset-inline-start: 0;
top: 0;
height: var(--rdp-nav-height);
display: inline-flex;
}

.root[data-nav-layout="around"] .button_next {
position: absolute;
inset-inline-end: 0;
top: 0;
height: var(--rdp-nav-height);
display: inline-flex;
justify-content: center;
}

.root[data-nav-layout="after"] .month_caption {
margin-inline-end: calc(var(--rdp-nav_button-width) * 2);
}

.months {
position: relative;
display: flex;
Expand Down
15 changes: 15 additions & 0 deletions src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@ export interface PropsBase {
* @see https://daypicker.dev/docs/customization#caption-layouts
*/
captionLayout?: "label" | "dropdown" | "dropdown-months" | "dropdown-years";

/**
* Adjust the positioning of the navigation buttons.
*
* - `around`: Buttons are displayed on either side of the caption.
* - `after`: Buttons are displayed after the caption. This option ensures the
* tab order matches the visual order.
*
* If not set, the buttons default to being displayed after the caption, but
* the tab order may not align with the visual order.
*
* @since 9.7.0
* @see https://daypicker.dev/docs/customization#navigation-layouts
*/
navLayout?: "around" | "after" | undefined;
/**
* Display always 6 weeks per each month, regardless of the month’s number of
* weeks. Weeks will be filled with the days from the next month.
Expand Down
9 changes: 9 additions & 0 deletions src/useAnimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ export function useAnimation(
previousWeekdaysEl.style.opacity = "0";
}

const animatedButtons = previousMonthEl.querySelectorAll(
"[data-animated-button]"
);
animatedButtons.forEach((button) => {
if (button instanceof HTMLElement) {
button.style.visibility = "hidden";
}
});

const previousCaptionEl = queryCaptionEl(previousMonthEl);
if (previousCaptionEl) {
previousCaptionEl.classList.add(
Expand Down
47 changes: 38 additions & 9 deletions website/docs/docs/customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ sidebar_position: 3

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

| Prop Name | Type | Description |
| ----------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `captionLayout` | `"label"`<br/> \| `"dropdown"`<br/> \| `"dropdown-months"`<br/> \| `"dropdown-years"` | Choose the layout of the month caption. Default is `label`. |
| `fixedWeeks` | `boolean` | Display 6 weeks per month. |
| `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). |
| `hideWeekdays` | `boolean` | Hide the row displaying the weekday names. |
| `numberOfMonths` | `number` | The number of displayed months. Default is `1`. |
| `showOutsideDays` | `boolean` | Display the days falling into other months. |
| `showWeekNumber` | `boolean` | Display the column with the [week numbers](#showweeknumber). |
| Prop Name | Type | Description |
| ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `captionLayout` | `"label"`<br/> `"dropdown"`<br/> `"dropdown-months"`<br/> `"dropdown-years"` | Choose the layout of the month caption. Default is `label`. |
| `navLayout` | `around` \| `end` | Adjust the positioning of the navigation buttons. |
| `fixedWeeks` | `boolean` | Display 6 weeks per month. |
| `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). |
| `hideWeekdays` | `boolean` | Hide the row displaying the weekday names. |
| `numberOfMonths` | `number` | The number of displayed months. Default is `1`. |
| `showOutsideDays` | `boolean` | Display the days falling into other months. |
| `showWeekNumber` | `boolean` | Display the column with the [week numbers](#showweeknumber). |

## Caption Layouts

Expand Down Expand Up @@ -54,6 +55,34 @@ Without specifying the `from*` and `to*` properties, the dropdown will display t

:::

## Navigation Layouts

Use the `navLayout` prop to adjust the positioning of the navigation buttons.

| Navigation Layout | Description |
| ----------------- | ------------------------------------------------------------------------------------------ |
| `"around"` | Buttons are displayed on either side of the caption. |
| `"end"` | Buttons are displayed after the caption, ensuring the tab order matches the visual order. |
| `undefined` | Buttons are displayed after the caption, but the tab order may not match the visual order. |

```tsx
<DayPicker navLayout="around" />
```

<BrowserWindow>
<Examples.NavLayout />
</BrowserWindow>

See [Navigation](./navigation.mdx) for additional ways to customize the calendar’s navigation.

:::info Tab Order vs. Visual Order

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 `"end"` or to `"around"` instead of leaving it undefined.

In a future version, the default behavior will be changed to `"end"`.

:::

## Outside Days

By default, DayPicker hides the days falling into the other months. Use `showOutsideDays` to display them.
Expand Down
18 changes: 18 additions & 0 deletions website/src/components/Playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ export function Playground() {
<option value="dropdown-years">Dropdown years</option>
</select>
</label>
<label>
Navigation Layout:
<select
name="navLayout"
value={props.navLayout}
onChange={(e) => {
const newProps = {
...props,
navLayout: e.target.value ?? undefined
} as DayPickerProps;
setProps(newProps);
}}
>
<option value=""></option>
<option value="around">Around</option>
<option value="after">After</option>
</select>
</label>
<label>
<input
type="checkbox"
Expand Down