Alternatives
- Bottom drawer — Used for heavy forms, especially if they contain datatables, to support the user's main activity without disrupting it.
- Side drawer — Used for ligh forms and information content to support the user's main activity without disrupting it.
ProductMenuController
Each usage of a ProductMenu
needs to be wrapped in a ProductMenuController
. Generally, the usage of ProductMenuController WON'T look like it does in the live examples. The live examples are illustrating what you can do with them. The actual usage of the product menus will look more like the code below. What's important to note here is that the ProductMenuController
is outside of the component that renders the product menu. This is so that state is cleared out of the product menu when it's closed.
Good ✅
const SomeProductMenu = () => { return <ProductMenu>{/** ... content goes here */}</ProductMenu>;};return ( <ProductMenuController> <SomeProductMenu /> </ProductMenuController>);
Bad ❌
return ( <ProductMenuController> <ProductMenu>{/** ... content goes here */}</ProductMenu> </ProductMenuController>);
Opening and closing a product menu
The consumer is responsible for tracking and updating open/closed states. ProductMenuController
takes an isOpen
prop to track the open/closed state of the product menu and an onClose
prop which is used when the user presses Escape
.
const [isOpen, toggleIsOpen] = React.useReducer((bool) => !bool, false);return ( <ProductMenuController isOpen={isOpen} onClose={toggleIsOpen}> {/** ... product menu stuff goes here */} </ProductMenuController>);
Why can't I conditionally render?
We need the ProductMenuController
component so that we can do an exit transition for the product menu. If you just did isOpen && <MyProductMenu />
, the product menu would unmount immediately so it wouldn't be possible to have an exit transition.
If you see a usage of the
ProductMenuController
in an app where it's the directly above a<ProductMenu>
, that will be updated. It's only like that to ease the transition period from isOpen being a prop on product menus to being on theProductMenuController
.
ProductMenu
A product menu is a modal dialog that slides in from the left side of the screen. They are used to display menus such as app switchers.
There can only be one instance of a product menu open at a time. Product menus don't stack like side drawers. product menus should not contain forms or text content. They should only display a list of menu items. product menus, bottom drawers and side drawers cannot be used together i.e a product menu should never have a bottom drawer or a side drawer inside it and vice versa.
Backgroud
ProductMenu
supports base(default)
and muted
backgrounds via the background
prop:
<ProductMenu background="muted"></ProductMenu>
Focus
By default the product menu will focus the outter most element on open. It's possible to focus a different element instead by using the initialFocusRef
prop. Check out the full example here.
Accessibility props
To maintain accessibility in scenarios when opting for a custom implementation of header and/or title, the aria-label
and aria-labelledby
props can be used. Check out the examples below for more details:
Composition
ProductMenu
content is composed using a combination of primitives and custom implementations. The primitives cover the most common use cases and where they aren't sufficient, it's possible to opt out in favor of a custom implementation.
product menu primitives follow a component as namespace strategy i.e all the primitives relating to ProductMenu
are exposed as ProductMenu.SomePrimitive
. Below is a list of all the primitives, their purpose and some sample code.
The code samples are meant to convey the general idea, for concrete examples on composition using primitives and custom implementations check out the patterns section.
ProductMenu.Header
Used to display the title and some optional subtext or actions. To be used inside ProductMenu
or ProductMenu.Form
.
<ProductMenu> <ProductMenu.Header> {/** ... header content goes here **/} </ProductMenu.Header></ProductMenu>
Behaviour and styling
This is a container component that provides horizontal and vertical padding and renders children as flex row.
Variance, when and how to opt out?
The header is a convenience layout primitive and hence can be easily opted out of. However, there should be little reason to opt out of it entirely. In case of a full opt out, it's the consumers responsibility to provide proper padding and spacing between elements.
It's possible to compose a header UI containing a heading, description text and an action as below:
<> <Stack flex={1} gap="xsmall"> <ProductMenu.Title>Title</ProductMenu.Title> <Text color="muted" size="small"> Optional description text </Text> </Stack> <IconButton label="Delete" size="small" colorScheme="critical" variant="text" icon={Trash2Icon} onClick={onClose} /></>
ProductMenu.Title
Title of the product menu. To be used inside ProductMenu.Header
or custom header implementation.
<ProductMenu.Header> <ProductMenu.Title>Title</ProductMenu.Title></ProductMenu.Header>
Variance, when and how to opt out?
The title primitive is a level 3 heading
under the hood and it should suffice in most cases. It exists so that consumers don't have to guess the heading level every time. The title should be fairly consistent across most product menus and hence warrant little reason for opting out.
Custom implementations of title must be connected to the ProductMenu
by using the aria-labelledby
prop to support proper accessibility.
<ProductMenu aria-labelledby="custom-title"> <ProductMenu.Header> <Heading id='custom-title' level="2">Large title</Heading> </ProductMenu.Header> <ProductMenu>
ProductMenu.Body
Container for the main product menu content. To be used inside ProductMenu
or ProductMenu.Form
<ProductMenu> <ProductMenu.Body>{/** ... scrollable content goes here */}</ProductMenu.Body></ProductMenu>
Behaviour and styles
This is a container component that renders children as flex column. If the UI requires a row layout you can simply use a flex box inside ProductMenu.Body
to wrap you content.
<ProductMenu> <ProductMenu.Body> <Flex>{/** ... content here is rendered as flex row now */}</Flex> </ProductMenu.Body></ProductMenu>
It does not apply any padding, it's the consumer's responsibility to apply the correct padding to the body. In most cases, a Box
with padding will suffice. Be sure to align the horizontal padding with the header content.
<ProductMenu> <ProductMenu.Body> <Box paddingX="xlarge" paddingY="large"> {/** ... we now have padding */} </Box> </ProductMenu.Body></ProductMenu>
Overflowing content is scrolled. When the body content is scrollable and user scrolls up, a fully bleed divider appears between the header and the body to improve affordance.
Variance, when and how to opt out?
Although the body content itself may have a lot of variance, it is not recommended to opt out of using ProductMenu.Body
as it provides some critical scrolling and border behaviours. These behaviours are difficult to implement correctly and hence one should have very good reasons for opting out of this primitive.
Receipes
Below are some of the most common use cases of product menus and how to compose them using primitives and custom code.
Composing content
Simple supporting text that offers an explanation of the users main flow.
Custom header actions
Custom actions such as delete in the header.
Custom title
When using a custom title instead of ProductMenu.Title
, the aria-labelledby
prop must be
provided to ProductMenu
.
Loading states
When display a loading state on initial load, the aria-label
prop must be provided to
ProductMenu
till the title is rendered.