@spectrum-css/menu
Version:
The Spectrum CSS menu component
640 lines (625 loc) • 16.6 kB
JavaScript
import { default as IconStories } from "@spectrum-css/icon/stories/icon.stories.js";
import { Sizes } from "@spectrum-css/preview/decorators";
import { disableDefaultModes } from "@spectrum-css/preview/modes";
import { isActive, isDisabled, isFocused, isHovered, isSelected, size } from "@spectrum-css/preview/types";
import metadata from "../dist/metadata.json";
import packageJson from "../package.json";
import { MenuItemGroup, MenuTraySubmenu, MenuWithVariants } from "./menu.test.js";
import { DisabledItemGroup, OverflowGroup, SelectionGroup, SubmenuInPopover, Template } from "./template.js";
/**
* A menu is used for creating a menu list. The various elements inside a menu can be: a menu group, a menu item, or a
* menu divider. Often a menu will appear in a popover so that it displays as a toggling menu.
*/
export default {
title: "Menu",
component: "Menu",
argTypes: {
selectionMode: {
name: "Selection mode",
description: "Determines whether items in the menu can be selected, and how many",
type: { name: "string", required: true },
table: {
type: { summary: "string" },
category: "Selection",
},
options: ["none", "single", "multiple"],
control: "select",
},
size: size(["s", "m", "l", "xl"]),
shouldTruncate: {
name: "Truncate menu item label",
type: { name: "boolean" },
table: {
type: { summary: "boolean" },
category: "Component",
},
control: "boolean",
},
hasDividers: {
name: "Has dividers",
description: "Shows dividers between sections",
table: {
type: { summary: "boolean" },
category: "Component",
},
control: "boolean",
},
isTraySubmenu: {
name: "Is tray submenu",
type: { name: "boolean" },
table: {
type: { summary: "boolean" },
category: "Component",
},
control: "boolean",
},
labelledby: { table: { disable: true } },
items: { table: { disable: true } },
role: { table: { disable: true } },
subrole: { table: { disable: true } },
},
args: {
rootClass: "spectrum-Menu",
selectionMode: "none",
size: "m",
shouldTruncate: false,
role: "listbox",
subrole: "option",
hasDividers: false,
items: [
{ label: "Edit" },
{ label: "Select Inverse" },
{ label: "Save Selection" },
],
},
parameters: {
actions: {
handles: ["click .spectrum-Menu-item"],
},
docs: {
story: {
height: "300px"
}
},
design: {
type: "figma",
url: "https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2-%2F-Desktop?node-id=37252-553",
},
packageJson,
metadata,
},
};
export const Default = MenuWithVariants.bind({});
Default.tags = ["!autodocs"];
Default.argTypes = {
isTraySubmenu: { table: { disable: true } },
};
Default.args = {
items: [
{
idx: 1,
heading: "Menu header - Menu with icons",
id: "menu-heading-with-icons",
items: [
{
label: "Default menu item",
iconName: "Export"
},
{
label: "Focused menu item",
iconName: "FolderOpen",
isFocused: true,
isActive: true,
},
{
label: "A menu item with a longer label that causes the text to wrap to the next line",
iconName: "Send",
},
{
label: "Menu item with no icon",
},
{
label: "Disabled menu item",
iconName: "Share",
isDisabled: true,
},
],
},
{ type: "divider" },
{
idx: 2,
heading: "Menu header - With descriptions and icons",
id: "menu-heading-short-desc",
items: [
{
label: "Menu item with description",
description: "Short description",
},
{
label: "Selected item",
description: "This item is checked if single-select or multi-select mode is turned on",
isSelected: true,
},
{
label: "Selected item with icon",
iconName: "Cloud",
description: "This item is checked if single-select or multi-select mode is turned on",
isSelected: true,
},
],
},
{ type: "divider" },
{
idx: 3,
heading: "Menu header - With actions, icons, short descriptions, and values and longer header text that wraps",
id: "menu-heading-desc-icon-value",
hasActions: true,
items: [
{
label: "Menu item with action and a longer label that truncates if it is long enough to truncate",
iconName: "Cut",
description: "This item has a switch if multi-select mode is turned on.",
},
{
label: "Menu item with action",
iconName: "Copy",
description: "In multi-select mode, this item will be switched on. In single-select mode, this item will be checked.",
isSelected: true,
},
{
label: "Menu item with action and value",
iconName: "Paste",
description: "This item has a value. If multi-select mode is turned on, it also has a switch and the value can be used to label the switch.",
value: "⌘ C",
},
{
label: "Disabled menu item with action",
iconName: "Archive",
description: "Disabled menu item with description and icon",
isDisabled: true,
},
],
},
{
idx: 4,
heading: "Menu header - These menu items have drill-ins for a submenu",
id: "menu-heading-drillin",
items: [
{
label: "Menu item with drill-in",
isDrillIn: true,
},
{
label: "Menu item with drill-in and open submenu (not rendered)",
isDrillIn: true,
isOpen: true,
},
{
label: "Menu item with drill-in and value",
isDrillIn: true,
value: "Value",
},
{
label: "Disabled menu item with drill-in",
isDrillIn: true,
isDisabled: true,
}
],
},
],
};
/**
* When a menu is displayed within a tray, a submenu will replace the tray content when the parent menu item is
* selected. A submenu displays a back button (labeled by the title of the parent item) at the top of the tray to
* return the user to the previous level of the menu. The back arrow size scale used with the various menu sizes are
* small: 200, medium: 300, large: 400, and extra large: 500.
*/
export const TraySubmenu = MenuTraySubmenu.bind({});
TraySubmenu.storyName = "Submenu in tray";
TraySubmenu.argTypes = {
selectionMode: { table: { disable: true } },
hasDividers: { table: { disable: true } },
};
TraySubmenu.args = {
selectionMode: "multiple",
isTraySubmenu: true,
items: [
{
heading: "Snap to",
items: [
{
label: "Guides",
isSelected: true,
},
{
label: "Grid",
},
{
label: "Rulers",
},
]
}
],
};
TraySubmenu.parameters = {
layout: "fullscreen",
docs: {
story: {
inline: false,
height: "300px",
}
},
viewport: {
defaultViewport: "mobile2"
},
};
export const MenuItem = MenuItemGroup.bind({});
MenuItem.tags = ["!autodocs"];
MenuItem.argTypes = {
isDisabled,
isActive,
isFocused,
isHovered,
isSelected: {
...isSelected,
description: "Used with single or multi-select mode turned on",
},
label: {
name: "Label",
type: { name: "string" },
table: {
type: { summary: "string" },
category: "Content",
},
},
description: {
name: "Description",
description: "Additional information about the menu item",
type: { name: "string" },
table: {
type: { summary: "string" },
category: "Content",
},
},
value: {
name: "Value",
description: "Value of the menu item",
type: { name: "string" },
table: {
type: { summary: "string" },
category: "Content",
},
},
iconName: {
...(IconStories?.argTypes?.iconName ?? {}),
if: false,
},
hasActions: {
name: "Has switches",
description: "If multiple selection is enabled, show switches instead of checkboxes to show which items have been selected",
type: { name: "boolean" },
table: {
type: { summary: "boolean" },
category: "Selection",
},
control: "boolean",
if: { arg: "selectionMode", eq: "multiple" },
},
// These settings are not used in the MenuItem story
hasDividers: { table: { disable: true } },
isTraySubmenu: { table: { disable: true } },
};
MenuItem.args = {
label: "Start a chat",
iconName: "Chat",
description: "Menu item description",
value: "⌘ N",
isDisabled: false,
isActive: false,
isFocused: false,
isHovered: false,
isSelected: false,
hasActions: false,
};
MenuItem.parameters = {
design: {
type: "figma",
url: "https://www.figma.com/design/Mngz9H7WZLbrCvGQf3GnsY/S2-%2F-Desktop?node-id=37252-2242&node-type=frame&t=Kcz7zeePp3PeRusJ-11",
},
};
/**
* This option will display submenus in a collapsed, nested format within the parent menu’s container. It can be used
* for both popover and tray container styles.
*
* When displaying collapsible menu items in a tray, the tray should have a fixed height in order to prevent
* disorienting behavior when the items are collapsed or expanded.
*/
export const Collapsible = Template.bind({});
Collapsible.argTypes = {
selectionMode: { table: { disable: true } },
hasDividers: { table: { disable: true } },
isTraySubmenu: { table: { disable: true } },
};
Collapsible.parameters = {
chromatic: { disableSnapshot: true },
};
Collapsible.args = {
shouldTruncate: true,
items: [
{
label: "Web Design",
iconName: "DesktopAndMobile",
isCollapsible: true,
isOpen: true,
items: [
{
label: "Web Large",
itemValue: "1920 x 1080",
},
{
label: "Web Medium",
itemValue: "1366 x 768",
},
{
label: "Web Small",
itemValue: "1280 x 800",
},
],
},
{
label: "Mobile",
isCollapsible: true,
isOpen: true,
items: [
{
label: "Web Large",
itemValue: "1920 x 1080",
},
{
label: "Web Medium",
itemValue: "1366 x 768",
},
{
label: "Web Small",
itemValue: "1280 x 800",
},
],
},
{
label: "Tablet",
iconName: "DeviceTablet",
isCollapsible: true,
items: [
{ label: "Defaults to not visible within closed item" },
],
},
{
label: "Social Media",
iconName: "ShareAndroid",
isCollapsible: true,
items: [
{ label: "Defaults to not visible within closed item" },
],
},
{
label: "Watches",
iconName: "Watch",
isCollapsible: true,
items: [
{ label: "Defaults to not visible within closed item" },
],
},
],
};
// ********* DOCS ONLY ********* //
/**
* A menu item should always have a label that clearly describes the action or option that it represents. A menu item
* can also display a related value in the value area. Examples of values include the selected option from a submenu, a
* keyboard shortcut for the action, or other content that clarifies the menu item. When necessary, menu items may also
* display an icon and additional description text.
*
* Menu sizes should correspond to the size of the menu trigger component (such as an
* [action button](?path=/docs/components-action-button--docs)). Similarly, any components displayed inside a menu item
* (such as a [switch](?path=/docs/components-switch--docs)) must also be of the same size.
*/
export const Sizing = (args, context) => Sizes({
Template,
withHeading: false,
withBorder: false,
...args,
}, context);
Sizing.storyName = "Default";
Sizing.tags = ["!dev"];
Sizing.args = {
items: [
{
idx: 1,
label: "Menu item",
},
{
idx: 2,
label: "Menu item with icon",
iconName: "Cloud",
},
{
idx: 3,
label: "Menu item with optional description",
description: "Short description of menu item",
},
{
idx: 4,
label: "Menu item with value",
value: "Value",
},
{
idx: 5,
label: "Menu item with icon and description",
description: "Short description of menu item",
iconName: "Cloud",
},
]
};
Sizing.parameters = {
chromatic: { disableSnapshot: true },
};
/**
* When a menu item contains a submenu, a drill-in chevron will appear at the end of the menu item to show that a
* submenu is available.
*/
export const DrillInChevron = Template.bind({});
DrillInChevron.storyName = "Submenu drill-in chevron";
DrillInChevron.tags = ["!dev"];
DrillInChevron.parameters = {
chromatic: { disableSnapshot: true },
};
DrillInChevron.args = {
items: [
{
label: "Menu item (with submenu)",
isDrillIn: true,
},
{
label: "Menu item",
},
{
label: "Menu item",
}
],
};
export const PopoverSubmenu = SubmenuInPopover.bind({});
PopoverSubmenu.storyName = "Submenu in popover";
PopoverSubmenu.tags = ["!dev"];
PopoverSubmenu.parameters = {
layout: "padded",
chromatic: { disableSnapshot: true },
};
/**
* A menu section has the options of single selection, multiple selection, or having no selection. By default, menu
* items have no selection, and perform an action on press.
*
* For single selection menu sections, menu items show a single checkmark to indicate the selected item. Multiple
* selection menu sections display checkboxes or switches beside each menu item.
*/
export const MenuItemSelection = SelectionGroup.bind({});
MenuItemSelection.storyName = "Selection of menu items";
MenuItemSelection.tags = ["!dev"];
MenuItemSelection.parameters = {
chromatic: { disableSnapshot: true },
};
/**
* The last item in each of these menus is disabled. A menu item in a disabled state shows that an option exists, but
* is not available in that circumstance. This state can be used to maintain layout continuity and to communicate that
* an action may become available later.
*/
export const DisabledItems = DisabledItemGroup.bind({});
DisabledItems.storyName = "Disabled items";
DisabledItems.tags = ["!dev"];
DisabledItems.parameters = {
chromatic: { disableSnapshot: true },
};
/**
* When a menu item’s label or description exceed the available horizontal space, the default behavior is to wrap the
* text to a new line.
*
* Menu item labels and headings can be truncated with an ellipsis by using the `spectrum-Menu-itemLabel--truncate`
* class. Truncation can only occur if the menu has a set `inline-size` or `max-inline-size`, or it is constrained by
* the width of its parent element(s). For demonstration purposes, a `max-inline-size` is set on the following menu
* examples. When text is truncated, it should be revealed with a tooltip on hover (not displayed here).
*
* Truncation can be used in conjunction with icons, values, and drill-ins in a menu item. Note that descriptions
* within a menu item always wrap by default and do not have a truncation option.
*/
export const TextOverflow = OverflowGroup.bind({});
TextOverflow.storyName = "Text overflow";
TextOverflow.tags = ["!dev"];
TextOverflow.parameters = {
chromatic: { disableSnapshot: true },
};
TextOverflow.args = {
customStyles: {
"max-inline-size": "150px",
}
};
// story used in Picker component as well as docs page
/**
* Menu sections contain groupings of related menu items. Dividers appear between sections when two or more sections
* are used within the same menu.
*/
export const WithDividers = Template.bind({});
WithDividers.storyName = "Sections with dividers";
WithDividers.tags = ["!dev"];
WithDividers.parameters = {
chromatic: { disableSnapshot: true },
};
WithDividers.args = {
items: [
{ label: "Deselect" },
{ label: "Select inverse" },
{ label: "Feather" },
{ label: "Select and mask" },
{ type: "divider" },
{ label: "Save selection" },
{ label: "Make work path", isDisabled: true },
],
hasDividers: true,
};
/**
* Use a section header when a menu section requires a descriptor. Section headers are helpful when two or more
* sections differ in their functionality or relationships.
*/
export const WithDividersAndHeaders = Template.bind({});
WithDividersAndHeaders.storyName = "Sections with dividers and headers";
WithDividersAndHeaders.tags = ["!dev"];
WithDividersAndHeaders.parameters = {
chromatic: { disableSnapshot: true },
};
WithDividersAndHeaders.args = {
hasDividers: true,
selectionMode: "single",
items: [
{
idx: 1,
heading: "Tools",
id: "menu-tools",
selectionMode: "single",
items: [
{
label: "Marquee",
isSelected: true,
iconName: "Selection",
},
{
label: "Add",
iconName: "SelectAdd",
},
{
label: "Subtract",
iconName: "SelectSubtract",
},
]
},
{ type: "divider" },
{
idx: 2,
heading: "Actions",
id: "menu-actions",
selectionMode: "single",
items: [
{
label: "Deselect",
iconName: "Deselect",
isDisabled: true,
}
]
}
],
};
// ********* VRT ONLY ********* //
export const WithForcedColors = MenuItemGroup.bind({});
WithForcedColors.tags = ["!autodocs", "!dev"];
WithForcedColors.parameters = {
chromatic: {
forcedColors: "active",
modes: disableDefaultModes
},
};