From 403496c5cb7655524f5828c704e53928c996f9ab Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Fri, 27 Feb 2026 11:14:10 +0100 Subject: [PATCH] Migrate Menu component from useStyles to useDefinition Split monolithic MenuDefinition (17 classNames) into 9 per-sub-component definitions (Menu, MenuListBox, MenuAutocomplete, MenuAutocompleteListbox, MenuItem, MenuListBoxItem, MenuSection, MenuSeparator, MenuEmptyState). MenuItem color prop uses dataAttribute with default in propDefs. Defaults for placement, virtualized, selectionMode moved to propDefs. Signed-off-by: Johan Persson --- packages/ui/report.api.md | 181 +++++----- packages/ui/src/components/Menu/Menu.tsx | 328 ++++++------------ packages/ui/src/components/Menu/definition.ts | 172 ++++++++- packages/ui/src/components/Menu/index.ts | 8 + packages/ui/src/components/Menu/types.ts | 104 ++++-- 5 files changed, 440 insertions(+), 353 deletions(-) diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 75f00f7211..3c379c2927 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1481,57 +1481,59 @@ export const MenuAutocompleteListbox: ( ) => JSX_2.Element; // @public (undocumented) -export interface MenuAutocompleteListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) +export type MenuAutocompleteListBoxOwnProps = { placeholder?: string; - // (undocumented) placement?: PopoverProps_2['placement']; - // (undocumented) + selectionMode?: ListBoxProps['selectionMode']; virtualized?: boolean; -} + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; + +// @public (undocumented) +export interface MenuAutocompleteListBoxProps + extends MenuAutocompleteListBoxOwnProps, + Omit, keyof MenuAutocompleteListBoxOwnProps> {} + +// @public (undocumented) +export type MenuAutocompleteOwnProps = { + placeholder?: string; + placement?: PopoverProps_2['placement']; + virtualized?: boolean; + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; // @public (undocumented) export interface MenuAutocompleteProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placeholder?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuAutocompleteOwnProps, + Omit, keyof MenuAutocompleteOwnProps> {} // @public export const MenuDefinition: { + readonly styles: { + readonly [key: string]: string; + }; readonly classNames: { - readonly root: 'bui-Menu'; - readonly popover: 'bui-MenuPopover'; + readonly root: 'bui-MenuPopover'; readonly inner: 'bui-MenuInner'; readonly content: 'bui-MenuContent'; - readonly section: 'bui-MenuSection'; - readonly sectionHeader: 'bui-MenuSectionHeader'; - readonly item: 'bui-MenuItem'; - readonly itemListBox: 'bui-MenuItemListBox'; - readonly itemListBoxCheck: 'bui-MenuItemListBoxCheck'; - readonly itemWrapper: 'bui-MenuItemWrapper'; - readonly itemContent: 'bui-MenuItemContent'; - readonly itemArrow: 'bui-MenuItemArrow'; - readonly separator: 'bui-MenuSeparator'; - readonly searchField: 'bui-MenuSearchField'; - readonly searchFieldInput: 'bui-MenuSearchFieldInput'; - readonly searchFieldClear: 'bui-MenuSearchFieldClear'; - readonly emptyState: 'bui-MenuEmptyState'; + }; + readonly propDefs: { + readonly placement: { + readonly default: 'bottom start'; + }; + readonly virtualized: { + readonly default: false; + }; + readonly maxWidth: {}; + readonly maxHeight: {}; + readonly style: {}; + readonly className: {}; }; }; @@ -1539,16 +1541,18 @@ export const MenuDefinition: { export const MenuItem: (props: MenuItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuItemProps - extends MenuItemProps_2, - Omit { - // (undocumented) - children: React.ReactNode; - // (undocumented) - color?: 'primary' | 'danger'; - // (undocumented) +export type MenuItemOwnProps = { iconStart?: React.ReactNode; -} + children: React.ReactNode; + color?: 'primary' | 'danger'; + href?: MenuItemProps_2['href']; + className?: string; +}; + +// @public (undocumented) +export interface MenuItemProps + extends MenuItemOwnProps, + Omit {} // @public (undocumented) export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; @@ -1557,59 +1561,74 @@ export const MenuListBox: (props: MenuListBoxProps) => JSX_2.Element; export const MenuListBoxItem: (props: MenuListBoxItemProps) => JSX_2.Element; // @public (undocumented) -export interface MenuListBoxItemProps - extends ListBoxItemProps, - Omit { - // (undocumented) +export type MenuListBoxItemOwnProps = { children: React.ReactNode; -} + className?: string; +}; + +// @public (undocumented) +export interface MenuListBoxItemProps + extends MenuListBoxItemOwnProps, + Omit {} + +// @public (undocumented) +export type MenuListBoxOwnProps = { + placement?: PopoverProps_2['placement']; + selectionMode?: ListBoxProps['selectionMode']; + virtualized?: boolean; + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; // @public (undocumented) export interface MenuListBoxProps - extends ListBoxProps, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) + extends MenuListBoxOwnProps, + Omit, keyof MenuListBoxOwnProps> {} + +// @public (undocumented) +export type MenuOwnProps = { placement?: PopoverProps_2['placement']; - // (undocumented) virtualized?: boolean; -} + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; // @public (undocumented) export interface MenuProps - extends MenuProps_2, - Omit, 'children'> { - // (undocumented) - maxHeight?: string; - // (undocumented) - maxWidth?: string; - // (undocumented) - placement?: PopoverProps_2['placement']; - // (undocumented) - virtualized?: boolean; -} + extends MenuOwnProps, + Omit, keyof MenuOwnProps> {} // @public (undocumented) export const MenuSection: (props: MenuSectionProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSectionProps - extends MenuSectionProps_2, - Omit, 'children'> { - // (undocumented) - children: React.ReactNode; - // (undocumented) +export type MenuSectionOwnProps = { title: string; -} + children: React.ReactNode; + className?: string; +}; + +// @public (undocumented) +export interface MenuSectionProps + extends MenuSectionOwnProps, + Omit, keyof MenuSectionOwnProps> {} // @public (undocumented) export const MenuSeparator: (props: MenuSeparatorProps) => JSX_2.Element; // @public (undocumented) -export interface MenuSeparatorProps extends SeparatorProps {} +export type MenuSeparatorOwnProps = { + className?: string; +}; + +// @public (undocumented) +export interface MenuSeparatorProps + extends MenuSeparatorOwnProps, + Omit {} // @public (undocumented) export const MenuTrigger: (props: MenuTriggerProps) => JSX_2.Element; diff --git a/packages/ui/src/components/Menu/Menu.tsx b/packages/ui/src/components/Menu/Menu.tsx index a9835b2f68..354be33ce7 100644 --- a/packages/ui/src/components/Menu/Menu.tsx +++ b/packages/ui/src/components/Menu/Menu.tsx @@ -33,8 +33,18 @@ import { Virtualizer, ListLayout, } from 'react-aria-components'; -import { useStyles } from '../../hooks/useStyles'; -import { MenuDefinition } from './definition'; +import { useDefinition } from '../../hooks/useDefinition'; +import { + MenuDefinition, + MenuListBoxDefinition, + MenuAutocompleteDefinition, + MenuAutocompleteListboxDefinition, + MenuItemDefinition, + MenuListBoxItemDefinition, + MenuSectionDefinition, + MenuSeparatorDefinition, + MenuEmptyStateDefinition, +} from './definition'; import type { MenuTriggerProps, SubmenuTriggerProps, @@ -56,8 +66,6 @@ import { isInternalLink, createRoutingRegistration, } from '../InternalLinkProvider'; -import styles from './Menu.module.css'; -import clsx from 'clsx'; import { Box } from '../Box'; import { BgReset } from '../../hooks/useBg'; @@ -68,13 +76,9 @@ const { RoutingProvider, useRoutingRegistrationEffect } = const rowHeight = 32; const MenuEmptyState = () => { - const { classNames } = useStyles(MenuDefinition); + const { ownProps } = useDefinition(MenuEmptyStateDefinition, {}); - return ( -
- No results found. -
- ); + return
No results found.
; }; /** @public */ @@ -89,43 +93,26 @@ export const SubmenuTrigger = (props: SubmenuTriggerProps) => { /** @public */ export const Menu = (props: MenuProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - placement = 'bottom start', - virtualized = false, - maxWidth, - maxHeight, - style, - ...rest - } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuDefinition, props); + const { classes, placement, virtualized, maxWidth, maxHeight, style } = + ownProps; let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const menuContent = ( } style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} + {...restProps} /> ); return ( - + - + {virtualized ? ( ) => { /** @public */ export const MenuListBox = (props: MenuListBoxProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); + const { ownProps, restProps } = useDefinition(MenuListBoxDefinition, props); const { - className, - selectionMode = 'single', - placement = 'bottom start', - virtualized = false, + classes, + selectionMode, + placement, + virtualized, maxWidth, maxHeight, style, - ...rest - } = cleanedProps; + } = ownProps; let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const listBoxContent = ( ); return ( - + - + {virtualized ? ( ) => { /** @public */ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); + const { ownProps, restProps } = useDefinition( + MenuAutocompleteDefinition, + props, + ); const { - className, - placement = 'bottom start', - virtualized = false, + classes, + placement, + virtualized, maxWidth, maxHeight, style, - ...rest - } = cleanedProps; + placeholder, + } = ownProps; const { contains } = useFilter({ sensitivity: 'base' }); let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const menuContent = ( } style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} + {...restProps} /> ); return ( - + - + - + @@ -288,65 +246,47 @@ export const MenuAutocomplete = (props: MenuAutocompleteProps) => { export const MenuAutocompleteListbox = ( props: MenuAutocompleteListBoxProps, ) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); + const { ownProps, restProps } = useDefinition( + MenuAutocompleteListboxDefinition, + props, + ); const { - className, - selectionMode = 'single', - placement = 'bottom start', - virtualized = false, + classes, + selectionMode, + placement, + virtualized, maxWidth, maxHeight, style, - ...rest - } = cleanedProps; + placeholder, + } = ownProps; const { contains } = useFilter({ sensitivity: 'base' }); let newMaxWidth = maxWidth || (virtualized ? '260px' : 'undefined'); const listBoxContent = ( } selectionMode={selectionMode} style={{ width: newMaxWidth, maxHeight, ...style }} - {...rest} + {...restProps} /> ); return ( - + - + - + @@ -371,15 +311,11 @@ export const MenuAutocompleteListbox = ( /** @public */ export const MenuItem = (props: MenuItemProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { - className, - iconStart, - color = 'primary', - children, - href, - ...rest - } = cleanedProps; + const { ownProps, restProps, dataAttributes } = useDefinition( + MenuItemDefinition, + props, + ); + const { classes, iconStart, children, href } = ownProps; useRoutingRegistrationEffect(href); @@ -387,30 +323,18 @@ export const MenuItem = (props: MenuItemProps) => { if (href && !isInternalLink(href)) { return ( window.open(href, '_blank', 'noopener,noreferrer')} - {...rest} + {...restProps} > -
-
+
+
{iconStart} {children}
-
+
@@ -420,27 +344,18 @@ export const MenuItem = (props: MenuItemProps) => { return ( -
-
+
+
{iconStart} {children}
-
+
@@ -450,36 +365,21 @@ export const MenuItem = (props: MenuItemProps) => { /** @public */ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { children, className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition( + MenuListBoxItemDefinition, + props, + ); + const { classes, children } = ownProps; return ( -
-
-
+
+
+
{children} @@ -491,26 +391,12 @@ export const MenuListBoxItem = (props: MenuListBoxItemProps) => { /** @public */ export const MenuSection = (props: MenuSectionProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { children, className, title, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuSectionDefinition, props); + const { classes, children, title } = ownProps; return ( - - - {title} - + + {title} {children} ); @@ -518,17 +404,7 @@ export const MenuSection = (props: MenuSectionProps) => { /** @public */ export const MenuSeparator = (props: MenuSeparatorProps) => { - const { classNames, cleanedProps } = useStyles(MenuDefinition, props); - const { className, ...rest } = cleanedProps; + const { ownProps, restProps } = useDefinition(MenuSeparatorDefinition, props); - return ( - - ); + return ; }; diff --git a/packages/ui/src/components/Menu/definition.ts b/packages/ui/src/components/Menu/definition.ts index 860ad1d729..e789832490 100644 --- a/packages/ui/src/components/Menu/definition.ts +++ b/packages/ui/src/components/Menu/definition.ts @@ -14,30 +14,172 @@ * limitations under the License. */ -import type { ComponentDefinition } from '../../types'; +import { defineComponent } from '../../hooks/useDefinition'; +import type { + MenuOwnProps, + MenuListBoxOwnProps, + MenuAutocompleteOwnProps, + MenuAutocompleteListBoxOwnProps, + MenuItemOwnProps, + MenuListBoxItemOwnProps, + MenuSectionOwnProps, + MenuSeparatorOwnProps, +} from './types'; +import styles from './Menu.module.css'; /** * Component definition for Menu * @public */ -export const MenuDefinition = { +export const MenuDefinition = defineComponent()({ + styles, classNames: { - root: 'bui-Menu', - popover: 'bui-MenuPopover', + root: 'bui-MenuPopover', inner: 'bui-MenuInner', content: 'bui-MenuContent', - section: 'bui-MenuSection', - sectionHeader: 'bui-MenuSectionHeader', - item: 'bui-MenuItem', - itemListBox: 'bui-MenuItemListBox', - itemListBoxCheck: 'bui-MenuItemListBoxCheck', + }, + propDefs: { + placement: { default: 'bottom start' }, + virtualized: { default: false }, + maxWidth: {}, + maxHeight: {}, + style: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuListBoxDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-MenuPopover', + inner: 'bui-MenuInner', + content: 'bui-MenuContent', + }, + propDefs: { + placement: { default: 'bottom start' }, + selectionMode: { default: 'single' }, + virtualized: { default: false }, + maxWidth: {}, + maxHeight: {}, + style: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuAutocompleteDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-MenuPopover', + inner: 'bui-MenuInner', + content: 'bui-MenuContent', + searchField: 'bui-MenuSearchField', + searchFieldInput: 'bui-MenuSearchFieldInput', + searchFieldClear: 'bui-MenuSearchFieldClear', + }, + propDefs: { + placeholder: {}, + placement: { default: 'bottom start' }, + virtualized: { default: false }, + maxWidth: {}, + maxHeight: {}, + style: {}, + className: {}, + }, + }); + +/** @internal */ +export const MenuAutocompleteListboxDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-MenuPopover', + inner: 'bui-MenuInner', + content: 'bui-MenuContent', + searchField: 'bui-MenuSearchField', + searchFieldInput: 'bui-MenuSearchFieldInput', + searchFieldClear: 'bui-MenuSearchFieldClear', + }, + propDefs: { + placeholder: {}, + placement: { default: 'bottom start' }, + selectionMode: { default: 'single' }, + virtualized: { default: false }, + maxWidth: {}, + maxHeight: {}, + style: {}, + className: {}, + }, + }); + +/** @internal */ +export const MenuItemDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-MenuItem', itemWrapper: 'bui-MenuItemWrapper', itemContent: 'bui-MenuItemContent', itemArrow: 'bui-MenuItemArrow', - separator: 'bui-MenuSeparator', - searchField: 'bui-MenuSearchField', - searchFieldInput: 'bui-MenuSearchFieldInput', - searchFieldClear: 'bui-MenuSearchFieldClear', - emptyState: 'bui-MenuEmptyState', }, -} as const satisfies ComponentDefinition; + propDefs: { + iconStart: {}, + children: {}, + color: { dataAttribute: true, default: 'primary' }, + href: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuListBoxItemDefinition = + defineComponent()({ + styles, + classNames: { + root: 'bui-MenuItemListBox', + itemWrapper: 'bui-MenuItemWrapper', + itemContent: 'bui-MenuItemContent', + check: 'bui-MenuItemListBoxCheck', + }, + propDefs: { + children: {}, + className: {}, + }, + }); + +/** @internal */ +export const MenuSectionDefinition = defineComponent()({ + styles, + classNames: { + root: 'bui-MenuSection', + header: 'bui-MenuSectionHeader', + }, + propDefs: { + title: {}, + children: {}, + className: {}, + }, +}); + +/** @internal */ +export const MenuSeparatorDefinition = defineComponent()( + { + styles, + classNames: { + root: 'bui-MenuSeparator', + }, + propDefs: { + className: {}, + }, + }, +); + +/** @internal */ +export const MenuEmptyStateDefinition = defineComponent<{}>()({ + styles, + classNames: { + root: 'bui-MenuEmptyState', + }, + propDefs: {}, +}); diff --git a/packages/ui/src/components/Menu/index.ts b/packages/ui/src/components/Menu/index.ts index f42de8a7eb..113ba0a029 100644 --- a/packages/ui/src/components/Menu/index.ts +++ b/packages/ui/src/components/Menu/index.ts @@ -31,11 +31,19 @@ export type { MenuTriggerProps, SubmenuTriggerProps, MenuProps, + MenuOwnProps, MenuListBoxProps, + MenuListBoxOwnProps, MenuAutocompleteProps, + MenuAutocompleteOwnProps, MenuAutocompleteListBoxProps, + MenuAutocompleteListBoxOwnProps, MenuItemProps, + MenuItemOwnProps, MenuListBoxItemProps, + MenuListBoxItemOwnProps, MenuSectionProps, + MenuSectionOwnProps, MenuSeparatorProps, + MenuSeparatorOwnProps, } from './types'; diff --git a/packages/ui/src/components/Menu/types.ts b/packages/ui/src/components/Menu/types.ts index 6d4852a4b3..9290879f03 100644 --- a/packages/ui/src/components/Menu/types.ts +++ b/packages/ui/src/components/Menu/types.ts @@ -33,70 +33,112 @@ export interface MenuTriggerProps extends RAMenuTriggerProps {} export interface SubmenuTriggerProps extends RAMenuSubmenuTriggerProps {} /** @public */ -export interface MenuProps - extends RAMenuProps, - Omit, 'children'> { +export type MenuOwnProps = { placement?: RAPopoverProps['placement']; virtualized?: boolean; maxWidth?: string; maxHeight?: string; -} + style?: React.CSSProperties; + className?: string; +}; + +/** @public */ +export interface MenuProps + extends MenuOwnProps, + Omit, keyof MenuOwnProps> {} + +/** @public */ +export type MenuListBoxOwnProps = { + placement?: RAPopoverProps['placement']; + selectionMode?: RAListBoxProps['selectionMode']; + virtualized?: boolean; + maxWidth?: string; + maxHeight?: string; + style?: React.CSSProperties; + className?: string; +}; /** @public */ export interface MenuListBoxProps - extends RAListBoxProps, - Omit, 'children'> { + extends MenuListBoxOwnProps, + Omit, keyof MenuListBoxOwnProps> {} + +/** @public */ +export type MenuAutocompleteOwnProps = { + placeholder?: string; placement?: RAPopoverProps['placement']; virtualized?: boolean; maxWidth?: string; maxHeight?: string; -} + style?: React.CSSProperties; + className?: string; +}; /** @public */ export interface MenuAutocompleteProps - extends RAMenuProps, - Omit, 'children'> { + extends MenuAutocompleteOwnProps, + Omit, keyof MenuAutocompleteOwnProps> {} + +/** @public */ +export type MenuAutocompleteListBoxOwnProps = { placeholder?: string; placement?: RAPopoverProps['placement']; + selectionMode?: RAListBoxProps['selectionMode']; virtualized?: boolean; maxWidth?: string; maxHeight?: string; -} + style?: React.CSSProperties; + className?: string; +}; /** @public */ export interface MenuAutocompleteListBoxProps - extends RAListBoxProps, - Omit, 'children'> { - placeholder?: string; - placement?: RAPopoverProps['placement']; - virtualized?: boolean; - maxWidth?: string; - maxHeight?: string; -} + extends MenuAutocompleteListBoxOwnProps, + Omit, keyof MenuAutocompleteListBoxOwnProps> {} /** @public */ -export interface MenuItemProps - extends RAMenuItemProps, - Omit { +export type MenuItemOwnProps = { iconStart?: React.ReactNode; children: React.ReactNode; color?: 'primary' | 'danger'; -} + href?: RAMenuItemProps['href']; + className?: string; +}; + +/** @public */ +export interface MenuItemProps + extends MenuItemOwnProps, + Omit {} + +/** @public */ +export type MenuListBoxItemOwnProps = { + children: React.ReactNode; + className?: string; +}; /** @public */ export interface MenuListBoxItemProps - extends RAListBoxItemProps, - Omit { + extends MenuListBoxItemOwnProps, + Omit {} + +/** @public */ +export type MenuSectionOwnProps = { + title: string; children: React.ReactNode; -} + className?: string; +}; /** @public */ export interface MenuSectionProps - extends RAMenuSectionProps, - Omit, 'children'> { - title: string; - children: React.ReactNode; -} + extends MenuSectionOwnProps, + Omit, keyof MenuSectionOwnProps> {} /** @public */ -export interface MenuSeparatorProps extends RAMenuSeparatorProps {} +export type MenuSeparatorOwnProps = { + className?: string; +}; + +/** @public */ +export interface MenuSeparatorProps + extends MenuSeparatorOwnProps, + Omit {}