Skip to content

feat(TimeRangeField): Implement TimeRangeField Component#2206

Merged
zernonia merged 8 commits into
v2from
feat-2182-time-range-field
Mar 3, 2026
Merged

feat(TimeRangeField): Implement TimeRangeField Component#2206
zernonia merged 8 commits into
v2from
feat-2182-time-range-field

Conversation

@epr3
Copy link
Copy Markdown
Collaborator

@epr3 epr3 commented Oct 5, 2025

Closes #2182

Summary by CodeRabbit

  • New Features

    • Added a Time Range Field for selecting start/end times with keyboard navigation, accessibility, locale/hour‑cycle support, configurable granularity/step, min/max validation, disabled/readonly states, and programmatic focus control.
  • Documentation

    • Published comprehensive docs, demos (CSS & Tailwind), examples, API reference, and usage guides.
  • Tests

    • Added accessibility and keyboard/interaction tests.
  • Stories

    • Added Storybook examples showcasing variants, validation, granularity, and locale/timezone scenarios.

@epr3 epr3 self-assigned this Oct 5, 2025
@epr3 epr3 added the v2 label Oct 5, 2025
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Oct 5, 2025

Open in StackBlitz

npm i https://pkg.pr.new/reka-ui@2206

commit: 108cc76

@zernonia
Copy link
Copy Markdown
Member

@epr3 will merge this to the next minor release, alongside with another possible component 🤞🏻

@epr3
Copy link
Copy Markdown
Collaborator Author

epr3 commented Oct 21, 2025

Sounds good! 👍

@edimitchel
Copy link
Copy Markdown
Contributor

edimitchel commented Jan 14, 2026

This would be a really cool feature to publish @zernonia ;)

@edimitchel
Copy link
Copy Markdown
Contributor

@epr3 can you please update code to update the pkg-pr-new package

@epr3 epr3 force-pushed the feat-2182-time-range-field branch from cf88efc to 6e7c671 Compare January 24, 2026 16:22
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03775c4 and 108cc76.

📒 Files selected for processing (5)
  • docs/.vitepress/config.ts
  • docs/components/demo/TimeRangeField/css/index.vue
  • docs/components/demo/TimeRangeField/css/styles.css
  • docs/components/demo/TimeRangeField/tailwind/index.vue
  • docs/content/docs/components/time-range-field.md
✅ Files skipped from review due to trivial changes (1)
  • docs/content/docs/components/time-range-field.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/.vitepress/config.ts
  • docs/components/demo/TimeRangeField/css/styles.css
  • docs/components/demo/TimeRangeField/css/index.vue
  • docs/components/demo/TimeRangeField/tailwind/index.vue

📝 Walkthrough

Walkthrough

Adds a TimeRangeField feature: new Root and Input components with context API, locale-aware formatting, granularity, keyboard navigation, validation, types/exports, tests, stories, demos, docs, and site navigation entry.

Changes

Cohort / File(s) Summary
Docs & Site
docs/.vitepress/config.ts, docs/content/docs/components/time-range-field.md, docs/content/meta/TimeRangeFieldRoot.md, docs/content/meta/TimeRangeFieldInput.md, time-range-field.md
Added documentation pages, meta files, and sidebar entry for Time Range Field.
Demos (CSS & Tailwind)
docs/components/demo/TimeRangeField/css/index.vue, docs/components/demo/TimeRangeField/css/styles.css, docs/components/demo/TimeRangeField/tailwind/index.vue, docs/components/demo/TimeRangeField/tailwind/tailwind.config.js
New demo implementations and styling (CSS + Tailwind) illustrating segment rendering and styles.
Core Components
packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue, packages/core/src/TimeRangeField/TimeRangeFieldInput.vue, packages/core/src/TimeRangeField/index.ts
Introduced TimeRangeFieldRoot (props, emits, context API, formatter, segments, validation, nav, expose API) and TimeRangeFieldInput (segment input, keyboard/focus handling); added re-exports.
Types & Exports
packages/core/src/shared/date/types.ts, packages/core/constant/components.ts, packages/core/src/index.ts
Added TimeRange type, added timeRangeField to public components map, and exported TimeRangeField from core index.
Stories & Story Helpers
packages/core/src/TimeRangeField/story/...
.../TimeRangeFieldDefault.story.vue, .../TimeRangeFieldChromatic.story.vue, .../TimeRangeFieldGranular.story.vue, .../TimeRangeFieldValidation.story.vue, .../_TimeRangeField.vue, .../_DummyTimeRangeField.vue
Added Storybook stories and helper components covering default, controlled/uncontrolled, granularity, validation, locale, and chromatic variants.
Tests
packages/core/src/TimeRangeField/TimeRangeField.test.ts
New test suite: accessibility (axe), population from Time/CalendarDateTime/ZonedDateTime, keyboard navigation, and two-way binding.
Related Cleanup
packages/core/src/DateRangeField/DateRangeFieldRoot.vue, packages/core/src/TimeField/TimeFieldRoot.vue
Removed unused imports; DateRangeFieldRoot added a typed slot declaration.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Input as TimeRangeFieldInput
    participant Root as TimeRangeFieldRoot
    participant Formatter as DateFormatter
    participant Context as RootContext

    User->>Input: focus / click segment
    Input->>Root: request focus / emit interaction
    Root->>Context: update focused element / segment state
    Root->>Formatter: format segment value (locale, hourCycle)
    Formatter-->>Root: formatted segment parts
    Root->>Input: provide segment display & attributes
    Input-->>User: render segment (aria, value)

    User->>Input: type value / press Arrow
    Input->>Root: emit update:modelValue or navigation event
    Root->>Root: validate (min/max/unavailable, order)
    Root-->>Input: update segment state or move focus
    Input-->>User: update UI and focus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through hours, minutes, and more,
Segments aligned from start to end door,
Keys nudged the focus, locales hummed the tune,
Root handed parts, Input danced under the moon,
A tiny rabbit cheers — time blossoms soon! ⏰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: implementing a TimeRangeField component, which directly aligns with the primary objective from the linked issue #2182.
Linked Issues check ✅ Passed The PR implements a TimeRangeField component analogous to DateRangeField for Time values, fully meeting the requirements specified in issue #2182.
Out of Scope Changes check ✅ Passed All changes directly support the TimeRangeField implementation: component logic, tests, documentation, configuration, exports, and types. No unrelated modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-2182-time-range-field

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

🤖 Fix all issues with AI agents
In `@docs/components/demo/TimeRangeField/css/styles.css`:
- Around line 17-28: Remove the redundant border-width declaration in the
.TimeField rule: the existing border-width: 1px; is immediately overridden by
the border: 1px solid var(--gray-9); shorthand, so delete the border-width line
from the .TimeField CSS (locate the .TimeField selector in styles.css).
- Around line 54-55: The selector `.TimeFieldSegment:[aria-valuetext='Empty']`
is invalid CSS; remove the extraneous colon so the class and attribute selector
are combined (i.e., place the attribute selector immediately after
`.TimeFieldSegment`) to target elements with class TimeFieldSegment that have
aria-valuetext="Empty". Update the rule for `.TimeFieldSegment` +
`[aria-valuetext='Empty']` accordingly so the color declaration applies
correctly.

In `@docs/components/demo/TimeRangeField/tailwind/index.vue`:
- Around line 20-22: The v-for loops over segments.start and segments.end use
:key="item.part" which can collide for multiple 'literal' parts; change both
loops (the one iterating over segments.start and the one over segments.end) to
include the loop index (e.g., (item, idx) in segments.start / segments.end) and
build a stable unique key combining item.part and the index (such as
`${item.part}-${idx}`) so each rendered fragment has a unique key and Vue
warnings are eliminated.

In `@docs/content/docs/components/time-range-field.md`:
- Around line 675-700: The example uses Vue's watch but doesn't import it;
update the <script setup> imports to include watch (alongside ref) so the
watcher on timeRange works—locate the import line that brings in ref (and
possibly Time) and add watch to that import list to ensure the watch(timeRange,
...) call is defined.
- Around line 161-169: The examples use the Time constructor but do not import
it; add an import for Time from 'reka-ui' alongside TimeRangeFieldInput and
TimeRangeFieldRoot in the <script setup> blocks (e.g., import { Time,
TimeRangeFieldInput, TimeRangeFieldRoot } from 'reka-ui') so new Time(9, 0) and
other occurrences resolve; apply the same import addition to all subsequent
examples in this file that call new Time(...).

In `@docs/content/meta/TimeRangeFieldInput.md`:
- Around line 18-21: Update the 'part' field's description to refer to "time"
instead of "date" in the TimeRangeFieldInput metadata: change the description
string for 'part' (currently "<p>The part of the date to render</p>\n") to use
"time" ("<p>The part of the time to render</p>\n"); if this file is generated,
update the source template that produces the 'part' description so future
regenerations use "time" not "date".

In `@docs/content/meta/TimeRangeFieldRoot.md`:
- Around line 18-105: The docs for TimeRangeFieldRoot contain date/calendar
wording and a redundant granularity sentence; update the generator templates
(source templates/generator) that produce the descriptions for properties like
defaultPlaceholder, defaultValue, granularity, placeholder (and any other
TimeValue/TimeRange fields) to use time-specific wording (e.g., "time" instead
of "date/calendar") and rewrite the granularity description to a single clear
sentence explaining that it controls the displayed time segments up to the
specified unit (hour|minute|second) and the default behavior; ensure the
generator emits the corrected descriptions for 'defaultPlaceholder',
'defaultValue', 'granularity', and 'placeholder'.

In `@packages/core/src/DateRangeField/DateRangeFieldRoot.vue`:
- Around line 114-123: Update the JSDoc in the defineSlots call for
DateRangeFieldRoot to replace references to "time" with "date" or "date range"
so the slot description correctly reflects the component; specifically update
the comment for modelValue (currently "The current time of the field") to "The
current date or date range of the field" and adjust the segments comment
(currently "The time field segment contents") to "The date field segment
contents" while keeping the existing props names (modelValue, segments,
isInvalid) and types unchanged.

In `@packages/core/src/TimeRangeField/story/_DummyTimeRangeField.vue`:
- Around line 17-20: The v-for over segments.start uses item.part as the key
which can repeat (e.g., multiple "literal" segments); update the keying in both
the start and end loops (the template iterating segments.start and the
corresponding template for segments.end around lines 38-41) to use a stable
unique key such as the loop index or a composite key combining item.part with
the index (e.g., `${item.part}-${index}`) to avoid duplicate keys and render
instability.

In `@packages/core/src/TimeRangeField/story/_TimeRangeField.vue`:
- Around line 42-46: Remove the invalid disabled attribute from the <span>
element (the element with data-testid="value" that renders {{ modelValue }});
keep tabindex="-1" to prevent focus and, if you need to convey
non-interactive/disabled semantics, add aria-disabled="true" to that same span
instead of disabled.
- Around line 23-27: The v-for uses non-unique keys (item.part) in
TimeRangeFieldInput which can repeat (e.g., multiple 'literal' segments); update
the v-for to include the iteration index (e.g., v-for="(item, idx) in
segments.start") and change the :key to a composite value using the part and
index (e.g., `${item.part}-${idx}`) to guarantee uniqueness; apply the same
change for the corresponding segments.end v-for loop so both TimeRangeFieldInput
lists use unique keys.

In `@packages/core/src/TimeRangeField/story/TimeRangeFieldDefault.story.vue`:
- Around line 13-16: The v-for templates use :key="item.part" which can
duplicate for repeated literal segments; update both v-for blocks (the template
iterating over segments.start and the template iterating over segments.end) to
use a unique key such as the loop index or a composite key (e.g.,
`${item.part}-${index}`) instead of item.part so each rendered segment has a
stable, unique key.

In `@packages/core/src/TimeRangeField/TimeRangeField.test.ts`:
- Line 49: The describe block for 'timeField' is incorrectly declared async;
remove the async modifier from the describe callback (the describe('timeField',
async () => { ... }) declaration) so that only the individual it/test callbacks
handle async operations—update the declaration to describe('timeField', () => {
... }) and ensure no awaits remain in that describe body.
- Around line 70-82: The test for ZonedDateTime is flaky due to locale-dependent
formatting; update the setup call in this spec to pin the locale and hour cycle
by passing locale: 'en-US' and hourCycle: 'h12' in the props (e.g., include
locale: 'en-US' and hourCycle: 'h12' alongside timeRangeFieldProps: {
modelValue: zonedDateTime }) so the AM/PM and EST expectations (and the 12-hour
end.hour calculation) are stable across environments.

In `@packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue`:
- Around line 419-437: The hourCycle is passed into
provideTimeRangeFieldRootContext as a static value (props.hourCycle) and won't
react to parent updates; change the argument to pass a reactive ref/computed
instead (e.g., computed(() => props.hourCycle) or ref(props.hourCycle)) or
update the context type to accept Ref<HourCycle|undefined> and provide
props.hourCycle as a ref so children consuming hourCycle from
provideTimeRangeFieldRootContext will update when the prop changes.

In `@time-range-field.md`:
- Around line 304-318: The helper function isTimeRangeValid is defined but never
used; either remove the unused function or wire it into the example validation
flow by invoking isTimeRangeValid wherever time-range checks occur (e.g., in the
validation handler or function that validates timeRange objects) and ensure it
relies on isTimeUnavailable for business-hours checks; update any callers or
docs to reference isTimeRangeValid (or delete the function and remove any
related references to avoid dead code).
🧹 Nitpick comments (5)
packages/core/src/TimeRangeField/story/_TimeRangeField.vue (1)

2-7: Align onUpdate:modelValue type with range value

update:modelValue for a range should use the range type, not a single TimeValue. This keeps story/test typing aligned with the component API.

✅ Suggested fix
-import type { TimeValue } from '@/shared/date'
...
-const props = defineProps<{ timeRangeFieldProps?: TimeRangeFieldRootProps, emits?: { 'onUpdate:modelValue'?: (data: TimeValue) => void } }>()
+const props = defineProps<{ timeRangeFieldProps?: TimeRangeFieldRootProps, emits?: { 'onUpdate:modelValue'?: (data: TimeRangeFieldRootProps['modelValue']) => void } }>()
packages/core/src/TimeRangeField/story/TimeRangeFieldChromatic.story.vue (1)

53-55: Timezone example variant doesn't demonstrate timezone functionality.

This variant is identical to the "Placeholder" variant and doesn't actually showcase timezone handling. Consider either removing it or adding a ZonedDateTime value to properly demonstrate timezone behavior.

packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue (3)

463-473: VisuallyHidden input value may display "undefined - undefined".

When both start and end are undefined, the hidden input's value will be the string "undefined - undefined". Consider providing a fallback empty string for better form handling.

Proposed fix
     <VisuallyHidden
       :id="id"
       as="input"
       feature="focusable"
       tabindex="-1"
-      :value="`${modelValue?.start?.toString()} - ${modelValue?.end?.toString()}`"
+      :value="modelValue?.start && modelValue?.end ? `${modelValue.start.toString()} - ${modelValue.end.toString()}` : ''"
       :name="name"
       :disabled="disabled"
       :required="required"

234-242: Computed setters return values unnecessarily.

The return newValue statements in the computed setters for convertedStartValue, convertedEndValue, and convertedPlaceholder are unnecessary. Vue computed setters don't use return values.

Proposed fix for convertedStartValue (apply same pattern to others)
   set(newValue) {
     if (newValue) {
       startValue.value = startValue.value && 'day' in startValue.value ? newValue : new Time(newValue.hour, newValue.minute, newValue.second, startValue.value?.millisecond)
     }
     else {
       startValue.value = newValue
     }
-    return newValue
   },

Also applies to: 251-259, 278-282


326-340: Duplicate watchers on convertedModelValue.

There are two separate watch calls on convertedModelValue (lines 326-340 and 364-367). These could be consolidated into a single watcher for clarity and to avoid potential ordering issues.

Proposed consolidation
 watch(convertedModelValue, (_modelValue) => {
   const isStartChanged = _modelValue?.start && convertedStartValue.value
     ? _modelValue.start.compare(convertedStartValue.value) !== 0
     : _modelValue?.start !== convertedStartValue.value
   if (isStartChanged) {
     convertedStartValue.value = _modelValue?.start?.copy()
   }

   const isEndChanged = _modelValue?.end && convertedEndValue.value
     ? _modelValue.end.compare(convertedEndValue.value) !== 0
     : _modelValue?.end !== convertedEndValue.value
   if (isEndChanged) {
     convertedEndValue.value = _modelValue?.end?.copy()
   }
+
+  // Sync placeholder with start value
+  if (_modelValue && _modelValue.start !== undefined && placeholder.value.compare(_modelValue.start) !== 0)
+    placeholder.value = _modelValue.start.copy()
 })
-
-watch(convertedModelValue, (_modelValue) => {
-  if (_modelValue && _modelValue.start !== undefined && placeholder.value.compare(_modelValue.start) !== 0)
-    placeholder.value = _modelValue.start.copy()
-})

Also applies to: 364-367

Comment thread docs/components/demo/TimeRangeField/css/styles.css
Comment thread docs/components/demo/TimeRangeField/css/styles.css Outdated
Comment thread docs/components/demo/TimeRangeField/tailwind/index.vue Outdated
Comment thread docs/content/docs/components/time-range-field.md
Comment thread docs/content/docs/components/time-range-field.md
Comment thread packages/core/src/TimeRangeField/TimeRangeField.test.ts Outdated
Comment thread packages/core/src/TimeRangeField/TimeRangeField.test.ts
Comment thread packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue
Comment thread time-range-field.md
@epr3 epr3 force-pushed the feat-2182-time-range-field branch from 6e7c671 to 03775c4 Compare February 11, 2026 18:29
@github-actions github-actions Bot requested a deployment to reka-ui (Production) February 11, 2026 18:31 Abandoned
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@docs/components/demo/TimeRangeField/css/styles.css`:
- Line 24: Replace the long hex color in the background-color declaration in
styles.css (the background-color: `#ffffff`; line) with the shorthand form `#fff` to
satisfy the color-hex-length lint rule; update the background-color property
value to use `#fff` wherever that exact declaration appears in TimeRangeField's
styles.

In `@packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue`:
- Around line 148-150: The locale-change watcher is calling getSegmentElements
(which selects [data-reka-date-field-segment]) but the mounted code and
TimeRangeField expect time-field segments; update the watcher to call
getTimeFieldSegmentElements instead so it repopulates segmentElements with the
correct [data-reka-time-field-segment] nodes. Locate the locale watcher that
currently uses getSegmentElements and replace that call with
getTimeFieldSegmentElements, ensuring it still iterates and adds items into the
segmentElements Set the same way as the onMounted block.

In `@time-range-field.md`:
- Line 172: The v-for in the template iterates over segments.start using
:key="item.part" which can produce duplicate keys (e.g., repeated "literal"
parts); update the template's v-for to capture the index (template v-for="(item,
index) in segments.start") and make the key unique by combining part and index
(use a template string like `${item.part}-${index}`) so each rendered segment
(in the template with v-for, segments.start, and item.part) has a stable unique
key.
- Line 62: The prop description in TimeRangeField for the granularity prop is
redundant (it currently says "Defaults to minute if a Time is provided,
otherwise defaults to minute"); update the granularity description in
time-range-field.md (the TimeRangeField prop docs) to a single clear default
statement such as "Defaults to minute." so it no longer repeats the same branch;
ensure the prop name "granularity" and the component name "TimeRangeField"
remain referenced correctly in the updated text.
🧹 Nitpick comments (3)
packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue (3)

326-340: Two separate watchers on convertedModelValue — consider merging.

There are two watch(convertedModelValue, ...) callbacks (lines 326 and 364). This splits related synchronization logic across two watchers on the same source, making the execution order harder to reason about. Consider merging them into a single watcher.

Also applies to: 364-367


292-292: initialSegments is not reactive to granularity changes.

initialSegments is computed once from inferredGranularity.value at initialization time. If granularity prop changes dynamically, the reset logic in the watchers (lines 348 and 375) will use stale segment shapes. Consider making it a computed.

Suggested fix
-const initialSegments = initializeTimeSegmentValues(inferredGranularity.value)
+const initialSegments = computed(() => initializeTimeSegmentValues(inferredGranularity.value))

Then update usages to initialSegments.value:

-    startSegmentValues.value = { ...initialSegments }
+    startSegmentValues.value = { ...initialSegments.value }

136-138: Formatter created with non-reactive locale.

useDateFormatter(locale.value, ...) receives the unwrapped string, so the formatter's internal hourCycle option is fixed at creation time. While the locale watcher (line 352) manually calls formatter.setLocale(value), the hourCycle normalization passed during construction won't be refreshed if hourCycle prop changes. This is consistent with the non-reactive hourCycle issue already flagged but worth noting the formatter is also affected.

Comment thread docs/components/demo/TimeRangeField/css/styles.css Outdated
Comment thread packages/core/src/TimeRangeField/TimeRangeFieldRoot.vue
Comment thread time-range-field.md
Comment thread time-range-field.md
@zernonia zernonia merged commit cf31bdd into v2 Mar 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: TimeRangeField component

3 participants