Skip to content

feat(Date Picker): Add Date Picker component#2660

Open
VincentSmedinga wants to merge 31 commits into
developfrom
feat/DES-1856-date-picker
Open

feat(Date Picker): Add Date Picker component#2660
VincentSmedinga wants to merge 31 commits into
developfrom
feat/DES-1856-date-picker

Conversation

@VincentSmedinga

@VincentSmedinga VincentSmedinga commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Add Date Picker component

Links

What

Adds DatePicker, a form control for selecting a single date or a date range, in Components / Forms.

Why

It completes the Calendar / Date Picker split. Calendar covers browsing content grouped by date; the Date Picker is the form control for choosing a date or range — a grid of day buttons with full keyboard support — which is a distinct concern with different markup and interaction.

How

  • Controlled component with a mode discriminated union: single mode takes a Date | null, range mode (mode="range") takes a DateRange ({ start, end }).
    The first pick sets the start, the second sets the end; picking before the current start begins a new range.
  • minDate and maxDate bound both selection and month navigation; isDateDisabled marks individual dates unavailable while keeping them reachable by keyboard.
  • Follows the WAI-ARIA grid pattern: role="grid" with a roving tabindex, arrow / Home / End / Page navigation, and Enter or Space to select. Focus follows keyboard navigation and click across month boundaries. The grid takes its accessible name from the visible month caption; each day button is labelled with its full date including weekday.
  • Range mode sets aria-multiselectable="true" on the grid and marks every date within the selection as aria-selected. The start and end dates append rangeStartAccessibleName / rangeEndAccessibleName to their accessible names (default: 'startdatum' / 'einddatum'), so screen readers distinguish them from intermediate selected dates.
  • Forced-colours support: selected and hovered dates use Highlight / HighlightText so the selection remains visible when the OS overrides colours.
  • All navigation button labels and range endpoint suffixes are overridable props for localisation.
  • Design tokens alias the Calendar tokens so the two components stay visually in step. Reuses the useMonthNavigation hook and date helpers shared with Calendar.

Checklist

  • Add or update unit tests
  • Add or update documentation
  • Add or update stories
  • Add or update exports in index.* files
  • Comment /chromatic test and verify visual regression tests pass
  • Start the PR title with a Conventional Commit prefix, as explained here.

Additional notes

  • Implements DES-1856.
  • Story to review: Components / Forms / Date Picker (link above once the feature-branch preview deploys).
  • The day hover background reuses the Icon Button's rgb(0 70 153 / 12.5%) value — there is no neutral-surface token yet, so this is a temporary workaround.
  • Page Up / Page Down clamp the focused day to the last valid day of the target month (e.g. 31 Jan → 28 Feb) rather than overflowing.

VincentSmedinga and others added 12 commits June 3, 2026 14:27
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…mmon

Move the month-navigation hook and the shared date math out of Calendar into packages/react/src/common, so calendar-style components (the upcoming Date Picker) can reuse them instead of duplicating the logic. No behaviour change to Calendar.
Add DatePicker, an accessible role=grid widget for selecting a single date or a date range, in Components/Forms. It is controlled (value/onChange) with a single | range mode union, supports minDate/maxDate bounds and per-date disabling, and implements the WAI-ARIA grid keyboard pattern with focus that follows across month boundaries. Its tokens and CSS alias the Calendar tokens so the two stay visually in step.
@VincentSmedinga VincentSmedinga requested a review from a team as a code owner June 4, 2026 06:53
@VincentSmedinga VincentSmedinga changed the title feat(DatePicker): Add the Date Picker component feat(Date Picker): Add Date Picker component Jun 4, 2026
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for React components

Status Category Percentage Covered / Total
🟢 Lines 100% 928 / 928
🟢 Statements 99.49% 986 / 991
🟠 Functions 98.84% 256 / 259
🟢 Branches 99.26% 672 / 677
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/DatePicker/DatePicker.tsx 100% 100% 100% 100%
packages/react/src/DatePicker/DatePickerBody.tsx 100% 100% 100% 100%
packages/react/src/DatePicker/DatePickerDay.tsx 100% 100% 100% 100%
packages/react/src/DatePicker/DatePickerHeader.tsx 100% 100% 100% 100%
packages/react/src/DatePicker/utils.ts 100% 100% 100% 100%
Generated in workflow #345 for commit fe3a920 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Size Change: +3.19 kB (+0.89%)

Total Size: 361 kB

📦 View Changed
Filename Size Change
packages-proprietary/tokens/dist/index.css 9.11 kB +146 B (+1.63%)
packages/css/dist/index.css 14.9 kB +208 B (+1.42%)
packages/react/dist/index.esm.js 42.9 kB +2.83 kB (+7.07%) 🔍
packages/react/dist/index.js 477 B +7 B (+1.49%)
ℹ️ View Unchanged
Filename Size
packages-proprietary/react-icons/dist/index.esm.js 51.1 kB
packages-proprietary/temp/assets-fonts.tar.gz 129 kB
packages-proprietary/temp/assets-others.tar.gz 112 kB
packages-proprietary/tokens/dist/compact.css 605 B

compressed-size-action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new DatePicker form component to the design system, including its React implementation (with keyboard-focused ARIA grid behavior), CSS styling, design tokens, Storybook documentation/stories, and unit tests—completing the Calendar vs DatePicker split described in the PR.

Changes:

  • Introduces DatePicker React component with header + grid + day subcomponents, date/range selection logic, and keyboard navigation helpers.
  • Adds CSS + design tokens for Date Picker (largely aliased to Calendar tokens) and wires the new stylesheet into the CSS bundle.
  • Adds Storybook stories/docs and comprehensive unit tests for the component and its utilities.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
storybook/src/components/DatePicker/DatePicker.test.stories.tsx Adds non-dev “test variants” story for visual/regression coverage.
storybook/src/components/DatePicker/DatePicker.stories.tsx Adds interactive single/range/disabled/limited examples for Storybook.
storybook/src/components/DatePicker/DatePicker.docs.mdx Adds documentation page content + token table for Date Picker.
packages/react/src/index.ts Exports DatePicker from the React package barrel.
packages/react/src/DatePicker/utils.ts Adds date/range utilities + keyboard focus date computation helpers.
packages/react/src/DatePicker/utils.test.ts Adds unit tests for the DatePicker utility functions.
packages/react/src/DatePicker/README.md Adds React package README that links to CSS docs.
packages/react/src/DatePicker/index.ts Adds component/module entrypoint exports for DatePicker + types.
packages/react/src/DatePicker/DatePickerHeader.tsx Adds month/year navigation header for DatePicker with IconButtons.
packages/react/src/DatePicker/DatePickerHeader.test.tsx Adds tests for header caption, labels, disabling, and handlers.
packages/react/src/DatePicker/DatePickerGrid.tsx Adds ARIA grid rendering of weekdays + weeks + day cells.
packages/react/src/DatePicker/DatePickerGrid.test.tsx Adds tests for grid labeling, weekday headers, and week structure.
packages/react/src/DatePicker/DatePickerDay.tsx Adds a selectable day button with accessible full-date label.
packages/react/src/DatePicker/DatePickerDay.test.tsx Adds tests for day rendering, selection semantics, and click behavior.
packages/react/src/DatePicker/DatePicker.tsx Adds the main controlled DatePicker with selection + keyboard navigation.
packages/react/src/DatePicker/DatePicker.test.tsx Adds integration-style tests for selection, bounds, and keyboard focus behavior.
packages/css/src/components/index.scss Includes Date Picker styles in the CSS components bundle.
packages/css/src/components/date-picker/README.md Adds CSS component documentation/guidelines for Date Picker.
packages/css/src/components/date-picker/date-picker.scss Adds Date Picker BEM styles for header/grid/day states.
packages-proprietary/tokens/src/components/ams/date-picker.tokens.json Adds design tokens for Date Picker, largely aliasing Calendar tokens.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/css/src/components/date-picker/date-picker.scss
Comment thread packages/react/src/DatePicker/DatePicker.tsx Outdated
Comment thread packages/react/src/DatePicker/DatePickerBody.tsx Outdated
Base automatically changed from feat/DES-1855-calendar to develop June 9, 2026 16:28
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 07:51 Destroyed
@VincentSmedinga VincentSmedinga requested a review from Copilot June 10, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Comment thread packages/react/src/DatePicker/DatePicker.tsx
Comment thread packages/react/src/DatePicker/DatePicker.tsx
Comment thread packages/react/src/DatePicker/DatePickerBody.tsx Outdated
- Replace display:contents on role="row" elements with CSS subgrid so
  rows stay in the accessibility tree
- Call preventDefault() for any recognised navigation key, even when
  the target date is out of bounds, to prevent unintended page scroll
- Correct the onChange docstring: single mode never calls onChange(null)
- Add the Monday-anchor comment to the weekday header date, matching
  CalendarBody
- Add a test covering unrecognised keys on the grid
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 08:10 Destroyed
@VincentSmedinga VincentSmedinga requested a review from Copilot June 10, 2026 08:12
@VincentSmedinga

Copy link
Copy Markdown
Contributor Author

/Chromatic test

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.

Comment thread packages/css/src/components/date-picker/date-picker.scss Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 08:20 Destroyed
The nested grid approach (display:grid + grid-column:1/-1) broke the
7-column layout. display:contents keeps cells as direct participants in
the parent grid, which is the only reliable approach for this structure.

Modern browsers (Chrome 85+, Firefox 98+, Safari 17.4+) keep elements
with explicit ARIA roles in the accessibility tree through display:contents,
so the role="row" semantics are preserved.
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 08:39 Destroyed
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 08:51 Destroyed
Applies the documentation content model from develop:
- Replaces README import + Markdown block with Title + Description pulled from TSDoc
- Adds Usage guidelines (when/when not/how to use), Features, and Accessibility sections
- Moves keyboard navigation into a dedicated Features subsection
- Rewrites See also entries with the dash separator format
- Deletes the CSS and React README files now that content is in the MDX
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 10, 2026 20:39 Destroyed
@github-actions github-actions Bot temporarily deployed to demo-DES-1856-date-picker June 11, 2026 09:57 Destroyed

@RubenSibon RubenSibon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review in progress...

Comment on lines +28 to +29
Use a [Date Input](/docs/components-forms-date-input--docs) instead when the user knows the date and can type it into a form field.
Use a [Calendar](/docs/components-navigation-calendar--docs) instead to browse content grouped by date, such as an events calendar.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Use a [Date Input](/docs/components-forms-date-input--docs) instead when the user knows the date and can type it into a form field.
Use a [Calendar](/docs/components-navigation-calendar--docs) instead to browse content grouped by date, such as an events calendar.
- Use a [Date Input](/docs/components-forms-date-input--docs) when the user knows the date and can type it into a form field.
- Use a [Calendar](/docs/components-navigation-calendar--docs) to browse content grouped by date, such as an events calendar.

“Instead” is not needed because the heading ‘When not to use’ already implies that these are alternative use-cases.

I think that making this a list adds clarity.

I’ve considered switching the use-case and the imperative form:

- When the user knows the date and can type it into a form field use a [Date Input](/docs/components-forms-date-input--docs).

But I guess it’s better the way it was.

Use `minDate` and `maxDate` to bound the selectable period.
Dates outside the range cannot be selected, and the month and year navigation buttons stop at the edges of the range.

<Canvas of={DatePickerStories.LimitedToOneMonth} />

@RubenSibon RubenSibon Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe it would be nice if this example or another example showed what the Date Picker looks like when the minDate or maxDate are inside the current month. What do the not-selectable dates look like?

It’s not possible to play around with the min- and maxDate in the controls. Same for many of the other props. Why is this so?

EDIT: I’ve learned why.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clicking on the Show code buttons in the demo takes me to an empty Storybook view. What’s going on?


<Primary />

<Controls />

@RubenSibon RubenSibon Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are many of the controls not editable?

EDIT: I’ve learned why.


### Range selection

<Canvas of={DatePickerStories.Range} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this example could use a bit more guidelines. How will users know that this a date range selection? Developers or content editors need to add that context with text. The component in its bare state does not communicate “I’m a date range picker!”.

mode: { control: false }, // The story wrapper owns this prop.
onChange: { control: false }, // The story wrapper owns this prop.
value: { control: false }, // The story wrapper owns this prop.
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okay, so now I understand why the controls are not editable. But users will not see these comments, so it might be confusing why they cannot play around with them.

And if we could have some kind of workaround to get these controls working, that would be a very nice feature. Disabling controls because of some internal technical reason is not optimal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice! Our own date utils instead of a third-party lib.

Comment on lines +11 to +13
export const isOutOfBounds = (date: Date, minDate?: Date, maxDate?: Date) =>
(minDate !== undefined && startOfDay(date) < startOfDay(minDate)) ||
(maxDate !== undefined && startOfDay(date) > startOfDay(maxDate))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess our linter allows this, but I don’t like arrow functions with an implicit return were the return block is on the next (multiple) lines (see also arrow-body-style#as-needed). I would prefer:

Suggested change
export const isOutOfBounds = (date: Date, minDate?: Date, maxDate?: Date) =>
(minDate !== undefined && startOfDay(date) < startOfDay(minDate)) ||
(maxDate !== undefined && startOfDay(date) > startOfDay(maxDate))
export const isOutOfBounds = (date: Date, minDate?: Date, maxDate?: Date) => {
return (minDate !== undefined && startOfDay(date) < startOfDay(minDate)) ||
(maxDate !== undefined && startOfDay(date) > startOfDay(maxDate))
}

Same for the other functions with this pattern.

Comment on lines +28 to +32
readonly goToNextMonth: () => void
readonly goToNextYear: () => void
readonly goToPreviousMonth: () => void
readonly goToPreviousYear: () => void
readonly month: Date

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do these props not have comments?

/** Selects a single date. This is the default mode. */
readonly mode?: 'single'
/** Called when the user selects a date. */
readonly onChange: (value: Date | null) => void

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

DatePickerSingleProps.onChange is typed (value: Date | null) => void, but implementation only ever calls it with a Date.

Why it matters: public API type/documentation mismatch; consumers may expect null events that never happen.

Suggestion: either narrow to (value: Date) => void or explicitly document future/intentional nullable callback behaviour.

Credits: GitHub Copilot.

Comment on lines +182 to +188
if (start !== null && isSameDay(date, start)) {
return rangeStartAccessibleName
}

if (end !== null && isSameDay(date, end)) {
return rangeEndAccessibleName
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In range mode, if user picks the same date twice (start === end), nextRange creates a complete range, but the accessible name only gets rangeStartAccessibleName (not end), because getBoundaryLabel returns early on start match.

  • packages/react/src/DatePicker/utils.ts (nextRange)
  • packages/react/src/DatePicker/DatePicker.tsx (getBoundaryLabel)

Why it matters: this makes a completed one-day range indistinguishable (for screen readers) from an incomplete range with only start set.

Suggestion: when start and end are both set and same day, append both labels (e.g. "startdatum, einddatum"), and add a test for this case.

At the moment, in these stories, a user cannot input the same date for both the start and end with mouse pointer, touch or keyboard. But maybe we still should make the underlying logic even more robust?

Good question — and yes, I’d still make it robust.

I do think this case is currently possible in the component itself:

  • In range mode, first pick sets { start: date, end: null }.
  • Picking the same date again goes through nextRange and returns { start: date, end: date }.
  • That can happen via click/tap or pressing Enter/Space twice on the focused day.

Even if your product flow usually avoids that, DatePicker is controlled, so a parent can also pass { start, end } with equal dates directly.

So I’d recommend a defensive fix anyway. Two valid approaches:

  1. Allow one-day ranges (recommended)
    Keep current selection logic, but if start === end, announce both boundaries in the accessible name (and add a test).

  2. Disallow one-day ranges
    Normalize in nextRange so a second pick on the same date does not set end (or restarts range), and document that rule.

Credits: GitHub Copilot

@RubenSibon RubenSibon Jun 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right now sharing is mostly date-picker -> calendar aliasing. That works, but conceptually it couples DatePicker to Calendar naming.

A cleaner long-term model is an internal foundation layer, e.g. ams.date-grid.*:

  • ams.date-grid.font-*
  • ams.date-grid.gap
  • ams.date-grid.inline-size
  • ams.date-grid.day.current.font-weight
  • ams.date-grid.day.outline-offset

Then:

  • ams.calendar.* aliases ams.date-grid.*
  • ams.date-picker.* aliases ams.date-grid.*
  • component-specific tokens stay local (calendar.day.link.*, date-picker.day.selected.*, etc.)

This gives shared styling without implying Calendar is the “source of truth” for DatePicker.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants