Skip to content

feat(DatePicker): New DatePicker component#3286

Open
aresnik11 wants to merge 76 commits intomainfrom
ajr-datepicker-localization
Open

feat(DatePicker): New DatePicker component#3286
aresnik11 wants to merge 76 commits intomainfrom
ajr-datepicker-localization

Conversation

@aresnik11
Copy link
Copy Markdown
Contributor

@aresnik11 aresnik11 commented Mar 17, 2026

Overview

Adds DatePicker to Gamut: a locale-aware, accessible date (or date range) picker with segmented inputs, a popover calendar, keyboard support, and optional composition via context.

Modes

  • Single dateselectedDate / setSelectedDate.
  • Date rangestartDate, endDate, setStartDate, setEndDate; optional startLabel / endLabel.
  • Calendar closes once a single date or date range is selected.

Default UI vs composition

  • Default — text input(s) + calendar in a popover under the input.
  • Custom — pass children for layout only; compose DatePickerInput and DatePickerCalendar (calendar requires DatePicker context).

Segmented inputs

  • Segmented entry (month / day / year) with locale-driven order and separators (Intl-based layout).
  • spinbutton pattern (role="spinbutton"), which matches Arrow Up/Down stepping and numeric constraints.
  • Live typing with blur normalization when input is invalid or partial.
  • Empty input clears the relevant selection (single or range bound).
  • Hidden input submits an ISO 8601 date-only value for forms (name / form supported).

Calendar & layout

  • Responsivetwo adjacent months from the xs breakpoint up; one month on smaller viewports.
  • Month navigation with nav adjusted for two-month view.
  • Week starts from Intl.Locale#getWeekInfo() (polyfill when needed), optional weekStartsOn on DatePickerCalendar.

Selection behavior

Disabled dates

  • disabledDates — unselectable days; integrated into range validation.

Footer

  • Today — select today and align visible month(s).
  • Clearrange mode; clears range; disabled when empty.

Keyboard & focus

  • Each input segment is a role="spinbutton" span (tabIndex={0} when enabled). Focus moves with Tab / Shift+Tab like normal focusable controls. **Arrow Left / Right ** moves focus within the segments. Arrow Up / Arrow Down steps the current segment up or down, clamped to min/max for that field. Month: 1–12. Day: 1–last day of month when month/year are known. Year: 1–9999; if empty, stepping uses sensible defaults (e.g. current year when stepping up from empty on year).
  • Alt + ArrowDown from input opens calendar and moves focus into the grid (or focuses grid if already open).
  • Open via click keeps focus on the input (pointer-friendly / WCAG-oriented).
  • Grid — arrows (day/week), Home / End (row), PageUp / PageDown (month; Shift for year), Enter / Space to select, Escape closes and returns focus to input.
  • Two-month — horizontal arrows can move between visible months appropriately.

Accessibility

  • Calendar role="dialog" with configurable aria-label.
  • Input shell uses role="group"; FormGroup associates the visible label with the first segment via htmlFor / id.
  • Visual focus: The shell uses :focus-within so the field still shows focus when any inner segment is focused.
  • Input segments use role="spinbutton", matching Arrow Up/Down stepping and numeric min/max.
  • Input segments include:
    • aria-valuemin / aria-valuemax — match spin bounds (day max depends on month/year when known).
    • aria-valuenow — when there is a numeric value; omitted when empty.
    • aria-valuetext — display string (digits or placeholders like MM / DD / YYYY).
    • aria-label — field name (month, day, year).
    • aria-invalid — validation/error state.
    • aria-disabled and tabIndex={-1} when disabled.
  • Grid tied to month heading and per-day accessible names.

Internationalization

  • locale usesIntl.LocalesArgument, defaults to runtime locale but ability to override via locale prop
  • translations for clear button, field labels, and dialog label. default values in English but ability to override via translations prop
  • weekStartsOn uses Intl.Locale#getWeekInfo() (polyfill when needed) but ability to override via weekStartsOn prop
  • Calendar month/year, weekday table headers, placeholder date format (MM/DD/YYYY), date cell aria labels, are automatically localized to the locale via Intl.DateTimeFormat
  • Last month/next month tip text and today button text are automatically localized to the locale via Intl.RelativeTimeFormat

Other

  • inputSize passes through to Input size in the default layout.

Things I know are missing/not completely working:

  • calendar is supposed to close after selecting a date(s)
  • tests
  • calendar quick actions

PR Checklist

Testing Instructions

Don't make me tap the sign.

  1. Go to DatePicker
  2. Play around with DatePicker and make sure all the functionality matches the description above
  3. Do that something in dark mode
  4. Check it with VO
  5. Finish and do a celebratory dance

PR Links and Envs

Repository PR Link
Monolith Monolith PR
Mono Mono PR

@codecademydev
Copy link
Copy Markdown
Collaborator

📬 Published Alpha Packages:

Package Version npm Diff
@codecademy/gamut 68.2.3-alpha.f19d29.0 npm diff
@codecademy/gamut-icons 9.57.3-alpha.f19d29.0 npm diff
@codecademy/gamut-illustrations 0.58.10-alpha.f19d29.0 npm diff
@codecademy/gamut-kit 0.6.593-alpha.f19d29.0 npm diff
@codecademy/gamut-patterns 0.10.29-alpha.f19d29.0 npm diff
@codecademy/gamut-styles 17.13.2-alpha.f19d29.0 npm diff
@codecademy/gamut-tests 5.3.4-alpha.f19d29.0 npm diff
@codecademy/variance 0.26.2-alpha.f19d29.0 npm diff
eslint-plugin-gamut 2.4.4-alpha.f19d29.0 npm diff

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

@aresnik11 aresnik11 marked this pull request as ready for review April 10, 2026 18:02
@aresnik11 aresnik11 requested a review from a team as a code owner April 10, 2026 18:02
bg: 'primary',
color: 'background',
borderRadius: 'md',
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tiny thing but I'm seeing the browser's default :focus-visible style, which both clashes with the hyper color and feels unnecessary since the :focus background color affordance is such a strong focus indicator.

Image
Suggested change
},
},
'&:focus-visible': {
outline: 'none',
},

if (disabled) return;

if (e.altKey && (e.key === 'ArrowDown' || e.key === 'Down')) {
e.preventDefault();
Copy link
Copy Markdown
Member

@sh0ji sh0ji Apr 10, 2026

Choose a reason for hiding this comment

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

In Storybook, Alt + ArrowDown also navigates to the next story so I couldn't navigate to the calendar from the input that way. Adding e.stopPropagation() should fix that.

My first thought that this should be fixed in Storybook, not in the component, but I actually think it makes sense to fix here. This should override any app has a global Alt + ArrowDown bindings when it's directed at the input/segment.

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.

5 participants