Skip to content

feat(DateRangePicker): add trigger: 'button' | 'input' prop#103

Merged
kyleknighted merged 5 commits into
mainfrom
devin/1777475873-date-picker-input-trigger
Apr 29, 2026
Merged

feat(DateRangePicker): add trigger: 'button' | 'input' prop#103
kyleknighted merged 5 commits into
mainfrom
devin/1777475873-date-picker-input-trigger

Conversation

@kyleknighted

@kyleknighted kyleknighted commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a trigger prop to DateRangePicker that lets callers choose between the existing outline-button trigger (default) and a new input-shaped trigger that lines up with adjacent form inputs.

  • trigger="button" (default) — unchanged. Compact outline <Button> with a leading CalendarIcon and the formatted label. Uses <PopoverTrigger asChild> as before.
  • trigger="input" — readonly Radix <TextField.Root> with a leading <TextField.Slot> that contains <CalendarIcon />. Styling (radius, border, focus ring, surface color, sizing) is inherited from @radix-ui/themes so it lines up perfectly with adjacent TextField.Root inputs.

Applies to both mode="single" and mode="range".

How the input variant drives the popover

<PopoverTrigger asChild> forwards onClick through TextField.Root onto the inner <input> only, which means clicks on the slot icon wouldn't open the popover and Enter / Space on a readonly input doesn't dispatch a click. To work around that, the input variant uses <PopoverAnchor asChild> (for positioning) plus manual event wiring:

  • onClick on the <TextField.Root> and the <TextField.Slot> → both call onOpenRequest()
  • onKeyDown on the input → Enter / Space / ArrowDown open the popover (with preventDefault so Space doesn't scroll the page)
  • aria-haspopup="dialog" + aria-expanded={isOpen} keep assistive tech in sync
  • Escape + outside-click closure is handled natively by <PopoverContent> — no change there

The parent (RangeCalendar / SingleCalendar) already owns isPopoverOpen for its own scroll-to-close logic, so threading isOpen + onOpenRequest through DateTrigger is zero extra state.

Shared part

All trigger logic lives in lib/compound/DateRangePicker/parts/DateTrigger.tsx. Both modes delegate to it; the types add one prop (trigger?: 'button' | 'input') on the shared IDateRangePickerCommonProps.

Verified in Storybook

  • Click input text → popover opens
  • Click calendar icon (slot) → popover opens
  • Keyboard focus + Enter → popover opens
  • Escape → popover closes
  • Date selection + Apply → updates the displayed value in the input (single + range modes)

New stories

  • InputTrigger / InputTriggerEmpty — range mode, filled + empty
  • SingleInputTrigger / SingleInputTriggerEmpty — single mode, filled + empty
  • InputTriggerDisabled — disabled state
  • InputTriggerOpenPopover / SingleInputTriggerOpenPopover — play-function stories for Chromatic snapshot parity

Review & Testing Checklist for Human

  • Approve the new Chromatic snapshots for the 7 new input-trigger stories.
  • Confirm the button-trigger (default) stories are visually identical — no pixel change expected.
  • Eyeball an input-variant picker next to a real TextField.Root in your theme to confirm they line up (both use size 2 / surface variant).
  • Keyboard pass: Tab onto the input trigger, hit Enter / Space — popover should open; Escape should close.

Notes

  • trigger defaults to 'button', so no existing callers need changes.
  • The input trigger is full-width by default (inherits Radix TextField layout); stories wrap it in a 320px container for deterministic Chromatic snapshots. Consumers constrain it with their own layout.

Link to Devin session: https://app.devin.ai/sessions/926c5fa4fe8a4df6997be146cb57a014
Requested by: @kyleknighted

Default remains the existing outline-button trigger. The new `trigger="input"` variant renders a full-width <button> visually styled to match Radix `TextField.Root` (surface, size 2) with a leading CalendarIcon slot, so the picker lines up with adjacent form inputs. Applies to both single and range modes.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor
Original prompt from Kyle

In the Issues sidebar we have a date picker, can we use the DateRangePicker as a single date selector or can it updated/duplicated to provide this functionality?

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Comment on lines +46 to +52
<span className={styles.inputTriggerLabel}>{label}</span>
) : (
<span
className={`${styles.inputTriggerLabel} ${styles.inputTriggerPlaceholder}`}
>
{placeholder}
</span>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could we use Radix Input here in readonly mode or something?

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.

Done — reworked in bb07b54 to use TextField.Root (readonly) with a leading TextField.Slot for the calendar icon. Deleted the .inputTrigger* CSS entirely — styling now comes from Radix directly.

The trick was to stop using PopoverTrigger asChild (which forwards onClick onto the inner <input> only, so clicks on the slot icon wouldn't open the popover and Enter/Space on a readonly input doesn't dispatch a click). Instead the input variant uses PopoverAnchor asChild + manual wiring:

  • onClick on TextField.Root + on TextField.Slot → both open the popover
  • onKeyDown handles Enter / Space / ArrowDown → opens the popover (with preventDefault so Space doesn't scroll)
  • aria-haspopup="dialog" + aria-expanded={isOpen} keep AT in sync with the popover state
  • Escape / outside-click closure is unchanged (handled natively by PopoverContent)

Verified all three open-paths (click input text, click calendar icon slot, Enter key) + Escape close + date selection round-trip in Storybook. New CI snapshots on the way.

kyleknighted and others added 2 commits April 29, 2026 15:34
…nput variant

Per review feedback on #103, use a real Radix `TextField.Root` (readonly) with a leading `TextField.Slot` for the input trigger, so theme/size/variant styling is inherited from Radix instead of re-implemented with custom CSS. Drives the popover via `PopoverAnchor` + manual `onClick` / `onKeyDown` (Enter, Space, ArrowDown) handlers so both the input text area and the slot icon open the popover, and keyboard focus + activation work natively. `aria-expanded` + `aria-haspopup="dialog"` on the input keep AT in sync. Verified in Storybook: click-input, click-slot, and Enter all open; Escape closes; selection + Apply round-trip updates the value.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… validity

axe flagged `aria-expanded` as an invalid ARIA attribute on a plain `<input>`. Adding `role="combobox"` makes `aria-expanded` valid (combobox is the correct semantics for a readonly input that opens a popup), and `aria-controls={popoverId}` ties it to the PopoverContent for AT. PopoverContent gains a matching `id` generated with React.useId() in both single and range modes.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@kyleknighted kyleknighted marked this pull request as ready for review April 29, 2026 16:25
Copilot AI review requested due to automatic review settings April 29, 2026 16:25
devin-ai-integration[bot]

This comment was marked as resolved.

Radix's controlled Popover does not fire `onOpenChange` when the `open` prop is mutated externally. The input trigger path calls `setIsPopoverOpen(true)` directly (via `PopoverAnchor` + manual event handlers), so the today/draftDate reset logic was never running for that variant \u2014 the popover would show stale `today` and stale draft selection from the previous open.

Extracts a shared `handleOpenPopover` helper and invokes it from both code paths in RangeCalendar and SingleCalendar.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This comment was marked as resolved.

Addresses Copilot review feedback on #103:

1. Click/Enter/Space on the input trigger now toggle the popover open/closed (matching PopoverTrigger's behavior on the button variant) rather than always opening. ArrowDown still only opens.

2. Calendar-icon slot click stops propagation so the toggle doesn't fire twice (once from the slot, once bubbling to TextField.Root).

3. Renames the prop from onOpenRequest to onToggleRequest(nextOpen) to match the new contract; doc comment updated.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@kyleknighted kyleknighted merged commit 205ffd3 into main Apr 29, 2026
9 checks passed
@kyleknighted kyleknighted deleted the devin/1777475873-date-picker-input-trigger branch April 29, 2026 16:44
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