Skip to content

feat(DateField): add stepSnapping support#2640

Merged
zernonia merged 7 commits into
unovue:v2from
yan-ad:feat/date-field-step-snapping
Jun 10, 2026
Merged

feat(DateField): add stepSnapping support#2640
zernonia merged 7 commits into
unovue:v2from
yan-ad:feat/date-field-step-snapping

Conversation

@yan-ad

@yan-ad yan-ad commented May 12, 2026

Copy link
Copy Markdown
Contributor

🔗 Linked issue

Resolves #2614

❓ Type of change

  • 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Adds stepSnapping support to DateFieldRoot when rendering time segments, matching the existing TimeFieldRoot behavior.

  • Exposes a stepSnapping prop on DateFieldRoot, defaulting to false
  • Passes stepSnapping through the date field context into useDateField
  • Runs the existing focus-out snapping behavior for DateField time segments
  • Documents the new stepSnapping prop in DateFieldRoot API metadata
  • Adds regression coverage for snapped and non-snapped typed minute values

📸 Screenshots (if appropriate)

N/A

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Summary by CodeRabbit

  • New Features

    • Added a stepSnapping option for date/time inputs to optionally snap typed minutes, hours, or seconds to the nearest configured step (off by default).
  • Documentation

    • Updated DateField docs to describe the new stepSnapping prop, its type, and default value.
  • Tests

    • Added tests covering snapping behavior for enabled and disabled modes, including boundary, equidistant, and non-default step scenarios.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new boolean prop stepSnapping to DateFieldRoot (default false), provides it via root context, forwards it to DateFieldInput/useDateField, extends segment focus-out handling, adds tests for snapping behavior, and updates documentation.

Changes

stepSnapping Feature

Layer / File(s) Summary
stepSnapping prop contract and context
packages/core/src/DateField/DateFieldRoot.vue
DateFieldRoot defines stepSnapping?: boolean (default false), adds it to the root context type, extracts it from props, and provides it via provideDateFieldRootContext.
DateFieldInput integration and focus handler
packages/core/src/DateField/DateFieldInput.vue
DateFieldInput reads stepSnapping from root context, passes it to useDateField, and extends non-literal segment focusout to call handleSegmentFocusOut() after setting hasLeftFocus.
stepSnapping behavior tests
packages/core/src/DateField/DateField.test.ts
New describe('stepSnapping') suite verifies snapping for stepSnapping: true with default and custom minute/hour/second steps, and verifies no snapping when stepSnapping: false.
stepSnapping prop documentation
docs/content/meta/DateFieldRoot.md
Adds stepSnapping to the PropsTable data and to the rendered **Props** markdown table (type boolean, default false).

Sequence Diagram(s)

sequenceDiagram
  participant DateFieldRoot
  participant DateFieldInput
  participant useDateField
  DateFieldRoot->>DateFieldInput: provide stepSnapping (Ref<boolean>)
  DateFieldInput->>useDateField: pass stepSnapping option when calling useDateField
  DateFieldInput->>DateFieldInput: on segment focusout -> handleSegmentFocusOut()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • unovue/reka-ui#2421: Both PRs implement the same "stepSnapping" plumbing and related focus/handler changes for field components.

Poem

🐰 I nibble at minutes, rounding them just right,
A gentle hop from root to input in the night,
When focus drifts away I tug the seconds tight,
Hours and minutes snap into tidy rows,
A little rabbit hums as the timefield grows.

🚥 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 accurately summarizes the primary change: adding stepSnapping support to DateField, matching the main objective described in the PR summary.
Linked Issues check ✅ Passed All coding requirements from #2614 are met: stepSnapping prop exposed on DateFieldRoot, prop passed through context, snapping behavior implemented, and test coverage provided.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing stepSnapping support as specified in #2614; no out-of-scope modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@pkg-pr-new

pkg-pr-new Bot commented May 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 987fa2c

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
packages/core/src/DateField/DateField.test.ts (1)

678-736: ⚡ Quick win

Consider adding explicit focus-out trigger and edge-case coverage.

The tests verify the core snapping behavior, but they don't explicitly trigger focusout after typing. While the model value update might implicitly handle this, explicitly blurring the field would make the test flow clearer and more closely match real user interaction.

Additionally, consider adding edge-case tests:

  • Values already on step boundaries (0, 15, 30, 45) - should remain unchanged
  • Boundary values that wrap (e.g., 59 → 0 of next hour when step is 15)
  • Snapping with different step values (e.g., step: { minute: 10 })
🧪 Example: Explicit focusout trigger
 const minute = getByTestId('minute')
 await user.click(minute)
 await user.keyboard('{2}{3}')
+await minute.blur() // Explicitly trigger focusout

 expect(minute).toHaveTextContent('30')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/DateField/DateField.test.ts` around lines 678 - 736, The
tests in DateField.test.ts for stepSnapping should explicitly trigger a
focus-out after typing and add edge-case specs; update the two existing it
blocks (the ones using setup, getByTestId('minute'), user.click and
user.keyboard) to perform an explicit blur/focusout on the minute field (e.g.,
call user.tab() or fireEvent.blur(minute) after typing) so snapping runs as on
real focus loss, and add new tests that assert: values already on step
boundaries remain unchanged, boundary wrap cases (e.g., typing 59 snaps to 00 of
next hour for step: { minute: 15 }), and a variant using a different step (e.g.,
step: { minute: 10 }) to verify different snapping intervals.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/DateField/DateField.test.ts`:
- Around line 678-736: The tests in DateField.test.ts for stepSnapping should
explicitly trigger a focus-out after typing and add edge-case specs; update the
two existing it blocks (the ones using setup, getByTestId('minute'), user.click
and user.keyboard) to perform an explicit blur/focusout on the minute field
(e.g., call user.tab() or fireEvent.blur(minute) after typing) so snapping runs
as on real focus loss, and add new tests that assert: values already on step
boundaries remain unchanged, boundary wrap cases (e.g., typing 59 snaps to 00 of
next hour for step: { minute: 15 }), and a variant using a different step (e.g.,
step: { minute: 10 }) to verify different snapping intervals.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 552cfa2d-99d0-4588-976f-19da68ffe351

📥 Commits

Reviewing files that changed from the base of the PR and between edbeb93 and a2815d6.

📒 Files selected for processing (4)
  • docs/content/meta/DateFieldRoot.md
  • packages/core/src/DateField/DateField.test.ts
  • packages/core/src/DateField/DateFieldInput.vue
  • packages/core/src/DateField/DateFieldRoot.vue

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (2)
packages/core/src/DateField/DateField.test.ts (2)

678-828: ⚡ Quick win

Consider extracting shared setup to reduce duplication.

Each of the 5 tests in this suite contains nearly identical setup code (~18 lines), differing only in the step.minute value and stepSnapping boolean. This duplication makes maintenance more challenging if the setup pattern needs to change.

Consider extracting a helper function like:

function setupStepSnappingTest(options: { step: number, stepSnapping: boolean }) {
  // shared setup logic
}

This would reduce the ~90 lines of duplicated code while maintaining test clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/DateField/DateField.test.ts` around lines 678 - 828, Tests
in the stepSnapping suite repeat the same setup block across five cases; extract
a helper (e.g., setupStepSnappingTest) that wraps the existing setup(...) call
and accepts parameters for step.minute and stepSnapping, return { user,
getByTestId, rerender } so each test calls setupStepSnappingTest({ step: 15,
stepSnapping: true }) (or other values) instead of duplicating the setup; update
each test to use the helper and keep the existing assertions (references: setup,
rerender, getByTestId, CalendarDateTime).

678-828: ⚖️ Poor tradeoff

Consider additional edge case coverage.

The current test suite provides solid coverage for minute segment snapping. Consider adding tests for:

  • Equidistant values (e.g., '07' with step 15, which is 7 minutes from both 0 and 15)
  • Edge case step: 1 (no effective snapping)
  • Non-divisor steps (e.g., step: 7 where 60 % 7 ≠ 0)
  • Hour and second segments if they also support snapping

These additions would strengthen confidence in edge case handling, though the current coverage validates the core functionality well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/DateField/DateField.test.ts` around lines 678 - 828, Add
new test cases inside the existing describe('stepSnapping') suite to cover the
suggested edge cases: create tests that use setup(...) and the same rerender
emit pattern to assert (1) equidistant snapping e.g., type '{0}{7}' into
getByTestId('minute') with step: { minute: 15 } and verify expected
deterministic snap, (2) step: { minute: 1 } to confirm no snapping, (3)
non-divisor step like step: { minute: 7 } to assert correct nearest-step
behavior, and (4) analogous tests for getByTestId('hour') and
getByTestId('second') if those segments support stepSnapping; reuse the existing
test structure (user.click, user.keyboard, user.click to blur,
expect(...).toHaveTextContent(...)) and the same
modelValue/granularity/step/stepSnapping props so tests are consistent with the
current rerender handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/core/src/DateField/DateField.test.ts`:
- Around line 678-828: Tests in the stepSnapping suite repeat the same setup
block across five cases; extract a helper (e.g., setupStepSnappingTest) that
wraps the existing setup(...) call and accepts parameters for step.minute and
stepSnapping, return { user, getByTestId, rerender } so each test calls
setupStepSnappingTest({ step: 15, stepSnapping: true }) (or other values)
instead of duplicating the setup; update each test to use the helper and keep
the existing assertions (references: setup, rerender, getByTestId,
CalendarDateTime).
- Around line 678-828: Add new test cases inside the existing
describe('stepSnapping') suite to cover the suggested edge cases: create tests
that use setup(...) and the same rerender emit pattern to assert (1) equidistant
snapping e.g., type '{0}{7}' into getByTestId('minute') with step: { minute: 15
} and verify expected deterministic snap, (2) step: { minute: 1 } to confirm no
snapping, (3) non-divisor step like step: { minute: 7 } to assert correct
nearest-step behavior, and (4) analogous tests for getByTestId('hour') and
getByTestId('second') if those segments support stepSnapping; reuse the existing
test structure (user.click, user.keyboard, user.click to blur,
expect(...).toHaveTextContent(...)) and the same
modelValue/granularity/step/stepSnapping props so tests are consistent with the
current rerender handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37792278-dbec-4d72-be73-0903c2759709

📥 Commits

Reviewing files that changed from the base of the PR and between a2815d6 and 643b444.

📒 Files selected for processing (1)
  • packages/core/src/DateField/DateField.test.ts

@yan-ad yan-ad force-pushed the feat/date-field-step-snapping branch from f13c9e8 to 8ae2621 Compare May 12, 2026 16:55

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/DateField/DateField.test.ts (1)

678-980: ⚡ Quick win

Consider adding truly equidistant minute test case.

The test suite includes an equidistant test for hours (line 889: value 10 with step 4, where 8 and 12 are both distance 2), which expects rounding up to 12. However, there's no equivalent truly equidistant test for minutes to confirm the rounding strategy is consistent across all time segments.

With step: { minute: 10 }, typing 25 would be equidistant between 20 and 30 (both at distance 5). Adding this test would verify that the rounding behavior matches the hour implementation (presumably rounding up to 30).

🧪 Proposed test case
it('snaps truly equidistant minute value up to the nearest step', async () => {
  const { user, getByTestId, rerender } = setup({
    dateFieldProps: {
      modelValue: new CalendarDateTime(1980, 1, 20, 12, 0, 0, 0),
      granularity: 'second',
      step: { minute: 10 },
      stepSnapping: true,
    },
    emits: {
      'onUpdate:modelValue': (data: DateValue) => {
        return rerender({
          dateFieldProps: {
            modelValue: data,
            granularity: 'second',
            step: { minute: 10 },
            stepSnapping: true,
          },
        })
      },
    },
  })

  const minute = getByTestId('minute')
  await user.click(minute)
  await user.keyboard('{2}{5}')
  await user.click(getByTestId('second'))

  expect(minute).toHaveTextContent('30')
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/DateField/DateField.test.ts` around lines 678 - 980, Add a
new equidistant minute test to the existing "stepSnapping" suite: create a test
(similar to the hour equidistant test) that uses setup with step: { minute: 10 }
and stepSnapping: true, clicks getByTestId('minute'), types '{2}{5}', clicks
getByTestId('second') and asserts the minute field text becomes '30'. Place it
alongside the other tests in DateField.test.ts so the behavior of minute
rounding matches the hour equidistant test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/DateField/DateField.test.ts`:
- Around line 799-827: Update the test description string for the spec in the
`it('snaps equidistant minute value down to the nearest deterministic step',
...)` test to accurately describe the behavior being asserted: it verifies
rounding to the nearest step when the entered minute (07) is closer to the lower
step (00) than the next step (15). Keep the test body and expectations
unchanged; only modify the human-readable description to something like "rounds
minute down to nearest step when closer to lower bound" so it correctly reflects
the scenario tested.

---

Nitpick comments:
In `@packages/core/src/DateField/DateField.test.ts`:
- Around line 678-980: Add a new equidistant minute test to the existing
"stepSnapping" suite: create a test (similar to the hour equidistant test) that
uses setup with step: { minute: 10 } and stepSnapping: true, clicks
getByTestId('minute'), types '{2}{5}', clicks getByTestId('second') and asserts
the minute field text becomes '30'. Place it alongside the other tests in
DateField.test.ts so the behavior of minute rounding matches the hour
equidistant test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a1b9294-8456-4ba2-b93b-08bb2c643d19

📥 Commits

Reviewing files that changed from the base of the PR and between 643b444 and f13c9e8.

📒 Files selected for processing (1)
  • packages/core/src/DateField/DateField.test.ts

Comment thread packages/core/src/DateField/DateField.test.ts Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/DateField/DateField.test.ts`:
- Around line 745-757: The test "snaps typed minute value using custom step" is
missing its closing bracket/paren before the next test declaration; close that
it(...) block by adding the required closing characters (e.g., ");" or "});")
immediately after the expect(minute).toHaveTextContent('30') line so the next
test "snaps typed minute value down to the nearest step" starts as a separate
block; target the it(...) block with the name 'snaps typed minute value using
custom step' to locate where to insert the closing tokens.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a408858-a8fe-4886-b080-e64e1a6401d9

📥 Commits

Reviewing files that changed from the base of the PR and between f13c9e8 and a3e3d8f.

📒 Files selected for processing (1)
  • packages/core/src/DateField/DateField.test.ts

Comment thread packages/core/src/DateField/DateField.test.ts
yan-ad and others added 2 commits May 13, 2026 00:00
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@zernonia

Copy link
Copy Markdown
Member

Nice work — clean mirror of the TimeField stepSnapping implementation, and the test coverage is thorough. ✅

One follow-up (non-blocking, out of scope here): stepSnapping isn't propagated to the rest of the date family yet —

  • DatePickerField forwards step to DateFieldRoot but not stepSnapping (DatePickerField.vue)
  • DateRangeFieldInput calls useDateField directly without passing stepSnapping

So DatePicker/DateRangeField users can't enable focus-out snapping yet. Worth a follow-up to keep the family in parity (the same way step propagates everywhere). Happy to merge this as-is.

@zernonia zernonia merged commit 3d32ac9 into unovue:v2 Jun 10, 2026
7 checks passed
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.

[Feature]: Add stepSnapping prop to DateField

2 participants