diff --git a/examples/AutoFocus.tsx b/examples/AutoFocus.tsx
new file mode 100644
index 000000000..82aad429e
--- /dev/null
+++ b/examples/AutoFocus.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+import { DayPicker } from "react-day-picker";
+
+/** Test for the next focus day to not cause an infinite recursion. */
+export function AutoFocus() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/Dialog.tsx b/examples/Dialog.tsx
index 8d7d12c4f..ee7213e6c 100644
--- a/examples/Dialog.tsx
+++ b/examples/Dialog.tsx
@@ -23,22 +23,16 @@ export function Dialog() {
// Function to toggle the dialog visibility
const toggleDialog = () => setIsDialogOpen(!isDialogOpen);
- // Hook to handle the body scroll behavior and focus trapping.
+ // Hook to handle the body scroll behavior and focus trapping. You may want to
+ // use your own trapping library as the body.style overflow will break the
+ // scroll position.
useEffect(() => {
- const handleBodyScroll = (isOpen: boolean) => {
- document.body.style.overflow = isOpen ? "hidden" : "";
- };
if (!dialogRef.current) return;
if (isDialogOpen) {
- handleBodyScroll(true);
dialogRef.current.showModal();
} else {
- handleBodyScroll(false);
dialogRef.current.close();
}
- return () => {
- handleBodyScroll(false);
- };
}, [isDialogOpen]);
/**
@@ -108,18 +102,21 @@ export function Dialog() {
aria-labelledby={headerId}
onClose={() => setIsDialogOpen(false)}
>
- Selected: {selectedDate.toDateString()}>
- )
- }
- />
+ {isDialogOpen && (
+ Selected: {selectedDate.toDateString()}>
+ )
+ }
+ />
+ )}
);
diff --git a/examples/__snapshots__/Range.test.tsx.snap b/examples/__snapshots__/Range.test.tsx.snap
index 5afc6c6e0..bca7c6591 100644
--- a/examples/__snapshots__/Range.test.tsx.snap
+++ b/examples/__snapshots__/Range.test.tsx.snap
@@ -159,7 +159,7 @@ exports[`should match the snapshot 1`] = `
@@ -353,7 +353,7 @@ exports[`should match the snapshot 1`] = `
diff --git a/examples/index.ts b/examples/index.ts
index 7013d6071..34d9be9cc 100644
--- a/examples/index.ts
+++ b/examples/index.ts
@@ -15,6 +15,7 @@ export * from "./DisableNavigation";
export * from "./Dropdown";
export * from "./DropdownMultipleMonths";
export * from "./Fixedweeks";
+export * from "./AutoFocus";
export * from "./FocusRecursive";
export * from "./Footer";
export * from "./Formatters";
diff --git a/src/DayPicker.tsx b/src/DayPicker.tsx
index 07f63e32c..b83787e6b 100644
--- a/src/DayPicker.tsx
+++ b/src/DayPicker.tsx
@@ -51,8 +51,7 @@ export function DayPicker(props: DayPickerProps) {
const modifiers = useModifiers(props, calendar, dateLib);
const selection = useSelection(props, dateLib);
- const focus = useFocus(props, calendar, modifiers, dateLib);
-
+ const focus = useFocus(props, calendar, modifiers, selection, dateLib);
const {
captionLayout,
dir,
@@ -537,6 +536,7 @@ export function DayPicker(props: DayPickerProps) {
style={styles?.[UI.DayButton]}
day={day}
modifiers={m}
+ focused={isFocused}
disabled={m.disabled || undefined}
tabIndex={
focus.isFocusTarget(day) ? 0 : -1
diff --git a/src/components/DayButton.tsx b/src/components/DayButton.tsx
index f90beeb93..d3ee84eca 100644
--- a/src/components/DayButton.tsx
+++ b/src/components/DayButton.tsx
@@ -16,14 +16,21 @@ import type { Modifiers } from "../types/index.js";
*/
export function DayButton(
props: {
+ /** Whether the day is focused. */
+ focused: boolean;
/** The day to render. */
day: CalendarDay;
/** The modifiers for the day. */
modifiers: Modifiers;
} & JSX.IntrinsicElements["button"]
) {
- const { day, modifiers, ...buttonProps } = props;
- return ;
+ const { day, modifiers, focused, ...buttonProps } = props;
+
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (focused) ref.current?.focus();
+ }, [focused]);
+ return ;
}
export type DayButtonProps = Parameters[0];
diff --git a/src/helpers/calculateFocusTarget.ts b/src/helpers/calculateFocusTarget.ts
new file mode 100644
index 000000000..343454eed
--- /dev/null
+++ b/src/helpers/calculateFocusTarget.ts
@@ -0,0 +1,48 @@
+import { DayFlag } from "../UI.js";
+import type { CalendarDay } from "../classes/index.js";
+import type { Modifiers } from "../types/index.js";
+import { UseCalendar } from "../useCalendar.js";
+
+export function calculateFocusTarget(
+ calendar: UseCalendar,
+ getModifiers: (day: CalendarDay) => Modifiers,
+ isSelected: (date: Date) => boolean,
+ lastFocused: CalendarDay | undefined
+) {
+ let focusTarget: CalendarDay | undefined;
+
+ let index = 0;
+ let found = false;
+
+ while (index < calendar.days.length && !found) {
+ const day = calendar.days[index];
+ const m = getModifiers(day);
+
+ if (!m[DayFlag.disabled] && !m[DayFlag.hidden] && !m[DayFlag.outside]) {
+ if (m[DayFlag.focused]) {
+ focusTarget = day;
+ found = true;
+ } else if (lastFocused?.isEqualTo(day)) {
+ focusTarget = day;
+ found = true;
+ } else if (isSelected(day.date)) {
+ focusTarget = day;
+ found = true;
+ } else if (m[DayFlag.today]) {
+ focusTarget = day;
+ found = true;
+ }
+ }
+
+ index++;
+ }
+
+ if (!focusTarget) {
+ // return the first day that is focusable
+ focusTarget = calendar.days.find((day) => {
+ const m = getModifiers(day);
+ return !m[DayFlag.disabled] && !m[DayFlag.hidden] && !m[DayFlag.outside];
+ });
+ }
+ return focusTarget;
+}
diff --git a/src/types/props.ts b/src/types/props.ts
index ed11374a5..55a3f010c 100644
--- a/src/types/props.ts
+++ b/src/types/props.ts
@@ -218,8 +218,11 @@ export interface PropsBase {
*
* Use this prop when you need to focus DayPicker after a user actions, for
* improved accessibility.
+ *
+ * @see https://daypicker.dev/next/using-daypicker/accessibility#autofocus
*/
autoFocus?: boolean;
+
/** Apply the `disabled` modifier to the matching days. */
disabled?: Matcher | Matcher[] | undefined;
/**
diff --git a/src/useFocus.ts b/src/useFocus.ts
index f0cd289bb..f0ad029df 100644
--- a/src/useFocus.ts
+++ b/src/useFocus.ts
@@ -1,16 +1,18 @@
-import { useEffect, useState } from "react";
+import { useState } from "react";
-import { DayFlag } from "./UI.js";
import type { CalendarDay } from "./classes/index.js";
+import { calculateFocusTarget } from "./helpers/calculateFocusTarget.js";
import { getNextFocus } from "./helpers/getNextFocus.js";
import type {
MoveFocusBy,
MoveFocusDir,
DateLib,
- DayPickerProps
+ DayPickerProps,
+ Mode
} from "./types/index.js";
import { UseCalendar } from "./useCalendar.js";
import { UseModifiers } from "./useModifiers.js";
+import { UseSelection } from "./useSelection.js";
export type UseFocus = {
/** The date that is currently focused. */
@@ -65,52 +67,28 @@ export function useFocus(
>,
calendar: UseCalendar,
modifiers: UseModifiers,
+ selection: UseSelection<{ mode: Mode }>,
dateLib: DateLib
): UseFocus {
const { getModifiers } = modifiers;
-
- const [focusedDay, setFocused] = useState();
+ const { autoFocus } = props;
const [lastFocused, setLastFocused] = useState();
- useEffect(() => {
- if (focusedDay) {
- getDayCell(focusedDay, (props.numberOfMonths ?? 1) > 1, dateLib)?.focus();
- }
- }, [dateLib, focusedDay, props.numberOfMonths]);
+ const focusTarget = calculateFocusTarget(
+ calendar,
+ getModifiers,
+ selection.isSelected,
+ lastFocused
+ );
+ const [focusedDay, setFocused] = useState(
+ autoFocus ? focusTarget : undefined
+ );
const blur = () => {
setLastFocused(focusedDay);
setFocused(undefined);
};
- let focusTarget: CalendarDay | undefined;
-
- calendar.days.map((day) => {
- const m = getModifiers(day);
- if (m[DayFlag.disabled]) return;
- if (m[DayFlag.hidden]) return;
- if (m[DayFlag.outside]) return;
-
- if (m[DayFlag.focused]) {
- focusTarget = day;
- return;
- }
- if (lastFocused?.isEqualTo(day)) {
- focusTarget = day;
- return;
- }
-
- if (m[DayFlag.today]) {
- focusTarget = day;
- return;
- }
-
- if (!focusTarget && dateLib.isSameDay(day.date, calendar.months[0].date)) {
- focusTarget = day;
- return;
- }
- });
-
const moveFocus = (moveBy: MoveFocusBy, moveDir: MoveFocusDir) => {
if (!focusedDay) return;
const nextFocus = getNextFocus(
@@ -133,8 +111,6 @@ export function useFocus(
};
const useFocus: UseFocus = {
- // focusTarget,
- // initiallyFocused,
isFocusTarget,
setFocused,
focused: focusedDay,
@@ -154,26 +130,3 @@ export function useFocus(
return useFocus;
}
-
-/**
- * Get the day cell element for the given day from the data-day and data-month
- * attribute.
- *
- * @private
- */
-function getDayCell(
- focused: CalendarDay,
- multipleMonths: boolean,
- dateLib: DateLib
-) {
- const dataDay = dateLib.format(focused.date, "yyyy-MM-dd");
- const dataMonth = dateLib.format(focused.displayMonth, "yyyy-MM");
- let selector = `[data-day="${dataDay}"]`;
- if (multipleMonths) {
- selector += `[data-month="${dataMonth}"]`;
- }
- const dayCell = window.document.querySelector(
- `${selector} button`
- ) as HTMLButtonElement | null;
- return dayCell;
-}
diff --git a/website/docs/using-daypicker/accessibility.mdx b/website/docs/using-daypicker/accessibility.mdx
index 3412a2d3e..818e5ebfc 100644
--- a/website/docs/using-daypicker/accessibility.mdx
+++ b/website/docs/using-daypicker/accessibility.mdx
@@ -64,6 +64,18 @@ export function AccessibleDatePicker() {
+## Autofocusing the Calendar {#autofocus}
+
+DayPicker manages focus automatically when the user interacts with the calendar. However, for better accessibility you may need to autofocus the calendar when it opens. To do this, you can use the `autofocus` prop:
+
+```tsx
+
+```
+
+
+
+
+
## Keyboard Navigation
DayPicker supports keyboard navigation to make it easier for users to navigate the calendar. The following keys are supported:
@@ -87,3 +99,7 @@ DayPicker supports keyboard navigation to make it easier for users to navigate t
Accessibility is an evolving field. If you find any issues with DayPicker, please [open an issue](https://github.com/gpbl/react-day-picker/issues/new/choose). Your feedback helps improve our library's accessibility.
Check out the [current accessibility issues](https://github.com/gpbl/react-day-picker/issues?q=is%3Aopen+label%3Aaccessibility+sort%3Aupdated-desc).
+
+```
+
+```