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

feat(Menu)!: updates menu variants and usage guidance #2993

Merged
merged 15 commits into from
Jan 31, 2025
2 changes: 1 addition & 1 deletion packages/gamut/src/DataList/Controls/FilterControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const FilterControl: React.FC<FilterProps> = ({
maxHeight={300}
overflowY="auto"
width="max-content"
variant="action"
variant="popover"
>
{[SELECT_ALL, ...options].map((opt) => {
const { text, value } = formatOption(opt);
Expand Down
2 changes: 1 addition & 1 deletion packages/gamut/src/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const Menu = forwardRef<
Omit<ComponentProps<typeof List>, 'root'>
>(
(
{ children, variant = 'select', spacing = 'normal', role, ...rest },
{ children, variant = 'popover', spacing = 'normal', role, ...rest },
ref
) => {
const currentContext = useMenu({ variant, role, spacing });
Expand Down
4 changes: 2 additions & 2 deletions packages/gamut/src/Menu/MenuContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { ListProps } from '../List';

export interface MenuContextProps {
spacing: 'normal' | 'condensed';
variant: 'select' | 'navigation' | 'action';
variant: 'popover' | 'fixed';
depth: number;
role: ListProps['role'];
}

export const MenuContext = createContext<MenuContextProps>({
spacing: 'normal',
variant: 'select',
variant: 'popover',
depth: 0,
role: undefined,
});
Expand Down
7 changes: 3 additions & 4 deletions packages/gamut/src/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ const getListItemType = (href: boolean, onClick: boolean) =>
href ? 'link' : onClick ? 'button' : 'item';

const activePropnames = {
navigation: 'active-navlink',
action: 'active',
select: 'selected',
fixed: 'active-navlink',
popover: 'active',
};

const currentItemText = {
Expand All @@ -41,7 +40,7 @@ export const MenuItem = forwardRef<
const computed = {
...props,
...rest,
variant: variant === 'select' ? 'select' : 'link',
variant: 'link',
role: role === 'menu' ? 'menuitem' : undefined,
[activeProp]: active,
} as ListItemProps;
Expand Down
4 changes: 0 additions & 4 deletions packages/gamut/src/Menu/MenuSeparator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import * as React from 'react';

import { Box, FlexBox } from '../Box';
import { FlexBoxProps } from '../Box/props';
import { useMenuContext } from './MenuContext';

interface MenuSeperatorProps extends FlexBoxProps {
children?: never;
}

export const MenuSeparator: React.FC<MenuSeperatorProps> = (props) => {
const { variant } = useMenuContext();
if (variant !== 'action') return null;

return (
<FlexBox as="li" role="separator" height={8} fit center {...props}>
<Box fit height="1px" bg="text-disabled" borderRadius="sm" mx={16} />
Expand Down
21 changes: 7 additions & 14 deletions packages/gamut/src/Menu/__tests__/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ describe('Menu', () => {
screen.getByRole('button');
expect(screen.queryByRole('menuitem')).toBeNull();
});
it('renders menu separators when the variant is action', () => {
it('renders menu separators when variant is popover', () => {
renderView({
variant: 'action',
variant: 'popover',
children: <MenuSeparator />,
});

screen.getByRole('separator');
});
it('renders deep menu separators while the parent variant is action', () => {
it('renders deep menu separators', () => {
renderView({
variant: 'action',
variant: 'popover',
children: (
<Menu>
<MenuSeparator />
Expand All @@ -59,20 +59,13 @@ describe('Menu', () => {

screen.getByRole('separator');
});
it('does not render separators when the variant is select + navigation', () => {
it('renders menu separators when variant is fixed', () => {
renderView({
variant: 'select',
variant: 'fixed',
children: <MenuSeparator />,
});

expect(screen.queryByRole('separator')).toBeNull();

renderView({
variant: 'navigation',
children: <MenuSeparator />,
});

expect(screen.queryByRole('separator')).toBeNull();
screen.getByRole('separator');
});
it('renders and icon only when specified', () => {
renderView({
Expand Down
17 changes: 14 additions & 3 deletions packages/gamut/src/Menu/elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ export interface ListProps extends ListStyleProps, StyleStateProps {
/** How offset spacing should be */
spacing?: 'normal' | 'condensed';
/** Menu variants for specific use cases and styles */
variant?: 'select' | 'navigation' | 'action';
variant?: 'popover' | 'fixed';
/** is root menu */
root?: boolean;
/** bordered */
as?: 'ul' | 'ol';
showBorder?: boolean;
}

const StyledList = styled('ul', styledOptions<'ul'>())<ListProps>(
Expand All @@ -54,6 +55,8 @@ const StyledList = styled('ul', styledOptions<'ul'>())<ListProps>(
minWidth: 192,
bg: 'background',
p: 0,
},
showBorder: {
border: 1,
borderRadius: 'sm',
},
Expand All @@ -65,8 +68,16 @@ const StyledList = styled('ul', styledOptions<'ul'>())<ListProps>(
export const List = forwardRef<
HTMLUListElement,
ComponentProps<typeof StyledList>
>(({ context = true, m = 0, root = true, ...rest }, ref) => (
<StyledList context={context} m={m} root={root} ref={ref} {...rest} />
>(({ context = true, m = 0, root = true, variant, ...rest }, ref) => (
<StyledList
context={context}
m={m}
root={root}
showBorder={variant !== 'fixed'}
ref={ref}
variant={variant}
{...rest}
/>
));

const interactiveVariants = system.variant({
Expand Down
98 changes: 54 additions & 44 deletions packages/styleguide/src/lib/Molecules/Menu/Menu.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Column, LayoutGrid } from '@codecademy/gamut';
import { Canvas, Controls, Meta } from '@storybook/blocks';

import { ComponentHeader } from '~styleguide/blocks';
import { ComponentHeader, ImageWrapper, LinkTo } from '~styleguide/blocks';

import * as MenuStories from './Menu.stories';

Expand All @@ -24,7 +23,8 @@ export const parameters = {
<ComponentHeader {...parameters} />

## Usage
While most Menus are used as layout elements, Menus can also be used as popover elements within components like Selects.

Use a Menu to organize and present a list of actions, options, or navigation links.

### Components

Expand All @@ -34,71 +34,81 @@ While most Menus are used as layout elements, Menus can also be used as popover

### Best practices:

- A `Menu` should only have the `menu` role when it is a list of actions or functions a user can evoke.
- For example - a popover menu that allows the user to copy text, paste text, or reset a workspace.
- If your `Menu` is simply a list of links it should not have the `menu` role.
- Adding the role of `menu` to the menu component will programmatically set the correct roles on its `MenuItem`s and `MenuSeparator`s.
- The Popover menu has the `menu` role by default, which is intended for presenting actions or options. However, if it contains only navigation links, it should use the `<nav>` element for semantic and accessible navigation.
- You can create floating menus by utilizing our <LinkTo id="Atoms/PopoverContainer">PopoverContainer</LinkTo>. Once you have your base positioning, you just need to adjust the y axis by 48 for the normal spacing or 32 for the condensed spacing for each of the MenuItems. You may also need to change the alignment of the PopoverContainer to ensure correct positioning.

### When NOT to use:

- Form Inputs - For selecting from a predefined set of choices within a form, use the SelectDropdown component.
- Expandable Menus - For organizing multiple collapsible sections of navigation links in a structured layout, use the LayoutMenu component.
- Switching between content - For toggling between multiple related views or content sections within the same context, use the Tabs component.

## Anatomy

<ImageWrapper src="./molecules/menuAnatomy.png" alt="Menu Anatomy diagram"/>

1. Leading Icon (optional)

- Use to visually reinforce the purpose of a menu item, improve scannability, or differentiate between actions.

2. Label

- Limit to 1-3 words.
- For actions, use a verb that clearly explains what will happen.
- For navigation to information, use a noun.
- Keep the same word form for each grouping. (For example, all nouns or all verbs.)
- Order and group by meaning or alphabetically.
- Avoid placing words that look the same (for example, that start with the exact same letters) adjacent to one another.

If you would like to know more about the `menu` role - check out the MDN web docs [here](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menu_role).
3. Menu separator (optional)

- Use to group menu items into sections when there is a significant number of items, making lengthy menus easier to scan and navigate.

## Variants

<LayoutGrid gap={32} py={24}>
### Popover

Use the Popover menu to present a temporary surface for actions, options, or links, ensuring access only when needed while keeping the interface clean and focused. The Popover menu has the `menu` role by default. But, if the menu contains only a list of navigation links, it should be nested in a `<nav>` element, which implicitly includes the `navigation` role.

<Column size={4} variant={false} mx={8}>
### Select
<Canvas of={MenuStories.Default} />
A general purpose menu type that can be used for actions or navigation.
</Column>
<Canvas of={MenuStories.Popover} />

<Column size={4} variant={false}>
### Action
<Canvas of={MenuStories.Action} />
These should be populated with MenuItems typed as buttons that execute some action.
</Column>
### Fixed

<Column size={4} variant={false}>
### Navigation
<Canvas of={MenuStories.Navigation} />
These should be populated with MenuItems typed as links.
</Column>
Use the Fixed menu to provide persistent access to navigation links, allowing users to seamlessly move between sections of the interface without opening additional surfaces. Fixed menus should always use the `<nav>` element to ensure semantic and accessible navigation.

</LayoutGrid>
<Canvas of={MenuStories.Fixed} />

## Spacing variants

Setting the Menu's `spacing` to `'condensed'` will reduce the padding around the MenuItems.

<LayoutGrid gap={32} py={24}>
### Popover condensed

<Column size={4} variant={false} mx={8}>
### Select condensed
<Canvas of={MenuStories.SelectCondensed} />
</Column>
<Canvas of={MenuStories.PopoverCondensed} />

<Column size={4} variant={false}>
### Action condensed
<Canvas of={MenuStories.ActionCondensed} />
</Column>
### Fixed condensed

<Column size={4} variant={false}>
### Navigation condensed
<Canvas of={MenuStories.NavigationCondensed} />
</Column>
<Canvas of={MenuStories.FixedCondensed} />

</LayoutGrid>
## Menu separator

## Popover menus
Use the `MenuSeparator` component to group menu items into sections when there is a significant number of items, making lengthy menus easier to scan and navigate.

You can create popover menus by utilizing our **PopoverContainer**. Once you have your base positioning, you just need to adjust the `y` axis by `48` for the `normal` spacing or `32` for the `condensed` spacing for each of the `MenuItem`s. You may also need to change the `alignment` of the `PopoverContainer` to ensure correct positioning.
<Canvas of={MenuStories.PopoverMenuSeparator} />
<Canvas of={MenuStories.FixedMenuSeparator} />

Popover Menus that have complex functionalities or actions, like the example below, should have the `menu` role.
## Floating menus

Click through each item to see how the `Menu`s appear (and disappear when the item is clicked again, or another item is selected.)
You can create floating menus by utilizing our <LinkTo id="Atoms/PopoverContainer">PopoverContainer</LinkTo>. Once you have your base positioning, you just need to adjust the y axis by 48 for the normal spacing or 32 for the condensed spacing for each of the MenuItems. You may also need to change the alignment of the PopoverContainer to ensure correct positioning.

<Canvas of={MenuStories.Popover} />
Floating menus that have complex functionalities or actions, like the example below, should have the menu role.

Click through each item to see how the `Menu`s appear (and disappear when the item is clicked again, or another item is selected.)

<Canvas of={MenuStories.FloatingMenuExample} />

## Playground

<Canvas sourceState="shown" of={MenuStories.Default} />

<Controls />
Loading
Loading