Skip to content
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

Tools Panel: Fix race conditions caused by conditionally displayed ToolsPanelItems #36588

Merged
merged 12 commits into from
Nov 23, 2021

Conversation

stacimc
Copy link
Contributor

@stacimc stacimc commented Nov 17, 2021

Description

Problem

In #36540 it was discovered that attempting to conditionally display a ToolsPanelItem causes unreliable panel behavior such as controls suddenly disappearing. This happens both when:

  • An entire ToolsPanelItem is rendered conditionally, meaning that it is registered/deregistered based on some condition:
{ !! height && (
    <ToolsPanelItem ... 
  • A ToolsPanelItem is shown by default conditionally, meaning that depending on some condition it is moved back and forth between the default and optional controls in the Menu:
<ToolsPanelItem
    isShownByDefault={ !! height }
    ....

This behavior is caused by race conditions in setting the state of the panel and menu items in quick sequence.

Fixes in this PR

  • updates state changes to rely on previous state, so that subsequent updates do not overwrite one another
  • fixes an issue with item de-registration, which was de-registering all items except the intended item
  • default controls will now indicate that they do not have a value when the control is cleared

How has this been tested?

Unit tests:
Run npm run test-unit "./packages/components/src/tools-panel/test/index.js"

  • Open the storybook with npm run storybook:dev
  • Under the experimental Components section, check out the two new examples:
    • With Conditional Default Control: this shows a control which is conditionally moved between the default and optional controls
    • With Conditionally Rendered Control: in this example the entire control is conditionally rendered in the panel
  • Play around with the examples, resetting values

Screenshots

conditional-default.mov
conditionally-rendered-control.mov

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • I've tested my changes with keyboard and screen readers.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR (please manually search all *.native.js files for terms that need renaming or removal).

@stacimc stacimc self-assigned this Nov 17, 2021
@github-actions
Copy link

github-actions bot commented Nov 17, 2021

Size Change: +66 B (0%)

Total Size: 1.1 MB

Filename Size Change
build/components/index.min.js 214 kB +66 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 960 B
build/admin-manifest/index.min.js 1.1 kB
build/annotations/index.min.js 2.75 kB
build/api-fetch/index.min.js 2.21 kB
build/autop/index.min.js 2.12 kB
build/blob/index.min.js 459 B
build/block-directory/index.min.js 6.28 kB
build/block-directory/style-rtl.css 1.01 kB
build/block-directory/style.css 1.01 kB
build/block-editor/default-editor-styles-rtl.css 378 B
build/block-editor/default-editor-styles.css 378 B
build/block-editor/index.min.js 139 kB
build/block-editor/style-rtl.css 14.4 kB
build/block-editor/style.css 14.4 kB
build/block-library/blocks/archives/editor-rtl.css 61 B
build/block-library/blocks/archives/editor.css 60 B
build/block-library/blocks/archives/style-rtl.css 65 B
build/block-library/blocks/archives/style.css 65 B
build/block-library/blocks/audio/editor-rtl.css 58 B
build/block-library/blocks/audio/editor.css 58 B
build/block-library/blocks/audio/style-rtl.css 111 B
build/block-library/blocks/audio/style.css 111 B
build/block-library/blocks/audio/theme-rtl.css 125 B
build/block-library/blocks/audio/theme.css 125 B
build/block-library/blocks/block/editor-rtl.css 161 B
build/block-library/blocks/block/editor.css 161 B
build/block-library/blocks/button/editor-rtl.css 470 B
build/block-library/blocks/button/editor.css 470 B
build/block-library/blocks/button/style-rtl.css 560 B
build/block-library/blocks/button/style.css 560 B
build/block-library/blocks/buttons/editor-rtl.css 291 B
build/block-library/blocks/buttons/editor.css 291 B
build/block-library/blocks/buttons/style-rtl.css 275 B
build/block-library/blocks/buttons/style.css 275 B
build/block-library/blocks/calendar/style-rtl.css 207 B
build/block-library/blocks/calendar/style.css 207 B
build/block-library/blocks/categories/editor-rtl.css 84 B
build/block-library/blocks/categories/editor.css 83 B
build/block-library/blocks/categories/style-rtl.css 79 B
build/block-library/blocks/categories/style.css 79 B
build/block-library/blocks/code/style-rtl.css 90 B
build/block-library/blocks/code/style.css 90 B
build/block-library/blocks/code/theme-rtl.css 134 B
build/block-library/blocks/code/theme.css 134 B
build/block-library/blocks/columns/editor-rtl.css 206 B
build/block-library/blocks/columns/editor.css 205 B
build/block-library/blocks/columns/style-rtl.css 503 B
build/block-library/blocks/columns/style.css 502 B
build/block-library/blocks/cover/editor-rtl.css 546 B
build/block-library/blocks/cover/editor.css 547 B
build/block-library/blocks/cover/style-rtl.css 1.19 kB
build/block-library/blocks/cover/style.css 1.19 kB
build/block-library/blocks/embed/editor-rtl.css 488 B
build/block-library/blocks/embed/editor.css 488 B
build/block-library/blocks/embed/style-rtl.css 417 B
build/block-library/blocks/embed/style.css 417 B
build/block-library/blocks/embed/theme-rtl.css 124 B
build/block-library/blocks/embed/theme.css 124 B
build/block-library/blocks/file/editor-rtl.css 300 B
build/block-library/blocks/file/editor.css 300 B
build/block-library/blocks/file/style-rtl.css 255 B
build/block-library/blocks/file/style.css 255 B
build/block-library/blocks/file/view.min.js 322 B
build/block-library/blocks/freeform/editor-rtl.css 2.44 kB
build/block-library/blocks/freeform/editor.css 2.44 kB
build/block-library/blocks/gallery/editor-rtl.css 977 B
build/block-library/blocks/gallery/editor.css 982 B
build/block-library/blocks/gallery/style-rtl.css 1.62 kB
build/block-library/blocks/gallery/style.css 1.62 kB
build/block-library/blocks/gallery/theme-rtl.css 122 B
build/block-library/blocks/gallery/theme.css 122 B
build/block-library/blocks/group/editor-rtl.css 159 B
build/block-library/blocks/group/editor.css 159 B
build/block-library/blocks/group/style-rtl.css 57 B
build/block-library/blocks/group/style.css 57 B
build/block-library/blocks/group/theme-rtl.css 78 B
build/block-library/blocks/group/theme.css 78 B
build/block-library/blocks/heading/style-rtl.css 114 B
build/block-library/blocks/heading/style.css 114 B
build/block-library/blocks/html/editor-rtl.css 332 B
build/block-library/blocks/html/editor.css 333 B
build/block-library/blocks/image/editor-rtl.css 731 B
build/block-library/blocks/image/editor.css 730 B
build/block-library/blocks/image/style-rtl.css 507 B
build/block-library/blocks/image/style.css 511 B
build/block-library/blocks/image/theme-rtl.css 124 B
build/block-library/blocks/image/theme.css 124 B
build/block-library/blocks/latest-comments/style-rtl.css 284 B
build/block-library/blocks/latest-comments/style.css 284 B
build/block-library/blocks/latest-posts/editor-rtl.css 137 B
build/block-library/blocks/latest-posts/editor.css 137 B
build/block-library/blocks/latest-posts/style-rtl.css 528 B
build/block-library/blocks/latest-posts/style.css 527 B
build/block-library/blocks/list/style-rtl.css 94 B
build/block-library/blocks/list/style.css 94 B
build/block-library/blocks/media-text/editor-rtl.css 266 B
build/block-library/blocks/media-text/editor.css 263 B
build/block-library/blocks/media-text/style-rtl.css 493 B
build/block-library/blocks/media-text/style.css 490 B
build/block-library/blocks/more/editor-rtl.css 431 B
build/block-library/blocks/more/editor.css 431 B
build/block-library/blocks/navigation-link/editor-rtl.css 649 B
build/block-library/blocks/navigation-link/editor.css 650 B
build/block-library/blocks/navigation-link/style-rtl.css 94 B
build/block-library/blocks/navigation-link/style.css 94 B
build/block-library/blocks/navigation-submenu/editor-rtl.css 299 B
build/block-library/blocks/navigation-submenu/editor.css 299 B
build/block-library/blocks/navigation-submenu/view.min.js 343 B
build/block-library/blocks/navigation/editor-rtl.css 1.89 kB
build/block-library/blocks/navigation/editor.css 1.89 kB
build/block-library/blocks/navigation/style-rtl.css 1.66 kB
build/block-library/blocks/navigation/style.css 1.65 kB
build/block-library/blocks/navigation/view.min.js 2.74 kB
build/block-library/blocks/nextpage/editor-rtl.css 395 B
build/block-library/blocks/nextpage/editor.css 395 B
build/block-library/blocks/page-list/editor-rtl.css 377 B
build/block-library/blocks/page-list/editor.css 377 B
build/block-library/blocks/page-list/style-rtl.css 172 B
build/block-library/blocks/page-list/style.css 172 B
build/block-library/blocks/paragraph/editor-rtl.css 157 B
build/block-library/blocks/paragraph/editor.css 157 B
build/block-library/blocks/paragraph/style-rtl.css 273 B
build/block-library/blocks/paragraph/style.css 273 B
build/block-library/blocks/post-author/style-rtl.css 175 B
build/block-library/blocks/post-author/style.css 176 B
build/block-library/blocks/post-comments-form/style-rtl.css 444 B
build/block-library/blocks/post-comments-form/style.css 444 B
build/block-library/blocks/post-comments/style-rtl.css 492 B
build/block-library/blocks/post-comments/style.css 493 B
build/block-library/blocks/post-excerpt/editor-rtl.css 73 B
build/block-library/blocks/post-excerpt/editor.css 73 B
build/block-library/blocks/post-excerpt/style-rtl.css 69 B
build/block-library/blocks/post-excerpt/style.css 69 B
build/block-library/blocks/post-featured-image/editor-rtl.css 771 B
build/block-library/blocks/post-featured-image/editor.css 771 B
build/block-library/blocks/post-featured-image/style-rtl.css 153 B
build/block-library/blocks/post-featured-image/style.css 153 B
build/block-library/blocks/post-template/editor-rtl.css 99 B
build/block-library/blocks/post-template/editor.css 98 B
build/block-library/blocks/post-template/style-rtl.css 391 B
build/block-library/blocks/post-template/style.css 392 B
build/block-library/blocks/post-terms/style-rtl.css 73 B
build/block-library/blocks/post-terms/style.css 73 B
build/block-library/blocks/post-title/style-rtl.css 80 B
build/block-library/blocks/post-title/style.css 80 B
build/block-library/blocks/preformatted/style-rtl.css 103 B
build/block-library/blocks/preformatted/style.css 103 B
build/block-library/blocks/pullquote/editor-rtl.css 198 B
build/block-library/blocks/pullquote/editor.css 198 B
build/block-library/blocks/pullquote/style-rtl.css 378 B
build/block-library/blocks/pullquote/style.css 378 B
build/block-library/blocks/pullquote/theme-rtl.css 167 B
build/block-library/blocks/pullquote/theme.css 167 B
build/block-library/blocks/query-pagination-numbers/editor-rtl.css 122 B
build/block-library/blocks/query-pagination-numbers/editor.css 121 B
build/block-library/blocks/query-pagination/editor-rtl.css 262 B
build/block-library/blocks/query-pagination/editor.css 255 B
build/block-library/blocks/query-pagination/style-rtl.css 234 B
build/block-library/blocks/query-pagination/style.css 231 B
build/block-library/blocks/query/editor-rtl.css 131 B
build/block-library/blocks/query/editor.css 132 B
build/block-library/blocks/quote/style-rtl.css 187 B
build/block-library/blocks/quote/style.css 187 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/rss/editor-rtl.css 202 B
build/block-library/blocks/rss/editor.css 204 B
build/block-library/blocks/rss/style-rtl.css 289 B
build/block-library/blocks/rss/style.css 288 B
build/block-library/blocks/search/editor-rtl.css 165 B
build/block-library/blocks/search/editor.css 165 B
build/block-library/blocks/search/style-rtl.css 397 B
build/block-library/blocks/search/style.css 398 B
build/block-library/blocks/search/theme-rtl.css 64 B
build/block-library/blocks/search/theme.css 64 B
build/block-library/blocks/separator/editor-rtl.css 99 B
build/block-library/blocks/separator/editor.css 99 B
build/block-library/blocks/separator/style-rtl.css 245 B
build/block-library/blocks/separator/style.css 245 B
build/block-library/blocks/separator/theme-rtl.css 172 B
build/block-library/blocks/separator/theme.css 172 B
build/block-library/blocks/shortcode/editor-rtl.css 474 B
build/block-library/blocks/shortcode/editor.css 474 B
build/block-library/blocks/site-logo/editor-rtl.css 772 B
build/block-library/blocks/site-logo/editor.css 772 B
build/block-library/blocks/site-logo/style-rtl.css 165 B
build/block-library/blocks/site-logo/style.css 165 B
build/block-library/blocks/site-tagline/editor-rtl.css 86 B
build/block-library/blocks/site-tagline/editor.css 86 B
build/block-library/blocks/site-title/editor-rtl.css 84 B
build/block-library/blocks/site-title/editor.css 84 B
build/block-library/blocks/social-link/editor-rtl.css 177 B
build/block-library/blocks/social-link/editor.css 177 B
build/block-library/blocks/social-links/editor-rtl.css 670 B
build/block-library/blocks/social-links/editor.css 669 B
build/block-library/blocks/social-links/style-rtl.css 1.32 kB
build/block-library/blocks/social-links/style.css 1.32 kB
build/block-library/blocks/spacer/editor-rtl.css 307 B
build/block-library/blocks/spacer/editor.css 307 B
build/block-library/blocks/spacer/style-rtl.css 48 B
build/block-library/blocks/spacer/style.css 48 B
build/block-library/blocks/table/editor-rtl.css 471 B
build/block-library/blocks/table/editor.css 472 B
build/block-library/blocks/table/style-rtl.css 481 B
build/block-library/blocks/table/style.css 481 B
build/block-library/blocks/table/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 146 B
build/block-library/blocks/tag-cloud/style.css 146 B
build/block-library/blocks/template-part/editor-rtl.css 560 B
build/block-library/blocks/template-part/editor.css 559 B
build/block-library/blocks/template-part/theme-rtl.css 101 B
build/block-library/blocks/template-part/theme.css 101 B
build/block-library/blocks/text-columns/editor-rtl.css 95 B
build/block-library/blocks/text-columns/editor.css 95 B
build/block-library/blocks/text-columns/style-rtl.css 166 B
build/block-library/blocks/text-columns/style.css 166 B
build/block-library/blocks/verse/style-rtl.css 87 B
build/block-library/blocks/verse/style.css 87 B
build/block-library/blocks/video/editor-rtl.css 569 B
build/block-library/blocks/video/editor.css 570 B
build/block-library/blocks/video/style-rtl.css 173 B
build/block-library/blocks/video/style.css 173 B
build/block-library/blocks/video/theme-rtl.css 124 B
build/block-library/blocks/video/theme.css 124 B
build/block-library/common-rtl.css 815 B
build/block-library/common.css 812 B
build/block-library/editor-rtl.css 9.85 kB
build/block-library/editor.css 9.86 kB
build/block-library/index.min.js 162 kB
build/block-library/reset-rtl.css 474 B
build/block-library/reset.css 474 B
build/block-library/style-rtl.css 10.4 kB
build/block-library/style.css 10.5 kB
build/block-library/theme-rtl.css 672 B
build/block-library/theme.css 677 B
build/block-serialization-default-parser/index.min.js 1.09 kB
build/block-serialization-spec-parser/index.min.js 2.79 kB
build/blocks/index.min.js 46.3 kB
build/components/style-rtl.css 15.3 kB
build/components/style.css 15.3 kB
build/compose/index.min.js 10.9 kB
build/core-data/index.min.js 13.2 kB
build/customize-widgets/index.min.js 11.4 kB
build/customize-widgets/style-rtl.css 1.5 kB
build/customize-widgets/style.css 1.49 kB
build/data-controls/index.min.js 631 B
build/data/index.min.js 7.47 kB
build/date/index.min.js 31.5 kB
build/deprecated/index.min.js 485 B
build/dom-ready/index.min.js 304 B
build/dom/index.min.js 4.5 kB
build/edit-navigation/index.min.js 16 kB
build/edit-navigation/style-rtl.css 3.76 kB
build/edit-navigation/style.css 3.76 kB
build/edit-post/classic-rtl.css 492 B
build/edit-post/classic.css 494 B
build/edit-post/index.min.js 29.6 kB
build/edit-post/style-rtl.css 7.1 kB
build/edit-post/style.css 7.09 kB
build/edit-site/index.min.js 31.4 kB
build/edit-site/style-rtl.css 6.29 kB
build/edit-site/style.css 6.29 kB
build/edit-widgets/index.min.js 16.5 kB
build/edit-widgets/style-rtl.css 4.18 kB
build/edit-widgets/style.css 4.18 kB
build/editor/index.min.js 37.8 kB
build/editor/style-rtl.css 3.78 kB
build/editor/style.css 3.77 kB
build/element/index.min.js 3.29 kB
build/escape-html/index.min.js 517 B
build/format-library/index.min.js 6.57 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.63 kB
build/html-entities/index.min.js 424 B
build/i18n/index.min.js 3.71 kB
build/is-shallow-equal/index.min.js 501 B
build/keyboard-shortcuts/index.min.js 1.8 kB
build/keycodes/index.min.js 1.39 kB
build/list-reusable-blocks/index.min.js 1.86 kB
build/list-reusable-blocks/style-rtl.css 838 B
build/list-reusable-blocks/style.css 838 B
build/media-utils/index.min.js 2.92 kB
build/notices/index.min.js 925 B
build/nux/index.min.js 2.08 kB
build/nux/style-rtl.css 747 B
build/nux/style.css 743 B
build/plugins/index.min.js 1.84 kB
build/primitives/index.min.js 924 B
build/priority-queue/index.min.js 582 B
build/react-i18n/index.min.js 671 B
build/redux-routine/index.min.js 2.65 kB
build/reusable-blocks/index.min.js 2.22 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11 kB
build/server-side-render/index.min.js 1.57 kB
build/shortcode/index.min.js 1.49 kB
build/token-list/index.min.js 639 B
build/url/index.min.js 1.9 kB
build/viewport/index.min.js 1.05 kB
build/warning/index.min.js 248 B
build/widgets/index.min.js 7.15 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.04 kB

compressed-size-action

@stacimc stacimc marked this pull request as ready for review November 17, 2021 23:37
@stacimc stacimc requested a review from ajitbohra as a code owner November 17, 2021 23:37
@andrewserong andrewserong added [Type] Bug An existing feature does not function as intended [Package] Components /packages/components labels Nov 18, 2021
Copy link
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

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

Nice work diving into this one @stacimc, looks like a particularly tricky issue, and I like the look of all the added callbacks passed to the set functions (and thanks for adding the storybook examples — they're working well for me 👍)

Unfortunately, I think this might introduce another regression, which I could replicate on this branch, but not on trunk where when switching between two similar blocks that both use the same ToolsPanel (in this case Typography), the default control winds up not rendering at all.

To replicate:

  1. Add a couple of paragraph blocks to a post, but don't change any of the Typography settings available
  2. Add another paragraph block, and add in a few more Typography controls and set a value
  3. Select the first paragraph block
  4. The Size control in the Typography panel is not visible (and there appear to be no panels registered)

Here's a gif:

Kapture 2021-11-18 at 11 48 59

I'm not quite sure what would be causing this, but I've added a note in the registerPanelItem function where we're mutating items, just in case that might be related. CC: @aaronrobertshaw and @ciampo in case either of them have a better idea of what might cause it.

I was also wondering if it'd be worth trying to write a unit test for this change, in case that'd help isolate the issue above? (Though, it could be tricky trying to cover the race condition 🤔)

packages/components/src/tools-panel/tools-panel/hook.ts Outdated Show resolved Hide resolved
@stacimc
Copy link
Contributor Author

stacimc commented Nov 18, 2021

Thanks @andrewserong for the testing -- it looks like I may need to make sure I'm keeping track of the panelId. I'll dig in here.

I was also wondering if it'd be worth trying to write a unit test for this change, in case that'd help isolate the issue above? (Though, it could be tricky trying to cover the race condition 🤔)

Definitely agreed!

@aaronrobertshaw aaronrobertshaw self-requested a review November 18, 2021 01:32
@stacimc
Copy link
Contributor Author

stacimc commented Nov 18, 2021

I think I have the issue @andrewserong brought up fixed; I'm definitely a bit shaky on some of the ToolsPanel implementation, so hoping @aaronrobertshaw can take a look here :)

I'll give some more unit tests a go now

Copy link
Contributor

@aaronrobertshaw aaronrobertshaw left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on @stacimc, there are a lot of moving parts to consider!

@andrewserong was too quick for me 🚀

I also ran into the same regression he outlined and got distracted looking into that and tests. We'll definitely want to improve the test coverage here. An entry into the components changelog will also be needed.

A couple of rough tests to start
diff --git a/packages/components/src/tools-panel/test/index.js b/packages/components/src/tools-panel/test/index.js
index 2d3fcf022f..6dab5ba148 100644
--- a/packages/components/src/tools-panel/test/index.js
+++ b/packages/components/src/tools-panel/test/index.js
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, fireEvent, within } from '@testing-library/react';
 
 /**
  * Internal dependencies
@@ -168,6 +168,7 @@ const selectMenuItem = async ( label ) => {
 describe( 'ToolsPanel', () => {
 	afterEach( () => {
 		controlProps.attributes.value = true;
+		altControlProps.attributes.value = false;
 	} );
 
 	describe( 'basic rendering', () => {
@@ -348,6 +349,162 @@ describe( 'ToolsPanel', () => {
 			// there.
 			expect( optionalItem ).not.toBeInTheDocument();
 		} );
+
+		it( 'should render default controls with conditional isShownByDefault', async () => {
+			const linkedControlProps = {
+				attributes: { value: false },
+				hasValue: jest.fn().mockImplementation( () => {
+					return !! linkedControlProps.attributes.value;
+				} ),
+				label: 'Linked',
+				onDeselect: jest.fn(),
+				onSelect: jest.fn(),
+			};
+
+			const { rerender } = render(
+				<ToolsPanel { ...defaultProps }>
+					<ToolsPanelItem
+						{ ...altControlProps }
+						isShownByDefault={ true }
+					>
+						<div>Default control</div>
+					</ToolsPanelItem>
+					<ToolsPanelItem
+						{ ...linkedControlProps }
+						isShownByDefault={ !! altControlProps.attributes.value }
+					>
+						<div>Linked control</div>
+					</ToolsPanelItem>
+				</ToolsPanel>
+			);
+
+			let linkedItem = screen.queryByText( 'Linked control' );
+			expect( linkedItem ).not.toBeInTheDocument();
+
+			// Simulate the main control having a value set which should
+			// trigger the linked control becoming a default control via the
+			// conditional `isShownByDefault` prop.
+			altControlProps.attributes.value = true;
+
+			rerender(
+				<ToolsPanel { ...defaultProps }>
+					<ToolsPanelItem
+						{ ...altControlProps }
+						isShownByDefault={ true }
+					>
+						<div>Default control</div>
+					</ToolsPanelItem>
+					<ToolsPanelItem
+						{ ...linkedControlProps }
+						isShownByDefault={ !! altControlProps.attributes.value }
+					>
+						<div>Linked control</div>
+					</ToolsPanelItem>
+				</ToolsPanel>
+			);
+
+			// The linked control should now be a default control and rendered
+			// despite not having a value.
+			linkedItem = screen.getByText( 'Linked control' );
+			expect( linkedItem ).toBeInTheDocument();
+
+			// The linked control should now appear in the default controls
+			// menu group and have been removed from the optional group.
+			openDropdownMenu();
+			const menuGroups = screen.getAllByRole( 'group' );
+
+			// There should now only be two groups. The default controls and
+			// and the group for the reset all option.
+			expect( menuGroups.length ).toEqual( 2 );
+
+			// The new default control item for the Linked control should be
+			// within the first menu group.
+			const defaultItem = within( menuGroups[ 0 ] ).getByText( 'Linked' );
+			expect( defaultItem ).toBeInTheDocument();
+
+			// Optional controls have an additional aria-label. This can be used
+			// to confirm the conditional default control has been removed from
+			// the optional menu item group.
+			const optionalItem = screen.queryByRole( 'menuitemcheckbox', {
+				name: 'Show Linked',
+			} );
+			expect( optionalItem ).not.toBeInTheDocument();
+		} );
+
+		it( 'should handle conditionally rendered default control', async () => {
+			const conditionalControlProps = {
+				attributes: { value: false },
+				hasValue: jest.fn().mockImplementation( () => {
+					return !! conditionalControlProps.attributes.value;
+				} ),
+				label: 'Conditional',
+				onDeselect: jest.fn(),
+				onSelect: jest.fn(),
+			};
+
+			const { rerender } = render(
+				<ToolsPanel { ...defaultProps }>
+					<ToolsPanelItem
+						{ ...altControlProps }
+						isShownByDefault={ true }
+					>
+						<div>Default control</div>
+					</ToolsPanelItem>
+					{ !! altControlProps.attributes.value && (
+						<ToolsPanelItem
+							{ ...conditionalControlProps }
+							isShownByDefault={ true }
+						>
+							<div>Conditional control</div>
+						</ToolsPanelItem>
+					) }
+				</ToolsPanel>
+			);
+
+			// The conditional control should not yet be rendered.
+			let conditionalItem = screen.queryByText( 'Conditional control' );
+			expect( conditionalItem ).not.toBeInTheDocument();
+
+			// Simulate the main control having a value set which will now
+			// render the new default control into the ToolsPanel.
+			altControlProps.attributes.value = true;
+
+			rerender(
+				<ToolsPanel { ...defaultProps }>
+					<ToolsPanelItem
+						{ ...altControlProps }
+						isShownByDefault={ true }
+					>
+						<div>Default control</div>
+					</ToolsPanelItem>
+					{ !! altControlProps.attributes.value && (
+						<ToolsPanelItem
+							{ ...conditionalControlProps }
+							isShownByDefault={ true }
+						>
+							<div>Conditional control</div>
+						</ToolsPanelItem>
+					) }
+				</ToolsPanel>
+			);
+
+			// The conditional control should now be rendered and included in
+			// the panel's menu.
+			conditionalItem = screen.getByText( 'Conditional control' );
+			expect( conditionalItem ).toBeInTheDocument();
+
+			// The conditional control should now appear in the default controls
+			// menu group.
+			openDropdownMenu();
+			const menuGroups = screen.getAllByRole( 'group' );
+
+			// The new default control item for the Conditional control should
+			// be within the first menu group.
+			const defaultItem = within( menuGroups[ 0 ] ).getByText(
+				'Conditional'
+			);
+			expect( defaultItem ).toBeInTheDocument();
+		} );
 	} );
 
 	describe( 'callbacks on menu item selection', () => {

P.S. Just prior to hitting submit on this I noticed fresh commits. I'll retest shortly.

@aaronrobertshaw
Copy link
Contributor

Checking the panelId before deregistering panel items solves the regression for me.

Nice work @stacimc

@andrewserong
Copy link
Contributor

Checking the panelId before deregistering panel items solves the regression for me.

Me too, that's testing well for me now. Nice find, Staci!

@stacimc
Copy link
Contributor Author

stacimc commented Nov 18, 2021

Added a couple of tests, working on top of the suggestions by @aaronrobertshaw, plus a unit test for the issue @andrewserong found. Confirmed that the test fails without that latest fix and passes now 😄

Copy link
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

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

Thanks for following up with the tests @stacimc! Unfortunately, I think I've found another issue, similar to the previous one, but where switching between the two blocks that share the same Typography control results in losing the value for the font size:

Kapture 2021-11-18 at 15 53 19

I'm wondering if it's possible that the onDeselect is somehow being triggered when switching between blocks?

@aaronrobertshaw
Copy link
Contributor

I'm wondering if it's possible that the onDeselect is somehow being triggered when switching between blocks?

Once we introduced the use of SlotFills to inject items into the ToolsPanel we encountered a similar issue (#35375).

My guess would be that the changes around flagItemCustomization open up a few extra possibilities that we now need to guard against. There is a unit test for the original onDeselect issue.

@aaronrobertshaw
Copy link
Contributor

I hope you don't mind @stacimc, I pushed a commit ( 5d16f7a ) that prevents the last regression we found. Please feel free to revert this if you see a better option. My thinking was pushing it now would help others to test this and the Post Featured Image Dimensions PR.

The check made before flagging a default control as customized in the menu was altered to not only update once a control had been customized by a user but also when they "undid" that customization. This also meant that when that second case occurred, the item's onDeselect callback was triggered in the other hook.

5d16f7a brings the UX back to what it is on trunk for default controls. During its development, it was requested that as soon as a user modifies a default control it should show as customized in the panel menu until reset via that menu. If we wish to change that behaviour I suggest we address it in another PR.

With the behaviour around flagging default controls as customized restored, this tested well for me. All the unit tests pass and selections are honoured when switching between blocks in the editor. Rebasing #36540 onto this has it testing fine as well.

Screen.Recording.2021-11-18.at.5.38.38.pm.mp4

Let me know what you think.

Copy link
Contributor

@ciampo ciampo left a comment

Choose a reason for hiding this comment

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

@andrewserong was too quick for me 🚀

You have all been extremely quick! Props to everyone for the excellent collaboration, and for not only fixing the issue, but also improving the unit tests and the storybook examples! 🙇

I had a look at the code and at the Storybook examples and everything worked well for me, although I'll let @andrewserong and @aaronrobertshaw give the final ✅

packages/components/src/tools-panel/test/index.js Outdated Show resolved Hide resolved
@stacimc
Copy link
Contributor Author

stacimc commented Nov 18, 2021

Thanks all for the quick responses and very thorough testing, much appreciated ✨

I hope you don't mind @stacimc, I pushed a commit ( 5d16f7a ) that prevents the last regression we found. Please feel free to revert this if you see a better option. My thinking was pushing it now would help others to test this and the Post Featured Image Dimensions PR.

I don't mind at all, thank you very much for the quick fix @aaronrobertshaw!

5d16f7a brings the UX back to what it is on trunk for default controls. During its development, it was requested that as soon as a user modifies a default control it should show as customized in the panel menu until reset via that menu. If we wish to change that behaviour I suggest we address it in another PR.

Thanks for this context -- very happy to leave the behavior as requested, and I agree that should be addressed elsewhere if we decide to revisit. The persistence of the dirty state in the menu when emptying the input stood out to me as I was testing the conditionally displayed panels, but I missed that this behavior is consistent with other controls and on trunk. Seems very reasonable to me.

The fix works great for me and I think it's the right way to go 👍

@aaronrobertshaw aaronrobertshaw self-requested a review November 18, 2021 22:34
Copy link
Contributor

@aaronrobertshaw aaronrobertshaw 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 @stacimc 👍

This is testing as advertised for me.

✅ Existing block support panels continue to function
✅ Storybook examples look good
✅ Rebasing changes from #36540 onto these tests well

I left a couple of minor comments which I'll leave up to you to decide if you want to action.

That aside, this LGTM :shipit:

packages/components/src/tools-panel/test/index.js Outdated Show resolved Hide resolved
packages/components/src/tools-panel/test/index.js Outdated Show resolved Hide resolved
Copy link
Contributor

@andrewserong andrewserong left a comment

Choose a reason for hiding this comment

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

This is looking good to me now, too:

✅ Regressions when switching between blocks with common panel items appear to be fixed
✅ Storybook examples still work nicely in manual testing

Thanks for the detailed work here @stacimc and for fixing up that last regression and providing the extra context @aaronrobertshaw!

LGTM 🎉

@stacimc stacimc force-pushed the fix/tools-panel-conditionally-displayed-items branch from ace59ca to 0cac6cc Compare November 19, 2021 20:06
@apeatling apeatling self-requested a review November 23, 2021 03:26
Copy link
Contributor

@apeatling apeatling left a comment

Choose a reason for hiding this comment

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

LGTM @stacimc -- can we get this one merged so we can move forward with #36540? It'd be good to get that one in because it was a 5.9 reported issue.

…ritten

It's possible to encounter race conditions when updating the panelItems and menuItems
if you have a ToolsPanelItem that's displayed conditionally, or which is shown by
default only conditionally. This can cause multiple calls to update the state in quick
sequence, which can result in those calls overwriting one another. To prevent this,
the calls to setState have been updated to depend on the previous state.
@stacimc stacimc force-pushed the fix/tools-panel-conditionally-displayed-items branch from 0cac6cc to 8e8db97 Compare November 23, 2021 18:14
@stacimc
Copy link
Contributor Author

stacimc commented Nov 23, 2021

Just rebased for merge conflicts in the changelog. When builds pass I think this should be ready for merge.

@stacimc stacimc merged commit 800d93c into trunk Nov 23, 2021
@stacimc stacimc deleted the fix/tools-panel-conditionally-displayed-items branch November 23, 2021 22:22
@github-actions github-actions bot added this to the Gutenberg 12.1 milestone Nov 23, 2021
noisysocks pushed a commit that referenced this pull request Jan 4, 2022
…olsPanelItems (#36588)

* Ensure state changes happen in order and flagged values are not overwritten

It's possible to encounter race conditions when updating the panelItems and menuItems
if you have a ToolsPanelItem that's displayed conditionally, or which is shown by
default only conditionally. This can cause multiple calls to update the state in quick
sequence, which can result in those calls overwriting one another. To prevent this,
the calls to setState have been updated to depend on the previous state.

* Add storybook examples

* Update storybook example name

* Do not mutate existing state when registering PanelItems

* Prevent deregistration of panelItems when switching between panels

* Linting

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>

* Add unit tests for conditionally displayed panel items

* Add unit test for regression with deregistration of panelItems

* Prevent triggering onDeselect

* Clean up tests

* Changelog

* Clarify comments in tests

Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
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.

5 participants