-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Description
I'd say the most important thing in API design is having consistent interfaces across different modules, especially when they're related and can be combined together. This lowers the learning curve, making it easier to use other modules when you're already familiar with one of them.
Unfortunately, a solid set of patterns that works consistently across all components in a component library is something really difficult to achieve. I've experienced this with Reakit, but it took a few years before we were able to come up with a set of patterns that would work for all modules in the library, and it's still not completely done. I started writing about this in 2018 on Introducing the Single Element Pattern and then gave a more technical update on that last year on this talk at React Finland.
And this is even more challenging on WordPress components given the components are designed to be more higher-level, which I agree is the right approach here. But it's easier to design component APIs when components render single HTML elements and you don't have to think about passing props to internal elements and customizing how they're rendered, not to mention React Native.
Because of that, instead of trying to include all components, I think we should take those that are related and can be combined together and make their APIs more similar.
Dropdown
(or Flyout
)
Currently, we have a Popover component that works as a generic element that can be positioned next to an anchor element or DOMRect. The anchor element can be passed via the anchorRef
prop. The DOMRect can be passed via the anchorRect
or getAnchorRect
props.
We also have the Dropdown component, which uses the generic Popover
underneath, but also renders a toggle button via the renderToggle
prop.
Finally, we're experimenting with a Flyout component that looks similar to Popover
and Dropdown
, but with a completely different API. The goal here is to find a concise API similar to the Dropdown
component API (with very few deprecations), but, ideally, we would use the Flyout
implementation.
<Dropdown label="Label" icon={ icon }>
<p>Content</p>
</Dropdown>
Aside from custom Dropdown
props, all the props passed to the Dropdown
component, including label
and icon
, would be passed over to the internal toggle button, which would render a Button
by default. This makes the toggleProps
prop obsolete.
The snippet above would be rendered to the DOM as something like this (simplified):
<button id="button" aria-label="Label"><svg /></button>
<div role="dialog" aria-labelledby="button">
<p>Content</p>
</div>
Controlled components
Currently, we have different ways to control the state of the Popover
components. The generic Popover
component has an onClose
prop. Dropdown
has onClose
and onToggle
.
Dropdown
also accepts render props like renderToggle
and renderContent
that pass isOpen
, onClose
and onToggle
as arguments to the render functions:
const args = { isOpen, onToggle: toggle, onClose: close }; |
Instead of relying on so many different ways to control the state, we can make semi-controlled components similar to how React deals with form inputs:
const [ isOpen, setIsOpen ] = useState( false );
<Dropdown text="Visible label" isOpen={ isOpen } onToggle={ setIsOpen }>
<p>Content</p>
</Dropdown>
This makes the renderToggle
and renderContent
props obsolete as we wouldn't need them to access the internal state anymore and the content could just be passed as children.
popoverProps
popoverProps
would remain the same.
<Dropdown popoverProps={ { className: 'my-popover' } } />
Menu
(or DropdownMenu
)
Currently, the DropdownMenu
has a controls
prop. I suggest we rename the component to Menu
to match the role="menu"
attribute and deprecate the controls
prop in favor of MenuItem
s passed as children. But DropdownMenu
would work as well.
<Menu label="Label" icon={ icon }>
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
</Menu>
The MenuItem component already exists and follow the same API as Button
, so we don't need to change anything here aside from connecting it to the Menu
component (through React Context) to provide a roving tabindex navigation similar to the Toolbar component.
The snippet above would be rendered to the DOM as something like this (simplified):
<button id="button" aria-label="Label"><svg /></button>
<div role="menu" aria-labelledby="button">
<button role="menuitem" aria-label="Label"><svg /></button>
<button role="menuitem" aria-label="Label"><svg /></button>
<button role="menuitem" aria-label="Label"><svg /></button>
<button role="menuitem" aria-label="Label"><svg /></button>
</div>
Sub menus
Because Menu
would use Dropdown
, and Dropdown
and MenuItem
has similar APIs, we could make Menu
aware of parent menus and render it as a sub menu:
<Menu label="Label" icon={ icon }>
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<Menu label="Label" icon={ icon }>
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
</Menu>
</Menu>
Toolbar
Finally, we can combine all those components together in the toolbar. We should provide components like ToolbarDropdown
and ToolbarMenu
that would render the toggle button as a toolbar item automatically:
<Toolbar label="Label">
<ToolbarButton label="Label" icon={ icon } />
<ToolbarButton label="Label" icon={ icon } />
<ToolbarDropdown label="Label" icon={ icon }>
<p>Content</p>
</ToolbarDropdown>
<ToolbarMenu label="Label" icon={ icon }>
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
<MenuItem label="Label" icon={ icon } />
</ToolbarMenu>
</Toolbar>
The Toolbar component already follows this API, so we don't need to change many things there.
How to connect those components to Toolbar
?
The components would be connected through the headless ToolbarItem component that already exists. ToolbarItem
passes down all the props necessary to render a toolbar item while being agnostic about which component will be rendered (it doesn't require it to be a Button
, for example). Here's an example of how the ToolbarDropdown
component could be created:
function ToolbarDropdown( props, ref ) {
return <ToolbarItem as={ Dropdown } ref={ ref } { ...props} />;
}
This is possible because we would pass the toolbar item props down to the toggle button on the Dropdown
component. We can leverage the same technique if we want to connect other components too. For example, we may have a headless MenuItem
component (maybe with another name to not conflict with the existing MenuItem
) to make any component a menu item.