Skip to content

chore(a11y): 🤖 enable keyboard date range picker selection#856

Open
punkbit wants to merge 64 commits intomainfrom
chore/enable-keyboard-date-range-picker-selection
Open

chore(a11y): 🤖 enable keyboard date range picker selection#856
punkbit wants to merge 64 commits intomainfrom
chore/enable-keyboard-date-range-picker-selection

Conversation

@punkbit
Copy link
Collaborator

@punkbit punkbit commented Feb 26, 2026

Why?

Enable keyboard date range picker selection

Added keyboard navigation support to the DatePicker component, allowing users to select dates without using a mouse. This improves accessibility and provides a faster workflow for power users.

⚠️ WARNING: Depends on #838 which should be merged first
🤖 Switch base branch to main once #838 is merged

How?

  • Replaced Dropdown with Popover component for better focus management
  • Added focus management with refs to track keyboard position
  • Implemented keyboard event handlers for Arrow keys, Enter, and Space
  • Active elements (today's date, selected date) temporarily revert to default styling when keyboard focused to ensure the yellow focus ring is always visible

Preview?

Latest update (feb 27 12:15pm GMT)

demo-datepicker-keyboard-navigation.mov

Contrast issue found, e.g. lack contrast on keyboard navigation matching an active element

demo-keyboard-nav-no-contrast-on-active-match.mov

Solution for contrast issue, e.g. replaces active by navigation default focus style

demo-date-picker-solved-selection-nav-matching-active.mov

Result use-cases

demo-date-picking-keyboard-nav-flow.mov

Base automatically changed from feat/date-range-picker-allow-jump-year to main February 26, 2026 16:45
@hoorayimhelping
Copy link
Member

How do you actually trigger the DatePicker with just the keyboard? This video is me getting the input focus then trying to open the DatePicker using enter, spacebar, and various keys on the keyboard

Screen.Recording.2026-03-03.at.9.25.53.AM.mov

Comment on lines +596 to +599
const monthGridRef = useRef<(HTMLButtonElement | null)[]>([]);
const yearGridRef = useRef<(HTMLButtonElement | null)[]>([]);
const headerNavRefs = useRef<(HTMLButtonElement | null)[]>([null, null, null]);

Copy link
Member

Choose a reason for hiding this comment

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

We prefer the form Array<type> rather than type[] for readability purposes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@hoorayimhelping, that's quite subjective, can we not impose it? If you hover a type in your text editor, TypeScript will show the form type[] and not Array, it's the default. A quick search in the project or control plane show plenty of examples of type[] usage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

newIndex = (index - columns + totalItems) % totalItems;
break;
case 'Enter':
case ' ':
Copy link
Member

Choose a reason for hiding this comment

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

Is ' ' the same as spacebar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Can we use space or spacebar or do we have to use ' ' since it's a value? I'd prefer spacebar since it's more explicit and clear


const renderYearsGrid = () => {
const years = [];
const years: number[] = [];
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const years: number[] = [];
const years: Array<number> = [];


describe('disabling dates', () => {
// this test was throwing an error if `vi.useFakeTimers` was called outside
// of beforeAll, so it needed to be put in here
Copy link
Member

Choose a reason for hiding this comment

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

I think you should leave this comment here. We rarely use beforeAll, so explaining why it's here and giving a hint that it shouldn't be changed is probably valuable

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok put it back

expect(document.activeElement).toBe(getByTestId('month-cell-1'));
});

it('calendar title is keyboard accessible', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
it('calendar title is keyboard accessible', async () => {
it('allows accessing the calendar title by keyboard', async () => {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok done

expect(getByTestId('years-grid')).toBeInTheDocument();
});

it('day grid supports keyboard navigation and Enter to select', async () => {
Copy link
Member

Choose a reason for hiding this comment

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

I think we this should be wrapped in a describe as well

describe('the day grid', () => {
  it('supports keyboard navigation and selects with Enter', async ()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok

selectedDate ? isSameDate(selectedDate, day.value) : isSameDate(today, day.value)
);

const [focusedDayIndex, setFocusedDayIndex] = useState(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const [focusedDayIndex, setFocusedDayIndex] = useState(
const [focusedDayIndex, setFocusedDayIndex] = useState<number>(

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok

const [focusedDayIndex, setFocusedDayIndex] = useState(
initialFocusIndex >= 0 ? initialFocusIndex : 0
);
const dayRefs = useRef<(HTMLTableCellElement | null)[]>([]);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const dayRefs = useRef<(HTMLTableCellElement | null)[]>([]);
const dayRefs = useRef<Array<(HTMLTableCellElement | null)>>([]);


return (
<Dropdown
<Popover.Root
Copy link
Member

Choose a reason for hiding this comment

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

Why the change from Dropdown to Popover?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@punkbit
Copy link
Collaborator Author

punkbit commented Mar 3, 2026

How do you actually trigger the DatePicker with just the keyboard? This video is me getting the input focus then trying to open the DatePicker using enter, spacebar, and various keys on the keyboard

Screen.Recording.2026-03-03.at.9.25.53.AM.mov

Tab, then space

@hoorayimhelping
Copy link
Member

hoorayimhelping commented Mar 3, 2026

Tab, then space

I can't get this to work in Firefox. I'm trying to load the preview in chrome. Where can I find the latest vercel preview? The one auto-posted in a comment is an older build. Chromatic requires a login, which I'm not going to both with on Chrome

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants