wwnorton/design-system

Menu Component [NDS-45]

sh0ji opened this issue · 3 comments

sh0ji commented

We need a component that can be used to display a list of actions.

References

NDS-45

sh0ji commented

On GitLab, I framed this as an "Actionable list of items." I'd like to reframe it as a "menu" but here's the post from GitLab for posterity.


We have a few different components that will use actionable lists of items where the visual design of those items should be consistent but the actions and the interaction designs may be different. How do we want to handle this?

For instance, Dropdown, Combo Box, and Menu should display a list of items on some user action.

  • Dropdown (#72) and Combo Box should both use it to display a list of selectable items (options).
    • For dropdown it should appear when the user clicks the dropdown button.
    • For combo box it should display items dynamically as the user types.
    • The action for both should be select, which simply chooses an option but does not submit it or navigate the user anywhere.
  • Menu should use it to display a list of actionable menu items.
    • If it's a navigation menu, the items should be actual links and the action should be navigation.
    • If it's an action menu, the items should be buttons and the action should be functional (generic action that does not navigate).

The shared visual design between these is effectively a non-interactive "list group," similar to Bootstrap's list group.

Anatomy/Toolkit (Work in progress)

@btri_wwn is already designing this as it relates to Menu and Dropdown and we discussed some of it already so I thought it'd be good to get the ideas down here. These are the components that we might use to compose a list of items.

  • ListItem: the generic single actionable item. Cannot contain anything except text. If it's in a listbox, it's selectable; if it's in a navigation menu, it's a link; etc.
    • Would it be better to create the different actions as different components (e.g., ListOption, ListLink, etc.), in which case the ListItem may be better named BaseListItem; or should we use properties in some clear way (e.g., <ListItem variant="link" />)?
  • ListGroup: the container for a collection of ListItems. Can contain any of the List-based items, including other ListGroups.
  • ListHeader: a non-actionable list item that can optionally appear as the first item in a ListGroup. Can only contain text.
  • ListDivider: a non-actionable separator used to create space between items. Cannot contain anything.
sh0ji commented

Here's an updated proposal in pseudocode, all modelled on the NERd Next menu mockup.

Starting with the React JSX render component, imagining some functions and props that might be in context:

<Menu aria-labelledby="menu-title">
	<MenuItemLabel id="menu-title">longuseremail@emailservice.com</MenuItemLabel>
	<MenuDivider />
	<MenuItemAction onClick={toggleClassic}>Switch to Classic View</MenuItemAction>
	<MenuGroup label="Instructor settings" hideLabel>
		<MenuItemLink href="/student-sets">Manage Student Sets</MenuItemLink>
		<MenuItemAction onClick={showAuthorizeModal}>Authorize an Instructor</MenuItemAction>
		<MenuItemLink href={`/purchase?isbn=${printIsbn}`}>Purchase a Print Textbook</MenuItemLink>
		<MenuItemLink href="/help">Help</MenuItemLink>
	</MenuGroup>
	<MenuGroup>
		<MenuItemLink href="/change-email">Change Your Email</MenuItemLink>
		<MenuItemLink href="/change-password">Change Password</MenuItemLink>
		<MenuItemAction onClick={logout}>Sign Out</MenuItemAction>
	</MenuGroup>
</Menu>

That would then render to something like this in HTML (omitting any classes or styling):

<ul role="menu" aria-labelledby="menu-title">
	<li role="none" id="menu-title">longuseremail@emailservice.com</li>
	<li role="separator"></li>
	<li role="none">
		<button type="button" role="menuitem">Switch to Classic View</button>
	</li>
	<li role="separator"></li>
	<li role="none">
		<ul role="group" aria-label="Instructor settings">
			<li role="none">
				<a role="menuitem" href="/student-sets">Manage Student Sets</a>
			</li>
			<li role="none">
				<button type="button" role="menuitem">Authorize an Instructor</button>
			</li>
			<li role="none">
				<a role="menuitem" href="/purchase?isbn=9780393674033">Purchase a Print Textbook</a>
			</li>
			<li role="none">
				<a role="menuitem" href="/help">Help</a>
			</li>
		</ul>
	</li>
	<li role="separator"></li>
	<li role="none">
		<ul role="group">
			<li role="none">
				<a role="menuitem" href="/change-email">Change Your Email</a>
			</li>
			<li role="none">
				<a role="menuitem" href="/change-password">Change Password</a>
			</li>
			<li role="none">
				<button type="button" role="menuitem">Sign Out</button>
			</li>
		</ul>
	</li>
</ul>

Components

  • Menu - the actual menu container, which doesn't provide any affordances for composition (i.e., dropping down or absolute positioning). That would be accomplished by placing it inside a BasePopper or using the usePopper hook to render it.
  • MenuGroup - a grouping component that can optionally include a MenuDivider before/after. Note that this has a very similar—if not identical—API to the Menu. The main difference is the role="group" vs role="menu".
    • We'll want to consider MenuGroup variants like MenuGroupRadio and MenuGroupCheckbox to accommodate choice menus like what's seen in the APG editor menubar. That shouldn't be a priority for the initial implementation, though.
  • MenuDivider is just an empty role="separator" item.
    • This could alternatively be rendered as a <li role="none"><hr /></li> if we want to leave the rendered divider line up to HTML (the <hr> element has an implicit role of separator).
    • Alternative names: MenuItemDivider, MenuSeparator, or MenuItemSeparator.
  • MenuItemLabel - all menu components except the MenuDivider can have a label, which is a good practice for accessibility. This component renders that label for Menu and MenuGroup.
  • MenuItemAction - a menu item that performs an action (the button version).
  • MenuItemLink - a menu item that navigates somewhere (the anchor version).

Content Model

That could all be abstracted into a single object that might look something like this, which could very well be a large props object for the Menu component (i.e., <Menu {...menuProps} />).

const menuProps = {
	label: 'longuseremail@emailservice.com',
	role: 'menu',
	items: [
		MenuDivider,  // explicit divider React component (renders as <li role="separator">)
		{
			label: 'Instructor settings',	// optional group label
			hideLabel: true,
			items: [  // nested items imply type='group' (renders as <ul role="group">)
				{
					label: 'Manage student sets',
					href: '/student-sets',	// href implies type='link' (renders as <a href>)
				},
				{
					label: 'Authorize an instructor',
					action: showAuthorizeModal,  // action implies type='action' (renders as <button>)
				},
				{
					label: 'Purchase a Print Textbook',
					href: `/purchase?isbn=${printIsbn}`,
				},
				{
					label: 'Help',
					href: '/help',
				},
			],
		},
		{
			items: [
				{
					label: 'Change Your Email',
					href: '/change-email',
				},
				{
					label: 'Change Password',
					href: '/change-email',
				},
				{
					label: 'Sign Out',
					action: logout,
				},
			],
		},
	],
};

Issue linked to JIRA Story: NDS-45