feat(DateRangePicker): add trigger: 'button' | 'input' prop#103
Conversation
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>
Original prompt from Kyle
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| <span className={styles.inputTriggerLabel}>{label}</span> | ||
| ) : ( | ||
| <span | ||
| className={`${styles.inputTriggerLabel} ${styles.inputTriggerPlaceholder}`} | ||
| > | ||
| {placeholder} | ||
| </span> |
There was a problem hiding this comment.
Could we use Radix Input here in readonly mode or something?
There was a problem hiding this comment.
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:
onClickonTextField.Root+ onTextField.Slot→ both open the popoveronKeyDownhandles Enter / Space / ArrowDown → opens the popover (withpreventDefaultso 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.
…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>
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>
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>
Summary
Adds a
triggerprop toDateRangePickerthat 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 leadingCalendarIconand 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/themesso it lines up perfectly with adjacentTextField.Rootinputs.Applies to both
mode="single"andmode="range".How the input variant drives the popover
<PopoverTrigger asChild>forwardsonClickthroughTextField.Rootonto 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:onClickon the<TextField.Root>and the<TextField.Slot>→ both callonOpenRequest()onKeyDownon the input → Enter / Space / ArrowDown open the popover (withpreventDefaultso Space doesn't scroll the page)aria-haspopup="dialog"+aria-expanded={isOpen}keep assistive tech in sync<PopoverContent>— no change thereThe parent (
RangeCalendar/SingleCalendar) already ownsisPopoverOpenfor its own scroll-to-close logic, so threadingisOpen+onOpenRequestthroughDateTriggeris 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 sharedIDateRangePickerCommonProps.Verified in Storybook
New stories
InputTrigger/InputTriggerEmpty— range mode, filled + emptySingleInputTrigger/SingleInputTriggerEmpty— single mode, filled + emptyInputTriggerDisabled— disabled stateInputTriggerOpenPopover/SingleInputTriggerOpenPopover— play-function stories for Chromatic snapshot parityReview & Testing Checklist for Human
TextField.Rootin your theme to confirm they line up (both use size 2 / surface variant).Notes
triggerdefaults to'button', so no existing callers need changes.Link to Devin session: https://app.devin.ai/sessions/926c5fa4fe8a4df6997be146cb57a014
Requested by: @kyleknighted