@spaced-out/genesis-mcp 1.0.48 → 1.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/design-system.json +9 -43
- package/package.json +1 -1
package/data/design-system.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"metadata": {
|
|
3
|
-
"buildDate": "2026-09-
|
|
4
|
-
"version": "0.6.35
|
|
3
|
+
"buildDate": "2026-09-22T08:30:45.196Z",
|
|
4
|
+
"version": "0.6.35",
|
|
5
5
|
"designSystemPath": "/home/runner/work/ui-design-system/ui-design-system"
|
|
6
6
|
},
|
|
7
7
|
"components": {
|
|
@@ -1617,36 +1617,24 @@
|
|
|
1617
1617
|
"files": {
|
|
1618
1618
|
"main": {
|
|
1619
1619
|
"path": "Menu.d.ts",
|
|
1620
|
-
"content": "import * as React from 'react';\nimport type { Flow } from 'flow-to-typescript-codemod';\nimport
|
|
1620
|
+
"content": "import * as React from 'react';\nimport type { Flow } from 'flow-to-typescript-codemod';\nimport type { MenuClassNames, MenuLabelTooltip } from '../../types/menu';\nimport type { IconType } from '../../components/Icon/Icon';\ntype ClassNames = MenuClassNames;\ntype OptionClassNames = Readonly<{\n wrapper?: string;\n}>;\nexport type Virtualization = Readonly<{\n enable: boolean;\n /**\n * Height of an option row. A number applies to every row; pass a function\n * for per-row heights (e.g. taller rows for options with descriptions).\n * `index` is the position of the option in the filtered option list —\n * group title rows are not counted.\n */\n itemHeight?: number | ((option: MenuOption, index: number) => number);\n /**\n * Height of a group title row when virtualizing `groupTitleOptions`.\n * A number applies to every title; pass a function for per-title heights\n * (e.g. taller custom title nodes).\n */\n groupTitleHeight?: number | ((groupTitle: React.ReactNode, groupIndex: number) => number);\n /** Max height of the whole card in px, search/header/footer included; never below the first row. */\n menuHeight?: number;\n}>;\nexport interface MenuOption {\n key: string;\n classNames?: OptionClassNames;\n label?: string;\n secondaryLabel?: string;\n customComponent?: React.ReactNode;\n iconLeft?: string;\n iconLeftType?: IconType;\n iconRight?: string;\n iconRightType?: IconType;\n disabled?: boolean;\n optionSize?: MenuSizeTypes;\n optionVariant?: MenuOptionsVariant;\n keepMenuOpenOnOptionSelect?: boolean;\n indeterminate?: boolean;\n testId?: string;\n}\nexport interface BaseMenuProps {\n onSelect?: (option: MenuOption, arg2?: React.SyntheticEvent<HTMLElement> | null | undefined) => unknown;\n selectedOption?: MenuOption | null | undefined;\n optionsVariant?: MenuOptionsVariant;\n selectedKeys?: Array<string>;\n classNames?: ClassNames;\n size?: MenuSizeTypes;\n width?: string;\n menuDisabled?: boolean;\n isFluid?: boolean;\n onTabOut?: () => unknown;\n allowSearch?: boolean;\n resolveLabel?: (option: MenuOption) => string | React.ReactNode;\n resolveSecondaryLabel?: (option: MenuOption) => string | React.ReactNode;\n virtualization?: Virtualization;\n header?: React.ReactNode;\n footer?: React.ReactNode;\n showResultText?: boolean;\n staticLabels?: {\n RESULT?: string;\n RESULTS?: string;\n SEARCH_PLACEHOLDER?: string;\n };\n showLabelTooltip?: MenuLabelTooltip;\n allowWrap?: boolean;\n testId?: string;\n}\nexport interface MenuOptionTypes {\n options?: Array<MenuOption>;\n composeOptions?: Array<Array<MenuOption>>;\n groupTitleOptions?: Array<MenuGroupTitleOption>;\n}\nexport type MenuSizeTypes = 'medium' | 'small';\nexport type MenuOptionsVariant = 'checkbox' | 'radio' | 'normal';\nexport interface MenuGroupTitleOption {\n groupTitle?: React.ReactNode;\n options?: Array<MenuOption>;\n showLineDivider?: boolean;\n}\nexport interface MenuProps extends BaseMenuProps, MenuOptionTypes {\n}\nexport interface RenderOptionProps extends Omit<MenuProps, 'searchText'> {\n searchText?: string;\n}\nexport declare const Menu: Flow.AbstractComponent<MenuProps, HTMLDivElement>;\nexport {};\n//# sourceMappingURL=Menu.d.ts.map"
|
|
1621
1621
|
},
|
|
1622
1622
|
"story": {
|
|
1623
1623
|
"path": "Menu.stories.tsx",
|
|
1624
|
-
"content": "import * as React from 'react';\n\nimport {difference, union} from 'lodash';\n\nimport {ClickAway} from 'src/utils/click-away';\nimport {getFilteredOptionsFromSearchText} from 'src/utils/menu';\n\nimport {Avatar} from 'src/components/Avatar';\nimport {Button, UnstyledButton} from 'src/components/Button';\nimport {Icon} from 'src/components/Icon';\nimport type {\n MenuGroupTitleOption,\n MenuOption,\n MenuProps,\n} from 'src/components/Menu/Menu';\nimport {Menu} from 'src/components/Menu/Menu';\nimport {\n useComboboxMenuTrigger,\n useMenuTrigger,\n} from 'src/components/Menu/useMenuTrigger';\nimport {SearchInput} from 'src/components/SearchInput';\nimport {\n ButtonTextMedium,\n ButtonTextSmallUnderline,\n FormLabelMedium,\n FormLabelSmall,\n SubTitleSmall,\n} from 'src/components/Text';\nimport {Tooltip} from 'src/components/Tooltip';\nimport {Truncate} from 'src/components/Truncate';\n\nimport css from 'src/components/Menu/Menu.stories.module.css';\n\n\nconst textOptions: MenuOption[] = [\n {key: '1', label: 'Option one'},\n {key: '2', label: 'Option two with some extra words to increase length'},\n {\n key: '3',\n label:\n 'Option three with more content to demonstrate truncation and test three lines wrap.',\n },\n {\n key: '4',\n label:\n 'Option four very long option that would truncate and now it is even longer than before to ensure truncation happens',\n },\n {key: '5', label: 'Option five(disabled)', disabled: true},\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n },\n {key: '7', label: 'Option Seven'},\n {key: '8', label: 'Option Eight'},\n {key: '9', label: 'Option Nine'},\n {key: '10', label: 'Option Ten'},\n {key: '11', label: 'Option Eleven'},\n {key: '12', label: 'Option Twelve'},\n {key: '13', label: 'Option Thirteen'},\n];\n\nconst checkboxOptions: MenuOption[] = [\n {key: '1', label: 'Option one', indeterminate: true},\n {key: '2', label: 'Option two'},\n {key: '3', label: 'Option three', indeterminate: true},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n },\n {key: '5', label: 'Option five(disabled)', disabled: true},\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n },\n {key: '7', label: 'Option Seven'},\n];\n\nconst textOptionsWithSecondaryLabel: MenuOption[] = [\n {key: '1', label: 'Option one', secondaryLabel: 'Sub Text'},\n {key: '2', label: 'Option two', secondaryLabel: 'Sub Text'},\n {key: '3', label: 'Option three', secondaryLabel: 'Sub Text'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n secondaryLabel: 'Option four very long option that would truncate',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n secondaryLabel: 'Sub Text',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n secondaryLabel: 'Sub Text',\n },\n {key: '7', label: 'Option Seven', secondaryLabel: 'Sub Text'},\n {key: '8', label: 'Option Eight', secondaryLabel: 'Sub Text'},\n {key: '9', label: 'Option Nine', secondaryLabel: 'Sub Text'},\n];\n\nconst iconLeftOptions: MenuOption[] = [\n {\n key: '1',\n label: 'Option one',\n iconLeft: 'coffee',\n },\n {key: '2', label: 'Option two', iconLeft: 'user'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconLeft: 'face-party',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconLeft: 'flag',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconLeft: 'camera',\n },\n];\n\nconst coloredOptions: MenuOption[] = [\n {\n key: '1',\n label: 'Edit',\n iconLeft: 'pen',\n },\n {\n key: '2',\n label: 'Delete',\n iconLeft: 'trash',\n classNames: {wrapper: css.dangerText},\n },\n];\n\nconst iconRightOptions: MenuOption[] = [\n {key: '1', label: 'Option one', iconRight: 'coffee'},\n {key: '2', label: 'Option two', iconRight: 'user'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconRight: 'face-party',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconRight: 'flag',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconRight: 'camera',\n },\n];\n\nconst iconLeftRightOptions: MenuOption[] = [\n {key: '1', label: 'Option one', iconRight: 'coffee', iconLeft: 'coffee'},\n {key: '2', label: 'Option two', iconRight: 'user', iconLeft: 'coffee'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconRight: 'face-party',\n iconLeft: 'coffee',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconRight: 'flag',\n iconLeft: 'coffee',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconRight: 'camera',\n iconLeft: 'coffee',\n },\n];\n\nexport default {\n tags: ['autodocs'],\n title: 'Components/Menu',\n argTypes: {\n resolveLabel: {\n description:\n 'A function that resolves the label for a MenuOption. This is useful when you want to customize the label of an option based on some condition',\n table: {\n type: {\n summary: '(option: MenuOption) => string | React.Node',\n },\n },\n },\n resolveSecondaryLabel: {\n description:\n 'A function that resolves the secondary label for a MenuOption. This is useful when you want to customize the secondary label of an option based on some condition',\n table: {\n type: {\n summary: '(option: MenuOption) => string | React.Node',\n },\n },\n },\n onSelect: {\n description:\n '**onSelect** handler is triggered when there is a change in selected option',\n action: 'clicked',\n table: {\n type: {\n summary:\n '(option: MenuOption, ?SyntheticEvent<HTMLElement>) => mixed',\n },\n },\n },\n onTabOut: {\n description:\n '**onTabOut** is called when Tab (or Shift+Tab) would take focus out of the menu entirely. Supplying it means the menu takes over that key: the default is prevented and the consumer is expected to close the menu and put focus somewhere sensible — normally back on the trigger. Omit it for an inline menu, so Tab simply moves on to the next thing on the page.',\n table: {\n type: {summary: '() => mixed'},\n },\n // Deliberately NOT an `action`: an injected spy is truthy, so the menu\n // would take over Tab and then nothing would close it or move focus,\n // making every inline story look like a keyboard trap.\n },\n onReturnFocusToInput: {\n description:\n 'Combobox consumers only. Called when Tab/Shift+Tab moves off a header/footer control back toward the trigger input, which lives outside the menu DOM and is therefore the centre of the tab order. Return focus to it and keep the menu open. Omit it for menus whose input is inside the menu.',\n table: {\n type: {summary: '() => void'},\n },\n },\n menuId: {\n description:\n 'DOM id applied to the option `listbox`. Wire it to the trigger\\'s `aria-controls` and it is also the prefix for the per-option ids used by `aria-activedescendant`. Falls back to a generated id.',\n table: {\n type: {summary: 'string'},\n },\n },\n ariaLabelledBy: {\n description:\n 'Id of the element that names the popup — normally the trigger, or the field label. `listbox` requires an accessible name, so supply this or `ariaLabel`.',\n table: {\n type: {summary: 'string'},\n },\n },\n ariaLabel: {\n description:\n 'Literal accessible name for the listbox, used only when `ariaLabelledBy` is absent. Prefer naming it after the field rather than echoing a placeholder.',\n table: {\n type: {summary: 'string'},\n },\n },\n initialFocusRef: {\n description:\n 'Marks the Menu as a popup that should take focus when it opens: focus moves to the search field, or the active option, and the resolved element is written back into the ref. Omit it for inline menus, and for combobox triggers using `externalKeyboardNav`, so focus is never stolen.',\n table: {\n type: {summary: 'RefObject<HTMLElement | null>'},\n },\n },\n activeOptionKey: {\n description:\n 'Key of the option that owns the roving tab stop, or — with `externalKeyboardNav` — the controlled active descendant.',\n table: {\n type: {summary: 'string | null'},\n },\n },\n externalKeyboardNav: {\n description:\n 'Set when an outside combobox input drives navigation (Dropdown / Typeahead): focus stays on that input, Menu does no option navigation of its own (Tab between its header, options and footer still works) and never moves focus into itself, and `activeOptionKey` becomes a controlled prop mirroring the input\\'s `aria-activedescendant`.',\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: 'false'},\n },\n },\n onActiveOptionKeyChange: {\n description:\n 'Reports active-option changes back to the parent. Required when `externalKeyboardNav` is set, since the parent owns that state.',\n table: {\n type: {summary: '(key: string | null) => void'},\n },\n },\n options: {\n description: `list of Options to be displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<{\n key?: string,\n label: React.Node,\n customComponent?: React.Node,\n secondaryLabel?: React.Node,\n iconLeft?: string,\n iconLeftType?: IconType,\n iconRight?: string,\n iconRightType?: IconType,\n disabled?: boolean,\n optionSize?: MenuSizeTypes,\n indeterminate?: boolean,\n }>`,\n },\n },\n },\n composeOptions: {\n description: `list of list of Options are grouped and displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<Array<{\n key?: string,\n label: React.Node,\n customComponent?: React.Node,\n secondaryLabel?: React.Node,\n iconLeft?: string,\n iconLeftType?: IconType,\n iconRight?: string,\n iconRightType?: IconType,\n disabled?: boolean,\n optionSize?: MenuSizeTypes,\n indeterminate?: boolean,\n }>>`,\n },\n },\n },\n groupTitleOptions: {\n description: `List of Options are grouped with a title(each group contains a set of options) and displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<{\n groupTitle?: React.Node,\n options?: Array<MenuOption>\n }>`,\n },\n },\n },\n selectedOption: {\n description: `Used in cases where you want to pre-select an option as soon as the component renders.\n Selection is triggered when the selected option exactly matches one of the passed option.\n **Note:** This prop would be deprecated soon. Please use \\`selectedKeys\\` instead\n `,\n control: {\n type: 'object',\n },\n table: {\n type: {summary: 'MenuOption'},\n },\n },\n optionsVariant: {\n description: 'Menu supports multiple options variant',\n control: {\n type: 'select',\n },\n options: ['checkbox', 'radio', 'normal'],\n table: {\n type: {summary: 'enum'},\n defaultValue: {summary: 'normal'},\n },\n },\n selectedKeys: {\n description:\n 'Selected keys corresponding to options. These options are shown as selected in UI',\n control: {\n type: 'array',\n },\n table: {\n type: {summary: 'Array<string>'},\n },\n },\n classNames: {\n description:\n 'External classnames to be applied. Wrapper gets applied to Menu wrapper and option gets applied to each option in the menu.',\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary:\n '{wrapper?: string, option?: string, groupTitle?: string, optionTextContainer?: string, optionTextLabel?: string, header?: string, footer?: string}',\n },\n },\n },\n size: {\n description:\n 'Menu supports only two size variants namely small and medium. `small` < `medium`',\n control: {\n type: 'radio',\n },\n options: ['small', 'medium'],\n table: {\n type: {summary: 'enum'},\n defaultValue: {summary: 'medium'},\n },\n },\n width: {\n description:\n 'Manual width control. This overrides the default width considerations of Menu',\n control: {\n type: 'text',\n },\n table: {\n type: {summary: 'string'},\n },\n },\n menuDisabled: {\n description: 'if **true**, the option is disabled',\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n isFluid: {\n description:\n 'if **true**, the component would take the width of the container it is placed in',\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n allowSearch: {\n description: `if **true**, the component would show a search input on top of menu.\n This is a simple **static search**\n which filter options by **matching search text to option label**`,\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n virtualization: {\n description: `If **Enabled**, the options will only be rendered when they are within the component's viewport. **menuHeight** accepts numerical input, interpreted as pixel values. **itemHeight** accepts a number (same height for every option row) or a function \\`(option, index) => number\\` for per-row heights — \\`index\\` is the option's position in the filtered option list (group titles are not counted). **groupTitleHeight** sizes group title rows when virtualizing \\`groupTitleOptions\\`; it accepts a number or a function \\`(groupTitle, groupIndex) => number\\`.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary:\n '{enable: boolean, menuHeight?: number, itemHeight?: number | ((option, index) => number), groupTitleHeight?: number | ((groupTitle, groupIndex) => number)}',\n },\n },\n },\n header: {\n description: 'Custom Header For the Menu',\n table: {\n type: {\n summary: 'React.Node',\n },\n },\n },\n footer: {\n description: 'Custom Header For the Menu',\n table: {\n type: {\n summary: 'React.Node',\n },\n },\n },\n showResultText: {\n description: `if **true**, shows result text below **search input**.\n **Note:** For the result text to be displayed below the search input, please ensure that allowSearch is set to true.`,\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: true},\n },\n },\n staticLabels: {\n description:\n 'External labels to be assigned to static text displayed in the UI.',\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: '{[key: string]: string}',\n },\n defaultValue: {\n summary: `{RESULT: 'result', RESULTS: 'results', SEARCH_PLACEHOLDER: 'Search...'}`,\n },\n },\n },\n showLabelTooltip: {\n description: 'Show Tooltip on the label of the menu option if truncated',\n control: {\n type: 'object',\n },\n table: {\n type: {summary: 'MenuLabelTooltip'},\n },\n },\n allowWrap: {\n description:\n 'Allows the label to wrap up to 3 lines. If truncated, a tooltip will display up to 10 lines.',\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n testId: {\n description: 'Test ID for the component',\n control: {\n type: 'text',\n },\n table: {\n type: {summary: 'string'},\n },\n },\n },\n parameters: {\n docs: {\n subtitle: 'Generates a Menu component',\n description: {\n component: `\n\\`\\`\\`js\nimport { Menu } from \"@spaced-out/ui-design-system/lib/components/Menu\";\n\\`\\`\\`\n**Menu** is a list of choices on a temporary surface. It appears on interacts with a button, or other control. This is a standalone Menu component,\nin real world use cases Menu component would be used in conjunction with other component like Button, Input, Typeahead, etc. Each option in the Menu\ncomponent is implemented as an \\`UnstyledButton\\`.\n- The component also supports custom **header** as \\`React Component\\` and this is then passed in\nthe **<a href=\"../?path=/docs/components-menu--docs\">Menu</a>** Component.\n- The component also supports custom **footer** as \\`React Component\\` and this is then passed in\nthe **<a href=\"../?path=/docs/components-menu--docs\">Menu</a>** Component. This might be useful when you need\nto save some state in the parent component.\n `,\n },\n },\n storySource: {\n componentPath: '/src/components/Menu/Menu',\n },\n },\n};\n\nexport const TextOnlyStory = (args: MenuProps) => <Menu {...args} />;\n\nTextOnlyStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-only-story',\n};\nexport const TextWithTooltipStory = (args: MenuProps) => <Menu {...args} />;\n\nTextWithTooltipStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n showLabelTooltip: {maxLines: 2},\n testId: 'menu-text-with-tooltip-story',\n};\n\nexport const MenuWithWrapStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n const selectKeys = (option: MenuOption) => {\n setSelectedKeys([option.key]);\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nMenuWithWrapStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n allowWrap: true,\n testId: 'menu-with-wrap-story',\n};\n\nexport const WithSearchStory = (args: MenuProps) => <Menu {...args} />;\n\nWithSearchStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n allowSearch: true,\n testId: 'menu-with-search-story',\n};\n\nexport const TextAndIconLeftStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndIconLeftStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconLeftOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-icon-left-story',\n};\n\n/**\n * To make one of the options colored, you can set the classNames wrapper property in the option object.\n * Eg:\n * You would need to set the color of the option and manage the hover, focus and active state yourself.\n * ### JS\n * ```\n * const coloredOptions: MenuOption[] = [\n * {\n * key: '1',\n * label: 'Edit',\n * iconLeft: 'coffee',\n * },\n * {\n * key: '2',\n * label: 'Delete',\n * iconLeft: 'trash',\n * classNames: { wrapper: css.dangerText },\n * },\n * ];\n * ```\n * ### CSS\n * ```\n * .dangerText:hover,\n * .dangerText:active,\n * .dangerText:focus {\n * color: colorTextDanger;\n * }\n * ```\n */\nexport const ColoredOptionStory = (args: MenuProps) => <Menu {...args} />;\n\nColoredOptionStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: coloredOptions,\n testId: 'menu-colored-option-story',\n};\n\nexport const TextAndIconRightStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndIconRightStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconRightOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-icon-right-story',\n};\n\nexport const TextAndDualIconStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndDualIconStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconLeftRightOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-dual-icon-story',\n};\n\nexport const DualTextStory = (args: MenuProps) => <Menu {...args} />;\n\nDualTextStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptionsWithSecondaryLabel,\n selectedKeys: ['2'],\n testId: 'menu-dual-text-story',\n};\n\nexport const WithResolversStory = (args: MenuProps) => {\n const resolveLabel = (option: MenuOption) => (\n <div className={css.customComponent}>\n {parseInt(option.key) < 6 ? (\n <Avatar\n imageSrc={`https://cdn.sensehq.com/genesis/hashed/static/images/personas/${option.key}.png`}\n />\n ) : (\n <Avatar iconName=\"user\" />\n )}\n\n <ButtonTextMedium className={css.text} color=\"secondary\">\n <Truncate>{option.label}</Truncate>\n </ButtonTextMedium>\n </div>\n );\n\n return <Menu {...args} resolveLabel={resolveLabel} />;\n};\n\nWithResolversStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n testId: 'menu-with-resolvers-story',\n};\n\nconst composeMenuOptions: Array<Array<MenuOption>> = [\n [\n {\n key: '1',\n label: 'Option one',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n {\n key: '2',\n label: 'Option two',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n {\n key: '3',\n label: 'Option three',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n ],\n [\n {\n iconLeft: 'coffee',\n key: '4',\n label: 'Option one',\n optionSize: 'medium',\n },\n ],\n [\n {\n iconLeft: 'coffee',\n key: '5',\n label: 'Option one',\n optionSize: 'medium',\n },\n ],\n];\n\nexport const ComposeMenuStory = (args: MenuProps) => <Menu {...args} />;\n\nComposeMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n composeOptions: composeMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-compose-story',\n};\n\nconst customComponentMenuOptions: Array<MenuOption> = [\n {\n key: '1',\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n color=\"blue\"\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/1.png\"\n iconName=\"face-party\"\n iconType=\"regular\"\n size=\"medium\"\n />\n <ButtonTextMedium className={css.text}>Angelina White</ButtonTextMedium>\n </div>\n ),\n },\n {\n key: '2',\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/4.png\"\n color=\"green\"\n size=\"medium\"\n text=\"John\"\n />\n <ButtonTextMedium className={css.text}>John Smith</ButtonTextMedium>\n </div>\n ),\n },\n {\n key: '3',\n\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/6.png\"\n color=\"orange\"\n size=\"medium\"\n text=\"Jacobs Marley\"\n />\n <ButtonTextMedium className={css.text}>Sarah Parker</ButtonTextMedium>\n </div>\n ),\n },\n];\n\nexport const CustomComponentMenuStory = (args: MenuProps) => <Menu {...args} />;\n\nCustomComponentMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: customComponentMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-custom-component-story',\n};\n\nconst groupTitleComponentMenuOptions: Array<MenuGroupTitleOption> = [\n {\n groupTitle: (\n <div className={css.titleWrapper}>\n <FormLabelSmall color=\"tertiary\">Fast Food</FormLabelSmall>\n <div className={css.infoContainer}>\n <Tooltip title=\"Hello!\" elevation=\"menu\">\n <Icon name=\"circle-info\" size=\"small\" color=\"tertiary\" />\n </Tooltip>\n </div>\n </div>\n ),\n options: [\n {\n key: '1',\n iconLeft: 'burger',\n label: 'Burger',\n },\n {\n key: '2',\n iconLeft: 'pizza',\n label: 'Pizza',\n },\n {\n key: '3',\n iconLeft: 'french-fries',\n label: 'Fries',\n },\n ],\n },\n {\n showLineDivider: true,\n groupTitle: (\n <div className={css.titleWrapper}>\n <FormLabelSmall color=\"tertiary\">Fruits</FormLabelSmall>\n <div className={css.infoContainer}>\n <Tooltip title=\"Hello!\" elevation=\"menu\">\n <Icon name=\"circle-info\" size=\"small\" color=\"tertiary\" />\n </Tooltip>\n </div>\n </div>\n ),\n options: [\n {\n key: '7',\n iconLeft: 'banana',\n label: 'Banana',\n },\n {\n key: '8',\n iconLeft: 'pear',\n label: 'Pear',\n },\n {\n key: '9',\n iconLeft: 'lemon',\n label: 'Lemon',\n },\n ],\n },\n];\n\nexport const GroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n groupTitleOptions: groupTitleComponentMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-group-title-story',\n};\n\nexport const VirtualizedGroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nVirtualizedGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n groupTitleOptions: Array.from({length: 20}, (_, groupIdx) => ({\n groupTitle: `Group ${groupIdx + 1}`,\n options: Array.from({length: 25}, (_, optionIdx) => ({\n key: `${groupIdx}-${optionIdx}`,\n label: `Group ${groupIdx + 1} option ${optionIdx + 1}`,\n // every 5th option carries a description, so it needs a taller row\n ...(optionIdx % 5 === 0\n ? {secondaryLabel: 'Option with a description'}\n : {}),\n })),\n })),\n virtualization: {\n enable: true,\n // size rows from the option itself, not its index\n itemHeight: (option: MenuOption) => (option.secondaryLabel ? 56 : 36),\n groupTitleHeight: 32,\n menuHeight: 320,\n },\n selectedKeys: ['0-1'],\n testId: 'menu-virtualized-group-title-story',\n};\n\nexport const VirtualizedCustomGroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nVirtualizedCustomGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n groupTitleOptions: Array.from({length: 10}, (_, groupIdx) => ({\n // every other group has a two-line custom title node\n groupTitle:\n groupIdx % 2 === 0 ? (\n <div>\n <div>Group {groupIdx + 1}</div>\n <div style={{fontSize: '11px', fontWeight: 'normal'}}>\n Two-line custom header\n </div>\n </div>\n ) : (\n `Group ${groupIdx + 1}`\n ),\n options: Array.from({length: 15}, (_, optionIdx) => ({\n key: `${groupIdx}-${optionIdx}`,\n label: `Group ${groupIdx + 1} option ${optionIdx + 1}`,\n })),\n })),\n virtualization: {\n enable: true,\n itemHeight: 36,\n // tall rows for the two-line custom titles, default for the rest\n groupTitleHeight: (_groupTitle: React.ReactNode, groupIndex: number) =>\n groupIndex % 2 === 0 ? 56 : 32,\n menuHeight: 320,\n },\n testId: 'menu-virtualized-custom-group-title-story',\n};\n\n/*\n * Roving focus over a virtualized list: `allowSearch` is off, so the options\n * themselves own focus rather than a search field, and arrow keys / Home / End\n * / type-ahead move real DOM focus between rows that mount and unmount as the\n * list scrolls. Kept as its own story because that pairing is what breaks when\n * a virtualized row is re-created underneath the focused element.\n */\nexport const VirtualizedRovingFocusMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nVirtualizedRovingFocusMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: false,\n groupTitleOptions: Array.from({length: 20}, (_, groupIdx) => ({\n groupTitle: `Group ${groupIdx + 1}`,\n options: Array.from({length: 25}, (_, optionIdx) => ({\n key: `${groupIdx}-${optionIdx}`,\n label: `Group ${groupIdx + 1} option ${optionIdx + 1}`,\n })),\n })),\n virtualization: {\n enable: true,\n itemHeight: 36,\n groupTitleHeight: 32,\n menuHeight: 320,\n },\n selectedKeys: ['0-1'],\n testId: 'menu-virtualized-roving-focus-story',\n};\n\nexport const VirtualizedVariableHeightMenuStory = (args: MenuProps) => (\n <Menu {...args} />\n);\n\nVirtualizedVariableHeightMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n options: Array.from({length: 200}, (_, optionIdx) => ({\n key: `${optionIdx}`,\n label: `Option ${optionIdx + 1}`,\n ...(optionIdx % 4 === 0\n ? {secondaryLabel: 'Option with a description'}\n : {}),\n })),\n virtualization: {\n enable: true,\n itemHeight: (option: MenuOption) => (option.secondaryLabel ? 56 : 36),\n menuHeight: 320,\n },\n selectedKeys: ['1'],\n testId: 'menu-virtualized-variable-height-story',\n};\n\n/*\n * Bulk actions over a filtered checkbox list.\n *\n * The actions live in the menu `header`, NOT in a group title: a group title is\n * rendered inside the `role=\"listbox\"`, which may only own `option` and `group`\n * children, so interactive controls in there are invalid (axe reports\n * `aria-required-children`, and AT announces a button where it expects an\n * option). The header sits outside the listbox, which is where controls that\n * act ON the list belong. The label naming the list is handed to Menu as\n * `ariaLabelledBy`, so the listbox is named \"Values (n)\" rather than the\n * generic default.\n */\nconst GroupTitleActionMenu = (args: MenuProps) => {\n const valuesLabelId = 'menu-group-title-action-values';\n const [searchText, setSearchText] = React.useState<string>('');\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n const optionsFiltered = getFilteredOptionsFromSearchText(\n textOptions,\n searchText,\n );\n const optionsFilteredKeys = optionsFiltered\n .filter(({disabled}) => !disabled)\n .map(({key}) => key);\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(difference(selectedKeys, [option.key]));\n } else {\n setSelectedKeys(union(selectedKeys, [option.key]));\n }\n };\n\n const selectAllKeys: React.MouseEventHandler<HTMLButtonElement> = () => {\n setSelectedKeys(union(selectedKeys, optionsFilteredKeys));\n };\n\n const clearSelectedKeys: React.MouseEventHandler<HTMLButtonElement> = () => {\n setSelectedKeys(difference(selectedKeys, optionsFilteredKeys));\n };\n\n return (\n <Menu\n {...args}\n optionsVariant=\"checkbox\"\n selectedKeys={selectedKeys}\n onSelect={selectKeys}\n ariaLabelledBy={valuesLabelId}\n classNames={{\n header: css.groupTitleActionHeader,\n }}\n header={\n <>\n <SearchInput\n value={searchText}\n onChange={(e) => setSearchText(e.target.value)}\n onClear={() => setSearchText('')}\n size={args.size}\n />\n <div className={css.groupTitleActionRow}>\n <span id={valuesLabelId}>\n <FormLabelMedium color=\"tertiary\">\n {`Values (${optionsFiltered.length})`}\n </FormLabelMedium>\n </span>\n <div className={css.groupTitleActions}>\n <UnstyledButton onClick={clearSelectedKeys}>\n <ButtonTextSmallUnderline className={css.groupTitleAction}>\n Clear\n </ButtonTextSmallUnderline>\n </UnstyledButton>\n <UnstyledButton onClick={selectAllKeys}>\n <ButtonTextSmallUnderline\n color=\"clickable\"\n className={css.groupTitleAction}\n >\n Select All\n </ButtonTextSmallUnderline>\n </UnstyledButton>\n </div>\n </div>\n </>\n }\n options={optionsFiltered}\n />\n );\n};\n\nexport const GroupTitleActionMenuStory = (args: MenuProps) => (\n <GroupTitleActionMenu {...args} />\n);\n\nGroupTitleActionMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n selectedKeys: ['2'],\n testId: 'menu-group-title-action-story',\n};\n\nexport const CheckboxMenuStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(selectedKeys.filter((item) => item !== option.key));\n } else {\n setSelectedKeys([...selectedKeys, option.key]);\n }\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nCheckboxMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: checkboxOptions,\n optionsVariant: 'checkbox',\n selectedKeys: ['1', '4'],\n testId: 'menu-checkbox-story',\n};\n\nexport const RadioMenuStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n setSelectedKeys([option.key]);\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nRadioMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: textOptions,\n optionsVariant: 'radio',\n selectedKeys: ['1'],\n testId: 'menu-radio-story',\n};\n\nexport const MenuHeader = () => (\n <div className={css.headerComponent}>\n <SubTitleSmall>Menu Header</SubTitleSmall>\n </div>\n);\n\nexport const MenuFooter = ({\n onSave,\n onCancel,\n}: {\n onSave?: () => void;\n onCancel?: () => void;\n}) => (\n <div className={css.footerComponent}>\n <Button size=\"small\" type=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button size=\"small\" type=\"secondary\" onClick={onSave}>\n Save\n </Button>\n </div>\n);\n\nexport const WithHeaderStory = (args: MenuProps) => (\n <Menu {...args} header={<MenuHeader />} />\n);\n\nWithHeaderStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'small',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-with-header-story',\n};\n\nexport const WithFooterStory = (args: MenuProps) => (\n <Menu {...args} footer={<MenuFooter />} />\n);\n\nWithFooterStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'small',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-with-footer-story',\n};\n\nexport const WithHeaderAndFooterStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(selectedKeys.filter((item) => item !== option.key));\n } else {\n setSelectedKeys([...selectedKeys, option.key]);\n }\n };\n\n return (\n <Menu\n {...args}\n selectedKeys={selectedKeys}\n onSelect={selectKeys}\n header={<MenuHeader />}\n footer={<MenuFooter />}\n />\n );\n};\n\nWithHeaderAndFooterStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n options: textOptions,\n optionsVariant: 'checkbox',\n selectedKeys: ['1', '4'],\n testId: 'menu-with-header-and-footer-story',\n};\n/*\n * The two trigger hooks, wired the way a consumer would wire them. These are the\n * only stories that mount `useMenuTrigger` / `useComboboxMenuTrigger` directly,\n * so a consumer-side change to either shows up here first: the trigger's ARIA\n * before and after opening, ArrowDown opening the popup, focus moving in (or\n * staying on the input), and Tab/Escape closing it and returning focus.\n */\nconst ButtonTriggerMenu = (args: MenuProps) => {\n // A trigger that already has an id of its own: hand it to the hook rather\n // than setting it on the element as well, and the hook keeps it — a generated\n // one would break whatever the consumer points at it — and names the listbox\n // through it unless `ariaLabel` says otherwise. Setting it in both places\n // would be a duplicate prop, which is what `getTriggerProps` returning the\n // resolved id exists to avoid.\n const menuTrigger = useMenuTrigger({id: 'menu-trigger-demo'});\n return (\n <ClickAway>\n {({isOpen, onOpen, clickAway, boundaryRef, triggerRef}) => (\n <div className={css.triggerDemo}>\n <Button\n ref={triggerRef as React.RefObject<HTMLButtonElement>}\n {...menuTrigger.getTriggerProps({isOpen, onOpen})}\n onClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n >\n Actions\n </Button>\n {isOpen && (\n <div\n ref={boundaryRef as React.RefObject<HTMLDivElement>}\n className={css.triggerDemoPopup}\n >\n <Menu\n {...args}\n {...menuTrigger.menuProps}\n onSelect={() => clickAway()}\n onTabOut={clickAway}\n />\n </div>\n )}\n </div>\n )}\n </ClickAway>\n );\n};\n\nexport const WithButtonTriggerStory = (args: MenuProps) => (\n <ButtonTriggerMenu {...args} />\n);\n\nWithButtonTriggerStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n header: <MenuHeader />,\n footer: <MenuFooter />,\n testId: 'menu-with-button-trigger-story',\n};\n\nconst ComboboxTriggerMenu = (args: MenuProps) => {\n const menuRef = React.useRef<HTMLDivElement>(null);\n const triggerWrapRef = React.useRef<HTMLDivElement>(null);\n const clickAwayFnRef = React.useRef<(() => void) | null>(null);\n const [isMenuOpen, setIsMenuOpen] = React.useState(false);\n const [selected, setSelected] = React.useState<MenuOption | null>(\n args.selectedOption ?? null,\n );\n const menu: MenuProps = {...args, selectedOption: selected};\n const focusInput = () =>\n triggerWrapRef.current?.querySelector('input')?.focus();\n const select = (option: MenuOption) => {\n setSelected(option);\n clickAwayFnRef.current?.();\n focusInput();\n };\n const combobox = useComboboxMenuTrigger({\n menu,\n menuRef,\n isOpen: isMenuOpen,\n onSelectOption: select,\n onClose: () => clickAwayFnRef.current?.(),\n focusTrigger: focusInput,\n triggerAriaLabel: 'Fruit',\n });\n return (\n <ClickAway<HTMLDivElement, HTMLDivElement> onChange={setIsMenuOpen}>\n {({isOpen, onOpen, clickAway, boundaryRef, triggerRef}) => {\n clickAwayFnRef.current = clickAway;\n return (\n <div className={css.triggerDemo}>\n <div ref={triggerWrapRef} className={css.triggerDemoInput}>\n <SearchInput\n {...combobox.getTriggerProps({isOpen, onOpen})}\n boxRef={triggerRef}\n readOnly\n value={selected?.label ?? ''}\n placeholder=\"Select a fruit\"\n onContainerClick={(e) => {\n e.stopPropagation();\n onOpen();\n }}\n onChange={() => undefined}\n />\n </div>\n {isOpen && (\n <div\n ref={boundaryRef as React.RefObject<HTMLDivElement>}\n className={css.triggerDemoPopup}\n >\n <Menu\n {...menu}\n ref={menuRef}\n {...combobox.menuProps}\n onSelect={select}\n onTabOut={clickAway}\n />\n </div>\n )}\n </div>\n );\n }}\n </ClickAway>\n );\n};\n\nexport const WithComboboxTriggerStory = (args: MenuProps) => (\n <ComboboxTriggerMenu {...args} />\n);\n\nWithComboboxTriggerStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: textOptions,\n footer: <MenuFooter />,\n testId: 'menu-with-combobox-trigger-story',\n};\n\nTextOnlyStory.storyName = 'Text Only';\nWithButtonTriggerStory.storyName = 'With Button Trigger (useMenuTrigger)';\nWithComboboxTriggerStory.storyName =\n 'With Combobox Trigger (useComboboxMenuTrigger)';\nTextWithTooltipStory.storyName = 'Text With Tooltip';\nMenuWithWrapStory.storyName = 'Menu With Wrap';\nWithSearchStory.storyName = 'With Search';\nTextAndIconLeftStory.storyName = 'Text And Icon Left';\nColoredOptionStory.storyName = 'Colored Option';\nTextAndIconRightStory.storyName = 'Text And Icon Right';\nTextAndDualIconStory.storyName = 'Text And Dual Icon';\nDualTextStory.storyName = 'Dual Text';\nWithResolversStory.storyName = 'With Resolvers';\nComposeMenuStory.storyName = 'Compose Menu';\nCustomComponentMenuStory.storyName = 'Custom Component Menu';\nGroupTitleMenuStory.storyName = 'Group Title Menu';\nVirtualizedGroupTitleMenuStory.storyName = 'Virtualized Group Title Menu';\nVirtualizedCustomGroupTitleMenuStory.storyName =\n 'Virtualized Custom Group Title Menu';\nVirtualizedVariableHeightMenuStory.storyName =\n 'Virtualized Variable Height Menu';\nVirtualizedRovingFocusMenuStory.storyName =\n 'Virtualized Menu With Roving Focus';\nGroupTitleActionMenuStory.storyName = 'Group Title Action Menu';\nCheckboxMenuStory.storyName = 'Checkbox Menu';\nRadioMenuStory.storyName = 'Radio Menu';\nWithHeaderStory.storyName = 'With Header';\nWithFooterStory.storyName = 'With Footer';\nWithHeaderAndFooterStory.storyName = 'With Header And Footer';\n"
|
|
1624
|
+
"content": "import * as React from 'react';\n\nimport {difference, union} from 'lodash';\n\nimport {getFilteredOptionsFromSearchText} from 'src/utils/menu';\n\nimport {Avatar} from 'src/components/Avatar';\nimport {Button, UnstyledButton} from 'src/components/Button';\nimport {Icon} from 'src/components/Icon';\nimport type {\n MenuGroupTitleOption,\n MenuOption,\n MenuProps,\n} from 'src/components/Menu/Menu';\nimport {Menu} from 'src/components/Menu/Menu';\nimport {SearchInput} from 'src/components/SearchInput';\nimport {\n ButtonTextMedium,\n ButtonTextSmallUnderline,\n FormLabelMedium,\n FormLabelSmall,\n SubTitleSmall,\n} from 'src/components/Text';\nimport {Tooltip} from 'src/components/Tooltip';\nimport {Truncate} from 'src/components/Truncate';\n\nimport css from 'src/components/Menu/Menu.stories.module.css';\n\n\nconst textOptions: MenuOption[] = [\n {key: '1', label: 'Option one'},\n {key: '2', label: 'Option two with some extra words to increase length'},\n {\n key: '3',\n label:\n 'Option three with more content to demonstrate truncation and test three lines wrap.',\n },\n {\n key: '4',\n label:\n 'Option four very long option that would truncate and now it is even longer than before to ensure truncation happens',\n },\n {key: '5', label: 'Option five(disabled)', disabled: true},\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n },\n {key: '7', label: 'Option Seven'},\n {key: '8', label: 'Option Eight'},\n {key: '9', label: 'Option Nine'},\n {key: '10', label: 'Option Ten'},\n {key: '11', label: 'Option Eleven'},\n {key: '12', label: 'Option Twelve'},\n {key: '13', label: 'Option Thirteen'},\n];\n\nconst checkboxOptions: MenuOption[] = [\n {key: '1', label: 'Option one', indeterminate: true},\n {key: '2', label: 'Option two'},\n {key: '3', label: 'Option three', indeterminate: true},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n },\n {key: '5', label: 'Option five(disabled)', disabled: true},\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n },\n {key: '7', label: 'Option Seven'},\n];\n\nconst textOptionsWithSecondaryLabel: MenuOption[] = [\n {key: '1', label: 'Option one', secondaryLabel: 'Sub Text'},\n {key: '2', label: 'Option two', secondaryLabel: 'Sub Text'},\n {key: '3', label: 'Option three', secondaryLabel: 'Sub Text'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n secondaryLabel: 'Option four very long option that would truncate',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n secondaryLabel: 'Sub Text',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n secondaryLabel: 'Sub Text',\n },\n {key: '7', label: 'Option Seven', secondaryLabel: 'Sub Text'},\n {key: '8', label: 'Option Eight', secondaryLabel: 'Sub Text'},\n {key: '9', label: 'Option Nine', secondaryLabel: 'Sub Text'},\n];\n\nconst iconLeftOptions: MenuOption[] = [\n {\n key: '1',\n label: 'Option one',\n iconLeft: 'coffee',\n },\n {key: '2', label: 'Option two', iconLeft: 'user'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconLeft: 'face-party',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconLeft: 'flag',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconLeft: 'camera',\n },\n];\n\nconst coloredOptions: MenuOption[] = [\n {\n key: '1',\n label: 'Edit',\n iconLeft: 'pen',\n },\n {\n key: '2',\n label: 'Delete',\n iconLeft: 'trash',\n classNames: {wrapper: css.dangerText},\n },\n];\n\nconst iconRightOptions: MenuOption[] = [\n {key: '1', label: 'Option one', iconRight: 'coffee'},\n {key: '2', label: 'Option two', iconRight: 'user'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconRight: 'face-party',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconRight: 'flag',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconRight: 'camera',\n },\n];\n\nconst iconLeftRightOptions: MenuOption[] = [\n {key: '1', label: 'Option one', iconRight: 'coffee', iconLeft: 'coffee'},\n {key: '2', label: 'Option two', iconRight: 'user', iconLeft: 'coffee'},\n {\n key: '4',\n label: 'Option four very long option that would truncate',\n iconRight: 'face-party',\n iconLeft: 'coffee',\n },\n {\n key: '5',\n label: 'Option five(disabled)',\n disabled: true,\n iconRight: 'flag',\n iconLeft: 'coffee',\n },\n {\n key: '6',\n label: 'Option six(disabled) very long option that would truncate',\n disabled: true,\n iconRight: 'camera',\n iconLeft: 'coffee',\n },\n];\n\nexport default {\n tags: ['autodocs'],\n title: 'Components/Menu',\n argTypes: {\n resolveLabel: {\n description:\n 'A function that resolves the label for a MenuOption. This is useful when you want to customize the label of an option based on some condition',\n table: {\n type: {\n summary: '(option: MenuOption) => string | React.Node',\n },\n },\n },\n resolveSecondaryLabel: {\n description:\n 'A function that resolves the secondary label for a MenuOption. This is useful when you want to customize the secondary label of an option based on some condition',\n table: {\n type: {\n summary: '(option: MenuOption) => string | React.Node',\n },\n },\n },\n onSelect: {\n description:\n '**onSelect** handler is triggered when there is a change in selected option',\n action: 'clicked',\n table: {\n type: {\n summary:\n '(option: MenuOption, ?SyntheticEvent<HTMLElement>) => mixed',\n },\n },\n },\n onTabOut: {\n description:\n '**onTabOut** callback is triggered when the user navigates outside of the menu using the tab key',\n action: 'tabout',\n table: {\n type: {summary: '() => mixed'},\n },\n },\n options: {\n description: `list of Options to be displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<{\n key?: string,\n label: React.Node,\n customComponent?: React.Node,\n secondaryLabel?: React.Node,\n iconLeft?: string,\n iconLeftType?: IconType,\n iconRight?: string,\n iconRightType?: IconType,\n disabled?: boolean,\n optionSize?: MenuSizeTypes,\n indeterminate?: boolean,\n }>`,\n },\n },\n },\n composeOptions: {\n description: `list of list of Options are grouped and displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<Array<{\n key?: string,\n label: React.Node,\n customComponent?: React.Node,\n secondaryLabel?: React.Node,\n iconLeft?: string,\n iconLeftType?: IconType,\n iconRight?: string,\n iconRightType?: IconType,\n disabled?: boolean,\n optionSize?: MenuSizeTypes,\n indeterminate?: boolean,\n }>>`,\n },\n },\n },\n groupTitleOptions: {\n description: `List of Options are grouped with a title(each group contains a set of options) and displayed in the menu.\n Menu component accepts options / composeOptions / groupTitleOptions whatever is available first and not all.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: `Array<{\n groupTitle?: React.Node,\n options?: Array<MenuOption>\n }>`,\n },\n },\n },\n selectedOption: {\n description: `Used in cases where you want to pre-select an option as soon as the component renders.\n Selection is triggered when the selected option exactly matches one of the passed option.\n **Note:** This prop would be deprecated soon. Please use \\`selectedKeys\\` instead\n `,\n control: {\n type: 'object',\n },\n table: {\n type: {summary: 'MenuOption'},\n },\n },\n optionsVariant: {\n description: 'Menu supports multiple options variant',\n control: {\n type: 'select',\n },\n options: ['checkbox', 'radio', 'normal'],\n table: {\n type: {summary: 'enum'},\n defaultValue: {summary: 'normal'},\n },\n },\n selectedKeys: {\n description:\n 'Selected keys corresponding to options. These options are shown as selected in UI',\n control: {\n type: 'array',\n },\n table: {\n type: {summary: 'Array<string>'},\n },\n },\n classNames: {\n description:\n 'External classnames to be applied. Wrapper gets applied to Menu wrapper and option gets applied to each option in the menu.',\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary:\n '{wrapper?: string, option?: string, groupTitle?: string, optionTextContainer?: string, optionTextLabel?: string, header?: string, footer?: string}',\n },\n },\n },\n size: {\n description:\n 'Menu supports only two size variants namely small and medium. `small` < `medium`',\n control: {\n type: 'radio',\n },\n options: ['small', 'medium'],\n table: {\n type: {summary: 'enum'},\n defaultValue: {summary: 'medium'},\n },\n },\n width: {\n description:\n 'Manual width control. This overrides the default width considerations of Menu',\n control: {\n type: 'text',\n },\n table: {\n type: {summary: 'string'},\n },\n },\n menuDisabled: {\n description: 'if **true**, the option is disabled',\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n isFluid: {\n description:\n 'if **true**, the component would take the width of the container it is placed in',\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n allowSearch: {\n description: `if **true**, the component would show a search input on top of menu.\n This is a simple **static search**\n which filter options by **matching search text to option label**`,\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n virtualization: {\n description: `If **Enabled**, the options will only be rendered when they are within the component's viewport. **menuHeight** accepts numerical input, interpreted as pixel values. **itemHeight** accepts a number (same height for every option row) or a function \\`(option, index) => number\\` for per-row heights — \\`index\\` is the option's position in the filtered option list (group titles are not counted). **groupTitleHeight** sizes group title rows when virtualizing \\`groupTitleOptions\\`; it accepts a number or a function \\`(groupTitle, groupIndex) => number\\`.`,\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary:\n '{enable: boolean, menuHeight?: number, itemHeight?: number | ((option, index) => number), groupTitleHeight?: number | ((groupTitle, groupIndex) => number)}',\n },\n },\n },\n header: {\n description: 'Custom Header For the Menu',\n table: {\n type: {\n summary: 'React.Node',\n },\n },\n },\n footer: {\n description: 'Custom Header For the Menu',\n table: {\n type: {\n summary: 'React.Node',\n },\n },\n },\n showResultText: {\n description: `if **true**, shows result text below **search input**.\n **Note:** For the result text to be displayed below the search input, please ensure that allowSearch is set to true.`,\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: true},\n },\n },\n staticLabels: {\n description:\n 'External labels to be assigned to static text displayed in the UI.',\n control: {\n type: 'object',\n },\n table: {\n type: {\n summary: '{[key: string]: string}',\n },\n defaultValue: {\n summary: `{RESULT: 'result', RESULTS: 'results', SEARCH_PLACEHOLDER: 'Search...'}`,\n },\n },\n },\n showLabelTooltip: {\n description: 'Show Tooltip on the label of the menu option if truncated',\n control: {\n type: 'object',\n },\n table: {\n type: {summary: 'MenuLabelTooltip'},\n },\n },\n allowWrap: {\n description:\n 'Allows the label to wrap up to 3 lines. If truncated, a tooltip will display up to 10 lines.',\n control: {\n type: 'boolean',\n },\n table: {\n type: {summary: 'boolean'},\n defaultValue: {summary: false},\n },\n },\n testId: {\n description: 'Test ID for the component',\n control: {\n type: 'text',\n },\n table: {\n type: {summary: 'string'},\n },\n },\n },\n parameters: {\n docs: {\n subtitle: 'Generates a Menu component',\n description: {\n component: `\n\\`\\`\\`js\nimport { Menu } from \"@spaced-out/ui-design-system/lib/components/Menu\";\n\\`\\`\\`\n**Menu** is a list of choices on a temporary surface. It appears on interacts with a button, or other control. This is a standalone Menu component,\nin real world use cases Menu component would be used in conjunction with other component like Button, Input, Typeahead, etc. Each option in the Menu\ncomponent is implemented as an \\`UnstyledButton\\`.\n- The component also supports custom **header** as \\`React Component\\` and this is then passed in\nthe **<a href=\"../?path=/docs/components-menu--docs\">Menu</a>** Component.\n- The component also supports custom **footer** as \\`React Component\\` and this is then passed in\nthe **<a href=\"../?path=/docs/components-menu--docs\">Menu</a>** Component. This might be useful when you need\nto save some state in the parent component.\n `,\n },\n },\n storySource: {\n componentPath: '/src/components/Menu/Menu',\n },\n },\n};\n\nexport const TextOnlyStory = (args: MenuProps) => <Menu {...args} />;\n\nTextOnlyStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-only-story',\n};\nexport const TextWithTooltipStory = (args: MenuProps) => <Menu {...args} />;\n\nTextWithTooltipStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n showLabelTooltip: {maxLines: 2},\n testId: 'menu-text-with-tooltip-story',\n};\n\nexport const MenuWithWrapStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n const selectKeys = (option: MenuOption) => {\n setSelectedKeys([option.key]);\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nMenuWithWrapStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n selectedKeys: ['2'],\n allowWrap: true,\n testId: 'menu-with-wrap-story',\n};\n\nexport const WithSearchStory = (args: MenuProps) => <Menu {...args} />;\n\nWithSearchStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n allowSearch: true,\n testId: 'menu-with-search-story',\n};\n\nexport const TextAndIconLeftStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndIconLeftStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconLeftOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-icon-left-story',\n};\n\n/**\n * To make one of the options colored, you can set the classNames wrapper property in the option object.\n * Eg:\n * You would need to set the color of the option and manage the hover, focus and active state yourself.\n * ### JS\n * ```\n * const coloredOptions: MenuOption[] = [\n * {\n * key: '1',\n * label: 'Edit',\n * iconLeft: 'coffee',\n * },\n * {\n * key: '2',\n * label: 'Delete',\n * iconLeft: 'trash',\n * classNames: { wrapper: css.dangerText },\n * },\n * ];\n * ```\n * ### CSS\n * ```\n * .dangerText:hover,\n * .dangerText:active,\n * .dangerText:focus {\n * color: colorTextDanger;\n * }\n * ```\n */\nexport const ColoredOptionStory = (args: MenuProps) => <Menu {...args} />;\n\nColoredOptionStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: coloredOptions,\n testId: 'menu-colored-option-story',\n};\n\nexport const TextAndIconRightStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndIconRightStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconRightOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-icon-right-story',\n};\n\nexport const TextAndDualIconStory = (args: MenuProps) => <Menu {...args} />;\n\nTextAndDualIconStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: iconLeftRightOptions,\n selectedKeys: ['2'],\n testId: 'menu-text-and-dual-icon-story',\n};\n\nexport const DualTextStory = (args: MenuProps) => <Menu {...args} />;\n\nDualTextStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptionsWithSecondaryLabel,\n selectedKeys: ['2'],\n testId: 'menu-dual-text-story',\n};\n\nexport const WithResolversStory = (args: MenuProps) => {\n const resolveLabel = (option: MenuOption) => (\n <div className={css.customComponent}>\n {parseInt(option.key) < 6 ? (\n <Avatar\n imageSrc={`https://cdn.sensehq.com/genesis/hashed/static/images/personas/${option.key}.png`}\n />\n ) : (\n <Avatar iconName=\"user\" />\n )}\n\n <ButtonTextMedium className={css.text} color=\"secondary\">\n <Truncate>{option.label}</Truncate>\n </ButtonTextMedium>\n </div>\n );\n\n return <Menu {...args} resolveLabel={resolveLabel} />;\n};\n\nWithResolversStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n options: textOptions,\n testId: 'menu-with-resolvers-story',\n};\n\nconst composeMenuOptions: Array<Array<MenuOption>> = [\n [\n {\n key: '1',\n label: 'Option one',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n {\n key: '2',\n label: 'Option two',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n {\n key: '3',\n label: 'Option three',\n secondaryLabel: 'Sub Text',\n optionSize: 'small',\n },\n ],\n [\n {\n iconLeft: 'coffee',\n key: '4',\n label: 'Option one',\n optionSize: 'medium',\n },\n ],\n [\n {\n iconLeft: 'coffee',\n key: '5',\n label: 'Option one',\n optionSize: 'medium',\n },\n ],\n];\n\nexport const ComposeMenuStory = (args: MenuProps) => <Menu {...args} />;\n\nComposeMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'medium',\n composeOptions: composeMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-compose-story',\n};\n\nconst customComponentMenuOptions: Array<MenuOption> = [\n {\n key: '1',\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n color=\"blue\"\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/1.png\"\n iconName=\"face-party\"\n iconType=\"regular\"\n size=\"medium\"\n />\n <ButtonTextMedium className={css.text}>Angelina White</ButtonTextMedium>\n </div>\n ),\n },\n {\n key: '2',\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/4.png\"\n color=\"green\"\n size=\"medium\"\n text=\"John\"\n />\n <ButtonTextMedium className={css.text}>John Smith</ButtonTextMedium>\n </div>\n ),\n },\n {\n key: '3',\n\n customComponent: (\n <div className={css.customComponent}>\n <Avatar\n imageSrc=\"https://cdn.sensehq.com/genesis/hashed/static/images/personas/6.png\"\n color=\"orange\"\n size=\"medium\"\n text=\"Jacobs Marley\"\n />\n <ButtonTextMedium className={css.text}>Sarah Parker</ButtonTextMedium>\n </div>\n ),\n },\n];\n\nexport const CustomComponentMenuStory = (args: MenuProps) => <Menu {...args} />;\n\nCustomComponentMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: customComponentMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-custom-component-story',\n};\n\nconst groupTitleComponentMenuOptions: Array<MenuGroupTitleOption> = [\n {\n groupTitle: (\n <div className={css.titleWrapper}>\n <FormLabelSmall color=\"tertiary\">Fast Food</FormLabelSmall>\n <div className={css.infoContainer}>\n <Tooltip title=\"Hello!\" elevation=\"menu\">\n <Icon name=\"circle-info\" size=\"small\" color=\"tertiary\" />\n </Tooltip>\n </div>\n </div>\n ),\n options: [\n {\n key: '1',\n iconLeft: 'burger',\n label: 'Burger',\n },\n {\n key: '2',\n iconLeft: 'pizza',\n label: 'Pizza',\n },\n {\n key: '3',\n iconLeft: 'french-fries',\n label: 'Fries',\n },\n ],\n },\n {\n showLineDivider: true,\n groupTitle: (\n <div className={css.titleWrapper}>\n <FormLabelSmall color=\"tertiary\">Fruits</FormLabelSmall>\n <div className={css.infoContainer}>\n <Tooltip title=\"Hello!\" elevation=\"menu\">\n <Icon name=\"circle-info\" size=\"small\" color=\"tertiary\" />\n </Tooltip>\n </div>\n </div>\n ),\n options: [\n {\n key: '7',\n iconLeft: 'banana',\n label: 'Banana',\n },\n {\n key: '8',\n iconLeft: 'pear',\n label: 'Pear',\n },\n {\n key: '9',\n iconLeft: 'lemon',\n label: 'Lemon',\n },\n ],\n },\n];\n\nexport const GroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n groupTitleOptions: groupTitleComponentMenuOptions,\n selectedKeys: ['2'],\n testId: 'menu-group-title-story',\n};\n\nexport const VirtualizedGroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nVirtualizedGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n groupTitleOptions: Array.from({length: 20}, (_, groupIdx) => ({\n groupTitle: `Group ${groupIdx + 1}`,\n options: Array.from({length: 25}, (_, optionIdx) => ({\n key: `${groupIdx}-${optionIdx}`,\n label: `Group ${groupIdx + 1} option ${optionIdx + 1}`,\n // every 5th option carries a description, so it needs a taller row\n ...(optionIdx % 5 === 0\n ? {secondaryLabel: 'Option with a description'}\n : {}),\n })),\n })),\n virtualization: {\n enable: true,\n // size rows from the option itself, not its index\n itemHeight: (option: MenuOption) => (option.secondaryLabel ? 56 : 36),\n groupTitleHeight: 32,\n menuHeight: 320,\n },\n selectedKeys: ['0-1'],\n testId: 'menu-virtualized-group-title-story',\n};\n\nexport const VirtualizedCustomGroupTitleMenuStory = (args: MenuProps) => (\n <Menu {...args} classNames={{groupTitle: css.gpTitle}} />\n);\n\nVirtualizedCustomGroupTitleMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n groupTitleOptions: Array.from({length: 10}, (_, groupIdx) => ({\n // every other group has a two-line custom title node\n groupTitle:\n groupIdx % 2 === 0 ? (\n <div>\n <div>Group {groupIdx + 1}</div>\n <div style={{fontSize: '11px', fontWeight: 'normal'}}>\n Two-line custom header\n </div>\n </div>\n ) : (\n `Group ${groupIdx + 1}`\n ),\n options: Array.from({length: 15}, (_, optionIdx) => ({\n key: `${groupIdx}-${optionIdx}`,\n label: `Group ${groupIdx + 1} option ${optionIdx + 1}`,\n })),\n })),\n virtualization: {\n enable: true,\n itemHeight: 36,\n // tall rows for the two-line custom titles, default for the rest\n groupTitleHeight: (_groupTitle: React.ReactNode, groupIndex: number) =>\n groupIndex % 2 === 0 ? 56 : 32,\n menuHeight: 320,\n },\n testId: 'menu-virtualized-custom-group-title-story',\n};\n\nexport const VirtualizedVariableHeightMenuStory = (args: MenuProps) => (\n <Menu {...args} />\n);\n\nVirtualizedVariableHeightMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n allowSearch: true,\n options: Array.from({length: 200}, (_, optionIdx) => ({\n key: `${optionIdx}`,\n label: `Option ${optionIdx + 1}`,\n ...(optionIdx % 4 === 0\n ? {secondaryLabel: 'Option with a description'}\n : {}),\n })),\n virtualization: {\n enable: true,\n itemHeight: (option: MenuOption) => (option.secondaryLabel ? 56 : 36),\n menuHeight: 320,\n },\n selectedKeys: ['1'],\n testId: 'menu-virtualized-variable-height-story',\n};\n\nconst GroupTitleActionMenu = (args: MenuProps) => {\n const [searchText, setSearchText] = React.useState<string>('');\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n const optionsFiltered = getFilteredOptionsFromSearchText(\n textOptions,\n searchText,\n );\n const optionsFilteredKeys = optionsFiltered\n .filter(({disabled}) => !disabled)\n .map(({key}) => key);\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(difference(selectedKeys, [option.key]));\n } else {\n setSelectedKeys(union(selectedKeys, [option.key]));\n }\n };\n\n const selectAllKeys: React.MouseEventHandler<HTMLButtonElement> = () => {\n setSelectedKeys(union(selectedKeys, optionsFilteredKeys));\n };\n\n const clearSelectedKeys: React.MouseEventHandler<HTMLButtonElement> = () => {\n setSelectedKeys(difference(selectedKeys, optionsFilteredKeys));\n };\n\n return (\n <Menu\n {...args}\n optionsVariant=\"checkbox\"\n selectedKeys={selectedKeys}\n onSelect={selectKeys}\n classNames={{\n header: css.groupTitleActionHeader,\n groupTitle: css.groupTitle,\n }}\n header={\n <SearchInput\n value={searchText}\n onChange={(e) => setSearchText(e.target.value)}\n onClear={() => setSearchText('')}\n size={args.size}\n />\n }\n groupTitleOptions={[\n {\n groupTitle: (\n <>\n <FormLabelMedium color=\"tertiary\">\n {`Values (${optionsFiltered.length})`}\n </FormLabelMedium>\n <div className={css.groupTitleActions}>\n <UnstyledButton onClick={clearSelectedKeys}>\n <ButtonTextSmallUnderline className={css.groupTitleAction}>\n Clear\n </ButtonTextSmallUnderline>\n </UnstyledButton>\n <UnstyledButton onClick={selectAllKeys}>\n <ButtonTextSmallUnderline\n color=\"clickable\"\n className={css.groupTitleAction}\n >\n Select All\n </ButtonTextSmallUnderline>\n </UnstyledButton>\n </div>\n </>\n ),\n options: optionsFiltered,\n },\n ]}\n />\n );\n};\n\nexport const GroupTitleActionMenuStory = (args: MenuProps) => (\n <GroupTitleActionMenu {...args} />\n);\n\nGroupTitleActionMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n selectedKeys: ['2'],\n testId: 'menu-group-title-action-story',\n};\n\nexport const CheckboxMenuStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(selectedKeys.filter((item) => item !== option.key));\n } else {\n setSelectedKeys([...selectedKeys, option.key]);\n }\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nCheckboxMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: checkboxOptions,\n optionsVariant: 'checkbox',\n selectedKeys: ['1', '4'],\n testId: 'menu-checkbox-story',\n};\n\nexport const RadioMenuStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n setSelectedKeys([option.key]);\n };\n\n return <Menu {...args} selectedKeys={selectedKeys} onSelect={selectKeys} />;\n};\n\nRadioMenuStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n size: 'medium',\n options: textOptions,\n optionsVariant: 'radio',\n selectedKeys: ['1'],\n testId: 'menu-radio-story',\n};\n\nexport const MenuHeader = () => (\n <div className={css.headerComponent}>\n <SubTitleSmall>Menu Header</SubTitleSmall>\n </div>\n);\n\nexport const MenuFooter = ({\n onSave,\n onCancel,\n}: {\n onSave?: () => void;\n onCancel?: () => void;\n}) => (\n <div className={css.footerComponent}>\n <Button size=\"small\" type=\"ghost\" onClick={onCancel}>\n Cancel\n </Button>\n <Button size=\"small\" type=\"secondary\" onClick={onSave}>\n Save\n </Button>\n </div>\n);\n\nexport const WithHeaderStory = (args: MenuProps) => (\n <Menu {...args} header={<MenuHeader />} />\n);\n\nWithHeaderStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'small',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-with-header-story',\n};\n\nexport const WithFooterStory = (args: MenuProps) => (\n <Menu {...args} footer={<MenuFooter />} />\n);\n\nWithFooterStory.args = {\n isFluid: false,\n menuDisabled: false,\n size: 'small',\n options: textOptions,\n selectedKeys: ['2'],\n testId: 'menu-with-footer-story',\n};\n\nexport const WithHeaderAndFooterStory = (args: MenuProps) => {\n const [selectedKeys, setSelectedKeys] = React.useState(\n args.selectedKeys || [],\n );\n\n React.useEffect(() => {\n setSelectedKeys(args.selectedKeys || []);\n }, [args.selectedKeys]);\n\n const selectKeys = (option: MenuOption) => {\n if (selectedKeys.includes(option.key)) {\n setSelectedKeys(selectedKeys.filter((item) => item !== option.key));\n } else {\n setSelectedKeys([...selectedKeys, option.key]);\n }\n };\n\n return (\n <Menu\n {...args}\n selectedKeys={selectedKeys}\n onSelect={selectKeys}\n header={<MenuHeader />}\n footer={<MenuFooter />}\n />\n );\n};\n\nWithHeaderAndFooterStory.args = {\n isFluid: false,\n menuDisabled: false,\n classNames: {option: css.optionText},\n options: textOptions,\n optionsVariant: 'checkbox',\n selectedKeys: ['1', '4'],\n testId: 'menu-with-header-and-footer-story',\n};\nTextOnlyStory.storyName = 'Text Only';\nTextWithTooltipStory.storyName = 'Text With Tooltip';\nMenuWithWrapStory.storyName = 'Menu With Wrap';\nWithSearchStory.storyName = 'With Search';\nTextAndIconLeftStory.storyName = 'Text And Icon Left';\nColoredOptionStory.storyName = 'Colored Option';\nTextAndIconRightStory.storyName = 'Text And Icon Right';\nTextAndDualIconStory.storyName = 'Text And Dual Icon';\nDualTextStory.storyName = 'Dual Text';\nWithResolversStory.storyName = 'With Resolvers';\nComposeMenuStory.storyName = 'Compose Menu';\nCustomComponentMenuStory.storyName = 'Custom Component Menu';\nGroupTitleMenuStory.storyName = 'Group Title Menu';\nVirtualizedGroupTitleMenuStory.storyName = 'Virtualized Group Title Menu';\nVirtualizedCustomGroupTitleMenuStory.storyName =\n 'Virtualized Custom Group Title Menu';\nVirtualizedVariableHeightMenuStory.storyName =\n 'Virtualized Variable Height Menu';\nGroupTitleActionMenuStory.storyName = 'Group Title Action Menu';\nCheckboxMenuStory.storyName = 'Checkbox Menu';\nRadioMenuStory.storyName = 'Radio Menu';\nWithHeaderStory.storyName = 'With Header';\nWithFooterStory.storyName = 'With Footer';\nWithHeaderAndFooterStory.storyName = 'With Header And Footer';\n"
|
|
1625
1625
|
},
|
|
1626
1626
|
"css": {
|
|
1627
1627
|
"path": "Menu.module.css",
|
|
1628
|
-
"content": "@value (\n borderWidthPrimary,\n borderWidthNone,\n borderWidthTertiary,\n borderRadiusMedium,\n borderRadiusSmall\n) from '../../styles/variables/_border.css';\n\n@value (\n colorBackgroundSecondary,\n colorFillSecondary,\n colorFillPrimary,\n colorTextDisabled,\n colorTextPrimary,\n colorTextSecondary,\n colorTextTertiary,\n colorBackgroundTertiary,\n colorFocusSecondary\n) from '../../styles/variables/_color.css';\n\n@value (\n colorBorderPrimary\n) from '../../styles/variables/_color.css';\n\n@value (\n size160,\n size42,\n size34,\n size50,\n size300,\n size240,\n size228,\n size276,\n sizeFluid\n) from '../../styles/variables/_size.css';\n\n@value (\n spaceNone,\n spaceSmall,\n spaceXSmall,\n spaceXXSmall,\n spaceMedium\n) from '../../styles/variables/_space.css';\n\n@value (\n elevationMenu,\n elevationCard\n) from '../../styles/variables/_elevation.css';\n\n.menuCard {\n position: relative;\n composes: boxShadow2 from '../../styles/shadow.module.css';\n composes: borderPrimary from '../../styles/border.module.css';\n background-color: colorBackgroundTertiary;\n display: flex;\n flex-flow: column;\n padding: spaceXSmall;\n min-width: size160;\n border-radius: borderRadiusMedium;\n overflow: auto;\n z-index: elevationMenu;\n}\n\n/*\n * The options carry their own scroll container, so the card must not scroll as\n * well — otherwise both would show a scrollbar. `hidden` also keeps a pinned\n * header / footer inside the border radius.\n */\n.menuCardTopPaddingZero,\n.menuCardBottomPaddingZero,\n.menuCardNoScroll {\n overflow: hidden;\n}\n\n.menuCardTopPaddingZero {\n padding-top: spaceNone;\n}\n\n.menuCardBottomPaddingZero {\n padding-bottom: spaceNone;\n}\n\n/*\n * Scrolls the options only. A header / footer is a flex sibling of this element\n * rather than something sticky inside it, so the scrollbar spans just the option\n * list — it no longer runs the whole height of the card behind the bars — and no\n * option can be clipped under them. `min-height: 0` is what lets a flex item\n * actually shrink and scroll instead of growing to fit its content.\n */\n.optionsScroll {\n display: flex;\n flex-flow: column;\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n/*\n * Collapse a menu that renders nothing. `:empty` alone no longer suffices: the\n * card always holds the (possibly empty) role=\"listbox\" element so the trigger's\n * aria-controls resolves, so Menu also sets `.menuCardEmpty` explicitly.\n */\n.menuCard:empty,\n.menuCard.menuCardEmpty {\n padding: spaceNone;\n border: none;\n}\n\n.medium {\n width: size300;\n min-width: size300;\n max-height: size276;\n}\n\n.mediumWithHeader,\n.mediumWithFooter {\n max-height: calc(size276 + size50);\n}\n\n.mediumWithHeaderAndFooter {\n max-height: calc(size276 + size50 + size50);\n}\n\n.small {\n width: size240;\n min-width: size240;\n max-height: size228;\n}\n\n.smallWithHeader,\n.smallWithFooter {\n max-height: calc(size228 + size50);\n}\n\n.smallWithHeaderAndFooter {\n max-height: calc(size228 + size50 + size50);\n}\n\n.fluid {\n width: sizeFluid;\n min-width: size160;\n}\n\n.option {\n composes: motionEaseInEaseOut from '../../styles/animation.module.css';\n flex: 0 0 auto;\n width: sizeFluid;\n display: flex;\n border-radius: borderRadiusSmall;\n align-items: center;\n cursor: pointer;\n column-gap: spaceSmall;\n color: colorTextSecondary;\n}\n\n.optionTextContainer {\n display: flex;\n flex-flow: column;\n gap: spaceXXSmall;\n}\n\n.optionTextLabel,\n.optionTextSecondaryLabel {\n display: flex;\n flex-flow: column;\n align-items: flex-start;\n text-align: left;\n}\n\n.withIconLeft {\n padding-left: spaceXSmall;\n}\n\n.withIconRight {\n padding-right: spaceXSmall;\n}\n\n.icon {\n color: inherit;\n}\n\n.rightIcon {\n margin-left: auto;\n}\n\n.optionSmall {\n composes: menuTextSmall from '../../styles/typography.module.css';\n padding: spaceXSmall spaceSmall;\n}\n\n.optionMedium {\n composes: menuTextMedium from '../../styles/typography.module.css';\n padding: spaceSmall spaceSmall;\n}\n\n.optionTextSecondaryLabel {\n composes: bodySmall from '../../styles/typography.module.css';\n color: colorTextTertiary;\n}\n\n/*\n * Background for the highlighted option (the \"cursor\"). There is only ever one —\n * the Menu keeps it in sync across pointer and keyboard. `.active` covers the\n * combobox active-descendant case (focus stays on the input); :hover covers a\n * plain mouse-over.\n */\n.option:hover,\n.option:active,\n.option.active {\n outline: none;\n color: colorTextPrimary;\n background: colorBackgroundSecondary;\n}\n\n.option:hover .optionTextSecondaryLabel,\n.option:active .optionTextSecondaryLabel,\n.option.active .optionTextSecondaryLabel,\n.option:focus .optionTextSecondaryLabel {\n color: colorTextSecondary;\n}\n\n/*\n * Selected value (committed), shown as a stronger fill than the active highlight.\n *\n * Deliberately no left accent bar: a 2px inset box-shadow gets tapered by the\n * option's 8px border-radius, so it renders as a small curved sliver at the edge\n * that reads as a stray focus artefact rather than an intentional indicator. The\n * checkbox / radio glyph carries the state in those variants, and forced-colors\n * mode below re-expresses it as a full outline, which the radius follows cleanly.\n */\n.option.selected {\n background: colorFillSecondary;\n outline: none;\n color: colorTextPrimary;\n}\n\n/*\n * Focus ring — KEYBOARD ONLY. A focus indicator has to mean \"focus is here\", so\n * it must not appear under the mouse pointer:\n *\n * - `.activeRing` is set by Menu for the combobox modes (Dropdown / Typeahead),\n * where DOM focus stays on the input and the active option is virtual. Menu\n * withholds it when the highlight came from the pointer.\n * - `:focus-visible` covers the roving menus, which move real DOM focus. It is\n * exactly the right selector here: arrow-key navigation calls .focus() after a\n * key press so the ring shows, while hover-driven .focus() follows a pointer\n * interaction so it does not.\n *\n * Declared after `.selected`, which sets no box-shadow of its own, so a selected\n * option that also has focus shows the ring without anything to fight over.\n */\n.option.activeRing,\n.option:focus-visible {\n outline: none;\n color: colorTextPrimary;\n box-shadow: inset borderWidthNone borderWidthNone borderWidthNone\n borderWidthTertiary colorFocusSecondary;\n}\n\n/*\n * Windows High Contrast / forced-colors mode drops box-shadow and flattens\n * background, so the bar + tint indicators above vanish. Re-express the active\n * highlight and selected state with system-color outlines (honored in HCM).\n */\n@media (forced-colors: active) {\n .option:hover,\n .option.active {\n outline: borderWidthTertiary solid Highlight;\n outline-offset: calc(-1 * borderWidthTertiary);\n }\n\n .option.selected {\n outline: borderWidthTertiary solid SelectedItem;\n outline-offset: calc(-1 * borderWidthTertiary);\n }\n\n /*\n * Keyboard focus must stay distinguishable from BOTH the hover highlight and\n * the selected state, and Highlight / SelectedItem commonly resolve to the same\n * system color — so change the outline STYLE rather than add a third color.\n * Declared last so a selected option that also has focus still shows it.\n */\n .option.activeRing,\n .option:focus-visible {\n outline: borderWidthTertiary dashed Highlight;\n outline-offset: calc(-1 * borderWidthTertiary);\n }\n}\n\n.disabled {\n color: colorTextDisabled;\n pointer-events: none;\n}\n\n.disabled .optionTextSecondaryLabel {\n color: colorTextDisabled;\n}\n\n.menuDivider {\n padding-top: spaceXSmall;\n padding-bottom: spaceXSmall;\n}\n\n.menuDivider:not(:first-of-type) {\n border-top: borderWidthPrimary solid colorBorderPrimary;\n padding-top: spaceXSmall;\n padding-bottom: spaceXSmall;\n}\n\n.hideDivider {\n border-top: none;\n}\n\n.menuDivider:first-child {\n padding-top: spaceNone;\n}\n\n.menuDivider:last-child {\n padding-bottom: spaceNone;\n}\n\n.optionsWrapper {\n display: flex;\n flex-flow: column;\n}\n\n/*\n * Wraps only the option list so the listbox role contains option children\n * exclusively (search/header/footer stay outside it). display:contents keeps\n * the existing flex layout of .menuCard unchanged.\n */\n.optionsListbox {\n display: contents;\n}\n\n/*\n * The listbox as the element that actually scrolls. Under virtualization\n * react-window's own scroll box carries the role (see ListboxOuter); this is the\n * same arrangement for a plain option list, and it matters beyond tidiness: the\n * options are not focusable themselves whenever a combobox trigger drives the\n * menu, so a\n * scroll container that is NOT the combobox's popup reads as an unreachable\n * scroll area (axe `scrollable-region-focusable`). Making the listbox the\n * scroller means the region IS the popup, which that rule exempts.\n */\n.optionsListboxScroll {\n display: flex;\n flex-flow: column;\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n/*\n * role=\"group\" wrapper around each titled option group. display:contents keeps\n * the existing flex layout (title + options) unchanged.\n */\n.optionGroup {\n display: contents;\n}\n\n.groupTitleWrapper {\n composes: formLabelSmall from '../../styles/typography.module.css';\n display: flex;\n padding-bottom: spaceXSmall;\n padding-left: spaceSmall;\n padding-right: calc(spaceXSmall / 2);\n color: colorTextTertiary;\n padding-top: spaceSmall;\n}\n\n.filterOptionsResultText {\n margin-top: calc(spaceXSmall * 2);\n margin-bottom: spaceXSmall;\n margin-left: spaceSmall;\n}\n\n/*\n * Pinned by living OUTSIDE the options scroll container rather than by sticking\n * inside it, so it never has the option list's scrollbar running behind it and\n * never has an option tucked under it. `relative` positions the divider below.\n */\n.menuHeader {\n position: relative;\n flex: 0 0 auto;\n background-color: colorBackgroundTertiary;\n padding: spaceXSmall spaceNone;\n margin-bottom: spaceXSmall;\n z-index: calc(elevationCard/4);\n min-height: size50;\n max-height: size50;\n}\n\n.menuHeader::after {\n content: '';\n position: absolute;\n left: calc(spaceXSmall * -1);\n right: calc(spaceXSmall * -1);\n bottom: spaceNone;\n border-bottom: borderWidthPrimary solid colorBorderPrimary;\n}\n\n/* As `.menuHeader`, pinned by sitting outside the options scroll container. */\n.menuFooter {\n position: relative;\n flex: 0 0 auto;\n background-color: colorBackgroundTertiary;\n padding: spaceXSmall spaceNone;\n margin-top: spaceXSmall;\n min-height: size50;\n max-height: size50;\n}\n\n.menuFooter::before {\n content: '';\n position: absolute;\n left: calc(spaceXSmall * -1);\n right: calc(spaceXSmall * -1);\n top: spaceNone;\n border-top: borderWidthPrimary solid colorBorderPrimary;\n}\n"
|
|
1628
|
+
"content": "@value (\n borderWidthPrimary,\n borderWidthNone,\n borderWidthTertiary,\n borderRadiusMedium,\n borderRadiusSmall\n) from '../../styles/variables/_border.css';\n\n@value (\n colorBackgroundSecondary,\n colorFillSecondary,\n colorTextDisabled,\n colorTextPrimary,\n colorTextSecondary,\n colorTextTertiary,\n colorBackgroundTertiary,\n colorFocusSecondary\n) from '../../styles/variables/_color.css';\n\n@value (\n colorBorderPrimary\n) from '../../styles/variables/_color.css';\n\n@value (\n size160,\n size42,\n size34,\n size50,\n size300,\n size240,\n size228,\n size276,\n sizeFluid\n) from '../../styles/variables/_size.css';\n\n@value (\n spaceNone,\n spaceSmall,\n spaceXSmall,\n spaceXXSmall,\n spaceMedium\n) from '../../styles/variables/_space.css';\n\n@value (\n elevationMenu,\n elevationCard\n) from '../../styles/variables/_elevation.css';\n\n.menuCard {\n position: relative;\n composes: boxShadow2 from '../../styles/shadow.module.css';\n composes: borderPrimary from '../../styles/border.module.css';\n background-color: colorBackgroundTertiary;\n display: flex;\n flex-flow: column;\n padding: spaceXSmall;\n min-width: size160;\n border-radius: borderRadiusMedium;\n overflow: auto;\n z-index: elevationMenu;\n}\n\n.menuCardTopPaddingZero {\n padding-top: spaceNone;\n}\n\n.menuCardBottomPaddingZero {\n padding-bottom: spaceNone;\n}\n\n.menuCard:empty {\n padding: spaceNone;\n border: none;\n}\n\n.medium {\n width: size300;\n min-width: size300;\n max-height: size276;\n}\n\n.mediumWithHeader,\n.mediumWithFooter {\n max-height: calc(size276 + size50);\n}\n\n.mediumWithHeaderAndFooter {\n max-height: calc(size276 + size50 + size50);\n}\n\n.small {\n width: size240;\n min-width: size240;\n max-height: size228;\n}\n\n.smallWithHeader,\n.smallWithFooter {\n max-height: calc(size228 + size50);\n}\n\n.smallWithHeaderAndFooter {\n max-height: calc(size228 + size50 + size50);\n}\n\n.fluid {\n width: sizeFluid;\n min-width: size160;\n}\n\n.option {\n composes: motionEaseInEaseOut from '../../styles/animation.module.css';\n flex: 0 0 auto;\n width: sizeFluid;\n display: flex;\n border-radius: borderRadiusSmall;\n align-items: center;\n cursor: pointer;\n column-gap: spaceSmall;\n color: colorTextSecondary;\n}\n\n.optionTextContainer {\n display: flex;\n flex-flow: column;\n gap: spaceXXSmall;\n}\n\n.optionTextLabel,\n.optionTextSecondaryLabel {\n display: flex;\n flex-flow: column;\n align-items: flex-start;\n text-align: left;\n}\n\n.withIconLeft {\n padding-left: spaceXSmall;\n}\n\n.withIconRight {\n padding-right: spaceXSmall;\n}\n\n.icon {\n color: inherit;\n}\n\n.rightIcon {\n margin-left: auto;\n}\n\n.optionSmall {\n composes: menuTextSmall from '../../styles/typography.module.css';\n padding: spaceXSmall spaceSmall;\n}\n\n.optionMedium {\n composes: menuTextMedium from '../../styles/typography.module.css';\n padding: spaceSmall spaceSmall;\n}\n\n.optionTextSecondaryLabel {\n composes: bodySmall from '../../styles/typography.module.css';\n color: colorTextTertiary;\n}\n\n.option:hover,\n.option:active {\n background: colorBackgroundSecondary;\n color: colorTextPrimary;\n}\n\n.option:hover .optionTextSecondaryLabel {\n color: colorTextSecondary;\n}\n\n.option:active .optionTextSecondaryLabel {\n color: colorTextSecondary;\n}\n\n.option:focus {\n outline: none;\n color: colorTextPrimary;\n}\n\n.option:focus-visible {\n outline: none;\n color: colorTextPrimary;\n box-shadow: inset borderWidthNone borderWidthNone borderWidthNone\n borderWidthTertiary colorFocusSecondary;\n}\n\n.option:focus-within {\n outline: none;\n color: colorTextPrimary;\n /* box-shadow: none; */\n}\n\n.option.selected {\n background: colorFillSecondary;\n outline: none;\n color: colorTextPrimary;\n}\n\n.option:focus .optionTextSecondaryLabel {\n color: colorTextSecondary;\n}\n\n.disabled {\n color: colorTextDisabled;\n pointer-events: none;\n}\n\n.disabled .optionTextSecondaryLabel {\n color: colorTextDisabled;\n}\n\n.menuDivider {\n padding-top: spaceXSmall;\n padding-bottom: spaceXSmall;\n}\n\n.menuDivider:not(:first-of-type) {\n border-top: borderWidthPrimary solid colorBorderPrimary;\n padding-top: spaceXSmall;\n padding-bottom: spaceXSmall;\n}\n\n.hideDivider {\n border-top: none;\n}\n\n.menuDivider:first-child {\n padding-top: spaceNone;\n}\n\n.menuDivider:last-child {\n padding-bottom: spaceNone;\n}\n\n.optionsWrapper {\n display: flex;\n flex-flow: column;\n}\n\n.groupTitleWrapper {\n composes: formLabelSmall from '../../styles/typography.module.css';\n display: flex;\n padding-bottom: spaceXSmall;\n padding-left: spaceSmall;\n padding-right: calc(spaceXSmall / 2);\n color: colorTextTertiary;\n padding-top: spaceSmall;\n}\n\n.filterOptionsResultText {\n margin-top: calc(spaceXSmall * 2);\n margin-bottom: spaceXSmall;\n margin-left: spaceSmall;\n}\n\n.menuHeader {\n position: sticky;\n background-color: colorBackgroundTertiary;\n padding: spaceXSmall spaceNone;\n top: spaceNone;\n margin-bottom: spaceXSmall;\n z-index: calc(elevationCard/4);\n min-height: size50;\n max-height: size50;\n}\n\n.menuHeader::after {\n content: '';\n position: absolute;\n left: calc(spaceXSmall * -1);\n right: calc(spaceXSmall * -1);\n bottom: spaceNone;\n border-bottom: borderWidthPrimary solid colorBorderPrimary;\n}\n\n.menuFooter {\n position: sticky;\n background-color: colorBackgroundTertiary;\n padding: spaceXSmall spaceNone;\n bottom: spaceNone;\n margin-top: spaceXSmall;\n min-height: size50;\n max-height: size50;\n}\n\n.menuFooter::before {\n content: '';\n position: absolute;\n left: calc(spaceXSmall * -1);\n right: calc(spaceXSmall * -1);\n top: spaceNone;\n border-top: borderWidthPrimary solid colorBorderPrimary;\n}\n"
|
|
1629
1629
|
},
|
|
1630
1630
|
"index": {
|
|
1631
1631
|
"path": "index.d.ts",
|
|
1632
|
-
"content": "export * from '../../components/Menu/Menu';\nexport * from '../../components/Menu/MenuOptionButton';\
|
|
1632
|
+
"content": "export * from '../../components/Menu/Menu';\nexport * from '../../components/Menu/MenuOptionButton';\n//# sourceMappingURL=index.d.ts.map"
|
|
1633
1633
|
},
|
|
1634
1634
|
"additional": {
|
|
1635
1635
|
"MenuOptionButton.d.ts": {
|
|
1636
1636
|
"path": "MenuOptionButton.d.ts",
|
|
1637
|
-
"content": "import * as React from 'react';\nimport type { CSSProperties } from 'react';\nimport type { BaseMenuProps, MenuOption } from '../../components/Menu/Menu';\nexport interface MenuOptionProps extends Omit<BaseMenuProps, 'option' | 'style'> {\n option: MenuOption;\n
|
|
1638
|
-
},
|
|
1639
|
-
"menuOptionId.d.ts": {
|
|
1640
|
-
"path": "menuOptionId.d.ts",
|
|
1641
|
-
"content": "/**\n * The DOM id of an option row — the one thing the row itself, the listbox's\n * `aria-activedescendant`, and a combobox trigger's `aria-activedescendant`\n * must all agree on, so it is built in exactly one place.\n *\n * Option keys are consumer data and may contain whitespace (\"New York\"), which\n * an id can hold but an IDREF cannot: `aria-activedescendant=\"m-option-New\n * York\"` is a list of two references, neither of which resolves. Encoding the\n * key keeps the id a single token.\n */\nexport declare const menuOptionId: (menuId: string, key: string) => string;\n//# sourceMappingURL=menuOptionId.d.ts.map"
|
|
1642
|
-
},
|
|
1643
|
-
"useMenuKeyboardNavigation.d.ts": {
|
|
1644
|
-
"path": "useMenuKeyboardNavigation.d.ts",
|
|
1645
|
-
"content": "import * as React from 'react';\n/**\n * Keyboard navigation for the Menu option list. Three interaction models,\n * selected via `mode`:\n *\n * 1. `'roving'` — ROVING FOCUS (default; menu-button popups). Options are native\n * <button>s, so ArrowUp/ArrowDown move real DOM focus between the enabled\n * options and Space/Enter activate the focused option natively. Home/End jump\n * to the first/last option, PageUp/PageDown move by a page, and typing a\n * printable character jumps to the next matching label (type-ahead).\n *\n * 2. `'activedescendant'` — SELECT-ONLY COMBOBOX. DOM focus stays on the\n * combobox input; the \"active\" option is a virtual highlight reported via\n * `setActiveOptionKey` (surfaced as the input's aria-activedescendant) and\n * NEVER takes real focus. Arrows/Home/End/PageUp/PageDown move the highlight,\n * printable characters type-ahead, and Enter selects the active option.\n *\n * 3. `'search'` — EDITABLE COMBOBOX. Like `'activedescendant'`, but the input is\n * a free-text field: printable keys, Left/Right and Home/End are left to the\n * input for text editing (no type-ahead). Arrows/PageUp/PageDown move the\n * highlight and Enter selects.\n *\n * Navigation wraps around at the ends. Options are discovered from the DOM\n * (`[data-menu-option-key]`) so it always matches what is rendered after search\n * filtering / grouping.\n *\n * Tab / Shift+Tab keep focus moving through the menu's own focus zones in DOM\n * order — header → search → options → footer — and only close the menu (via\n * `onTabOut`) when focus would leave the menu entirely (Tab past the last\n * focusable, Shift+Tab before the first). This keeps focus from silently\n * dropping to <body> when the popup renders in a portal with nothing after it.\n */\nexport type MenuNavigationMode = 'roving' | 'activedescendant' | 'search';\n/** One navigable option, for menus whose option DOM is windowed. */\nexport interface MenuNavigationOption {\n key: string;\n label: string;\n}\ninterface UseMenuKeyboardNavigationArgs {\n /** Ref to the element that contains the option buttons (the Menu listbox). */\n containerRef: React.RefObject<HTMLElement | null>;\n /** Disable all handling (e.g. when the menu is disabled or closed). */\n enabled?: boolean;\n /** Interaction model — see the file header. Defaults to `'roving'`. */\n mode?: MenuNavigationMode;\n /** Key of the option currently highlighted (needed for the non-roving modes). */\n activeOptionKey?: string | null;\n /** Select the option with the given key (used for Enter in the combobox modes). */\n onSelectKey?: (key: string) => void;\n /** Report the option key that owns the roving tab stop / active descendant. */\n setActiveOptionKey: (key: string | null) => void;\n /** Close the menu when Tab / Shift+Tab would move focus out of it. */\n onTabOut?: () => void;\n /**\n * Combobox menus only: return focus to the trigger input (keeping the menu\n * open) when Tab / Shift+Tab moves off a header/footer control back toward the\n * input — which sits OUTSIDE the menu DOM, so the input is the logical centre\n * of the tab order (header ← input → footer), not an edge. When omitted (the\n * roving menus) the input lives inside the menu, so the simple boundary close\n * applies instead.\n */\n onReturnFocusToInput?: () => void;\n /**\n * Enabled options in render order. Supply this when the option DOM is WINDOWED\n * (react-window virtualization): only a slice of the rows exists at any moment,\n * so DOM discovery would confine Home/End, PageUp/PageDown, wrap-around and\n * type-ahead to that slice — e.g. End on a 100-page Pagination would stop at\n * page 10. When supplied, all index maths resolve against this list and the\n * consumer is responsible for scrolling the active option into view (Menu does\n * this from `activeOptionKey`). Omit it for plain menus: the DOM is then the\n * source of truth, which keeps grouped/compose layouts and `resolveLabel`\n * type-ahead working off the text actually rendered.\n */\n options?: MenuNavigationOption[];\n}\n/**\n * Focusable controls inside the menu's sticky header / footer region, in DOM\n * order. Used by combobox triggers (Dropdown / Typeahead) so Tab / Shift+Tab can\n * move focus into the footer / header instead of closing the menu when one\n * holds focusable controls (e.g. Cancel / Save buttons).\n */\nexport declare const getMenuRegionFocusables: (menu: HTMLElement | null, region: \"header\" | \"footer\") => HTMLElement[];\n/**\n * Move focus into the menu's footer (Tab) or header (Shift+Tab) edge if that\n * region has focusable controls. Returns true when it moved focus, so a combobox\n * trigger can close the menu only when there was no chrome to land on.\n */\nexport declare const moveFocusToMenuChrome: (menu: HTMLElement | null, shiftKey: boolean) => boolean;\nexport declare const useMenuKeyboardNavigation: ({ containerRef, enabled, mode, activeOptionKey, onSelectKey, setActiveOptionKey, onTabOut, onReturnFocusToInput, options, }: UseMenuKeyboardNavigationArgs) => {\n onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => void;\n};\nexport {};\n//# sourceMappingURL=useMenuKeyboardNavigation.d.ts.map"
|
|
1646
|
-
},
|
|
1647
|
-
"useMenuTrigger.d.ts": {
|
|
1648
|
-
"path": "useMenuTrigger.d.ts",
|
|
1649
|
-
"content": "import * as React from 'react';\nimport type { MenuOption, MenuProps } from '../../components/Menu/Menu';\n/**\n * Wires a trigger to the Menu it opens.\n *\n * Three things have to live on the trigger rather than in Menu, because they\n * exist while the menu does NOT: `aria-haspopup`, `aria-expanded=\"false\"`, and\n * the ArrowDown/ArrowUp that opens the popup in the first place. That is the\n * floor — no Menu prop can supply them — so these hooks hand them over as props\n * to spread instead, and own everything else: the ids, the tie between the\n * trigger's name and the listbox, and the ref Menu focuses into on open.\n *\n * `useMenuTrigger` is for triggers whose popup options own focus — a button, a\n * tab overflow chevron, an inline label — so arrows move real DOM focus.\n * `useComboboxMenuTrigger` is for triggers that are text inputs, where focus\n * stays put and the highlight is virtual (`aria-activedescendant`).\n *\n * `getTriggerProps` is a getter rather than a static object because open state\n * belongs to the consumer's `ClickAway`, which exposes it through a render\n * callback — where a hook cannot be called.\n *\n * const menuTrigger = useMenuTrigger({id: props.id, ariaLabel: menu?.ariaLabel});\n * <ClickAway>\n * {({isOpen, onOpen, clickAway}) => (\n * <>\n * <Button {...props} {...menuTrigger.getTriggerProps({isOpen, onOpen})} />\n * {isOpen && <Menu {...menu} {...menuTrigger.menuProps} onTabOut={clickAway} />}\n * </>\n * )}\n * </ClickAway>\n */\nexport interface UseMenuTriggerArgs {\n /**\n * The trigger's own `id`, when the consumer supplied one. It is kept — the\n * hook only mints an id when there is none, since the listbox is named after\n * the trigger through it.\n */\n id?: string;\n /** The listbox id, when the consumer set `menu.menuId`. Minted otherwise. */\n menuId?: string;\n /** Literal name for the listbox. Wins over naming it after the trigger. */\n ariaLabel?: string;\n /** Id of the element naming the listbox. Defaults to the trigger itself. */\n ariaLabelledBy?: string;\n}\n/**\n * Generic in the trigger element so the returned handler matches whatever it is\n * spread onto — inferred from the trigger's own `onKeyDown` where it has one.\n */\nexport interface GetTriggerPropsArgs<T extends HTMLElement = HTMLElement> {\n /** The consumer's popup state — what ClickAway reports. */\n isOpen: boolean;\n onOpen: () => void;\n /** The trigger's own key handler, if it has one. Runs first. */\n onKeyDown?: React.KeyboardEventHandler<T>;\n}\nexport interface MenuTriggerProps<T extends HTMLElement = HTMLElement> {\n id: string;\n 'aria-haspopup': 'listbox';\n 'aria-expanded': boolean;\n 'aria-controls': string | undefined;\n onKeyDown: React.KeyboardEventHandler<T>;\n}\nexport interface MenuTriggerMenuProps {\n menuId: string;\n ariaLabelledBy?: string;\n ariaLabel?: string;\n initialFocusRef: React.RefObject<HTMLElement | null>;\n}\nexport declare const useMenuTrigger: ({ id, menuId: consumerMenuId, ariaLabel, ariaLabelledBy, }?: UseMenuTriggerArgs) => {\n getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetTriggerPropsArgs<T>) => MenuTriggerProps<T>;\n menuProps: MenuTriggerMenuProps;\n};\n/**\n * Which of ARIA's two combobox patterns the trigger implements.\n *\n * - `select` — a read-only field standing in for a `<select>`. An option is\n * highlighted as soon as the list opens, Space selects it (the field has no\n * typing role), and Enter or Space opens the list in the first place.\n * - `autocomplete` — an editable field that filters the list as you type.\n * Nothing is highlighted until the user arrows to it (APG calls this manual\n * selection: auto-highlighting would let Enter select something they never\n * chose), Space types a space, and only the arrow keys open the list.\n */\nexport type ComboboxPattern = 'select' | 'autocomplete';\nexport interface UseComboboxMenuTriggerArgs {\n /** Which pattern this trigger implements. Defaults to `select`. */\n pattern?: ComboboxPattern;\n /**\n * The menu this trigger opens. Read for the option source (unless `options`\n * is given), the selection, `virtualization`, `menuDisabled`, `resolveLabel`,\n * `allowSearch` and the consumer's own `menuId` / `ariaLabel` /\n * `ariaLabelledBy`, all of which win over what the hook would derive.\n */\n menu?: MenuProps;\n /**\n * Option source, when it is not `menu` — a consumer that owns filtering and\n * hands the already-filtered options to Menu supplies them here instead. Every\n * derivation (navigation list, highlight seed, selection lookup) uses it.\n */\n options?: Array<MenuOption>;\n /**\n * Refuse to open, e.g. until enough has been typed. Consulted only for the\n * keys that would otherwise open the list.\n */\n canOpen?: () => boolean;\n /** Ref on the rendered Menu, for reaching its header/footer controls. */\n menuRef: React.RefObject<HTMLElement | null>;\n /**\n * The consumer's popup state — what ClickAway reports. Drives which keys the\n * trigger handles (open-state navigation vs. closed-state opening) and the\n * highlight's lifecycle. If the listbox can be WITHHELD while the popup is\n * open (still loading, no matches), keep passing the open state here and hand\n * `isExpanded` to `getTriggerProps` for the ARIA — routing keys off the\n * rendered state instead would make an arrow key call `onOpen` on an\n * already-open popup, and `onOpen` toggles.\n */\n isOpen: boolean;\n /** Apply a selection. Called with the option the keyboard acted on. */\n onSelectOption: (option: MenuOption) => void;\n /** Close the popup — used when Tab leaves a menu with no chrome to land on. */\n onClose: () => void;\n /** Put focus back on the trigger input, keeping the menu open. */\n focusTrigger: () => void;\n /** What already labels the trigger, used to name the listbox after it. */\n triggerAriaLabelledBy?: string;\n triggerAriaLabel?: string;\n /**\n * Literal name for the listbox when nothing else supplies one. Defaults to\n * `'Options'`; a suggestion list wants something truer to what it holds.\n */\n defaultAriaLabel?: string;\n}\nexport interface GetComboboxTriggerPropsArgs<T extends HTMLElement = HTMLElement> extends GetTriggerPropsArgs<T> {\n /**\n * Whether the listbox is actually rendered right now, when that can differ\n * from `isOpen` (a popup withheld while loading or with no matches). Drives\n * `aria-expanded`, `aria-controls` and `aria-activedescendant`, so the trigger\n * never claims an expanded listbox that is not in the DOM. Defaults to\n * `isOpen`.\n */\n isExpanded?: boolean;\n}\nexport interface ComboboxTriggerProps<T extends HTMLElement = HTMLElement> {\n role: 'combobox';\n 'aria-haspopup': 'listbox';\n 'aria-expanded': boolean;\n 'aria-controls': string | undefined;\n 'aria-autocomplete': 'none' | 'list';\n 'aria-activedescendant': string | undefined;\n onKeyDown: React.KeyboardEventHandler<T>;\n}\nexport interface ComboboxMenuProps {\n menuId: string;\n ariaLabelledBy?: string;\n ariaLabel?: string;\n externalKeyboardNav: boolean;\n activeOptionKey?: string | null;\n onActiveOptionKeyChange?: (key: string | null) => void;\n onReturnFocusToInput?: () => void;\n initialFocusRef?: React.RefObject<HTMLElement | null>;\n}\n/**\n * Wires a COMBOBOX trigger — a text input — to the Menu it opens. Focus stays\n * on the input and the highlight is virtual, reported through\n * `aria-activedescendant`, because the input has to keep receiving the keys it\n * filters with.\n *\n * Driving the highlight from outside Menu means owning the active option as\n * state, and owning it means deriving the same option list, initial highlight\n * and navigation list that Menu derives internally. Both combobox consumers had\n * grown their own copies of all three; they come from Menu itself now.\n *\n * A menu that carries its own filter field (`menu.allowSearch`) inverts the\n * model: navigation lives inside Menu, so the trigger stops driving it and\n * hands over an `initialFocusRef` instead. Both shapes come out of this hook.\n */\nexport declare const useComboboxMenuTrigger: ({ pattern, menu, options, canOpen, menuRef, isOpen, onSelectOption, onClose, focusTrigger, triggerAriaLabelledBy, triggerAriaLabel, defaultAriaLabel, }: UseComboboxMenuTriggerArgs) => {\n getTriggerProps: <T extends HTMLElement = HTMLElement>(args: GetComboboxTriggerPropsArgs<T>) => ComboboxTriggerProps<T>;\n menuProps: ComboboxMenuProps;\n activeOptionKey: string | null;\n};\n//# sourceMappingURL=useMenuTrigger.d.ts.map"
|
|
1637
|
+
"content": "import * as React from 'react';\nimport type { CSSProperties } from 'react';\nimport type { BaseMenuProps, MenuOption } from '../../components/Menu/Menu';\nexport interface MenuOptionProps extends Omit<BaseMenuProps, 'option' | 'isLastItem' | 'style'> {\n option: MenuOption;\n isLastItem?: boolean;\n style?: CSSProperties | undefined;\n testId?: string;\n}\nexport declare const MenuOptionButton: (props: MenuOptionProps) => React.JSX.Element;\n//# sourceMappingURL=MenuOptionButton.d.ts.map"
|
|
1650
1638
|
}
|
|
1651
1639
|
}
|
|
1652
1640
|
},
|
|
@@ -1656,10 +1644,7 @@
|
|
|
1656
1644
|
"Menu.stories.tsx",
|
|
1657
1645
|
"Menu.tsx",
|
|
1658
1646
|
"MenuOptionButton.tsx",
|
|
1659
|
-
"index.ts"
|
|
1660
|
-
"menuOptionId.ts",
|
|
1661
|
-
"useMenuKeyboardNavigation.ts",
|
|
1662
|
-
"useMenuTrigger.ts"
|
|
1647
|
+
"index.ts"
|
|
1663
1648
|
]
|
|
1664
1649
|
},
|
|
1665
1650
|
"Modal": {
|
|
@@ -3338,25 +3323,6 @@
|
|
|
3338
3323
|
"useInputState.ts"
|
|
3339
3324
|
]
|
|
3340
3325
|
},
|
|
3341
|
-
"useInteractionModality": {
|
|
3342
|
-
"name": "useInteractionModality",
|
|
3343
|
-
"path": "src/hooks/useInteractionModality",
|
|
3344
|
-
"files": {
|
|
3345
|
-
"main": {
|
|
3346
|
-
"path": "useInteractionModality.d.ts",
|
|
3347
|
-
"content": "export type InteractionModality = 'keyboard' | 'pointer';\n/**\n * How the user last interacted with the page, read imperatively.\n *\n * Use this when the answer is needed during render or in an effect rather than\n * as reactive state — for example a popup deciding on mount whether it was opened\n * by keyboard, where re-rendering on later modality changes would be wrong.\n */\nexport declare const getInteractionModality: () => InteractionModality;\n/**\n * How the user last interacted with the page, as reactive state. Re-renders the\n * caller when the user switches between keyboard and pointer.\n */\nexport declare const useInteractionModality: () => InteractionModality;\n//# sourceMappingURL=useInteractionModality.d.ts.map"
|
|
3348
|
-
},
|
|
3349
|
-
"story": null,
|
|
3350
|
-
"index": {
|
|
3351
|
-
"path": "index.d.ts",
|
|
3352
|
-
"content": "export * from '../../hooks/useInteractionModality/useInteractionModality';\n//# sourceMappingURL=index.d.ts.map"
|
|
3353
|
-
}
|
|
3354
|
-
},
|
|
3355
|
-
"allFiles": [
|
|
3356
|
-
"index.ts",
|
|
3357
|
-
"useInteractionModality.ts"
|
|
3358
|
-
]
|
|
3359
|
-
},
|
|
3360
3326
|
"useLockedBody": {
|
|
3361
3327
|
"name": "useLockedBody",
|
|
3362
3328
|
"path": "src/hooks/useLockedBody",
|
|
@@ -5165,7 +5131,7 @@
|
|
|
5165
5131
|
"files": {
|
|
5166
5132
|
"click-away.d.ts": {
|
|
5167
5133
|
"path": "click-away.d.ts",
|
|
5168
|
-
"content": "import * as React from 'react';\ntype BoundaryRefType<T extends HTMLElement> = React.RefObject<T | null>;\ntype TriggerRefType<T extends HTMLElement> = React.RefObject<T | null>;\nexport interface ChildProps<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> {\n onOpen: () => void;\n isOpen: boolean;\n height: number | null | undefined;\n pageBottom: number | null | undefined;\n clickAway: () => void;\n boundaryRef: BoundaryRefType<TBoundary>;\n triggerRef: TriggerRefType<TTrigger>;\n}\nexport interface ClickAwayRefType {\n forceClose: () => void;\n forceOpen: () => void;\n}\nexport interface ClickAwayProps<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> {\n closeOnEscapeKeypress?: boolean;\n children: (props: ChildProps<TTrigger, TBoundary>) => React.ReactNode;\n onChange?: (isOpen: boolean) => unknown;\n clickAwayRef?: React.RefObject<ClickAwayRefType | null>;\n /**\n * When containsNestedFloatingPortals is true, prevents ClickAway from closing the dropdown if the click target\n * is inside a floating portal root element.\n *\n * This is necessary for nested dropdowns rendered using floating portals,\n * such as via `@floating-ui/react`. Floating portals render elements outside\n * the normal DOM tree (often directly into `document.body` or another container)\n * using `ReactDOM.createPortal`. As a result, these nested dropdowns do not\n * appear inside the parent dropdown's `boundaryRef` or `triggerRef` subtree.\n *\n * To address this, the floating portal ensures that all nested dropdowns\n * are mounted into a **shared root element** — effectively maintaining a\n * common DOM ancestry. This allows parent dropdowns to detect when clicks\n * occur within any of their nested children, even if rendered via portals.\n * The shared root element is the closest parent element that has a `data-floating-ui-portal` attribute.\n * Because of this, the ClickAway logic will not close the parent menu if the click target is inside boundaryRef's parentElement.\n *\n * Enabling `containsNestedFloatingPortals` allows the ClickAway logic to account\n * for this shared root and avoids incorrectly closing the parent menu when\n * interacting with nested dropdowns.\n */\n containsNestedFloatingPortals?: boolean;\n}\ninterface ClickAwayState {\n isOpen: boolean;\n height: number | null | undefined;\n pageBottom: number | null | undefined;\n}\nexport declare class ClickAway<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> extends React.Component<ClickAwayProps<TTrigger, TBoundary>, ClickAwayState> {\n static defaultProps: {\n containsNestedFloatingPortals?: boolean;\n closeOnEscapeKeypress?: boolean;\n };\n state: ClickAwayState;\n el: HTMLElement | null | undefined;\n boundaryRef: BoundaryRefType<TBoundary>;\n triggerRef: TriggerRefType<TTrigger>;\n
|
|
5134
|
+
"content": "import * as React from 'react';\ntype BoundaryRefType<T extends HTMLElement> = React.RefObject<T | null>;\ntype TriggerRefType<T extends HTMLElement> = React.RefObject<T | null>;\nexport interface ChildProps<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> {\n onOpen: () => void;\n isOpen: boolean;\n height: number | null | undefined;\n pageBottom: number | null | undefined;\n clickAway: () => void;\n boundaryRef: BoundaryRefType<TBoundary>;\n triggerRef: TriggerRefType<TTrigger>;\n}\nexport interface ClickAwayRefType {\n forceClose: () => void;\n forceOpen: () => void;\n}\nexport interface ClickAwayProps<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> {\n closeOnEscapeKeypress?: boolean;\n children: (props: ChildProps<TTrigger, TBoundary>) => React.ReactNode;\n onChange?: (isOpen: boolean) => unknown;\n clickAwayRef?: React.RefObject<ClickAwayRefType | null>;\n /**\n * When containsNestedFloatingPortals is true, prevents ClickAway from closing the dropdown if the click target\n * is inside a floating portal root element.\n *\n * This is necessary for nested dropdowns rendered using floating portals,\n * such as via `@floating-ui/react`. Floating portals render elements outside\n * the normal DOM tree (often directly into `document.body` or another container)\n * using `ReactDOM.createPortal`. As a result, these nested dropdowns do not\n * appear inside the parent dropdown's `boundaryRef` or `triggerRef` subtree.\n *\n * To address this, the floating portal ensures that all nested dropdowns\n * are mounted into a **shared root element** — effectively maintaining a\n * common DOM ancestry. This allows parent dropdowns to detect when clicks\n * occur within any of their nested children, even if rendered via portals.\n * The shared root element is the closest parent element that has a `data-floating-ui-portal` attribute.\n * Because of this, the ClickAway logic will not close the parent menu if the click target is inside boundaryRef's parentElement.\n *\n * Enabling `containsNestedFloatingPortals` allows the ClickAway logic to account\n * for this shared root and avoids incorrectly closing the parent menu when\n * interacting with nested dropdowns.\n */\n containsNestedFloatingPortals?: boolean;\n}\ninterface ClickAwayState {\n isOpen: boolean;\n height: number | null | undefined;\n pageBottom: number | null | undefined;\n}\nexport declare class ClickAway<TTrigger extends HTMLElement = HTMLButtonElement, TBoundary extends HTMLElement = HTMLDivElement> extends React.Component<ClickAwayProps<TTrigger, TBoundary>, ClickAwayState> {\n static defaultProps: {\n containsNestedFloatingPortals?: boolean;\n closeOnEscapeKeypress?: boolean;\n };\n state: ClickAwayState;\n el: HTMLElement | null | undefined;\n boundaryRef: BoundaryRefType<TBoundary>;\n triggerRef: TriggerRefType<TTrigger>;\n componentDidMount(): void;\n componentDidUpdate(_prevProps: Readonly<ClickAwayProps<TTrigger, TBoundary>>, prevState: Readonly<ClickAwayState>): void;\n componentWillUnmount(): void;\n render(): React.ReactNode;\n handleOpenClick: () => void;\n handleCloseClick: (evt: MouseEvent) => void;\n handleCloseOnEscapeKeypress: (evt?: KeyboardEvent) => void;\n forceClose: () => void;\n handleOnChange: () => unknown;\n pageBottom(): number;\n}\nexport {};\n//# sourceMappingURL=click-away.d.ts.map"
|
|
5169
5135
|
},
|
|
5170
5136
|
"index.d.ts": {
|
|
5171
5137
|
"path": "index.d.ts",
|
package/package.json
CHANGED