# SpinButton onChange behavior Largely based on discussion from https://github.com/microsoft/fluentui/issues/5326 -- I think this is pretty much the same proposal that the discussion thread eventually arrived at. ### Intermediate vs. committed values SpinButton can have two types of values: **intermediate** and **committed**. A value is **intermediate** at any of the following times: - User is editing the text field - (possibly) User is spinning via mouse or keyboard The value is **committed** when any of the following happens: - Blur - Enter key press (while editing field) - On each spin *(see discussion later)* The commit phase consists of these steps: 1. Call `onIncrement`, `onDecrement`, or `onValidate` as appropriate 2. Only if the value changed, call `onChange` (if provided) 3. Clear the intermediate value 4. Switch to displaying the committed value: - If uncontrolled, update the stored value and display that - If controlled, display `props.value` (which the user likely updated when `onChange` was called; we do NOT store the value in any other internal state in this case) ### Determining and storing the displayed value Where is the value stored? - Controlled: use `props.value` except while editing - Uncontrolled: ~~`uncontrolledValue` state~~ `value` returned by `useControllableValue` - While editing: `intermediateValue` state (or a better name) What is the initial value? (in order of precedence) 1. `props.value` (no validation) 2. `defaultValue` (no validation) 3. `min` (which defaults to 0) What is the currently displayed value? 1. `intermediateValue` if set 2. `props.value` if provided (controlled) 3. `uncontrolledValue` (uncontrolled) What is the value of `ISpinButton.value`? 1. `props.value` if provided (controlled) 2. `uncontrolledValue` (uncontrolled) 3. Probably should NOT ever reflect `intermediateValue` here Edge cases: - If the user is typing and deletes the value (without committing), don't replace it with the previous value or anything else (be careful with falsy checks) - Possibly: While editing, if the parent re-renders with the *same* `props.value` we should *retain* `intermediateValue` (remain in edit mode). But if it re-renders with a different `props.value`, we should *clear* `intermediateValue`. ### When is `onChange` called? `onChange` should only be called in the commit phase as described above, **not on every keystroke** ~~or spin~~. This is the sequence of events each time `onChange` is called: 1. If value changed, call `onChange` 2. If applicable, clear `intermediateValue` - If controlled, this should cause displayed value to revert to `props.value`, whether or not it's been updated 3. If uncontrolled, update `uncontrolledValue` Details of each case where `onChange` can be called: **Blur** 1. Call `onValidate` 2. `onChange` sequence (see below) **Enter keypress**: same as blur **Arrow button click or spin** 1. If the user was already typing (`intermediateValue` is set): 1. `onBlur` should be triggered, which should call `onValidate` + `onChange` sequence and likely a value update 2. *Wait for the value to be updated* (either setting `uncontrolledValue` state or the parent updating `props.value`) - Will need to think about how to implement this for controlled: must ensure that if the parent updates `props.value` based on `onChange`, we use that new value for the arrow button action - (This handles the case where the user types garbage and starts spinning) 2. On each increment, call `onIncrement`/`onDecrement` followed by `onChange` sequence - ~~*Open question: should we call `onChange` at this point? (maybe not? though that's harder to implement and validate, and different from previous behavior)*~~ 3. ~~*(if we decide not to call every spin)* After spinning stops, `onChange` sequence~~ **Arrow key press or spin** 1. If the user was already typing (`intermediateValue` is set): 1. Call `onValidate` 2. `onChange` sequence 3. *Wait for the value to be updated* (same race condition issue as buttons) 2. Same behavior as with buttons ### Questions - Should we ever validate `props.value`? - I think not, to follow pattern of other components? - Should we validate `defaultValue`? - I think not - Should `onChange` be fired for every incremental value while spinning? - After discussion with David, **yes** we should call it every spin. One reason being because it's not uncommon that on every press, something in the app will react (kind of like with a slider) -- like a font size going up and down - Previous thoughts: - Might prefer not to, but that would be inconsistent with when `onIncrement`/`onDecrement` are called *and* inconsistent with previous behavior. (Also a bit harder to implement.) - The reason it's inconsistent with previous behavior is because we'd only update `ISpinButton.value` at the same times as `onChange` is called. Right now `ISpinButton.value` is updated on every spin. - What should `ISpinButton.value` return? Probably the committed value, but is there any scenario where we ought to provide access to the intermediate value? (besides tests) - Should we make the component fully controllable with just `onIncrement`/`onDecrement`/`onValidate`? - There are likely some people who have it implemented this way today since that was the only way in the past to have a fully controlled component *(verify)*. But it would probably be hard to make both approaches work simultaneously. ### Not supported - Implementing all of increment/decrement/validate using just `onChange`. This would cause the `onChange` signature to be very convoluted (and it would be hard to provide default inc/dec/val handlers). - Calling `onChange` for any other reason besides changes from the user (it shouldn't be called on internal validation or updates of props or state) ### Notes - Release notes - Providing `value` now results in a controlled component (probably wasn't the case before; *this is one reason it's a breaking change*) Tests (see https://github.com/microsoft/fluentui/pull/7577): - It should call onChange with normalized value when input box blurs. - It should call onChange when click increase or decrease button. - It should revert to the original valid value if user inputs some random string.