Skip to content

Conversation

@ItsJonQ
Copy link

@ItsJonQ ItsJonQ commented Oct 7, 2020

This update reverts 2 recent updates on the InputControl component:

#25609
#25753

I've noticed some issues in the BoxControl component in Gutenberg with the updates. In particular...

  • adding new values and pressing Tab to commit
  • drag to update no longer works :(

Screen Shot 2020-10-07 at 12 08 36 PM

cc'ing @stokesman

Looking back at the initial issue (#24460), these updates are attempting to fix some number input entering.

I haven't been able to replicate the issues. Maybe I'm not entering things correctly (to break it) in my keyboard?
I'd love to know more info about this.
On top of reverts, I'd like to add solutions for the issues mentioned in #24460

Updates

Beyond the revert, the changes can be seen here:

https://github.com/WordPress/gutenberg/pull/25913/files#diff-559f6ca9af4f175b7f7d4b1741a7f4d6R66-R83

https://github.com/WordPress/gutenberg/pull/25913/files#diff-559f6ca9af4f175b7f7d4b1741a7f4d6R30-R42

📹 Demo + Walkthrough

I recorded a video demo + walkthrough that showcases the issue and describes the fixes:
https://www.loom.com/share/22b6d3b1b61140e4a667ed61a6858f52

⚛️ Fixing State Handling

The issue was that the inner number input wasn't correctly handling and coordinating values/changes between the UI element (input[type="number"]) and the state that exists within the wrapping RangeControl component.

The solution involved swapping useControlledState for a simple useState + useEffect hook combination for state management and syncing within the inner number InputField.

The solution also involved (re)adding the HTML validation step before "commiting" (firing the onChange) callback. This allows for "invalid" state to be held within the number input before pushing it upwards.

What this means, is that users will be able to type in values like 123, when the min attribute is set to 20.

Previously, it would jump to 20 because typing in the initial character (1) would trigger onChange immediately... which clamps the value and propagates it updates.

This is no longer the case! (Thank goodness)

🗜 Clamping onChange

Secondly, the final value that is sent from the RangeControl onChange callback is clamped. Previously, this was not the case. We also didn't spot this problem due to the above mentioned issue.

How has this been tested?

  • Confirm reverting the commits fixes BoxControl issues and overall drag to update interaction
  • Confirm RangeControl state flow is working

(Note: For drag to update, I think order matters for the onMouseDown callback. This isn't as tricky as the update that adjusts the update mechanism to rely on the blur event)

How to test

  • Run npm run dev
  • Add a Cover block
  • Adjust the height using the input control. This should work.
  • Add a Columns block
  • Interfacing with the RangeControl + accompanying number input should work.

Checklist:

  • My code is tested.
  • [n/a] My code follows the WordPress code style.
  • [n/a] My code follows the accessibility standards.
  • [n/a] My code has proper inline documentation.
  • [n/a] I've included developer documentation if appropriate.
  • [n/a] I've updated all React Native files affected by any refactorings/renamings in this PR.

@ItsJonQ ItsJonQ added [Type] Bug An existing feature does not function as intended [Package] Components /packages/components labels Oct 7, 2020
@ItsJonQ ItsJonQ self-assigned this Oct 7, 2020
@github-actions
Copy link

github-actions bot commented Oct 7, 2020

Size Change: +9.75 kB (0%)

Total Size: 1.19 MB

Filename Size Change
build/a11y/index.js 1.14 kB +1 B
build/annotations/index.js 3.54 kB +22 B (0%)
build/block-directory/index.js 8.55 kB -1 B
build/block-editor/index.js 129 kB +25 B (0%)
build/block-editor/style-rtl.css 10.9 kB -26 B (0%)
build/block-editor/style.css 10.9 kB -25 B (0%)
build/block-library/editor-rtl.css 8.65 kB +6 B (0%)
build/block-library/editor.css 8.65 kB +6 B (0%)
build/block-library/index.js 145 kB +9.79 kB (6%) 🔍
build/blocks/index.js 47.6 kB +54 B (0%)
build/components/index.js 169 kB -249 B (0%)
build/components/style-rtl.css 15.5 kB +50 B (0%)
build/components/style.css 15.5 kB +52 B (0%)
build/compose/index.js 9.43 kB +4 B (0%)
build/core-data/index.js 12.1 kB +12 B (0%)
build/date/index.js 31.9 kB +1 B
build/edit-navigation/index.js 10.6 kB -10 B (0%)
build/edit-post/index.js 306 kB -2 B (0%)
build/edit-post/style-rtl.css 6.29 kB +3 B (0%)
build/edit-post/style.css 6.28 kB +5 B (0%)
build/edit-site/index.js 21 kB +145 B (0%)
build/edit-site/style-rtl.css 3.73 kB +14 B (0%)
build/edit-site/style.css 3.73 kB +12 B (0%)
build/edit-widgets/index.js 21.2 kB -189 B (0%)
build/edit-widgets/style-rtl.css 3.02 kB +19 B (0%)
build/edit-widgets/style.css 3.02 kB +20 B (0%)
build/editor/index.js 45.5 kB -5 B (0%)
build/element/index.js 4.45 kB +5 B (0%)
build/format-library/index.js 7.49 kB +7 B (0%)
build/i18n/index.js 3.54 kB +1 B
build/list-reusable-blocks/index.js 3.02 kB -3 B (0%)
build/media-utils/index.js 5.12 kB -1 B
build/nux/index.js 3.27 kB +2 B (0%)
build/plugins/index.js 2.44 kB +1 B
build/redux-routine/index.js 2.85 kB +1 B
build/rich-text/index.js 13 kB -2 B (0%)
build/shortcode/index.js 1.7 kB +1 B
build/url/index.js 4.06 kB -1 B
build/viewport/index.js 1.74 kB +2 B (0%)
build/warning/index.js 1.14 kB +7 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/api-fetch/index.js 3.35 kB 0 B
build/autop/index.js 2.72 kB 0 B
build/blob/index.js 668 B 0 B
build/block-directory/style-rtl.css 943 B 0 B
build/block-directory/style.css 942 B 0 B
build/block-library/style-rtl.css 7.66 kB 0 B
build/block-library/style.css 7.65 kB 0 B
build/block-library/theme-rtl.css 741 B 0 B
build/block-library/theme.css 741 B 0 B
build/block-serialization-default-parser/index.js 1.78 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/data-controls/index.js 685 B 0 B
build/data/index.js 8.6 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 4.42 kB 0 B
build/edit-navigation/style-rtl.css 868 B 0 B
build/edit-navigation/style.css 871 B 0 B
build/editor/editor-styles-rtl.css 492 B 0 B
build/editor/editor-styles.css 493 B 0 B
build/editor/style-rtl.css 3.85 kB 0 B
build/editor/style.css 3.84 kB 0 B
build/escape-html/index.js 734 B 0 B
build/format-library/style-rtl.css 547 B 0 B
build/format-library/style.css 548 B 0 B
build/hooks/index.js 1.74 kB 0 B
build/html-entities/index.js 621 B 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keyboard-shortcuts/index.js 2.39 kB 0 B
build/keycodes/index.js 1.85 kB 0 B
build/list-reusable-blocks/style-rtl.css 476 B 0 B
build/list-reusable-blocks/style.css 476 B 0 B
build/notices/index.js 1.69 kB 0 B
build/nux/style-rtl.css 671 B 0 B
build/nux/style.css 668 B 0 B
build/primitives/index.js 1.34 kB 0 B
build/priority-queue/index.js 790 B 0 B
build/server-side-render/index.js 2.6 kB 0 B
build/token-list/index.js 1.24 kB 0 B
build/wordcount/index.js 1.17 kB 0 B

compressed-size-action

@stokesman
Copy link
Contributor

stokesman commented Oct 7, 2020

EDITED: @ItsJonQ, I should have caught the drag to update breakage. Avoiding that would be easy enough but you're right about the commit on blur being trickier. I'd made a PR thinking a fix was simple but I was overlooking the 'parse unit on change' action of UnitControl. I'll probably take another look cause I think it can't be too tricky.

To reproduce the issue from #24460 it's probably easiest in Storybook where you can set the range control to have a min/max of something like what the Spacer Block used to have (20-500). Or try typing a negative value, anything out of range should trigger it. Also, I'm pretty sure this was the code that used to keep the validation from interfering with input.

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

@stokesman Thank you! I'll take a look at RangeControl. I left a comment in your draft PR as well :)

Update: Amazing! That RangeControl example with the min/max was exactly what I needed for testing.

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

Update: I believe the issue is isolated within RangeControl (as you noted @stokesman )

I recorded a GIF demo below:

range-control-input-issue

The inputs are using a min/max of 20 and 400.
There are 3 "number" inputs:

  1. HTML Native input
  2. NumberControl
  3. NumberControl within RangeControl

The HTML input and unwrapped NumberControl work as expected.
That is, entering the number 1 is accepted.

However... attempting to enter 1 in the RangeControl input jumps to 20.


With all that said! Will see if I can come up with a solution.

If not, I think it's probably better to merge the revert as it'll fix the inputs in other areas


P.S. @stokesman Just wanted to thank you for your care and patience with this one! It's tricky. I appreciate you diving in and helping 🙏


Update: If I replace the NumberInput within RangeControl with a regular HTML input, it suffers the same problem. I think the tricky thing here is the cross input value syncing/transforming logic. I'm still working through it... but having (hopefully) isolated the problem, maybe it can help 😂

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

Update: @stokesman I think I got it!!

Will push update + record a demo very soon

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

Okay! I think this is ready.


Added the content below the original Github PR description to make it easier to find.


Beyond the revert, the changes can be seen here:

https://github.com/WordPress/gutenberg/pull/25913/files#diff-559f6ca9af4f175b7f7d4b1741a7f4d6R66-R83

https://github.com/WordPress/gutenberg/pull/25913/files#diff-559f6ca9af4f175b7f7d4b1741a7f4d6R30-R42

📹 Demo + Walkthrough

I recorded a video demo + walkthrough that showcases the issue and describes the fixes:
https://www.loom.com/share/22b6d3b1b61140e4a667ed61a6858f52

⚛️ Fixing State Handling

The issue was that the inner number input wasn't correctly handling and coordinating values/changes between the UI element (input[type="number"]) and the state that exists within the wrapping RangeControl component.

The solution involved swapping useControlledState for a simple useState + useEffect hook combination for state management and syncing within the inner number InputField.

The solution also involved (re)adding the HTML validation step before "commiting" (firing the onChange) callback. This allows for "invalid" state to be held within the number input before pushing it upwards.

What this means, is that users will be able to type in values like 123, when the min attribute is set to 20.

Previously, it would jump to 20 because typing in the initial character (1) would trigger onChange immediately... which clamps the value and propagates it updates.

This is no longer the case! (Thank goodness)

🗜 Clamping onChange

Secondly, the final value that is sent from the RangeControl onChange callback is clamped. Previously, this was not the case. We also didn't spot this problem due to the above mentioned issue.

@stokesman
Copy link
Contributor

I took this for a spin to look for a couple of tangential and obscure issues I thought may reappear. In doing so I ran across a new one which I'll describe first. In WordPress, while editing a spacer block, clear the number input, and then try typing any number besides '1'. It'll give you a '1' anyway. This seems to be unique to the Spacer block as I couldn't reproduce it in Storybook.

One of the obscure issues I mentioned is also with the Spacer block and can be reproduced by clearing the number input then dragging the in-canvas resize handle. The input won't update once the drag ends and after that direct input is not possible (the range input slider can recover it though).

Screen recording of both of the above issues

It could be that both of those are related to the reset that can be triggered by onChange and the fact that the Spacer block doesn't handle it well. It errors with: "Warning: Received NaN for the max attribute. If this is expected, cast the value to a string." but script execution continues.

See recording

There also seems to be some new issues with resets when clearing number inputs of range controls with the Gallery and Button blocks, but I'm going to take a break for now.

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

@stokesman Amazing! Thank you so much. This is really helpful 🙏

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 8, 2020

@stokesman Haii! I just pushed a fix. I think this does it 🤞 .
I've written inline comments to describe the flow.

68aa804#diff-559f6ca9af4f175b7f7d4b1741a7f4d6R72

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 9, 2020

Unrelated update!

This PR was tremendously helpful for me.

🔦 Spotlighting

I appreciated it as it highlights some of the trickiness that happens with seemingly simple input controls. It's also helped me rethink and refine approaches for improving how state could be handled within this controls (as a single unit), and how it could be shared (across multiple units, like a slider and an input).

✨ Updates

I've applied some of the ideas from this PR (and some of the explorations from @stokesman ) into the TextInput component within the G2 Components project.

Here's a quick gif demo that shows the use cases (mentioned above) working with a mixture of input interactions, and state flowing from internal and external changes:

Screen Capture on 2020-10-08 at 19-59-02

Link to live example

The most exciting thing (for me) is that how simple the UI code is in combining a Slider control and a TextInput control together. It looks like this:

const CombinedControl = ({ max, min, onChange, value }) => {
	return (
		<Grid>
			<Slider max={max} min={min} onChange={onChange} value={value} />
			<TextInput
				max={max}
				min={min}
				onChange={onChange}
				type="number"
				value={value}
			/>
		</Grid>
	);
};

♻️ State Management

The biggest difference between how the UI works in the above example and the current RangeControl component.. is that the Slider and TextInput are separate components. They can be composed together with a simple wrapper.

Whereas the current RangeControl does this.. kind awkward wrangling of an input[type="range"] with an accompanying number input.

The strategy for controlling state flow is also different.

The Gutenberg InputControl relies on a reducer pattern.

The G2 TextInput uses a pubsub like pattern that's similar to that of React Recoil. This state management system is powered by Zustand, which is used in libraries like react-spring (makes of Zustand!).

If you're curious, here's the state management for TextInput component:
https://github.com/ItsJonQ/g2/blob/master/packages/components/src/TextInput/useTextInput.js#L42


This update is totally not related to this PR's change. However, I thought it may be useful in shedding some light in how we may be able to improve the stability, usability, reliability, and performance for our base components. Hopefully this will come with the adoption of G2 Components, either in it's spirit, it's parts, or it's totality (🙏 )


P.S. The TextInput currently does not accommodate a "unit" parsing feature. Will need to figure out a clean way to extend it to support this (and potentially other middleware-like enhancements in the future)

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

Great work here @ItsJonQ 💯 Thanks!

I've left some comments but nothing serious :)

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 9, 2020

@ntsekouras Thank you so much! I applied the changes.

Once tests are green and happy, I can merge it up!

Copy link
Contributor

@ntsekouras ntsekouras left a comment

Choose a reason for hiding this comment

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

Looks good! Green CI and land this 💯

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 9, 2020

Something's up with the E2E tests.

Previously, Admin 2 failed (due to a timeout).
I ran the tests again.
Now Admin 2 is successful, but Admin 3 failed.

I'm going to re-run them one more time 🤞

@stokesman
Copy link
Contributor

I'd like it if we can hold off merging this for just a bit for two reasons. I did retry the PR yesterday after 68aa804 and still found some issues (that may not need to considered in-scope of this PR). But the larger reason is I've tinkered a bit and found some non-reverting changes that fix this (and offer a small unlocked improvement to UnitControl) just need to clean up and put it in a PR.

@stokesman
Copy link
Contributor

I've updated my previous PR #25933. It could do with maybe a little more description or screen recordings but the changes are pretty mild.

@ItsJonQ, I don't know, but I think you might like the UnitControl update there even if we decide to go with this PR.

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 9, 2020

@stokesman Interesting! I took a quick look at your PR. I didn't dive in too closely as it was still a draft PR. I'd be happy to give it a spin if you think it's ready!

I did retry the PR yesterday after 68aa804 and still found some issues (that may not need to considered in-scope of this PR)

Oh dear! Do you mind sharing what those issues may be? I'd mostly like to know improve my awareness of edge/test cases 😊

@stokesman
Copy link
Contributor

@ItsJonQ, I think besides a comment I forgot to finish updating or other nitpicks that PR is good to go.

I was also meaning to update you on the issues I mentioned which was actually just the previous one I brought up wasn't fully solved. That is the number input of the range control for the Spacer block could still fail to update if the in-canvas resizer was dragged once the input had been cleared. It's so obscure and I think existed before #24460 so I'd not insist it matters for this PR.

The only other thing I can think of at the moment is tangential because it also existed before #24460 and it's more to do with the reset behavior and may be the responsibility of consumers to fix in their implementations. I can add some more detail in case you'd like.

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 13, 2020

Update: I tested out the PR from @stokesman :
#25933

It looks good! We can proceed with that one instead of this revert PR.
We can revisit things if issues persist :)

@ItsJonQ
Copy link
Author

ItsJonQ commented Oct 14, 2020

Closing this in favour of #25933

@ItsJonQ ItsJonQ closed this Oct 14, 2020
@aristath aristath deleted the fix/input-control-updates branch November 10, 2020 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Components /packages/components [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants