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

Add <BoxModelOverlay> component #40253

Closed
wants to merge 4 commits into from
Closed

Conversation

kevin940726
Copy link
Member

@kevin940726 kevin940726 commented Apr 12, 2022

What?

Address a part of #40057 by introducing a new component: <BoxModelOverlay>.

Why?

See #40057 for more info.

How?

The new component calculates the box model properties of the target element by using window.getComputedStyle to correctly reflects its style. The component also uses <Popover> to position the overlay outside the target element's tree so that it won't interfere with the styles as mentioned in the original issue.

Why using window.getComputedStyle?

Currently, <BoxControl.Visualizer> accepts a values prop with the defined padding or margin to render the overlays. This is fundamentally flawed because we don't have enough context for the actual rendered style of the target. This is especially true when we're using percentage values in padding since that padding percentage is based on the parent element's width.

The only reliable way (AFAIK) to calculate the values is to use the browser's getComputedStyle API. For that to work, we have to get a reference of the target element itself. This also means that we don't need to pass in the values prop anymore, but we do need to listen to the style/size changes of the element to update the overlays.

Why the name BoxModelOverlay?

BoxControl.Visualizer is more like a name that ties to the <BoxControl> component: it visualizes the box control's values; On the other hand, BoxModelOverlay matches more closely to what the component does: it renders a box model overlay on top of the target element.

Even though the name has "box model" in it, we could probably evolve when we decide to also visualize other properties like flex, grid, or gap.

Testing Instructions

Tests should pass.

  1. Start the Storybook server: npm run storybook:dev
  2. Visit http://localhost:50240/?path=/story/components-experimental-boxmodeloverlay--default to play with the stories

Screenshots or screencast

Kapture.2022-04-12.at.21.03.26.mp4

Next steps

  1. Migrate existing usages of <BoxControl.Visualizer> to using <BoxModelOverlay>. (I think currently only Cover is using it?)
  2. Graduate <BoxModelOverlay> from being experimental and deprecate <BoxControl.Visualizer>.
  3. Try integrating with other design tools like block gaps.

@kevin940726 kevin940726 added the [Package] Components /packages/components label Apr 12, 2022
@github-actions
Copy link

github-actions bot commented Apr 12, 2022

Size Change: +2.06 kB (0%)

Total Size: 1.23 MB

Filename Size Change
build/block-directory/index.min.js 6.51 kB +19 B (0%)
build/block-editor/index.min.js 150 kB +938 B (+1%)
build/block-editor/style-rtl.css 15.7 kB +147 B (+1%)
build/block-editor/style.css 15.6 kB +144 B (+1%)
build/block-library/blocks/navigation/view.min.js 395 B -2.45 kB (-86%) 🏆
build/block-library/blocks/table/editor-rtl.css 504 B +33 B (+7%) 🔍
build/block-library/blocks/table/editor.css 504 B +32 B (+7%) 🔍
build/block-library/blocks/table/style-rtl.css 625 B +144 B (+30%) 🚨
build/block-library/blocks/table/style.css 625 B +144 B (+30%) 🚨
build/block-library/common-rtl.css 993 B +59 B (+6%) 🔍
build/block-library/common.css 990 B +58 B (+6%) 🔍
build/block-library/editor-rtl.css 10.2 kB +33 B (0%)
build/block-library/editor.css 10.2 kB +33 B (0%)
build/block-library/index.min.js 175 kB +235 B (0%)
build/block-library/style-rtl.css 11.5 kB +197 B (+2%)
build/block-library/style.css 11.5 kB +194 B (+2%)
build/blocks/index.min.js 47 kB +133 B (0%)
build/components/index.min.js 224 kB +1.09 kB (0%)
build/components/style-rtl.css 14.9 kB -5 B (0%)
build/components/style.css 14.9 kB -6 B (0%)
build/core-data/index.min.js 14.5 kB +54 B (0%)
build/data/index.min.js 8.66 kB +19 B (0%)
build/edit-post/index.min.js 30.1 kB +99 B (0%)
build/edit-site/index.min.js 47.1 kB +152 B (0%)
build/edit-site/style-rtl.css 8.02 kB +239 B (+3%)
build/edit-site/style.css 8.01 kB +241 B (+3%)
build/edit-widgets/index.min.js 16.3 kB +22 B (0%)
build/editor/index.min.js 38.5 kB +60 B (0%)
ℹ️ View Unchanged
Filename Size
build/a11y/index.min.js 993 B
build/admin-manifest/index.min.js 1.24 kB
build/annotations/index.min.js 2.77 kB
build/api-fetch/index.min.js 2.27 kB
build/autop/index.min.js 2.15 kB
build/blob/index.min.js 487 B
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-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 150 B
build/block-library/blocks/audio/editor.css 150 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/avatar/editor-rtl.css 116 B
build/block-library/blocks/avatar/editor.css 116 B
build/block-library/blocks/avatar/style-rtl.css 59 B
build/block-library/blocks/avatar/style.css 59 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 445 B
build/block-library/blocks/button/editor.css 445 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 292 B
build/block-library/blocks/buttons/editor.css 292 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 103 B
build/block-library/blocks/code/style.css 103 B
build/block-library/blocks/code/theme-rtl.css 124 B
build/block-library/blocks/code/theme.css 124 B
build/block-library/blocks/columns/editor-rtl.css 108 B
build/block-library/blocks/columns/editor.css 108 B
build/block-library/blocks/columns/style-rtl.css 406 B
build/block-library/blocks/columns/style.css 406 B
build/block-library/blocks/comment-author-avatar/editor-rtl.css 125 B
build/block-library/blocks/comment-author-avatar/editor.css 125 B
build/block-library/blocks/comment-template/style-rtl.css 127 B
build/block-library/blocks/comment-template/style.css 127 B
build/block-library/blocks/comments-pagination-numbers/editor-rtl.css 123 B
build/block-library/blocks/comments-pagination-numbers/editor.css 121 B
build/block-library/blocks/comments-pagination/editor-rtl.css 222 B
build/block-library/blocks/comments-pagination/editor.css 209 B
build/block-library/blocks/comments-pagination/style-rtl.css 235 B
build/block-library/blocks/comments-pagination/style.css 231 B
build/block-library/blocks/comments-query-loop/editor-rtl.css 95 B
build/block-library/blocks/comments-query-loop/editor.css 95 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.56 kB
build/block-library/blocks/cover/style.css 1.56 kB
build/block-library/blocks/embed/editor-rtl.css 293 B
build/block-library/blocks/embed/editor.css 293 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 353 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 961 B
build/block-library/blocks/gallery/editor.css 964 B
build/block-library/blocks/gallery/style-rtl.css 1.51 kB
build/block-library/blocks/gallery/style.css 1.51 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 333 B
build/block-library/blocks/group/editor.css 333 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 76 B
build/block-library/blocks/heading/style.css 76 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 529 B
build/block-library/blocks/image/style.css 535 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 199 B
build/block-library/blocks/latest-posts/editor.css 198 B
build/block-library/blocks/latest-posts/style-rtl.css 447 B
build/block-library/blocks/latest-posts/style.css 446 B
build/block-library/blocks/list/style-rtl.css 88 B
build/block-library/blocks/list/style.css 88 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 708 B
build/block-library/blocks/navigation-link/editor.css 706 B
build/block-library/blocks/navigation-link/style-rtl.css 115 B
build/block-library/blocks/navigation-link/style.css 115 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 375 B
build/block-library/blocks/navigation/editor-rtl.css 2.03 kB
build/block-library/blocks/navigation/editor.css 2.04 kB
build/block-library/blocks/navigation/style-rtl.css 1.93 kB
build/block-library/blocks/navigation/style.css 1.92 kB
build/block-library/blocks/navigation/view-modal.min.js 2.65 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 363 B
build/block-library/blocks/page-list/editor.css 363 B
build/block-library/blocks/page-list/style-rtl.css 175 B
build/block-library/blocks/page-list/style.css 175 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 260 B
build/block-library/blocks/paragraph/style.css 260 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 446 B
build/block-library/blocks/post-comments-form/style.css 446 B
build/block-library/blocks/post-comments/style-rtl.css 521 B
build/block-library/blocks/post-comments/style.css 521 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 721 B
build/block-library/blocks/post-featured-image/editor.css 721 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 323 B
build/block-library/blocks/post-template/style.css 323 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 370 B
build/block-library/blocks/pullquote/style.css 370 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 221 B
build/block-library/blocks/query-pagination/editor.css 211 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 369 B
build/block-library/blocks/query/editor.css 369 B
build/block-library/blocks/quote/style-rtl.css 213 B
build/block-library/blocks/quote/style.css 213 B
build/block-library/blocks/quote/theme-rtl.css 223 B
build/block-library/blocks/quote/theme.css 226 B
build/block-library/blocks/read-more/style-rtl.css 132 B
build/block-library/blocks/read-more/style.css 132 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 140 B
build/block-library/blocks/separator/editor.css 140 B
build/block-library/blocks/separator/style-rtl.css 233 B
build/block-library/blocks/separator/style.css 233 B
build/block-library/blocks/separator/theme-rtl.css 194 B
build/block-library/blocks/separator/theme.css 194 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 759 B
build/block-library/blocks/site-logo/editor.css 759 B
build/block-library/blocks/site-logo/style-rtl.css 181 B
build/block-library/blocks/site-logo/style.css 181 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 674 B
build/block-library/blocks/social-links/editor.css 673 B
build/block-library/blocks/social-links/style-rtl.css 1.37 kB
build/block-library/blocks/social-links/style.css 1.36 kB
build/block-library/blocks/spacer/editor-rtl.css 332 B
build/block-library/blocks/spacer/editor.css 332 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/theme-rtl.css 188 B
build/block-library/blocks/table/theme.css 188 B
build/block-library/blocks/tag-cloud/style-rtl.css 226 B
build/block-library/blocks/tag-cloud/style.css 227 B
build/block-library/blocks/template-part/editor-rtl.css 149 B
build/block-library/blocks/template-part/editor.css 149 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 571 B
build/block-library/blocks/video/editor.css 572 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/reset-rtl.css 478 B
build/block-library/reset.css 478 B
build/block-library/theme-rtl.css 689 B
build/block-library/theme.css 694 B
build/block-serialization-default-parser/index.min.js 1.12 kB
build/block-serialization-spec-parser/index.min.js 2.83 kB
build/compose/index.min.js 11.2 kB
build/customize-widgets/index.min.js 11 kB
build/customize-widgets/style-rtl.css 1.39 kB
build/customize-widgets/style.css 1.39 kB
build/data-controls/index.min.js 663 B
build/date/index.min.js 32 kB
build/deprecated/index.min.js 518 B
build/dom-ready/index.min.js 336 B
build/dom/index.min.js 4.58 kB
build/edit-navigation/index.min.js 15.8 kB
build/edit-navigation/style-rtl.css 4.04 kB
build/edit-navigation/style.css 4.05 kB
build/edit-post/classic-rtl.css 546 B
build/edit-post/classic.css 547 B
build/edit-post/style-rtl.css 7.18 kB
build/edit-post/style.css 7.18 kB
build/edit-widgets/style-rtl.css 4.4 kB
build/edit-widgets/style.css 4.39 kB
build/editor/style-rtl.css 3.71 kB
build/editor/style.css 3.71 kB
build/element/index.min.js 4.29 kB
build/escape-html/index.min.js 548 B
build/format-library/index.min.js 6.62 kB
build/format-library/style-rtl.css 571 B
build/format-library/style.css 571 B
build/hooks/index.min.js 1.66 kB
build/html-entities/index.min.js 454 B
build/i18n/index.min.js 3.79 kB
build/is-shallow-equal/index.min.js 535 B
build/keyboard-shortcuts/index.min.js 1.83 kB
build/keycodes/index.min.js 1.41 kB
build/list-reusable-blocks/index.min.js 1.75 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.94 kB
build/notices/index.min.js 957 B
build/nux/index.min.js 2.12 kB
build/nux/style-rtl.css 751 B
build/nux/style.css 749 B
build/plugins/index.min.js 1.98 kB
build/preferences/index.min.js 1.2 kB
build/primitives/index.min.js 949 B
build/priority-queue/index.min.js 611 B
build/react-i18n/index.min.js 704 B
build/react-refresh-entry/index.min.js 8.44 kB
build/react-refresh-runtime/index.min.js 7.31 kB
build/redux-routine/index.min.js 2.69 kB
build/reusable-blocks/index.min.js 2.24 kB
build/reusable-blocks/style-rtl.css 256 B
build/reusable-blocks/style.css 256 B
build/rich-text/index.min.js 11.2 kB
build/server-side-render/index.min.js 1.61 kB
build/shortcode/index.min.js 1.52 kB
build/token-list/index.min.js 668 B
build/url/index.min.js 1.99 kB
build/vendors/react-dom.min.js 38.5 kB
build/vendors/react.min.js 4.34 kB
build/viewport/index.min.js 1.08 kB
build/warning/index.min.js 280 B
build/widgets/index.min.js 7.21 kB
build/widgets/style-rtl.css 1.16 kB
build/widgets/style.css 1.16 kB
build/wordcount/index.min.js 1.07 kB

compressed-size-action

@kevin940726 kevin940726 changed the title Add <BoxModelOverlay> component Add <BoxModelOverlay> component Apr 12, 2022
Comment on lines +29 to +30
'**/@(__mocks__|__tests__|test)/**/*.[tj]s?(x)',
'**/@(storybook|stories)/**/*.[tj]s?(x)',
Copy link
Member Author

Choose a reason for hiding this comment

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

To support writing tests in .ts and .tsx.

Copy link
Contributor

Choose a reason for hiding this comment

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

We're still working on the best way to enable TypeScript unit tests in Jest, for the time being we should write unit tests in JS (see ongoing work in #39436)

Copy link
Contributor

Choose a reason for hiding this comment

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

Update: #39436 has been merged, we should be able to remove the changes to this file in this PR, and rebase on top of trunk to support TypeScript tests

Comment on lines +264 to +269
export {
BoxModelOverlayProps,
BoxModelOverlayPropsWithChildren,
BoxModelOverlayPropsWithTargetRef,
BoxModelOverlayHandle,
};
Copy link
Member Author

Choose a reason for hiding this comment

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

These are only exported for the tests for now. Not sure how we can export them to the package level when we're still using .js in our entry file.

window.ResizeObserver = undefined;
} );

it( 'renders the overlay visible with the children prop', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

Honestly, these tests aren't that helpful for this particular component, since JSDOM doesn't render anything visually.

Perhaps we can set up an infrastructure to write unit tests to run in Playwright in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps we can set up an infrastructure to write unit tests to run in Playwright in the future.

This is something that's we've discussed in the past, and flagging limits like you've just done definitely helps justifying that effort (cc @mirka )

Comment on lines +54 to +57
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L931
const MARGIN_COLOR = 'rgba( 246, 178, 107, 0.66 )';
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L927
const PADDING_COLOR = 'rgba( 147, 196, 125, 0.55 )';
Copy link
Member Author

Choose a reason for hiding this comment

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

These values mimic the styles in Chrome's DevTools. Not sure which WordPress-related colors we can use here? Ideas welcome!

Copy link
Contributor

Choose a reason for hiding this comment

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

If we are looking for specific WordPress colors, perhaps the Color docs from the style guide might help. There's also a codepen with full list of official colors.

@kevin940726 kevin940726 marked this pull request as ready for review April 13, 2022 09:33
@kevin940726 kevin940726 added the Needs Design Feedback Needs general design feedback. label Apr 13, 2022
@apeatling apeatling self-requested a review April 13, 2022 17:00
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.

Great work on this! I was wondering if we could reduce the re-rendering delay? I notice when I drag the box the overlay lags to catch up:

2022-04-13 09 59 28

I think this kind of large scale visualization needs to feel buttery.

@mirka mirka requested review from mirka and ciampo April 14, 2022 03:16
@ramonjd
Copy link
Member

ramonjd commented Apr 14, 2022

+1 on the work here. Very impressive!

I was wondering if we could reduce the re-rendering delay? I notice when I drag the box the overlay lags to catch up:

I tested out a mix of debounce and transition CSS just for fun. It was a complete disaster 🤣

2022-04-14 14 41 33

However I guess we'd ideally want the visualizer to respond to every single value change. I'm not hugely experienced with the performance aspects of the observers. Do they play nicely with requestAnimationFrame maybe?

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.

This is impressive work and looks great so far @kevin940726! 👍

In testing this I only really hit two issues.

  1. As @apeatling mentioned, the re-rendering delay makes the experience feel pretty disjointed.
  2. The component will need to account for borders on its target.
Left border only Full Border
Screen Shot 2022-04-14 at 4 33 52 pm Screen Shot 2022-04-14 at 4 34 14 pm

I also have a few suggestions based on feedback I've received on recent component PRs that I'll list below but take them with a grain of salt.

It would be great to get some early feedback from @ciampo and @mirka regarding the latest approaches and best practices for the components package.

Some things we might wish to consider for this PR are:

  • Restructuring the component directory and files.
    • I was previously directed to the ItemGroup component as a great example of the latest approach for new components.
    • I followed this structure for the ToolsPanel, BorderControl, and BorderBoxControl components and it worked well for me.
  • Related to the above, could we split the types out into their own types.ts file for consistency with other components and a cleaner component file?
  • Aiming to avoid all hardcoded classnames within new components and leverage dynamic classnames instead e.g. via useCx.
    • Perhaps we could generate a dynamic classname and pass it to the Popover component and use that to hide the popover content.
  • I believe there is a preference for using Emotion's css now rather than styled components

Again, this is great work. Looking forward to seeing improved visualizers throughout our blocks!

Comment on lines +54 to +57
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L931
const MARGIN_COLOR = 'rgba( 246, 178, 107, 0.66 )';
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L927
const PADDING_COLOR = 'rgba( 147, 196, 125, 0.55 )';
Copy link
Contributor

Choose a reason for hiding this comment

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

If we are looking for specific WordPress colors, perhaps the Color docs from the style guide might help. There's also a codepen with full list of official colors.

@kevin940726
Copy link
Member Author

Thanks for the review!

I think the problem is in <Popover>. The positioning is not designed to be that responsive for constant resizing. You can see a similar effect in the storybook for <Popover>.

We'll need to decide if this is that much of a problem right now. Would we allow users to resize their elements smoothly while also showing the overlay simultaneously? If there's a legit use case, we'll need to improve the performance for sure. But if there's not, then I'd say let's not prematurely optimize it?

@kevin940726
Copy link
Member Author

The component will need to account for borders on its target.

Ahh, right! I totally forgot about borders 😅. Will push up a fix soon.

@apeatling
Copy link
Contributor

apeatling commented Apr 14, 2022

We'll need to decide if this is that much of a problem right now. Would we allow users to resize their elements smoothly while also showing the overlay simultaneously? If there's a legit use case, we'll need to improve the performance for sure. But if there's not, then I'd say let's not prematurely optimize it?

I'd say we're going to need to optimize this before it gets used in the editor. I think at this point you could simply confirm this direction isn't a dead end, and that kind of optimization is possible with <Popover>.

@kevin940726
Copy link
Member Author

I'd say we're going to need to optimize this before it gets used in the editor. I think at this point you could simply confirm this direction isn't a dead end, and that kind of optimization is possible with <Popover>.

I still think it depends on the usage. If the component is only going to be used when hovering over some <BoxControl>s, it probably doesn't need to be frame-perfect. Undoubtedly optimizing it is possible, but performance optimization is not always free. It's especially costly if we want it to be updated at 60fps.

The resizable box in the storybook is just a demonstration that it'll update itself when the size of the target changes. Maybe that's a bad example though and should probably just be a control field 😅.

Maybe we can provide an opt-in prop like priority="high" to toggle this behavior when needed. However, I still couldn't think of an example for that yet 🤔 .

Currently, the issue only surfaces itself when the users are trying to edit the content of the target, specifically when it changes its size. Let's think about the purpose of this component: it shows an overlay to visualize the box model of the target element; why would the users want to have an overlay on top of the content they're trying to edit at the same time?

Maybe there's a magical prop on <Popover> that can fix this issue in a very cheap way and I don't know about it 😅, and I'll be happy to turn it on! I guess what I'm saying is that I can't imagine a use case for that, so optimizing it now seems like a premature optimization. Happy to be proven wrong though! Maybe I'm just missing some context 🙇‍♂️.

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.

Hey @kevin940726 , thank you for working on this!

I gave the component a spin in Storybook, and noticed that the rendering of the overlay is a bit glitchy:

box-model-overlay.mp4

I'm going to echo @aaronrobertshaw 's comments:

  • Restructuring the component directory and files.
    • I was previously directed to the ItemGroup component as a great example of the latest approach for new components.
    • I followed this structure for the ToolsPanel, BorderControl, and BorderBoxControl components and it worked well for me.
  • Aiming to avoid all hardcoded classnames within new components and leverage dynamic classnames instead e.g. via useCx.
    • Perhaps we could generate a dynamic classname and pass it to the Popover component and use that to hide the popover content.
  • I believe there is a preference for using Emotion's css now rather than styled components

I'm going to add that most of those aspects are addressed in the contributing guidelines.


Regarding the rendering performance of the component, I wonder if we could improve it by:

  • debouncing the update function. Basically, while the update function is waiting to run and therefore while the overlay's values are out of sync, we could switch to a different UI (something very minimal that indicates this "waiting/recalculating" status)
  • re-write the update function so that it doesn't set styles and custom properties multiple times per call, which may cause multiple repaints / reflows in the browser. In that sense, re-writing those styles in Emotion (and batching their calculation "at once") may help a lot!

Comment on lines +29 to +30
'**/@(__mocks__|__tests__|test)/**/*.[tj]s?(x)',
'**/@(storybook|stories)/**/*.[tj]s?(x)',
Copy link
Contributor

Choose a reason for hiding this comment

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

We're still working on the best way to enable TypeScript unit tests in Jest, for the time being we should write unit tests in JS (see ongoing work in #39436)

This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

`<BoxModelOverlay>` component shows a visual overlay of the [box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model) (currently only paddings and margins are available) on top of the target element. This is often accompanied by the `<BoxControl>` component to show a preview of the styling changes in the editor.
Copy link
Contributor

Choose a reason for hiding this comment

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

(currently only paddings and margins are available)

If we're making references to the box model for this component, I believe that it should fully support borders as well, otherwise it would be a bit weird?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it should, but borders are usually already visible to users. We can discuss this further in another PR though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense, let's address this in a follow-up PR

};
```

`<BoxModelOverlay>` internally uses [`Popover`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/components/src/popover/README.md) to position the overlay. This means that you can use `<Popover.Slot>` to alternatively control where the overlay is rendered.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we should expose how internally a component works — hiding these implementation details would allow us to make changes in a non-breaking way in the future (for example, what if at some point we change how Popover works? Or what if we decided to switch to Flyout ?)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see this as an implementation detail though. This part is a fundamental part of the component. If we change to use Flyout, then <Popover.Slot> will no longer work, so we'll have to make breaking changes either way. If we change how <Popover> works... then it's already a breaking change 😆 ?

Anyway, this is still an experimental component and can change anytime. Maybe we'll like to deprecate this <Popover.Slot> usage and it's fine to do so without introducing breaking changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right — even if it's not ideal, given that <Popover.Slot> is currently a fundamental part of how BoxModalOverlay works, it's fine to keep this part of the README as-is.

Thank you for the explanation!

};
```

`<BoxModelOverlay>` under the hood listens to size and style changes of the target element to update the overlay style automatically using `ResizeObserver` and `MutationObserver`. In some edge cases when the observers aren't picking up the changes, you can use the instance method `update` on the ref of the overlay to update it manually.
Copy link
Contributor

Choose a reason for hiding this comment

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

Similarly to the previous comment, we usually don't want to explicitly explain how a component works internally

Suggested change
`<BoxModelOverlay>` under the hood listens to size and style changes of the target element to update the overlay style automatically using `ResizeObserver` and `MutationObserver`. In some edge cases when the observers aren't picking up the changes, you can use the instance method `update` on the ref of the overlay to update it manually.
`<BoxModelOverlay>` listens to the target element's size and style changes and updates the overlay style automatically. In some edge cases, in case those changes aren't picked up, you can use the instance method `update` on the `ref` of the overlay to update it manually.

Copy link
Member Author

Choose a reason for hiding this comment

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

Personally, I'd love to see documentation explaining how it works internally so that it feels less magical. (Or else the users have to read the source code which might be inaccessible to some given that it's written in TypeScript.)

I don't think changing the documentation would necessary mean breaking changes though. We didn't make any assumptions about how we use them. The purpose of this whole paragraph is even about when they don't work. I'd argue that the pros outweigh the cons if we expose some internal details, but that's just my opinion 😅 .

Copy link
Contributor

Choose a reason for hiding this comment

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

I see your point of view, but given how much WordPress cares about backwards compat, we need to be particularly careful — we treat our public APIs (and what we write in the README) as a contract with the consumers of the component. And therefore, we usually try not to reveal implementation details.

The README, in our views, should be used to understand how to use a component (and not necessarily how it works internally). For those aspects, we think that code comments are better suited (also because they don't represent a "contract", differently from the README).

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair point! Makes sense to me 👍 .

Comment on lines +114 to +117
// To ignore the error for incorrect types in BoxControl.
id={ undefined }
units={ undefined }
sides={ undefined }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this error something that should be fixed by marking these props as non-required in BoxControl ? Otherwise, I'd argue that we should try to use components as close as possible to how they're supposed to be used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, the error is from <BoxControl>'s types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. We can leave the code as-is for the purpose of this PR, but ideally we should follow-up in a new PR with those fixes

Comment on lines +33 to +69
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L931
const MARGIN_COLOR = 'rgba( 246, 178, 107, 0.66 )';
// Copied from Chrome's DevTools: https://github.com/ChromeDevTools/devtools-frontend/blob/088a8f175bd58f2e0e2d492e991a3253124d7c11/front_end/core/common/Color.ts#L927
const PADDING_COLOR = 'rgba( 147, 196, 125, 0.55 )';

const OverlayPopover = styled( Popover )`
&& {
pointer-events: none;
box-sizing: content-box;
border-style: solid;
border-color: ${ MARGIN_COLOR };
// The overlay's top-left point is positioned at the center of the target,
// so we'll have add some negative offsets.
transform: translate( -50%, -50% );

&::before {
content: '';
display: block;
position: absolute;
box-sizing: border-box;
height: var( --wp-box-model-overlay-height );
width: var( --wp-box-model-overlay-width );
top: var( --wp-box-model-overlay-top );
left: var( --wp-box-model-overlay-left );
border-color: ${ PADDING_COLOR };
border-style: solid;
border-width: var( --wp-box-model-overlay-padding-top )
var( --wp-box-model-overlay-padding-right )
var( --wp-box-model-overlay-padding-bottom )
var( --wp-box-model-overlay-padding-left );
}

.components-popover__content {
display: none;
}
}
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

These values should be moved to a separate styles.ts file and applied using Emotion

Comment on lines +217 to +220
mutationObserver.observe( target, {
attributes: true,
attributeFilter: [ 'style' ],
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this be able to catch style changes caused from external stylesheets ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope. That would be some edge cases where the update method might be helpful.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Let's keep this approach for now and keep our eyes open to understand how impactful this ay be

data-testid="box-model-overlay"
showValues={ DEFAULT_SHOW_VALUES }
>
<div data-testid="box" style={ { height: 300, width: 300 } } />
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything else we can use before resorting to data-testid ? Does the overlay have any role / accessible label or content that we could use instead?

This comment applies in general to all tests below using data-testid

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, the overlay is supposed to be inaccessible to SR.

I don't think using data-testid here is that bad though. It's a way to target a specific element in tests, which is precisely what I need in these tests. Furthermore, they're not part of the component but just part of the tests, so we don't have to limit ourselves to using accessible queries IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha — in that case, let's leave these tests as they currently are.

Comment on lines +78 to +83
const box = screen.getByTestId( 'box' );
const overlay = screen.getByTestId( 'box-model-overlay' );

act( () => {
expect( targetRef.current ).toBe( box );
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't we add some text in the "box" and then use screen.getByText to make sure it's rendered correctly? This way of testing is closer to how a user interacts with the component and expects it to work

Copy link
Member Author

Choose a reason for hiding this comment

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

Using screen.getByText only makes sure the text exists but doesn't make sure targetRef.current is the target element. Both are valid though and the former could be used in other tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

Understood — we can leave things as they currently are.

Comment on lines +175 to +186
it( 'should correctly unmount the component', async () => {
const { unmount } = render(
<BoxModelOverlay
data-testid="box-model-overlay"
showValues={ DEFAULT_SHOW_VALUES }
>
<div data-testid="box" style={ { height: 300, width: 300 } } />
</BoxModelOverlay>
);

unmount();
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

What are we testing for in particular in this test?

Copy link
Member Author

Choose a reason for hiding this comment

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

Testing that it unmounts without any errors. It'd be better if there's an explicit assertion to make though.

Copy link
Contributor

Choose a reason for hiding this comment

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

What in particular during the unmounting could go wrong? Is it the de-registration of the size / mutation observers? Maybe we could just add a quick code comment to explain why this test is required / important

Copy link
Member Author

Choose a reason for hiding this comment

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

TBH, it's more like a sanity check 😅. I'm okay if we just remove this and add it back when there's a need.

Copy link
Contributor

Choose a reason for hiding this comment

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

The main reason why I'm asking is because we don't usually test for component un-mounting, and so I was curious if there was a specific reason for it (and if we could learn something from your PR!).

I'm okay if we just remove this and add it back when there's a need.

Let's do this then, if you don't mind! Thank you :)

Comment on lines +146 to +158
### `children`

A single React element to rendered as the target. It should implicitly accept `ref` to be passed in.

- Type: `React.ReactElement`
- Required: Yes if `targetRef` is not passed

### `targetRef`

A ref object for the target element.

- Type: `Ref<HTMLElement>`
- Required: Yes if `children` is not passed
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, we'd like to avoid having conditional logic in the types (see this recent comment by @mirka ) — can we find an alternative here?

Copy link
Member Author

Choose a reason for hiding this comment

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

This feels backward to me, though. I don't think we should change how a component works because the tooling doesn't support it; we should update the tooling to support it instead. Is there any other reason why we shouldn't use conditional props?

Copy link
Member

Choose a reason for hiding this comment

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

Right, we're definitely not saying that docgen limitations should be the driving factor for designing component APIs. The general sentiment of wanting to avoid conditional props was triggered by situations where it just added to the maintenance burden. It's a slippery slope that can easily complicate itself over time into logic that is hard to hold in your head. This is nonetheless a cognitive burden that slows down dev work, no matter how well TypeScript can programatically enforce the conditions.

So it's actually not just about docgen tooling, but humans and human-facing docs in general, because there's still a limit to how coherently you can describe conditional props even in a handwritten README.md. It may seem simple enough now, but we also have to consider how those conditions may be forced to evolve/complicate over time, and how it adds to the cognitive load of both maintainers and consumers.

If it's absolutely worth those costs, sure! But it's good to consider whether there is a simpler, human-friendly API we can land on (e.g. remove the children usage, or export them as separate components).

@kevin940726
Copy link
Member Author

I gave the component a spin in Storybook, and noticed that the rendering of the overlay is a bit glitchy:

This looks like a bug in the <BoxControl> component, where onChangeShowVisualizer is behaving incorrectly when not hovering on the input.

I'm going to add that most of those aspects are addressed in the contributing guidelines.

Oh nice! I didn't know that there's a separate contributing guideline specifically for the components package. I'll check it out!

I have to admit that I don't agree with all of them though, but I guess it has already been discussed before? Is there any relevant discussion I can refer to? I'll still try to apply them in this PR though to make it consistent.

  • debouncing the update function. Basically, while the update function is waiting to run and therefore while the overlay's values are out of sync, we could switch to a different UI (something very minimal that indicates this "waiting/recalculating" status)

The problem isn't with debouncing or not. If any, the problem is actually caused by debouncing. Again, the rendering issue is caused by <Popover>, which debounces the update by 500ms, so there's a delay when resizing the target element.

I'll echo my comment here, performance optimization isn't free. We can make some trade-offs, but I'll like to see some solid use cases before we do so. Or else we're just chasing an invisible target blindly.

  • re-write the update function so that it doesn't set styles and custom properties multiple times per call, which may cause multiple repaints / reflows in the browser. In that sense, re-writing those styles in Emotion (and batching their calculation "at once") may help a lot!

Dynamic styles are not suitable to be generated in CSS-in-JS, each value will generate a unique hash, and AFAIK, emotion won't destroy these cache, which means there's going to be thousands of style tags in <head> 😅.

Using other techniques like overriding cssText directly could work though, but that means we'll have to create a wrapper for it so that it won't override the Popover's styles.

However, as I said before, I don't think the bottleneck is in that update function. Updating padding's values is relatively smooth.

@talldan
Copy link
Contributor

talldan commented Apr 20, 2022

@kevin940726 Looking at the performance, it does look like Popover only updates using an interval every half a second as you mention.

The problem might be that we're passing a stable reference for getAnchorRect through to popover, so it doesn't know when to update its position, it falls back to calling getAnchorRect on every interval.

The BoxModelOverlay could do something like pass a prop to control how often the update interval is triggered, but that seems like it'd result in a lot of needless updates—Popover would be calling getAnchorRect when it doesn't need to.

I wonder if it'd be better for BoxModelOverlay to use the anchorRect prop instead of getAnchorRect, and then it could control how frequently the updates are made to the popover position itself by only passing in a new reference when the position changes, and also possibly applying its own throttle to control the performance characteristics.

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.

This looks like a bug in the <BoxControl> component, where onChangeShowVisualizer is behaving incorrectly when not hovering on the input.

Alright, let's address this in a follow-up PR.

I have to admit that I don't agree with all of them though, but I guess it has already been discussed before? Is there any relevant discussion I can refer to? I'll still try to apply them in this PR though to make it consistent.

These conventions were discussed and applied at the time they were introduced — before I started working on components. We've trying to enforce then as a way to have a bit more of a coherent style across components.

Feel free to create a new issue and list out what you disagree with - we're always open to this kind of feedback (although we may not currently have the capacity to carry out large changes!)

The problem isn't with debouncing or not. If any, the problem is actually caused by debouncing. Again, the rendering issue is caused by <Popover>, which debounces the update by 500ms, so there's a delay when resizing the target element.

I'll echo my comment here, performance optimization isn't free. We can make some trade-offs, but I'll like to see some solid use cases before we do so. Or else we're just chasing an invisible target blindly.

Thank you for the explanation! What about making the component transparent while saiting for the debounced update to take effect?

Dynamic styles are not suitable to be generated in CSS-in-JS, each value will generate a unique hash, and AFAIK, emotion won't destroy these cache, which means there's going to be thousands of style tags in <head> 😅.

Using other techniques like overriding cssText directly could work though, but that means we'll have to create a wrapper for it so that it won't override the Popover's styles.

However, as I said before, I don't think the bottleneck is in that update function. Updating padding's values is relatively smooth.

Gotcha — let's definitely not over-optimise the update function for now, especially if we think it's not the bottleneck.


Finally, I'm going to recap a few follow-up tasks that were discussed:

  • add border support
  • fix BoxControl types
  • fix glitchy behaviour of onChangeShowVisualizer in BoxControl

Comment on lines +29 to +30
'**/@(__mocks__|__tests__|test)/**/*.[tj]s?(x)',
'**/@(storybook|stories)/**/*.[tj]s?(x)',
Copy link
Contributor

Choose a reason for hiding this comment

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

Update: #39436 has been merged, we should be able to remove the changes to this file in this PR, and rebase on top of trunk to support TypeScript tests

This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

`<BoxModelOverlay>` component shows a visual overlay of the [box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model) (currently only paddings and margins are available) on top of the target element. This is often accompanied by the `<BoxControl>` component to show a preview of the styling changes in the editor.
Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense, let's address this in a follow-up PR

};
```

`<BoxModelOverlay>` internally uses [`Popover`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/components/src/popover/README.md) to position the overlay. This means that you can use `<Popover.Slot>` to alternatively control where the overlay is rendered.
Copy link
Contributor

Choose a reason for hiding this comment

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

Right — even if it's not ideal, given that <Popover.Slot> is currently a fundamental part of how BoxModalOverlay works, it's fine to keep this part of the README as-is.

Thank you for the explanation!

};
```

`<BoxModelOverlay>` under the hood listens to size and style changes of the target element to update the overlay style automatically using `ResizeObserver` and `MutationObserver`. In some edge cases when the observers aren't picking up the changes, you can use the instance method `update` on the ref of the overlay to update it manually.
Copy link
Contributor

Choose a reason for hiding this comment

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

I see your point of view, but given how much WordPress cares about backwards compat, we need to be particularly careful — we treat our public APIs (and what we write in the README) as a contract with the consumers of the component. And therefore, we usually try not to reveal implementation details.

The README, in our views, should be used to understand how to use a component (and not necessarily how it works internally). For those aspects, we think that code comments are better suited (also because they don't represent a "contract", differently from the README).

Comment on lines +217 to +220
mutationObserver.observe( target, {
attributes: true,
attributeFilter: [ 'style' ],
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Let's keep this approach for now and keep our eyes open to understand how impactful this ay be

Comment on lines +114 to +117
// To ignore the error for incorrect types in BoxControl.
id={ undefined }
units={ undefined }
sides={ undefined }
Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. We can leave the code as-is for the purpose of this PR, but ideally we should follow-up in a new PR with those fixes

data-testid="box-model-overlay"
showValues={ DEFAULT_SHOW_VALUES }
>
<div data-testid="box" style={ { height: 300, width: 300 } } />
Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha — in that case, let's leave these tests as they currently are.

Comment on lines +78 to +83
const box = screen.getByTestId( 'box' );
const overlay = screen.getByTestId( 'box-model-overlay' );

act( () => {
expect( targetRef.current ).toBe( box );
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

Understood — we can leave things as they currently are.

Comment on lines +175 to +186
it( 'should correctly unmount the component', async () => {
const { unmount } = render(
<BoxModelOverlay
data-testid="box-model-overlay"
showValues={ DEFAULT_SHOW_VALUES }
>
<div data-testid="box" style={ { height: 300, width: 300 } } />
</BoxModelOverlay>
);

unmount();
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

What in particular during the unmounting could go wrong? Is it the de-registration of the size / mutation observers? Maybe we could just add a quick code comment to explain why this test is required / important

@youknowriad
Copy link
Contributor

Just found this PR. I accidentally solved the same issue while working on something else #40505
I didn't introduce any new component though. I think we don't really need a new component, "Popover" is already capable of solving this.

@kevin940726
Copy link
Member Author

I didn't introduce any new component though. I think we don't really need a new component, "Popover" is already capable of solving this.

I disagree though. IMO, using <BlockPopover> rather than introducing a new component just shifts the complexity to <BlockPopover>, which makes it even harder to maintain (added two __unstable* props for instance). It's better to make individual components focused on what they are trying to do and composite them together.

Furthermore, adding a new component here introduces the opportunity to write them in TypeScript, and make them testable in isolation, which I believe is the best practice forward? (At least it's what's suggested in the original issue 😅)

This PR also takes a different approach by using window.getComputedStyle() to calculate the values to take percentage paddings into account as mentioned in the PR description. To do that, we have to keep a reference of the target element via ref and listen to the size/style changes of it.

@youknowriad
Copy link
Contributor

The BlockPopover is a component that we already have and that have been used for several places "BlockToolbar", "InbetweenInserter"...

(added two __unstable* props for instance)

One of them is a temporary measure, because I didn't want to introduce a new dependency useResizeObserver (I'd like to do it in its own PR to replace react-resize-aware more globally) and the other one could actually be stable, I just was too cautious like you here by making the new component experimental.

Furthermore, adding a new component here introduces the opportunity to write them in TypeScript, and make them testable in isolation, which I believe is the best practice forward? (At least it's what's suggested in the original issue 😅)

Can we use this energy to type "Popover" instead of adding a new unused component (like we do for Flyout)

This PR also takes a different approach by using window.getComputedStyle() to calculate the values to take percentage paddings into account as mentioned in the PR description. To do that, we have to keep a reference of the target element via ref and listen to the size/style changes of it.

Sure, that's what useResizeAware or useResizeObserver is about :)

@kevin940726
Copy link
Member Author

The BlockPopover is a component that we already have and that have been used for several places "BlockToolbar", "InbetweenInserter"...

My point is that it's often better to focus on one thing in a specific component. Instead of adding more responsibility to <BlockPopover>, I'd prefer to create a separate component for this feature.

One of them is a temporary measure, because I didn't want to introduce a new dependency useResizeObserver (I'd like to do it in its own PR to replace react-resize-aware more globally) and the other one could actually be stable, I just was too cautious like you here by making the new component experimental.

The problem isn't only about using __unstable props, but also about adding two more props to a component that's already bloated with props. This makes it difficult to reason about the purpose of that component.

Can we use this energy to type "Popover" instead of adding a new unused component (like we do for Flyout)

Ideally, yes. In practice, though, <Popover> is being used everywhere and requires extra care if we want to rewrite it into TypeScript. It's not mutually exclusive either, we can still type it in the future. I don't see why we can't do both.

It's not an unused component, we have plans for this component (described in the Next steps section in the PR description.) I just want to make this PR minimal and then we can work on integrating it into the existing codebase.

Sure, that's what useResizeAware or useResizeObserver is about :)

I don't see how useResizeAware or useResizeObserver could solve this alone. AFAIK, they only listen to size changes and return the width and height of the target element, but not its paddings and margins. We still have to use window.getComputedStyle() to obtain the accurate values of them to take percentage paddings into account.

#40505 suffers from this bug and will not work if the paddings have percentage values and the parent element has a different width.

@youknowriad
Copy link
Contributor

My point is that it's often better to focus on one thing in a specific component. Instead of adding more responsibility to , I'd prefer to create a separate component for this feature.

I agree with the point of having one responsibility but as I see it, this component being proposed has two: compute the position and size, render the visualizer. I'm fine with extracting a visualizer component but computing size and potentially (margin/padding) with a different approach (getComputedStyle) should be reusable hooks:

const { padding, margin } = useComputeStyle( ref );

return <BlockPopover><Visualizer padding={ padding } margin={margin} /></BlockPopover>;

BlockPopover internally uses useResizeObserver to position a div on top of the block. That way that component is reusable also for BlockUI (which we already do and need anyway).

It also avoids having a weird component API: either take a ref as prop which is never a good thing because we can't watch refs (a mistake we did in the early days of "Popover") or clone element to inject a ref.

@kevin940726
Copy link
Member Author

I'm fine with extracting a visualizer component but computing size and potentially (margin/padding) with a different approach (getComputedStyle) should be reusable hooks:

const { padding, margin } = useComputeStyle( ref );

return <BlockPopover><Visualizer padding={ padding } margin={margin} /></BlockPopover>;

Making everything a hook feels over-engineered to me. The computing logic only exists because this component needs it, they have the same responsibility. Abstracting it away into a hook is premature optimization.

Moreover, using hooks also means that we're going to leverage React's reconciler to update the styles, which is an unnecessary performance overhead for a pure styling component like this.

BlockPopover internally uses useResizeObserver to position a div on top of the block. That way that component is reusable also for BlockUI (which we already do and need anyway).

We can always extract common parts if we need to. But AFAIK, useResizeObserver requires rendering a invisible element to the target element's children, which may or may not be possible for elements that don't accept children (input, textarea, etc). Maybe there's a way around it I don't know though, and I'll be happy to switch to that if existing API fits the needs.

It also avoids having a weird component API: either take a ref as prop which is never a good thing because we can't watch refs (a mistake we did in the early days of "Popover") or clone element to inject a ref.

These are strong words used here (weird, never), but I respectfully disagree.

Using refs as props is not an uncommon pattern. You can see that being used in Reakit and Ariakit too. Yes, it's not trivial to watch for ref changes, but we assume that we don't need to watch for ref changes that often anyway. An alternative would be using the DOM element itself as a prop, but that's often done wrong in the consumers' side. They often just pass in the element by using ref.current in the render function, that defeats the whole purpose of using DOM element as prop in the first place. It's the same as passing ref.current to a dependency array, which is an anti-pattern. However, as reviewers suggested above, I decided to deprecate the targetRef usage to prevent these sort of gotchas.

As for cloning elements, I believe some call this pattern "compound components". It's a pretty common pattern and it's used in reach-ui, ant-design, react-waypoint, and even gutenberg itself. I agree though that it's not an idiomatic API but I think it works great in this context. Alternative solutions are always welcome, but we still need a way to obtain the target ref, which AFAIK is what the currently merged solution is lack of.

In conclusion,

  1. The currently merged solution #40505 is flawed and has bugs (percentage paddings)
  2. This PR still has some work left to do, but I think it'll have a better overall quality with dedicated components and tests.
  3. What's your opinion of how we can take this forward? Now that #40505 is merged, do we still want to work on this approach or should we close this and enhance existing code? I think it also depends on whether we agree on my opinions above.

@youknowriad
Copy link
Contributor

Making everything a hook feels over-engineered to me. The computing logic only exists because this component needs it, they have the same responsibility. Abstracting it away into a hook is premature optimization.

I disagree, hooks are way simpler primitives to share behavior than components. Components are meant to share logic that is rendering related (visualizer is a good example) but hooks are better to share "DOM related"/"state/effect related logic.

We can always extract common parts if we need to. But AFAIK, useResizeObserver requires rendering a invisible element to the target element's children

That's true for react-resize-aware but not useResizeObserver which is why I have this PR #40509

What's your opinion of how we can take this forward?

  • BoxModelOverlay is the wrong abstraction
  • computed style is better than raw values to compute padding and margins, and we should switch to it. A dedicated hook useComputeStyle seems like the best primitive to have this behavior
  • I'm fine with having a UI component PaddingVisualizer or even a more generic BoxVisualizer that takes padding and margin but I don't see it as a necessity at the moment.

} );

// Percentage paddings are based on parent element's width,
// so we need to also listen to the parent's size changes.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is interesting.

What about if padding and margin use VH,VW? These depend on the window sizes, are we subscribing to these changes as well.

Also for Rem unit, should we subscribe to the font size of the whole document?

I'd love if we can encapsulate that logic in a reusable hook const { ref, padding, margin } = usePaddingMarginObserver() it can also support a ref argument const { padding, margin } = usePaddingMarginObserver( ref )

WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

What about if padding and margin use VH,VW? These depend on the window sizes, are we subscribing to these changes as well.

These will have to be handled by the developers manually via the ref.current.update() function. I don't think this can ever be done perfectly without major performance penalties (repeatedly calling window.getComputedStyle() in requestAnimationFrame for instance). I'd suggest handling it for 80% or more of the use cases by default, and also providing an escape hatch like ref.current.update() to handle the rest.

I'd love if we can encapsulate that logic in a reusable hook const { ref, padding, margin } = usePaddingMarginObserver() it can also support a ref argument const { padding, margin } = usePaddingMarginObserver( ref )

I don't think returning these styles is a good idea for this hook. The returned styles will have to be passed to the overlay's inline styles. This means whenever the observer observes the changes, the styles will be updated and the overlay component will have to be re-rendered. This could be costly and unnecessary, while we can update the style attribute on the DOM directly to achieve the same effects.

In case we want to update the design of the overlay to have more complex styles, we could render a canvas and draw on it instead. We can do all this inside the hook and encapsulate them all together inside a component.

Yes, the hook might be useful somewhere else, but let's not prematurely optimize it and assume that it'd be used by some other components. We can always extract the logic into a sharable hook if we need it. Doing it prematurely means we need to take care of wider API surfaces and be careful of the backward-compatibility issues. That's why I think making it a component is the right abstraction until proven otherwise.

This is just my opinion though, as long as you think it's easier to maintain, either way works fine with me.

@kevin940726 kevin940726 closed this Aug 9, 2023
@kevin940726 kevin940726 deleted the add/box-model-overlay branch August 9, 2023 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Design Feedback Needs general design feedback. [Package] Components /packages/components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants