Add buffered NumberInput for decimal-friendly width editing#1919
Merged
Conversation
Width/thickness fields in the style dialogs bound a controlled input
directly to a parsed number, which destroyed intermediate editing states
("1." collapsed to "1" because Number("1.") === 1). Replace all five
numeric fields (border width, line thickness, opacity) with a new
NumberInput component that maintains a local string buffer during
editing and canonicalises on blur. Also set step=0.5 on width/thickness
fields so steppers move in useful increments.
Exclude onBlur from NumberInputProps so callers can't silently replace the buffer reformat, use the typed getByLabelText generic instead of a cast, and drop the attribute-passthrough test that asserted wiring rather than behavior.
The previous parseNumberSafely(buffer) !== value guard was intended to distinguish own-edit echoes from external resets, but it could match incorrectly when useDeferredValue batched the transition. Replace with an explicit editing flag (React state, not a ref — the compiler can't track refs during render) that is set true in onChange and cleared after resync. When editing is true, the buffer is preserved (keeps "1." and "" intact during typing); when false, any value prop change forces a buffer rewrite (handles Reset to Default).
The editing-state flag got stuck when an edit did not change the parsed value (e.g. "1.40" → still 1.4), causing subsequent external resets to be ignored. Replace with reportedValue — the last parsed value sent upward. On resync, compare value against reportedValue: match means our own echo (keep buffer); mismatch means external change (force buffer). Add regression tests for the stuck-flag case, external non-default updates, and rapid sequential edits.
Both prior sync mechanisms (value-echo guard, then reportedValue comparison) tracked prop history to decide when to overwrite the local buffer, and both had states where an external reset was misread as an edit echo, leaving the field stale after Reset to Default. Remove the machinery entirely: a draft string exists only while the user is actively editing (set on change, cleared on blur). Whenever no draft is active the display derives straight from the value prop, so any external change — reset, style load, another tab — shows unconditionally. Clicking Reset blurs the input, which ends the draft.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Editing width values in the style dialogs was awkward: the fields bound a controlled input directly to a parsed number, so every keystroke round-tripped through
Number()→String(), collapsing intermediate editing states ("1."became"1", destroying the decimal point mid-edit). The steppers also moved by whole units, which is a big jump in Cytoscape.This adds a
NumberInputcomponent using a focused-draft pattern: while the user is editing, a raw draft string owns the display (so"1."and""survive); on blur the draft is discarded and the display derives directly from the value prop. Because there is no persistent local buffer, external changes — Reset to Default, loading a styles file, another tab — always show without any sync logic. All five numeric fields across the node and edge style dialogs now use it, and the three width/thickness fields getstep={0.5}(opacity keepsstep={0.1}).How to read
draftstate and a??is the whole mechanismValidation
useDeferredValue, which the style hooks use), edits that don't change the parsed value, and blur canonicalisation.1.4no longer drops the decimal point, clearing takes one delete, steppers move by 0.5, and Reset to Default restores the fields.pnpm checksandpnpm test(2078 tests) pass.Related Issues
Check List
pnpm checkspasses with no errors.pnpm testpasses with no failures.