Skip to content

fix: suffix should only be visual, not part of the value#605

Open
dinakars777 wants to merge 6 commits intopmndrs:mainfrom
dinakars777:fix/number-suffix-display-only
Open

fix: suffix should only be visual, not part of the value#605
dinakars777 wants to merge 6 commits intopmndrs:mainfrom
dinakars777:fix/number-suffix-display-only

Conversation

@dinakars777
Copy link
Copy Markdown

@dinakars777 dinakars777 commented Mar 20, 2026

Summary

  • Fixes issue where suffix (e.g., "ms") was being appended to the actual numeric value instead of just being displayed visually
  • The suffix is now only used for display purposes via the format function, while the stored value remains a pure number

Fixes

Fixes #548

Changes

  • Modified normalize function in number-plugin.ts to return just the numeric value (_value) instead of appending the suffix

Testing

  • All existing tests pass (18/18)

Summary by CodeRabbit

  • Bug Fixes
    • String inputs now update in real-time as you type.
    • Expand/collapse animations are smoother and more reliable with dynamic height handling.
    • Number inputs return numeric values (suffixes are preserved separately) and only finalize changes on blur or Enter.

Previously, string inputs only committed changes to the store on blur.
This change makes onUpdate fire on every keystroke for string inputs
only - number inputs retain their original commit behavior (on blur/enter).

Added comment explaining the type !== 'number' guard.

Fixes pmndrs#599
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
leva Ready Ready Preview, Comment Mar 23, 2026 4:58am

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 7eb8b43

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
leva Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

Fixes numeric-suffix handling, makes non-number inputs call onUpdate immediately on change, and replaces timeout-based expand/collapse sizing with a ResizeObserver-driven height management.

Changes

Cohort / File(s) Summary
Release metadata
\.changeset/plenty-spies-provide.md
Adds a changeset marking leva for a patch release with notes describing suffix/persist and folder height fixes.
Number plugin suffix fix
packages/leva/src/plugins/Number/number-plugin.ts
Normalization now returns only the numeric portion; suffix is preserved in settings but no longer appended to the normalized value.
Value input update behavior
packages/leva/src/components/ValueInput/ValueInput.tsx
onChange wrapped to call onChange(value) and to invoke onUpdate(value) immediately for non-number types while preserving blur/Enter-triggered onUpdate for number inputs.
Expand/collapse sizing
packages/leva/src/hooks/useToggle.ts
Replaced timeout/one-shot measurements with a ResizeObserver; uses cached refs, immediate collapse fast-path, explicit height fixes when no transition occurs, and observer cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nudged a suffix loose from its glue,
I nibble updates so strings show through,
I watch the folder grow with careful eyes,
ResizeObserver hums — no more surprise.
— twitch & hop, a tiny bug‑fix chew 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The pull request includes an additional fix to folder height updates via ResizeObserver in useToggle.ts, which appears outside the scope of issue #548. Clarify whether the useToggle.ts changes address a separate linked issue or if they should be split into a separate pull request.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely describes the main fix: that numeric suffixes should be visual only and not part of the stored value.
Linked Issues check ✅ Passed The code changes directly address issue #548 by ensuring suffixes remain purely visual while numeric values are stored without suffix appending.

✏️ 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.

@codesandbox-ci
Copy link
Copy Markdown

codesandbox-ci bot commented Mar 20, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7eb8b43:

Sandbox Source
leva-minimal Configuration
leva-busy Configuration
leva-scroll Configuration
leva-advanced-panels Configuration
leva-ui Configuration
leva-theme Configuration
leva-transient Configuration
leva-plugin-plot Configuration
leva-plugin-bezier Configuration
leva-plugin-spring Configuration
leva-plugin-dates Configuration
leva-custom-plugin Configuration

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
packages/leva/src/components/ValueInput/ValueInput.tsx (1)

87-92: Realtime onUpdate should be opt-in, not applied to all non-number inputs.

At Line 91, every non-number keystroke is now committed. For inputs like color editors (packages/leva/src/plugins/Color/Color.tsx), partial values can fail sanitize and get reverted via useInputSetters, causing edit disruption.

Suggested direction
 export type ValueInputProps = {
   ...
+  liveUpdate?: boolean
 }

 export function ValueInput({
   ...
+  liveUpdate = false,
 }: ValueInputProps) {
   ...
   onChange={update((value) => {
     onChange(value)
-    if (type !== "number") onUpdate(value)
+    if (liveUpdate && type !== 'number') onUpdate(value)
   })}

Then enable liveUpdate only for plain string inputs that are safe to commit per keystroke.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/leva/src/components/ValueInput/ValueInput.tsx` around lines 87 - 92,
The current ValueInput component calls onUpdate on every keystroke for all
non-number inputs, which breaks plugins like Color that rely on sanitized
partial values; change the condition inside the update callback so onUpdate is
invoked only when the input is a plain string and liveUpdate is explicitly
enabled (e.g., replace the `if (type !== "number") onUpdate(value)` logic with a
check like `type === "string" && props.liveUpdate`), leaving number inputs and
other specialized types (Color plugin / useInputSetters consumers) using their
existing commit-on-blur/Enter behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/plenty-spies-provide.md:
- Line 5: Replace the incorrect changeset headline "fix: update string input
value in real-time" with a succinct note that describes the actual fix (the
number suffix persistence bug) and update the body to mention that numeric
suffixes are now persisted across saves/roundtrips; specifically edit the
changeset title line and message in .changeset/plenty-spies-provide.md so it
references the suffix storage/persistence fix and briefly describes the
user-facing impact (e.g., "fix: persist numeric suffix on inputs" and a one-line
summary about suffix persistence being fixed).

In `@packages/leva/src/hooks/useToggle.ts`:
- Around line 113-117: The early return in useToggle (the firstRender.current
branch) prevents the ResizeObserver setup and overflow reset from running, so
initially-open folders never observe descendant size changes and can remain
overflow-clipped; modify the firstRender handling in useToggle.ts so that after
setting firstRender.current = false and calling updateHeight() you do NOT return
early—ensure the subsequent ResizeObserver setup (the observe call that uses
contentRef) and the overflow reset logic still run on first open (or,
alternatively, explicitly call the same observer setup and clear the wrapper
overflow before returning), referencing firstRender, updateHeight, contentRef
and the ResizeObserver/observe call.
- Around line 107-110: When collapsing (when !toggled) measure the element's
current pixel height and apply it as an inline height before transitioning to
'0px' so the CSS height transition can animate; inside useToggle (the block that
currently sets ref.style.height='0px'), read ref.offsetHeight (or
getBoundingClientRect().height), set ref.style.height = `${height}px`, then on
the next frame (requestAnimationFrame or setTimeout 0) set ref.style.height =
'0px' and only after the transition finishes set ref.style.overflow='hidden' and
call fixHeight() as needed—this ensures the element transitions from a concrete
pixel height instead of from height:auto.

In `@packages/leva/src/plugins/Number/number-plugin.ts`:
- Around line 53-54: sanitize() currently appends the suffix (return suffix ? f
+ suffix : f) which causes updates to persist suffixed strings; change sanitize
so it returns a numeric value suitable for storage (no suffix) and move the
suffix-appending logic into a separate formatting step (or add an optional
boolean param like sanitize(value, { forStorage: true })) so normalize() and the
update path store numbers, while a new format/display function handles adding
suffixes for UI only; update calls to sanitize/normalize in number-plugin.ts to
use the storage-mode sanitize or the new formatter where appropriate.

---

Nitpick comments:
In `@packages/leva/src/components/ValueInput/ValueInput.tsx`:
- Around line 87-92: The current ValueInput component calls onUpdate on every
keystroke for all non-number inputs, which breaks plugins like Color that rely
on sanitized partial values; change the condition inside the update callback so
onUpdate is invoked only when the input is a plain string and liveUpdate is
explicitly enabled (e.g., replace the `if (type !== "number") onUpdate(value)`
logic with a check like `type === "string" && props.liveUpdate`), leaving number
inputs and other specialized types (Color plugin / useInputSetters consumers)
using their existing commit-on-blur/Enter behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: addee39e-af6b-42e0-ac70-4cc0a0cd3883

📥 Commits

Reviewing files that changed from the base of the PR and between 402a1e9 and 217bc88.

📒 Files selected for processing (4)
  • .changeset/plenty-spies-provide.md
  • packages/leva/src/components/ValueInput/ValueInput.tsx
  • packages/leva/src/hooks/useToggle.ts
  • packages/leva/src/plugins/Number/number-plugin.ts

- Guard against transitionend not firing when height already matches target
- Update changeset to reflect actual fixes (suffix persistence, folder height)
Copy link
Copy Markdown

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

♻️ Duplicate comments (2)
packages/leva/src/hooks/useToggle.ts (2)

113-118: ⚠️ Potential issue | 🟠 Major

Initially-open folders never observe nested content changes.

The early return at line 117 prevents the ResizeObserver setup (lines 138-141) from running. Per Folder.tsx (lines 60-66), nested Folder components render inside contentRef. When a parent folder starts expanded, toggling nested folders won't update the parent wrapper's height.

Move ResizeObserver setup before the early return or restructure to ensure observation starts on first render:

💡 Proposed fix
+    const resizeObserver = new ResizeObserver(() => {
+      updateHeight()
+    })
+    resizeObserver.observe(contentEl)
+
     // prevents first animation on initial expand
     if (firstRender.current) {
       firstRender.current = false
       updateHeight()
-      return
+      return () => {
+        resizeObserver.disconnect()
+      }
     }

     const fixHeight = () => {
       ref.style.removeProperty('height')
       ref.style.removeProperty('overflow')
       contentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
     }

     ref.addEventListener('transitionend', fixHeight, { once: true })

     const { height } = contentEl.getBoundingClientRect()
     const currentHeight = ref.style.height
     ref.style.height = `${height}px`

     // If no transition will occur, call fixHeight directly
     if (currentHeight === `${height}px`) {
       ref.removeEventListener('transitionend', fixHeight)
       fixHeight()
     }

-    const resizeObserver = new ResizeObserver(() => {
-      updateHeight()
-    })
-    resizeObserver.observe(contentEl)
-
     return () => {
       ref.removeEventListener('transitionend', fixHeight)
       resizeObserver.disconnect()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/leva/src/hooks/useToggle.ts` around lines 113 - 118, The early
return in the useToggle hook using firstRender.current prevents the
ResizeObserver from being attached, so initially-open Folder components never
observe nested content changes; fix by ensuring the ResizeObserver for
contentRef is initialized before returning on first render (or by moving the
ResizeObserver setup out of the post-firstRender branch) so updateHeight() still
runs but observation starts immediately—modify the logic around
firstRender.current, updateHeight(), and the ResizeObserver setup in useToggle
so the observer is registered on first render even when returning early.

107-111: ⚠️ Potential issue | 🟠 Major

Collapse animation breaks after first expand.

After fixHeight clears the inline height (line 121), the wrapper returns to height: auto. On subsequent collapse, setting height: 0px directly from auto won't animate—CSS transitions require a concrete start value.

Measure the current height before collapsing to enable the transition:

💡 Proposed fix
     if (!toggled) {
+      const { height } = contentEl.getBoundingClientRect()
+      ref.style.height = `${height}px`
       ref.style.overflow = 'hidden'
-      ref.style.height = '0px'
+      requestAnimationFrame(() => {
+        ref.style.height = '0px'
+      })
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/leva/src/hooks/useToggle.ts` around lines 107 - 111, The collapse
animation breaks because after fixHeight clears the inline height (making the
wrapper height auto) later collapses set height directly to '0px' which cannot
transition from 'auto'; in useToggle, when toggled becomes false, first read the
element's current computed height (e.g., via getBoundingClientRect().height),
set ref.style.height to that measured px value, force a reflow (read
offsetHeight) and then set ref.style.height = '0px' so the CSS transition
animates; ensure fixHeight still clears the inline height after the transition
ends (or on transitionend) to restore auto.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/leva/src/hooks/useToggle.ts`:
- Around line 113-118: The early return in the useToggle hook using
firstRender.current prevents the ResizeObserver from being attached, so
initially-open Folder components never observe nested content changes; fix by
ensuring the ResizeObserver for contentRef is initialized before returning on
first render (or by moving the ResizeObserver setup out of the post-firstRender
branch) so updateHeight() still runs but observation starts immediately—modify
the logic around firstRender.current, updateHeight(), and the ResizeObserver
setup in useToggle so the observer is registered on first render even when
returning early.
- Around line 107-111: The collapse animation breaks because after fixHeight
clears the inline height (making the wrapper height auto) later collapses set
height directly to '0px' which cannot transition from 'auto'; in useToggle, when
toggled becomes false, first read the element's current computed height (e.g.,
via getBoundingClientRect().height), set ref.style.height to that measured px
value, force a reflow (read offsetHeight) and then set ref.style.height = '0px'
so the CSS transition animates; ensure fixHeight still clears the inline height
after the transition ends (or on transitionend) to restore auto.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc73a8c5-8c8b-47be-b47b-64255eb5944e

📥 Commits

Reviewing files that changed from the base of the PR and between 217bc88 and cf24206.

📒 Files selected for processing (2)
  • .changeset/plenty-spies-provide.md
  • packages/leva/src/hooks/useToggle.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/plenty-spies-provide.md

@dinakars777
Copy link
Copy Markdown
Author

Thanks for the review! Updated changeset to reflect actual fixes: number suffix persistence and folder height dynamic updates. Resolved.

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.

Suffix will get added to numerical input

1 participant