-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Link Control require user to manually submit any changes #50668
Conversation
Avoid passing a default when default is handled internally. Set default to be an object as that is what we are tracking.
value={ internalControlValue } | ||
settings={ settings } | ||
onChange={ onChange } | ||
onChange={ setInternalSettingValue } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes means that any changes to settings are stored in internal state and not submitted until the user confirms with "Apply".
disabled={ | ||
! valueHasChanges || currentInputIsEmpty | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means the Apply
button will not be active until changes to the internal value are detected.
@@ -2236,7 +2287,7 @@ describe( 'Controlling link title text', () => { | |||
).not.toBeInTheDocument(); | |||
} ); | |||
|
|||
it( 'should reset state on value change', async () => { | |||
it( 'should reset state upon controlled value change', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated change but it helps improve the meaning of the test.
// Todo: bug if the missing dep is introduced. Will need a fix. | ||
// eslint-disable-next-line react-hooks/exhaustive-deps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is unrelated and is caused by the enabling of the relevant eslint rule. Fixing the code to conform causes a bug so I don't want to fix this now.
if ( | ||
currentUrlInputValue !== value?.url || | ||
internalTextInputValue !== value?.title | ||
) { | ||
if ( valueHasChanges ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of testing for individual properties of the value, this uses a shallow comparison to check if the external (controlled) and internal values are different. If there is a difference then the internal value has changed and thus we need to submit those changes.
@@ -241,8 +298,7 @@ function LinkControl( { | |||
}; | |||
|
|||
const resetInternalValues = () => { | |||
setInternalUrlInputValue( value?.url ); | |||
setInternalTextInputValue( value?.title ); | |||
setInternalControlValue( value ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of resetting individual properties, "reset" now just means "sync back to whatever value
was/is"
name: 'Apply', | ||
} ); | ||
|
||
expect( submitButton ).toBeDisabled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there are no changes Submit should be disabled.
await user.click( opensInNewTabToggle ); | ||
|
||
// Check settings are **not** directly submitted | ||
// which would trigger the onChange handler. | ||
expect( mockOnChange ).not.toHaveBeenCalled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously changing a setting automatically "submitted" the value (i.e. calls onChange
). This asserts that this is no longer the case.
expect( submitButton ).toBeEnabled(); | ||
|
||
// Submit the changed setting value using the Apply button | ||
await user.click( submitButton ); | ||
|
||
// Assert the value is updated. | ||
expect( mockOnChange ).toHaveBeenCalledWith( | ||
expect.objectContaining( { | ||
opensInNewTab: true, | ||
} ) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now a user is required to manually submit any changes to the settings.
Size Change: +2.56 kB (0%) Total Size: 1.41 MB
ℹ️ View Unchanged
|
Noting that even if we reject the idea that the |
Me again with the same question: do we really need "Apply" and "Cancel" on a document that has almost infinite undo, saves itself and is versioned? Isn't the problem that the UI has some lacking in transmitting what happes? This "cancel or apply" flow makes sense for destructive or complex to undo operations. But for simple toggles it's a bit weird that no other setting from setting intensive UIs such as the inspector require a "commit" action, they just auto commit and hope for the best. E.g. you don't apply increasing padding. You don't apply Bold on a text. It just happens. But, aside from the complaints above, this PR makes things more unitary from an experience point of view so that's good 👏🏻 |
@draganescu Thanks for review. Looks like I'm technically heading in the right direciton.
I'm going to refer you to the revised UX here. I think any discussion about buttons should continue there 🙇 |
Ideally I think it would be better to change the toggle to a checkbox, but that should definitely be in a different PR. |
// Suggestions may contains "settings" values (e.g. `opensInNewTab`) | ||
// which should not overide any existing settings values set by the | ||
// user. This filters out any settings values from the suggestion. | ||
const nonSettingsChanges = Object.keys( updatedValue ).reduce( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have a block of code very similar to this above too. Would moving it to the hook mean that we can combine them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't require a hook just a function but yes 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually in future this code won't be necessary so we can ignore it for now and clean up post-refactor if it's still around.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works as expected and is an improvement on the current experience. I left a few comments on how we could refactor the code, but nothing that should stop this from being merged.
I agree. It's going to be done in #50947 |
Flaky tests detected in 3e6f682. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/5079002929
|
// If the value prop changes, update the internal state. | ||
useEffect( () => { | ||
setInternalValue( ( prevValue ) => { | ||
if ( value && value !== prevValue ) { | ||
return value; | ||
} | ||
|
||
return prevValue; | ||
} ); | ||
}, [ value ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know if this sync hook is still needed? Now that value is an object; the synchronization can cause the content loss.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So we originally introduced the pattern to fix an issue whereby programmatically updating value
didn't update the UI.
@Mamaduka The example is codified in one of the unit tests. Basically if you create x2 separate and different links in a paragraph block and then click between them the UI will reflect the value of that link. Previous to this sync you would continue to see the previous link's value in the UI even if you clicked on another link.
You won't be able to replicate any more because we implemented another fix which passes a key
prop to the LinkControl in rich text to force a unique component instance per link instance.
I think we could look to remove the sync. Perhaps a PoC PR is a good place to start. I'll spin up. I've never liked this code and I agree is prone to error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, Dave. Let’s continue discussion in the new issue.
What?
Requires users to manually submit any changes to link "settings" (e.g.
Open in new tab
.etc) in the Link UI.Opens in new tab
will not automatically submit the value.Closes #50945
Why?
Since the inception of
<LinkControl>
toggling a setting (e.g.Open in new tab
) meant that the Link UI automatically submitted itself. This was intended functionality but the implementation had bugs.For example when users made a change to the link URL or Text, and then subsequently toggled the
Open in new tab
, the value would get submitted with the change to theopensInNewTab
setting, but without the changes to the URL or Text.This is not the correct behaviour and often resulted in confusion when changes to the URL of Text were not persisted.
We could have changed this so that toggling the toggle submits the entire value including changes to URL and Text, but that seems strange when we have an
Apply
button that implies all changes must be manually submitted. Let's lean on that.This PR changes things so that all changes to settings must be submitted using
Apply
. ClickingCancel
or otherwise opting out will cause the change to the toggle to be discarded.This UX expectation is reinforced by only enabling the
Apply
button when any part of the value has changed.How?
Rework the tracking of the internal (unsubmitted) values to track the entire
value
object and not just theurl
ortitle
fields.The purpose of the internal state is to track any as yet unsubmitted changes to the link. This is in order to allow users to discard their changes at any point.
Therefore we must check whether the internal state now differs from the original
value
passed in. We can safely (and quickly thanks to referential equality) do this with the shallow equality utility as the properties of thevalue
are always shallow.With this in place we can selectively track changes to the intenral link value and only submit when the user is ready. Moreover we can improve the UI to only enable
Apply
when changes have been made thereby providing an improved UX.Testing Instructions
Repeat the steps below in the places Link Control is used:
Steps:
Apply
button is not enabled unless you make changes to that link value in some wayTesting Instructions for Keyboard
Screenshots or screencast
Screen.Capture.on.2023-05-16.at.14-29-54.mp4