-
Notifications
You must be signed in to change notification settings - Fork 4.5k
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.
My leaning would be similar to @mirka's in "have some kind of conditional logic before the fills hit the slot". I don't think operating on a component instance's raw props externally is generally advisable, and there are a few specific issues with it here (children in React shouldn't be treated as a raw array, incompatibility with TypeScript types in how we're using "category" as a synthetic prop that isn't actually typed on the component).
If the issue is making this less ad hoc and more maintainable, I'd explore patterns for reusability at the component rendering level. Maybe we can have some sort of ContentBlockControls
component that conditionally renders its children depending on the block mode? I might have historically suggested a higher-order component for this sort of conditional rendering, but I suppose those aren't very common or advisable these days.
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.
@jeryj Slot
already has the feature you need! It's when the children
prop is a function:
<Slot name="content">
{ ( fills ) => {
return fills.filter( ( item ) => item.props.category === 'content' );
} }
</Slot>
or
<Slot name="content">
{ ( fills ) => {
if ( fills.length === 0 ) {
return <span>Empty</span>;
}
return (
<ul>{ fills.map( ( item ) => <li>{ item }</li> ) }</ul>
);
} }
</Slot>
It's used quite a lot across Gutenberg.
One important caveat that slots of the bubblesVirtually
type don't support it. These are rendered using portals, where the Slot
has zero intelligence and serves only as a target element for portals. It doesn't know anything about the rendered fills, it just renders and element and registers it. But your slots seem to be of the right type.
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.
@@ -41,7 +41,7 @@ const FormatToolbarContainer = ( { inline, editableContentElement } ) => { | |||
// Render regular toolbar. | |||
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.
@@ -726,6 +725,7 @@ export default function Image( { | |||
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 Symbol
s, 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