From dc298f322d1d24e917bfb3641cc976089cc748dc Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 11:34:13 +0200 Subject: [PATCH 1/7] feat(Menu): divider between sections --- .changeset/old-comics-sneeze.md | 5 + src/components/pickers/Menu/Menu.stories.tsx | 6 +- src/components/pickers/Menu/Menu.tsx | 144 ++++--------------- src/components/pickers/Menu/styled.tsx | 15 +- src/tokens.ts | 6 +- 5 files changed, 51 insertions(+), 125 deletions(-) create mode 100644 .changeset/old-comics-sneeze.md diff --git a/.changeset/old-comics-sneeze.md b/.changeset/old-comics-sneeze.md new file mode 100644 index 000000000..089e532df --- /dev/null +++ b/.changeset/old-comics-sneeze.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": minor +--- + +Remove divider support in Menu but add dividers between sections. diff --git a/src/components/pickers/Menu/Menu.stories.tsx b/src/components/pickers/Menu/Menu.stories.tsx index 18bdaf702..11272ecdb 100644 --- a/src/components/pickers/Menu/Menu.stories.tsx +++ b/src/components/pickers/Menu/Menu.stories.tsx @@ -14,7 +14,6 @@ import { Button, DialogContainer, DirectionIcon, - Divider, Flex, Menu, MenuTrigger, @@ -42,7 +41,6 @@ const MenuTemplate = (props) => { Item 1 Item 2 - Item 3 Item 4 @@ -176,6 +174,10 @@ export const Sections = (props) => { Name Descriptions + + Paste + Copy + Status Completed At diff --git a/src/components/pickers/Menu/Menu.tsx b/src/components/pickers/Menu/Menu.tsx index 3cd00ec48..3cefaa5e5 100644 --- a/src/components/pickers/Menu/Menu.tsx +++ b/src/components/pickers/Menu/Menu.tsx @@ -19,7 +19,6 @@ import { Styles, } from '../../../tasty'; import { mergeProps } from '../../../utils/react'; -import { Divider as BaseDivider } from '../../content/Divider'; import { useMenuContext } from './context'; import { MenuButtonProps, MenuSelectionType } from './MenuButton'; @@ -40,8 +39,6 @@ export interface CubeMenuProps sectionStyles?: Styles; sectionHeadingStyles?: Styles; qa?: BaseProps['qa']; - /** Menu accepts , , and as children, or a function for dynamic collections */ - children?: React.ReactNode | ((item: T) => React.ReactElement); /** Keys that should appear disabled */ disabledKeys?: Iterable; /** Selection mode for the menu: 'single' | 'multiple' */ @@ -66,48 +63,8 @@ function Menu( const contextProps = useMenuContext(); const completeProps = mergeProps(contextProps, rest); - // Check if children is a function (dynamic collection) - const isDynamicCollection = typeof completeProps.children === 'function'; - - // For dynamic collections, pass children directly to useTreeState - // For static collections, filter out Dividers and normalize keys - const treeProps = isDynamicCollection - ? completeProps - : (() => { - // Filter out Divider elements for collection creation. - const filteredChildren = React.Children.toArray( - completeProps.children, - ).filter( - (child) => - !( - React.isValidElement(child) && - // @ts-ignore - child.type === BaseDivider - ), - ); - - // React may prefix keys with '.$' when accessing them via Children.toArray. - // Strip that prefix so selection/disabled logic receives the original keys. - const normalizedChildren = filteredChildren.map((child) => { - if ( - React.isValidElement(child) && - child.key && - typeof child.key === 'string' - ) { - const cleanKey = child.key.replace(/^\.\$/, ''); - // Only clone if the key actually changed to avoid unnecessary work. - return cleanKey !== child.key - ? React.cloneElement(child, { key: cleanKey }) - : child; - } - return child; - }); - - return { - ...completeProps, - children: normalizedChildren, - }; - })(); + // Props used for collection building. + const treeProps = completeProps as typeof completeProps; const state = useTreeState(treeProps as typeof completeProps); const collectionItems = [...state.collection]; @@ -135,75 +92,25 @@ function Menu( > {header && {header}} {(() => { - // For dynamic collections, just render items from the collection - if (isDynamicCollection) { - return collectionItems.map((item) => { - if (item.type === 'section') { - return ( - - ); - } + // Build the list of menu elements, automatically inserting dividers between sections. + const renderedItems: React.ReactNode[] = []; + let isFirstSection = true; - let menuItem = ( - - ); - - if (item.props.wrapper) { - menuItem = item.props.wrapper(menuItem); + collectionItems.forEach((item) => { + if (item.type === 'section') { + // Insert a visual separator before every section except the first one. + if (!isFirstSection) { + renderedItems.push( + , + ); } - return cloneElement(menuItem, { - key: item.key, - }); - }); - } - - // For static collections, render children in the same order as they were provided, - // but leverage react-stately collection for interactive Menu items and sections. - let itemIndex = 0; - const children = React.Children.toArray(completeProps.children); - - return children.map((child, index) => { - // Divider handling - if ( - // Valid React element and its type equals to our Divider component - React.isValidElement(child) && - // @ts-ignore – comparing component types is acceptable here - child.type === BaseDivider - ) { - return ( - to be valid HTML. - key={`divider-${index}`} - as="li" - role="separator" - aria-orientation="horizontal" - /> - ); - } - - // Handle items/sections coming from react-stately collection - const item = collectionItems[itemIndex++]; - - if (!item) return null; - - if (item.type === 'section') { - return ( + renderedItems.push( ( itemStyles={itemStyles} headingStyles={sectionHeadingStyles} selectionIcon={selectionIcon} - /> + />, ); + + isFirstSection = false; + return; } let menuItem = ( @@ -231,10 +141,14 @@ function Menu( menuItem = item.props.wrapper(menuItem); } - return cloneElement(menuItem, { - key: item.key, - }); + renderedItems.push( + cloneElement(menuItem, { + key: item.key, + }), + ); }); + + return renderedItems; })()} ); diff --git a/src/components/pickers/Menu/styled.tsx b/src/components/pickers/Menu/styled.tsx index bd4bbea26..b1564a2ee 100644 --- a/src/components/pickers/Menu/styled.tsx +++ b/src/components/pickers/Menu/styled.tsx @@ -36,6 +36,7 @@ export const StyledMenu = tasty({ }); export const StyledDivider = tasty({ + qa: 'MenuDivider', as: 'li', styles: { display: 'flex', @@ -48,9 +49,9 @@ export const StyledDivider = tasty({ }); export const StyledMenuHeader = tasty(Space, { + qa: 'MenuHeader', as: 'li', styles: { - fill: '#light', color: '#dark-02', preset: 't2m', padding: '0.75x 2x', @@ -64,6 +65,7 @@ export const StyledMenuHeader = tasty(Space, { }); export const StyledMenuSection = tasty({ + qa: 'MenuSection', as: 'li', styles: { display: 'flex', @@ -76,6 +78,7 @@ export const StyledMenuSection = tasty({ }); export const StyledMenuItem = tasty({ + qa: 'MenuItem', as: 'li', styles: { display: 'flex', @@ -95,13 +98,13 @@ export const StyledMenuItem = tasty({ }); export const StyledMenuSectionHeading = tasty(Space, { - as: 'header', + as: 'MenuSectionHeading', styles: { - color: '#dark-03', - fill: '#light', + color: '#dark-04', preset: 'c2', - padding: '(1x - 1bw) 2x', - placeContent: 'space-between', + padding: '1x 2x 0', + height: '3x', + placeContent: 'center space-between', align: 'start', }, }); diff --git a/src/tokens.ts b/src/tokens.ts index 8a2bbf5fb..b63cc9f16 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -228,14 +228,16 @@ const TOKENS = { 'c1-font-size': '14px', 'c1-line-height': '20px', 'c1-letter-spacing': '0', - 'c1-font-weight': '500', + 'c1-font-weight': '600', + 'c1-font-bold-weight': '700', 'c1-text-transform': 'uppercase', 'c1-icon-size': '18px', // c2 'c2-font-size': '12px', 'c2-line-height': '18px', 'c2-letter-spacing': '0.01em', - 'c2-font-weight': '500', + 'c2-font-weight': '600', + 'c2-font-bold-weight': '700', 'c2-text-transform': 'uppercase', 'c2-icon-size': '16px', // tag From 24cfa34c1b43de08c1f0e48bf9ddb77fa2caedb1 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 13:20:40 +0200 Subject: [PATCH 2/7] feat(Select): sections support --- .changeset/mighty-bats-fix.md | 5 + .../fields/Select/Select.stories.tsx | 65 ++++++- src/components/fields/Select/Select.tsx | 170 +++++++++++++++--- src/components/pickers/Menu/Menu.tsx | 4 +- src/components/pickers/Menu/MenuItem.tsx | 6 +- src/components/pickers/Menu/MenuSection.tsx | 14 +- src/components/pickers/Menu/styled.tsx | 29 +-- 7 files changed, 239 insertions(+), 54 deletions(-) create mode 100644 .changeset/mighty-bats-fix.md diff --git a/.changeset/mighty-bats-fix.md b/.changeset/mighty-bats-fix.md new file mode 100644 index 000000000..2f84a2b46 --- /dev/null +++ b/.changeset/mighty-bats-fix.md @@ -0,0 +1,5 @@ +--- +"@cube-dev/ui-kit": minor +--- + +Add sections support for Select. diff --git a/src/components/fields/Select/Select.stories.tsx b/src/components/fields/Select/Select.stories.tsx index aad621082..59057aca4 100644 --- a/src/components/fields/Select/Select.stories.tsx +++ b/src/components/fields/Select/Select.stories.tsx @@ -12,7 +12,7 @@ export default { title: 'Pickers/Select', component: Select, args: { width: '200px' }, - subcomponents: { Item: Select.Item }, + subcomponents: { Item: Select.Item, Section: Select.Section }, parameters: { controls: { exclude: baseProps } }, argTypes: { ...SELECTED_KEY_ARG, @@ -137,3 +137,66 @@ export const WithDescription: StoryFn> = (args) => ( ); WithDescription.args = {}; WithDescription.play = WithDisabledOption.play; + +// ------------------------------ +// Section stories +// ------------------------------ + +export const SectionsStatic: StoryFn> = (args) => ( + +); + +SectionsStatic.storyName = 'Sections – static items'; +SectionsStatic.play = WithDisabledOption.play; + +export const SectionsDynamic: StoryFn> = (args) => { + const groups = [ + { + name: 'Fruits', + children: [ + { id: 'apple', label: 'Apple' }, + { id: 'orange', label: 'Orange' }, + { id: 'banana', label: 'Banana' }, + ], + }, + { + name: 'Vegetables', + children: [ + { id: 'carrot', label: 'Carrot' }, + { id: 'peas', label: 'Peas' }, + { id: 'broccoli', label: 'Broccoli' }, + ], + }, + ]; + + return ( + + ); +}; + +SectionsDynamic.storyName = 'Sections – dynamic collection'; +SectionsDynamic.play = WithDisabledOption.play; diff --git a/src/components/fields/Select/Select.tsx b/src/components/fields/Select/Select.tsx index e9f378346..d06e4d35a 100644 --- a/src/components/fields/Select/Select.tsx +++ b/src/components/fields/Select/Select.tsx @@ -1,5 +1,5 @@ import { AriaLabelingProps, CollectionBase, DOMRef } from '@react-types/shared'; -import { +import React, { cloneElement, forwardRef, ReactElement, @@ -16,12 +16,13 @@ import { useButton, useHover, useListBox, + useListBoxSection, useOption, useOverlay, useOverlayPosition, useSelect, } from 'react-aria'; -import { Item, useSelectState } from 'react-stately'; +import { Section as BaseSection, Item, useSelectState } from 'react-stately'; import styled from 'styled-components'; import { DownIcon, LoadingIcon } from '../../../icons/index'; @@ -60,6 +61,11 @@ import { } from '../../actions/index'; import { useFieldProps, useFormProps, wrapWithField } from '../../form'; import { OverlayWrapper } from '../../overlays/OverlayWrapper'; +import { + StyledDivider as ListDivider, + StyledSectionHeading as ListSectionHeading, + StyledSection as ListSectionWrapper, +} from '../../pickers/Menu/styled'; import { InvalidIcon } from '../../shared/InvalidIcon'; import { ValidIcon } from '../../shared/ValidIcon'; import { DEFAULT_INPUT_STYLES, INPUT_WRAPPER_STYLES } from '../index'; @@ -172,20 +178,35 @@ const SelectElement = tasty({ }, }); -const ListBoxElement = tasty({ +export const ListBoxElement = tasty({ as: 'ul', styles: { display: 'flex', - gap: '1bw', + gap: { + '': '1bw', + sections: 0, + }, flow: 'column', margin: '0', - padding: '.5x', + padding: { + '': '.5x', + sections: 0, + }, listStyle: 'none', - radius: '1cr', - border: true, + radius: { + '': '1cr', + section: '0', + }, + border: { + '': true, + section: false, + }, fill: '#white', - shadow: '0px 4px 16px #shadow', - height: 'initial 30x', + shadow: { + '': '0px 4px 16px #shadow', + section: false, + }, + height: 'initial max-content (50vh - 4x)', overflow: 'clip auto', scrollbar: 'styled', }, @@ -544,21 +565,64 @@ export function ListBoxPopup({ > state.close()} /> - - {Array.from(state.collection).map((item: any) => ( - + {(() => { + const hasSections = Array.from(state.collection).some( + (i: any) => i.type === 'section', + ); + + const renderedItems: React.ReactNode[] = []; + let isFirstSection = true; + + for (const item of state.collection) { + if (item.type === 'section') { + if (!isFirstSection) { + renderedItems.push( + , + ); + } + + renderedItems.push( + , + ); + + isFirstSection = false; + } else { + renderedItems.push( + @@ -608,19 +672,75 @@ function Option({ item, state, styles, shouldUseVirtualFocus }) { ); } +interface SelectSectionProps { + item: any; // react-stately Node + state: any; // TreeState + optionStyles?: Styles; + headingStyles?: Styles; + sectionStyles?: Styles; + shouldUseVirtualFocus?: boolean; +} + +function SelectSection(props: SelectSectionProps) { + const { + item, + state, + optionStyles, + headingStyles, + sectionStyles, + shouldUseVirtualFocus, + } = props; + + const heading = item.rendered; + + const { itemProps, headingProps, groupProps } = useListBoxSection({ + heading, + 'aria-label': item['aria-label'], + }); + + return ( + + {heading && ( + + {heading} + + )} + + {[...item.childNodes].map((node: any) => ( + + + ); +} + const _Select = forwardRef(Select); (_Select as any).cubeInputType = 'Select'; +type SectionComponent = typeof BaseSection; + +const SelectSectionComponent = Object.assign(BaseSection, { + displayName: 'Section', +}) as SectionComponent; + const __Select = Object.assign( _Select as typeof _Select & { Item: typeof Item; + Section: typeof SelectSectionComponent; }, { Item: Item as unknown as (props: { description?: ReactNode; [key: string]: any; - }) => null, + }) => ReactElement, + Section: SelectSectionComponent, }, ); diff --git a/src/components/pickers/Menu/Menu.tsx b/src/components/pickers/Menu/Menu.tsx index 3cefaa5e5..94f4ff980 100644 --- a/src/components/pickers/Menu/Menu.tsx +++ b/src/components/pickers/Menu/Menu.tsx @@ -24,7 +24,7 @@ import { useMenuContext } from './context'; import { MenuButtonProps, MenuSelectionType } from './MenuButton'; import { MenuItem } from './MenuItem'; import { MenuSection } from './MenuSection'; -import { StyledDivider, StyledMenu, StyledMenuHeader } from './styled'; +import { StyledDivider, StyledHeader, StyledMenu } from './styled'; export interface CubeMenuProps extends BasePropsWithoutChildren, @@ -90,7 +90,7 @@ function Menu( {...mergeProps(defaultProps, menuProps, filterBaseProps(completeProps))} ref={domRef} > - {header && {header}} + {header && {header}} {(() => { // Build the list of menu elements, automatically inserting dividers between sections. const renderedItems: React.ReactNode[] = []; diff --git a/src/components/pickers/Menu/MenuItem.tsx b/src/components/pickers/Menu/MenuItem.tsx index 97c944888..70f45afc4 100644 --- a/src/components/pickers/Menu/MenuItem.tsx +++ b/src/components/pickers/Menu/MenuItem.tsx @@ -8,7 +8,7 @@ import { ClearSlots, mergeProps, SlotProvider } from '../../../utils/react'; import { useMenuContext } from './context'; import { MenuButton, MenuSelectionType } from './MenuButton'; -import { StyledMenuItem } from './styled'; +import { StyledItem } from './styled'; export interface MenuItemProps { item: Node; @@ -87,7 +87,7 @@ export function MenuItem(props: MenuItemProps) { return ( - (props: MenuItemProps) { {contents} - + ); } diff --git a/src/components/pickers/Menu/MenuSection.tsx b/src/components/pickers/Menu/MenuSection.tsx index ab124f9e5..7d3fb2856 100644 --- a/src/components/pickers/Menu/MenuSection.tsx +++ b/src/components/pickers/Menu/MenuSection.tsx @@ -3,11 +3,7 @@ import { useMenuSection } from 'react-aria'; import { Styles } from '../../../tasty'; import { MenuItem, MenuItemProps } from './MenuItem'; -import { - StyledMenu, - StyledMenuSection, - StyledMenuSectionHeading, -} from './styled'; +import { StyledMenu, StyledSection, StyledSectionHeading } from './styled'; export interface CubeMenuSectionProps extends MenuItemProps { itemStyles?: Styles; @@ -25,11 +21,11 @@ export function MenuSection(props: CubeMenuSectionProps) { return ( <> - + {heading && ( - + {heading} - + )} {[...item.childNodes].map((node) => { @@ -50,7 +46,7 @@ export function MenuSection(props: CubeMenuSectionProps) { return item; })} - + ); } diff --git a/src/components/pickers/Menu/styled.tsx b/src/components/pickers/Menu/styled.tsx index b1564a2ee..2f6e0364d 100644 --- a/src/components/pickers/Menu/styled.tsx +++ b/src/components/pickers/Menu/styled.tsx @@ -9,14 +9,14 @@ export const StyledMenu = tasty({ flow: 'column', gap: { '': '1bw', - sections: '', + sections: false, }, fill: '#white', - margin: '0', + margin: 0, padding: { '': '0.5x', - section: '0.5x', // section menu - sections: '0', // has sections inside + section: 9, // section menu + sections: 9, // has sections inside }, overflow: { '': 'auto', @@ -24,7 +24,7 @@ export const StyledMenu = tasty({ }, border: { '': '#border', - section: '', + section: false, }, radius: '(1cr + 1bw)', boxShadow: { @@ -36,7 +36,7 @@ export const StyledMenu = tasty({ }); export const StyledDivider = tasty({ - qa: 'MenuDivider', + qa: 'Divider', as: 'li', styles: { display: 'flex', @@ -45,11 +45,12 @@ export const StyledDivider = tasty({ listStyle: 'none', fill: '#border', height: '1bw', + flexShrink: 0, }, }); -export const StyledMenuHeader = tasty(Space, { - qa: 'MenuHeader', +export const StyledHeader = tasty(Space, { + qa: 'Header', as: 'li', styles: { color: '#dark-02', @@ -64,8 +65,8 @@ export const StyledMenuHeader = tasty(Space, { }, }); -export const StyledMenuSection = tasty({ - qa: 'MenuSection', +export const StyledSection = tasty({ + qa: 'Section', as: 'li', styles: { display: 'flex', @@ -77,8 +78,8 @@ export const StyledMenuSection = tasty({ }, }); -export const StyledMenuItem = tasty({ - qa: 'MenuItem', +export const StyledItem = tasty({ + qa: 'Item', as: 'li', styles: { display: 'flex', @@ -97,8 +98,8 @@ export const StyledMenuItem = tasty({ }, }); -export const StyledMenuSectionHeading = tasty(Space, { - as: 'MenuSectionHeading', +export const StyledSectionHeading = tasty(Space, { + as: 'SectionHeading', styles: { color: '#dark-04', preset: 'c2', From 25060c0609d2e82896eb59be781e07111b396361 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 14:42:47 +0200 Subject: [PATCH 3/7] feat(ComboBox): sections support --- .../fields/ComboBox/ComboBox.stories.tsx | 70 ++++++++++++++++++- src/components/fields/ComboBox/ComboBox.tsx | 29 ++++++-- src/components/fields/Select/Select.tsx | 20 +++--- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/src/components/fields/ComboBox/ComboBox.stories.tsx b/src/components/fields/ComboBox/ComboBox.stories.tsx index 288c010bd..783bb16ea 100644 --- a/src/components/fields/ComboBox/ComboBox.stories.tsx +++ b/src/components/fields/ComboBox/ComboBox.stories.tsx @@ -14,7 +14,7 @@ import { ComboBox, CubeComboBoxProps } from './ComboBox'; export default { title: 'Pickers/ComboBox', component: ComboBox, - subcomponents: { Item: ComboBox.Item }, + subcomponents: { Item: ComboBox.Item, Section: ComboBox.Section }, args: { id: 'name', width: '200px', label: 'Choose your favourite color' }, parameters: { controls: { exclude: baseProps } }, argTypes: { ...SELECTED_KEY_ARG }, @@ -367,3 +367,71 @@ ItemsWithDescriptions.play = async ({ canvasElement }) => { await userEvent.click(trigger); }; + +// --------------------------------- +// Section stories for ComboBox +// --------------------------------- + +export const SectionsStatic: StoryFn> = (args) => ( + + + Red + Orange + Yellow + + + Teal + Cyan + + + Blue + Purple + + +); + +SectionsStatic.storyName = 'Sections – static items'; + +export const SectionsDynamic: StoryFn> = (args) => { + const groups = [ + { + name: 'Fruits', + children: [ + { id: 'apple', label: 'Apple' }, + { id: 'orange', label: 'Orange' }, + { id: 'banana', label: 'Banana' }, + ], + }, + { + name: 'Vegetables', + children: [ + { id: 'carrot', label: 'Carrot' }, + { id: 'peas', label: 'Peas' }, + { id: 'broccoli', label: 'Broccoli' }, + ], + }, + ]; + + return ( + + {(group: any) => ( + + {(item: any) => ( + {item.label} + )} + + )} + + ); +}; + +SectionsDynamic.storyName = 'Sections – dynamic collection'; diff --git a/src/components/fields/ComboBox/ComboBox.tsx b/src/components/fields/ComboBox/ComboBox.tsx index 6c3197934..0203b9337 100644 --- a/src/components/fields/ComboBox/ComboBox.tsx +++ b/src/components/fields/ComboBox/ComboBox.tsx @@ -17,7 +17,7 @@ import { useHover, useOverlayPosition, } from 'react-aria'; -import { Item, useComboBoxState } from 'react-stately'; +import { Section as BaseSection, Item, useComboBoxState } from 'react-stately'; import { useEvent } from '../../../_internal/index'; import { DownIcon, LoadingIcon } from '../../../icons'; @@ -207,13 +207,23 @@ export const ComboBox = forwardRef(function ComboBox( let isAsync = loadingState != null; let { contains } = useFilter({ sensitivity: 'base' }); - let state = useComboBoxState({ + let comboBoxStateProps: any = { ...props, defaultFilter: filter || contains, filter: undefined, allowsEmptyCollection: isAsync, menuTrigger, - }); + }; + + if (props.items && !isAsync) { + comboBoxStateProps = { + ...comboBoxStateProps, + defaultItems: props.items, + items: undefined, + }; + } + + let state = useComboBoxState(comboBoxStateProps); styles = extractStyles(otherProps, PROP_STYLES, styles); @@ -242,7 +252,7 @@ export const ComboBox = forwardRef(function ComboBox( buttonProps: triggerProps, } = useComboBox( { - ...props, + ...comboBoxStateProps, inputRef, buttonRef: triggerRef, listBoxRef, @@ -471,14 +481,21 @@ export const ComboBox = forwardRef(function ComboBox( ); }) as unknown as (( props: CubeComboBoxProps & { ref?: ForwardedRef }, -) => ReactElement) & { Item: typeof Item }; +) => ReactElement) & { Item: typeof Item; Section: typeof BaseSection }; + +type SectionComponentCB = typeof BaseSection; + +const ComboBoxSectionComponent = Object.assign(BaseSection, { + displayName: 'Section', +}) as SectionComponentCB; -// Extend typing on Item to accept optional `description` prop like Select does. ComboBox.Item = Item as unknown as (props: { description?: ReactNode; [key: string]: any; }) => ReactElement; +ComboBox.Section = ComboBoxSectionComponent; + Object.defineProperty(ComboBox, 'cubeInputType', { value: 'ComboBox', enumerable: false, diff --git a/src/components/fields/Select/Select.tsx b/src/components/fields/Select/Select.tsx index d06e4d35a..bc5058c25 100644 --- a/src/components/fields/Select/Select.tsx +++ b/src/components/fields/Select/Select.tsx @@ -706,15 +706,17 @@ function SelectSection(props: SelectSectionProps) { )} - {[...item.childNodes].map((node: any) => ( - ); From f70181476958e24bedd8c46096efc130ef9cdb82 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 18:27:57 +0200 Subject: [PATCH 4/7] feat(ComboBox): sections support * 2 --- src/components/fields/ComboBox/ComboBox.stories.tsx | 2 ++ src/components/fields/Select/Select.tsx | 7 ++----- src/components/pickers/Menu/styled.tsx | 5 ++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/fields/ComboBox/ComboBox.stories.tsx b/src/components/fields/ComboBox/ComboBox.stories.tsx index 783bb16ea..7ef31cf49 100644 --- a/src/components/fields/ComboBox/ComboBox.stories.tsx +++ b/src/components/fields/ComboBox/ComboBox.stories.tsx @@ -391,6 +391,7 @@ export const SectionsStatic: StoryFn> = (args) => ( ); SectionsStatic.storyName = 'Sections – static items'; +SectionsStatic.play = ItemsWithDescriptions.play; export const SectionsDynamic: StoryFn> = (args) => { const groups = [ @@ -435,3 +436,4 @@ export const SectionsDynamic: StoryFn> = (args) => { }; SectionsDynamic.storyName = 'Sections – dynamic collection'; +SectionsDynamic.play = ItemsWithDescriptions.play; diff --git a/src/components/fields/Select/Select.tsx b/src/components/fields/Select/Select.tsx index bc5058c25..897ceac1b 100644 --- a/src/components/fields/Select/Select.tsx +++ b/src/components/fields/Select/Select.tsx @@ -182,15 +182,12 @@ export const ListBoxElement = tasty({ as: 'ul', styles: { display: 'flex', - gap: { - '': '1bw', - sections: 0, - }, + gap: '1bw', flow: 'column', margin: '0', padding: { '': '.5x', - sections: 0, + section: 0, }, listStyle: 'none', radius: { diff --git a/src/components/pickers/Menu/styled.tsx b/src/components/pickers/Menu/styled.tsx index 2f6e0364d..cf8882fd4 100644 --- a/src/components/pickers/Menu/styled.tsx +++ b/src/components/pickers/Menu/styled.tsx @@ -15,8 +15,7 @@ export const StyledMenu = tasty({ margin: 0, padding: { '': '0.5x', - section: 9, // section menu - sections: 9, // has sections inside + section: 0, // section menu }, overflow: { '': 'auto', @@ -103,7 +102,7 @@ export const StyledSectionHeading = tasty(Space, { styles: { color: '#dark-04', preset: 'c2', - padding: '1x 2x 0', + padding: '1x 1.5x 0', height: '3x', placeContent: 'center space-between', align: 'start', From 988d0319cf015befb8c87295f11346179d5f59ff Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 18:37:27 +0200 Subject: [PATCH 5/7] feat(ComboBox): sections support * 3 --- src/components/pickers/Menu/styled.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pickers/Menu/styled.tsx b/src/components/pickers/Menu/styled.tsx index cf8882fd4..8c9e8b49a 100644 --- a/src/components/pickers/Menu/styled.tsx +++ b/src/components/pickers/Menu/styled.tsx @@ -102,7 +102,7 @@ export const StyledSectionHeading = tasty(Space, { styles: { color: '#dark-04', preset: 'c2', - padding: '1x 1.5x 0', + padding: '.5x 1.5x', height: '3x', placeContent: 'center space-between', align: 'start', From 026ae08620aceb30cbfc91666e683b6fa55e83f5 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 18:57:39 +0200 Subject: [PATCH 6/7] feat(ComboBox): sections support * 4 --- src/components/fields/ComboBox/ComboBox.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/fields/ComboBox/ComboBox.tsx b/src/components/fields/ComboBox/ComboBox.tsx index 0203b9337..f60307d5a 100644 --- a/src/components/fields/ComboBox/ComboBox.tsx +++ b/src/components/fields/ComboBox/ComboBox.tsx @@ -215,14 +215,6 @@ export const ComboBox = forwardRef(function ComboBox( menuTrigger, }; - if (props.items && !isAsync) { - comboBoxStateProps = { - ...comboBoxStateProps, - defaultItems: props.items, - items: undefined, - }; - } - let state = useComboBoxState(comboBoxStateProps); styles = extractStyles(otherProps, PROP_STYLES, styles); From 771f79671791362c85b259b871592eec3403b991 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 17 Jun 2025 19:01:50 +0200 Subject: [PATCH 7/7] feat(ComboBox): sections support * 5 --- src/components/fields/ComboBox/ComboBox.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/ComboBox/ComboBox.stories.tsx b/src/components/fields/ComboBox/ComboBox.stories.tsx index 7ef31cf49..b8dfda6e9 100644 --- a/src/components/fields/ComboBox/ComboBox.stories.tsx +++ b/src/components/fields/ComboBox/ComboBox.stories.tsx @@ -416,7 +416,7 @@ export const SectionsDynamic: StoryFn> = (args) => { return (