-
Notifications
You must be signed in to change notification settings - Fork 4.6k
POC: Filter toolbar items by prop in contentOnly #71469
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
base: trunk
Are you sure you want to change the base?
Conversation
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
1 similar comment
|
Warning: Type of PR label mismatch To merge this PR, it requires exactly 1 label indicating the type of PR. Other labels are optional and not being checked here.
Read more about Type labels in Gutenberg. Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task. |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +38 B (0%) Total Size: 1.93 MB
ℹ️ View Unchanged
|
|
I like the idea of this but I don't like it being a private api... The need to control when block controls need to be displayed is the same for 3rd party block developers as it is for core blocks. I understand that we are hesitant about public API's but this seems like a no brainer to have available to all equally :) |
| */ | ||
| import { unlock } from '../../lock-unlock'; | ||
|
|
||
| const { createPrivateSlotFill } = unlock( componentsPrivateApis ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: A similar helper was intentionally removed in the past - #67238.
| // Apply filter function if provided (private API) | ||
| const filteredFills = filter ? fills.filter( filter ) : fills; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jsnajdr @WordPress/gutenberg-components - Can we add filtering of fills for slots? I believe this would greatly simplify the work for write mode by giving us an ability to implement a consistent, simpler way to flag which block toolbar items and sidebar inspector tools to show depending on mode. I've implemented block toolbar filtering here as a POC of how it might work in a generic way, so don't read into the specifics of the code too much. Just the higher level:
- Slots can receive a filtering function
- Only render the fills that pass the filtering function
- Block Toolbar -> Passes a filtering function to check to see if a child component with x prop with x value exists. (I set it as
category="content", but this is unlikely to be the final name. I think the current recursive filtering function is going to be really inefficient, but the main idea here is if we can allow filtering on slots. )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My first impression is that a ReactNode filtering mechanism like this is not very practical in the general case, since every filtering function would pretty much require the level of complexity that you have in packages/block-editor/src/components/block-controls/slot.js.
What are some alternative approaches that were considered? I'm not super familiar with what you're trying to accomplish, but I would assume the straightforward approach is to have some kind of conditional logic before the fills hit the slot.
Or another approach that comes to mind is to selectively hide things at the CSS level, like with data- attributes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or another approach that comes to mind is to selectively hide things at the CSS level, like with data- attributes.
I considered this too, but wouldn't all the weight of the rendering still need to happen? It would be nice to avoid rendering a lot of unnecessary things.
I would assume the straightforward approach is to have some kind of conditional logic before the fills hit the slot.
This is what we're doing now - wrapping many things in isContentOnly checks. But it's very ad hoc at the moment with lots of different approaches. For maintainability and to allow block authors a way to easily hook in, being able to add a prop to the top level item would make their lives a lot easier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we filter block toolbar fills within the fill instead? It looks like it's being wrapped in something...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still probably not ideal, but wrapping on the fill level avoids needing to add a filtering prop to the slot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the info! I believe the Toolbar slots are bubblesVirtually, so I guess I can't use the children prop is a function filtering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I thought it's the other variant because in one of the earlier commits, now reverted, you added the filter prop only to the "base" slot, not bubblesVirtually. And then removed the bubblesVirtually prop from the Slot instance. That would likely break the block controls, as they wouldn't be able to read the current block context.
But bubblesVirtually has another feature that you might use, called fillProps 🙂
const fillProps = { shouldRender: ( element ) => element?.props.category === 'content' };
function MySlot() {
return <Slot name="content" bubblesVirtually fillProps={ fillProps } />
}
const shouldAlwaysRender = () => true;
function MyFill( { children } ) {
return (
<Fill name="content">
{ ( { shouldRender = shouldAlwaysRender } ) => {
if ( ! shouldRender( children ) ) {
return null;
}
return children;
} }
</Fill>
);
}You create a custom Slot which can decide which fills it wants to render, by passing a shouldRender callback. And then you create a custom Fill that calls that callback before rendering anything. It's the Slot that decides what it wants to render, and the custom Fill merely executes what the Slot tells it. That's somewhat similar to the bubblesVirtually case where it's also the Slot that implements the children callback, just in a very different slot.
This is a significant API difference between the two variants of slotfills. After #68056 is merged we could attempt to unify that API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, perfect! Thank you for the extra info! This is working better.
The next tricky part I'm exploring is if it's possible to drill down into components and check for the existence of a component + prop within a child component of the fill. I think this might be a stretch and is probably not recommended, but the idea is seeing if we could let people add semantics to their <ToolbarButtons /> and then we render only the buttons in the toolbar that are applicable for that mode:
<BlockControls group="default">
<ToolbarButton category="content">Content Button</ToolbarButton><-- Render
<ToolbarButton>Regular Button</ToolbarButton><-- Don't render
<ToolbarGroup><-- Render
<ToolbarButton category="content"><-- Render
Nested Content Button
</ToolbarButton>
<ToolbarButton>Nested Regular Button</ToolbarButton><-- Don't render
</ToolbarGroup>
</BlockControls>
This is just one idea. I've thought up a few other approaches, but I like the simplicity for authors to add semantics to their <ToolbarButton />, so I'm pushing a bit to figure out if there's a decent technical solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next tricky part I'm exploring is if it's possible to drill down into components
Inspecting the children components is already not a very good idea, as @aduth and @mirka pointed out, and drilling down into subcomponents makes that even worse 🙂 The problem is that it breaks down as soon as the children are not explicitly in the markup, but are rendered by a component. This is enough to break it completely:
function MyToolbarButton() {
return <ToolbarButton category="content">Content</ToolbarButton>
}
<BlockControls>
<MyToolbarButton />
</BlockControls>This is valid code, but the filtering won't work.
However, even after reading about the other approaches in #58233 (comment), I still don't know how to best make this work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inspecting the children components is already not a very good idea, as @aduth and @mirka pointed out, and drilling down into subcomponents makes that even worse 🙂
Yes, I figured as much, but figured it was worth pushing the limit to see if it's even possible :)
However, even after reading about the other approaches in #58233 (comment), I still don't know how to best make this work.
Agreed. I feel like there should be some technical solution to making this work, but I'm also unsure how to get there 🤔
| return ( | ||
| <BlockControls group="inline"> | ||
| <FormatToolbar /> | ||
| <FormatToolbar category="content" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the approach in #71058 for restricting formatting controls. If your approach is better and we can have one unified mechanism then we'd need to revert that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think they work alongside each other. This category="content" says "we want this component to show when in contentOnly mode" and yours is filtering which format controls should be shown within it.
| // Filter children in content-only mode | ||
| let filteredChildren = children; | ||
| if ( blockEditingMode === 'contentOnly' && Array.isArray( children ) ) { | ||
| filteredChildren = children.filter( ( child ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This filter will always create a new Array. For perf reasons I wonder if there's a way to avoid that 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can certainly look to memoize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the AI write a performance test case to compare:
Memoized - 5 items: 0.188ms average (1000 iterations)
Non-memoized - 5 items: 0.132ms average (1000 iterations)
Difference: 0.055ms (41.7%)This is significant! The memoized version is actually 41.7% slower than the non-memoized version for small arrays (5 items).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of the Array.isArray check it's better to use React.Children.toArray, which is a targeted helper that converts children to a valid array.
I wouldn't worry about copying children at all. Every React render creates a tremendous amount of arrays and objects and then dumps them after the render is finished.
Also, the fill is typically used as:
<BlockControlsFill>
<SomeItem />
<SomeOther Item />
</BlockControlsFill>Where the children is a new array on every render. It's quite rare that children would stay constant between renders.
| onError={ onUploadError } | ||
| name={ ! url ? __( 'Add image' ) : __( 'Replace' ) } | ||
| onReset={ () => onSelectImage( undefined ) } | ||
| category="content" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously we can make this a private API for now via Symbols, but don't bother until we've got more feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm not worried about that specific quite yet. I want to see about the overall approach first and then get into the details.
|
Flaky tests detected in df2028f. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/17498749936
|
d002c28 to
df2028f
Compare
What?
Closes
Trying to get an easier API for displaying which block toolbar items we want in different modes.
Why?
Right now we have a lot of ad hoc ways of determining which toolbar items are displayed. We need a better API for block authors (and ourselves).
How?
props.category === "content"if we are in contentOnly modecategory="content"to the component instead.Testing Instructions
Testing Instructions for Keyboard
Screenshots or screencast