UNPKG

@spectrum-css/menu

Version:
778 lines (763 loc) 15.6 kB
import { Template as ActionButton } from "@spectrum-css/actionbutton/stories/template.js"; import { Template as Checkbox } from "@spectrum-css/checkbox/stories/template.js"; import { Template as Divider } from "@spectrum-css/divider/stories/template.js"; import { Template as Icon } from "@spectrum-css/icon/stories/template.js"; import { Template as Popover } from "@spectrum-css/popover/stories/template.js"; import { Container, getRandomId } from "@spectrum-css/preview/decorators"; import { Template as Switch } from "@spectrum-css/switch/stories/template.js"; import { Template as Tray } from "@spectrum-css/tray/stories/template.js"; import { html } from "lit"; import { classMap } from "lit/directives/class-map.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { styleMap } from "lit/directives/style-map.js"; import { when } from "lit/directives/when.js"; import "../index.css"; import "../themes/spectrum.css"; /* Must be imported last */ import "../themes/express.css"; /** * Get the tray submenu back arrow name with scale number (defined in design spec). */ const iconWithScale = (size = "m", iconName = "ArrowLeft") => { switch (size) { case "s": return `${iconName}200`; case "l": return `${iconName}400`; case "xl": return `${iconName}500`; default: return `${iconName}300`; } }; export const MenuItem = ( { rootClass = "spectrum-Menu-item", label, description, iconName, iconSet = "workflow", hasActions = false, id = getRandomId("menuitem"), idx = 0, isActive = false, isCollapsible = false, isDisabled = false, isDrillIn = false, isFocused = false, isHighlighted = false, isHovered = false, isOpen = false, isSelected = false, items = [], role = "menuitem", shouldTruncate = false, size = "m", selectionMode = "none", value, customClasses = [], customStyles = {}, } = {}, context = {}, ) => html` <li class=${classMap({ [rootClass]: true, "is-highlighted": isHighlighted, "is-active": isActive, "is-focus-visible": isFocused, "is-selected": isSelected, "is-disabled": isDisabled, "is-hover": isHovered, [`${rootClass}--drillIn`]: isDrillIn, [`${rootClass}--collapsible`]: isCollapsible, "is-open": isOpen, ...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}), })} style=${styleMap(customStyles)} id=${ifDefined(id)} role=${ifDefined(role)} aria-selected=${isSelected ? "true" : "false"} aria-disabled=${isDisabled ? "true" : "false"} tabindex=${ifDefined(!isDisabled ? "0" : undefined)} > ${when(isCollapsible || (selectionMode == "single" && isSelected), () => Icon( { iconName: iconWithScale( size, isCollapsible ? "ChevronRight" : "Checkmark", ), setName: "ui", useRef: false, size, customClasses: [ `${rootClass}Icon`, isCollapsible ? "spectrum-Menu-chevron" : "spectrum-Menu-checkmark", ], }, context, ), )} ${when(selectionMode === "multiple" && !hasActions, () => Checkbox( { size, isEmphasized: true, isChecked: isSelected, isDisabled, id: `menu-checkbox-${idx}`, customClasses: [`${rootClass}Checkbox`], }, context, ), )} ${when(iconName, () => Icon( { iconName, setName: iconSet, size, customClasses: [`${rootClass}Icon`, `${rootClass}Icon--workflowIcon`], }, context, ), )} ${when( isCollapsible, () => html` <span class=${classMap({ ["spectrum-Menu-sectionHeading"]: true, [`${rootClass}Label--truncate`]: shouldTruncate, })} > ${label} </span> `, () => html` <span class=${classMap({ [`${rootClass}Label`]: true, ["spectrum-Switch-label"]: hasActions, [`${rootClass}Label--truncate`]: shouldTruncate, })} > ${label} </span> `, )} ${when( description, () => html` <span class=${classMap({ [`${rootClass}Description`]: true, })} > ${description} </span> `, )} ${when( value, () => html` <span class=${classMap({ [`${rootClass}Value`]: true, })} > ${value} </span> `, )} ${when( hasActions && selectionMode == "multiple", () => html` <div class=${classMap({ [`${rootClass}Actions`]: true, })} > ${Switch( { size, isChecked: isSelected, isDisabled, label: null, id: `menu-switch-${idx}`, customClasses: [`${rootClass}Switch`], }, context, )} </div> `, )} ${when(isDrillIn, () => Icon( { iconName: iconWithScale(size, "ChevronRight"), setName: "ui", useRef: false, size, customClasses: [`${rootClass}Icon`, "spectrum-Menu-chevron"], }, context, ), )} ${when(isCollapsible && items.length > 0, () => Template( { items, isOpen, size, shouldTruncate, }, context, ), )} </li> `; export const MenuGroup = ( { hasActions = false, heading, id = getRandomId("menugroup"), idx = 0, items = [], isDisabled = false, isSelectable = false, isTraySubmenu = false, shouldTruncate = false, subrole = "menuitem", size = "m", selectionMode = "none", customStyles = {}, } = {}, context = {}, ) => html` <li id=${ifDefined(id)} role="presentation"> ${when( !isTraySubmenu, () => html` <span class=${classMap({ ["spectrum-Menu-sectionHeading"]: true, ["spectrum-Menu-itemLabel--truncate"]: shouldTruncate, })} id=${ifDefined(id ?? `menu-heading-category-${idx}`)} aria-hidden="true" > ${heading} </span> `, () => html` <div class=${classMap({ ["spectrum-Menu-back"]: true, })} > <button aria-label="Back to previous menu" class="spectrum-Menu-backButton" type="button" role="menuitem" > ${Icon( { iconName: iconWithScale(size), setName: "ui", size, customClasses: ["spectrum-Menu-backIcon"], }, context, )} </button> ${when( heading, () => html` <span class=${classMap({ ["spectrum-Menu-sectionHeading"]: true, ["spectrum-Menu-itemLabel--truncate"]: shouldTruncate, })} style=${styleMap(customStyles)} id=${id} aria-hidden="true" > ${heading} </span> `, )} </div> `, )} ${Template( { role: "group", subrole, labelledby: id, hasActions, items, isDisabled, isSelectable, selectionMode, shouldTruncate: true, size, }, context, )} </li> `; export const Template = ( { rootClass = "spectrum-Menu", customClasses = [], customStyles = {}, id = getRandomId("menu"), hasActions = false, hasDividers = false, isDisabled = false, isOpen = false, isTraySubmenu = false, items = [], labelledby = getRandomId("menu-label"), role = "menu", selectionMode = "none", singleItemValue, shouldTruncate, size = "m", subrole = "menuitem", } = {}, context = {}, ) => { const menuMarkup = html` <ul class=${classMap({ [rootClass]: true, [`${rootClass}--size${size?.toUpperCase()}`]: typeof size !== "undefined", "is-selectable": selectionMode === "single", "is-selectableMultiple": selectionMode === "multiple", "is-open": isOpen, ...customClasses.reduce((a, c) => ({ ...a, [c]: true }), {}), })} id=${ifDefined(id)} role=${ifDefined(role)} aria-labelledby=${ifDefined(labelledby)} aria-disabled=${isDisabled ? "true" : "false"} style=${styleMap(customStyles)} > ${items.map((i, idx) => { if (i.type === "divider") return html`${hasDividers ? Divider({ tag: "li", size: "s", customClasses: [`${rootClass}-divider`], }) : ""}`; else if (i.heading || i.isTraySubmenu) return MenuGroup( { ...i, subrole, size, selectionMode, isTraySubmenu, shouldTruncate, }, context, ); else return MenuItem( { ...i, hasActions, idx, isDisabled: isDisabled || i.isDisabled, role: subrole, rootClass: `${rootClass}-item`, selectionMode, shouldTruncate, size, value: singleItemValue || i.value, }, context, ); })} </ul> `; if (isTraySubmenu) return Tray({ isOpen: true, content: [ menuMarkup ], }, context); return menuMarkup; }; export const DisabledItemGroup = (args, context) => { const groupData = [ { heading: "Menu with icons", items: [ { label: "Cut", iconName: "Cut", }, { label: "Copy", iconName: "Copy", }, { label: "Paste", iconName: "Paste", isDisabled: true, } ], }, { heading: "Menu with descriptions", items: [ { label: "Quick export", description: "Share a snapshot", }, { label: "Open a copy", description: "Illustrator for iPad", }, { label: "Share link", description: "Enable comments and download", isDisabled: true, } ] }, { heading: "Menu with icons & descriptions", items: [ { label: "Quick export", description: "Share a snapshot", iconName: "Export", }, { label: "Open a copy", description: "Illustrator for iPad", iconName: "FolderOpen", }, { label: "Share link", description: "Enable comments and download", iconName: "Share", isDisabled: true, } ] } ]; return Container({ withBorder: false, content: groupData.map((group) => html` ${Container({ heading: group.heading, content: html` ${Template({ ...args, context, shouldTruncate: group.shouldTruncate || false, items: group.items, })} ` }, context)} `) }, context); }; export const OverflowGroup = (args, context) => { const groupData = [ { heading: "Text overflow without descriptions", items: [ { label: "Small (works best for mobile phones)" }, { label: "Medium (all purpose)" }, { label: "Large (works best for printing)" } ], }, { heading: "Text overflow with descriptions", items: [ { label: "Small (works best for mobile phones)", description: "A small description about small is here", }, { label: "Medium (all purpose)", description: "A medium description about medium is here", }, { label: "Large (works best for printing)", description: "A large description about large is here", } ], }, { heading: "Text truncation with descriptions", shouldTruncate: true, items: [ { label: "Small (works best for mobile phones)", description: "A small description about small is here", }, { label: "Medium (all purpose)", description: "A medium description about medium is here", }, { label: "Large (works best for printing)", description: "A large description about large is here", } ], }, { heading: "Text truncation for section headings", shouldTruncate: true, items: [ { idx: 1, heading: "Section heading with longer text that truncates", id: "menu-heading", items: [ { label: "Small (works best for mobile phones)", }, { label: "Medium (all purpose)", }, { label: "Large (works best for printing)", } ] } ] }, { heading: "Text truncation with drill-ins and values", shouldTruncate:true, items: [ { label: "Quick export truncated text", iconName: "Export", description: "Share a low-res snapshot", }, { label: "Open a copy truncated text", iconName: "Copy", description: "Illustrator for iPad or desktop", isDrillIn: true, }, { label: "Preview timelapse truncated text", iconName: "Pending", value: "Value", } ] } ]; return Container({ withBorder: false, content: groupData.map((group) => html` ${Container({ heading: group.heading, content: html` ${Template({ ...args, context, shouldTruncate: group.shouldTruncate || false, items: group.items, })} ` })} `) }); }; export const SelectionGroup = (args, context) => { const groupData = [ { heading: "No selection", items: [ { label: "Cut" }, { label: "Copy" }, { label: "Paste" }, ], }, { heading: "Single selection", selectionMode: "single", items: [ { label: "Marquee", isSelected: true, }, { label: "Add", }, { label: "Subtract", } ], }, { heading: "Multiple selection with checkboxes", selectionMode: "multiple", items: [ { label: "Marquee", isSelected: true, }, { label: "Add", }, { label: "Subtract", } ], }, { heading: "Multiple selection with checkboxes and icons", selectionMode: "multiple", items: [ { label: "Marquee", iconName: "Selection", isSelected: true, }, { label: "Add", iconName: "SelectAdd", }, { label: "Subtract", iconName: "SelectSubtract", } ], }, { heading: "Multiple selection with switches", selectionMode: "multiple", hasActions: true, items: [ { label: "Guides", isSelected: true, }, { label: "Grid", }, { label: "Rulers", isSelected: true, } ], }, { heading: "Multiple selection with switches and icons", selectionMode: "multiple", hasActions: true, items: [ { label: "Marquee", iconName: "Selection", isSelected: true, }, { label: "Add", iconName: "SelectAdd", }, { label: "Subtract", iconName: "SelectSubtract", } ], }, ]; return Container({ withBorder: false, content: groupData.map((group) => Container({ heading: group.heading, content: Template({ ...args, context, selectionMode: group.selectionMode || "none", hasActions: group.hasActions || false, items: group.items, }) }, context)) }); }; export const SubmenuInPopover = (context) => Popover({ isOpen: true, position: "end-top", customStyles: { "inline-size": "200px", }, trigger: (args, context) => ActionButton({ label: "Settings", iconName: "Settings", ...args, }, context), content: [ (args, context) => Template({ items: [ { label: "Language", value: "English (US)", isDrillIn: true, isHovered: true, }, { label: "Notifications", }, { label: "Show grid", } ], ...args }, context), (args, context) => Popover({ isOpen: true, position: "end-top", customStyles: { top: "-120px", "inline-size": "120px", }, content: [ (args, context) => Template({ selectionMode: "single", items: [ { label: "Deutsch", }, { label: "English (US)", isSelected: true, }, { label: "Español", }, { label: "Français", }, { label: "Italiano", }, { label: "日本語", } ], ...args, }, context) ], ...args, }, context) ], }, context);