From 52f17d55f5280dfc76af3154fae2244b8e94d26f Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:38:52 +0600 Subject: [PATCH 1/9] wip: Select component with option group --- src/components/select/select-atom.stories.tsx | 81 ++++++++++++++++++ src/components/select/select-types.ts | 11 ++- src/components/select/select.tsx | 85 ++++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) diff --git a/src/components/select/select-atom.stories.tsx b/src/components/select/select-atom.stories.tsx index c514d367..49a9d24e 100644 --- a/src/components/select/select-atom.stories.tsx +++ b/src/components/select/select-atom.stories.tsx @@ -13,6 +13,32 @@ const options = [ { id: '8', name: 'Pink' }, ]; +const groupedOptions = [ + { + label: 'Warm Colors', + options: [ + { id: '1', name: 'Red' }, + { id: '2', name: 'Orange' }, + { id: '3', name: 'Yellow' }, + ], + }, + { + label: 'Cool Colors', + options: [ + { id: '4', name: 'Green' }, + { id: '5', name: 'Cyan' }, + { id: '6', name: 'Blue' }, + ], + }, + { + label: 'Other Colors', + options: [ + { id: '7', name: 'Purple' }, + { id: '8', name: 'Pink' }, + ], + }, +]; + const meta: Meta = { title: 'Atoms/Select', component: Select, @@ -21,6 +47,7 @@ const meta: Meta = { 'Select.Portal': Select.Portal, 'Select.Options': Select.Options, 'Select.Option': Select.Option, + 'Select.OptionGroup': Select.OptionGroup, } as Record>, parameters: { layout: 'centered', @@ -267,3 +294,57 @@ SelectWithSearchWithoutPortal.args = { combobox: true, disabled: false, }; + +const GroupedSelectTemplate: Story = ( { size, multiple, combobox, disabled } ) => ( +
+ +
+); + +export const GroupedSelect = GroupedSelectTemplate.bind( {} ); +GroupedSelect.args = { + size: 'md', + multiple: false, + combobox: false, + disabled: false, +}; + +// GroupedSelect.play = async ( { canvasElement } ) => { +// const canvas = within( canvasElement ); +// const selectButton = await canvas.findByRole( 'combobox' ); +// await userEvent.click( selectButton ); + +// const listBox = await screen.findByRole( 'listbox' ); +// expect( listBox ).toHaveTextContent( 'Warm Colors' ); +// expect( listBox ).toHaveTextContent( 'Cool Colors' ); +// expect( listBox ).toHaveTextContent( 'Red' ); + +// const allOptions = await screen.findAllByRole( 'option' ); +// await userEvent.click( allOptions[ 0 ] ); + +// expect( selectButton ).toHaveTextContent( 'Red' ); +// }; diff --git a/src/components/select/select-types.ts b/src/components/select/select-types.ts index c3325006..b3dc0da9 100644 --- a/src/components/select/select-types.ts +++ b/src/components/select/select-types.ts @@ -99,8 +99,17 @@ export interface SelectButtonProps extends AriaAttributes { className?: string; } +export interface SelectOptionGroupProps { + /** Label for the option group */ + label: string; + /** Children options */ + children: ReactNode; + /** Additional class name for the option group */ + className?: string; +} + export interface SelectOptionsProps { - /** Expects the `Select.Option` children of the Select.Options Component. */ + /** Expects the `Select.Option` or `Select.OptionGroup` children */ children?: ReactNode; /** Key used to identify searched value using the key. Default is 'id'. */ searchBy?: string; diff --git a/src/components/select/select.tsx b/src/components/select/select.tsx index d3d6ed02..25ec6883 100644 --- a/src/components/select/select.tsx +++ b/src/components/select/select.tsx @@ -43,6 +43,7 @@ import type { SelectPortalProps, SelectProps, SelectSizes, + SelectOptionGroupProps, } from './select-types'; // Context to manage the state of the select component. @@ -269,6 +270,46 @@ export function SelectButton( { ); } +export function SelectOptionGroup( { + label, + children, + className, +}: SelectOptionGroupProps ) { + const { sizeValue } = useSelectContext(); + + const groupClassNames = { + sm: 'text-xs py-1', + md: 'text-sm py-1.5', + lg: 'text-base py-2', + }; + + return ( +
+
+ { label } +
+
+ { children } +
+
+ ); +} + export function SelectOptions( { children, searchBy = 'name', // Used to identify searched value using the key. Default is 'id'. @@ -325,10 +366,26 @@ export function SelectOptions( { // Render children based on the search keyword. const renderChildren = useMemo( () => { - return Children.map( children, ( child, index ) => { + let childIndex = 0; + const processChild = ( child: React.ReactNode ): React.ReactNode => { if ( ! isValidElement( child ) ) { return null; } + + // Handle option groups + if ( child.type === SelectOptionGroup ) { + // Recursively process children of the option group. + const groupChildren = Children.map( child.props.children, processChild ); + // Only render group if it has visible children + return groupChildren?.some( ( c ) => c !== null ) ? ( + cloneElement( child, { + ...child.props, + children: groupChildren, + } ) + ) : null; + } + + // Handle regular options if ( searchKeyword ) { const valueProp = child.props.value; if ( typeof valueProp === 'object' ) { @@ -349,19 +406,37 @@ export function SelectOptions( { } return cloneElement( child, { ...child.props, - index, + index: childIndex++, } ); - } ); + }; + + return Children.map( children, processChild ); }, [ searchKeyword, value, selected, children ] ); const childrenCount = Children.count( renderChildren ); // Update the content list reference. useEffect( () => { listContentRef.current = []; - Children.forEach( children, ( child ) => { + // Get all children as an array. + let allChildren = Children.toArray( children ); + // If it's an option group and has children. + if ( + allChildren && + isValidElement( allChildren[ 0 ] ) && + allChildren[ 0 ].type === SelectOptionGroup + ) { + allChildren = Children.toArray( allChildren ) + .map( ( child ) => + isValidElement( child ) ? child.props.children : null + ) + .filter( Boolean ); + } + // Update the list content reference. + Children.forEach( allChildren, ( child ) => { if ( ! isValidElement( child ) ) { return; } + if ( child.props.value ) { if ( searchKeyword ) { const valueProp = child.props.value; @@ -794,10 +869,12 @@ SelectPortal.displayName = 'Select.Portal'; SelectButton.displayName = 'Select.Button'; SelectOptions.displayName = 'Select.Options'; SelectItem.displayName = 'Select.Option'; +SelectOptionGroup.displayName = 'Select.OptionGroup'; Select.Portal = SelectPortal; Select.Button = SelectButton; Select.Options = SelectOptions; Select.Option = SelectItem; +Select.OptionGroup = SelectOptionGroup; export default Select; From 2825b61fc9bd89b78b560170542bb0613264d4b5 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Wed, 25 Dec 2024 12:43:32 +0600 Subject: [PATCH 2/9] Added option group in select component Added option group divider --- src/components/select/component-style.ts | 8 + src/components/select/select-atom.stories.tsx | 41 +++-- src/components/select/select.tsx | 151 +++++++++++++----- 3 files changed, 145 insertions(+), 55 deletions(-) diff --git a/src/components/select/component-style.ts b/src/components/select/component-style.ts index 967a2ec5..d6eaae5f 100644 --- a/src/components/select/component-style.ts +++ b/src/components/select/component-style.ts @@ -49,3 +49,11 @@ export const disabledClassNames = { icon: 'group-disabled:text-icon-disabled', text: 'group-disabled:text-field-color-disabled', }; + +export const optionGroupDividerClassNames = + 'h-px my-2 w-full border-border-subtle border-b border-t-0 border-solid'; +export const optionGroupDividerSizeClassNames = { + sm: 'w-[calc(100%+0.75rem)] translate-x-[-0.375rem]', + md: 'w-[calc(100%+1rem)] translate-x-[-0.5rem]', + lg: 'w-[calc(100%+1rem)] translate-x-[-0.5rem]', +}; diff --git a/src/components/select/select-atom.stories.tsx b/src/components/select/select-atom.stories.tsx index 49a9d24e..e64d8663 100644 --- a/src/components/select/select-atom.stories.tsx +++ b/src/components/select/select-atom.stories.tsx @@ -295,9 +295,15 @@ SelectWithSearchWithoutPortal.args = { disabled: false, }; -const GroupedSelectTemplate: Story = ( { size, multiple, combobox, disabled } ) => ( +const GroupedSelectTemplate: Story = ( { + size, + multiple, + combobox, + disabled, +} ) => (