From 2fa3490ab2aab6c7bc40e93a6ea6b7a344e069fa Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 12:10:40 +0600 Subject: [PATCH 1/6] Update the select component - Upgrade Floating-UI library. - Changed strategy to `fixed`. - Add visually hidden dismiss button for mobile devices. --- package-lock.json | 2 +- package.json | 2 +- src/components/select/select.tsx | 8 +++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85b7e071..7028542a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "@emotion/is-prop-valid": "^1.3.0", - "@floating-ui/react": "^0.26.20", + "@floating-ui/react": "^0.26.28", "@lexical/react": "^0.17.0", "@lexical/utils": "^0.17.0", "clsx": "^2.1.1", diff --git a/package.json b/package.json index 58b30f11..3167c627 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@emotion/is-prop-valid": "^1.3.0", - "@floating-ui/react": "^0.26.20", + "@floating-ui/react": "^0.26.28", "@lexical/react": "^0.17.0", "@lexical/utils": "^0.17.0", "clsx": "^2.1.1", diff --git a/src/components/select/select.tsx b/src/components/select/select.tsx index ef2d8852..f4ae1f58 100644 --- a/src/components/select/select.tsx +++ b/src/components/select/select.tsx @@ -602,7 +602,11 @@ export function SelectOptions( { { /* Dropdown */ } { isOpen && ( <> - + { /* Dropdown Wrapper */ }
@@ -849,6 +854,7 @@ const SelectComponent = ( { }; const { refs, floatingStyles, context } = useFloating( { + strategy: 'fixed', placement: 'bottom-start', open: isOpen, onOpenChange: setIsOpen, From b38843f296843b888d165d07bf01f9e8554ee95d Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 16:24:09 +0600 Subject: [PATCH 2/6] Enhance DropdownMenu component - Introduced DropdownMenu.ContentWrapper for improved structure. - Updated stories to demonstrate usage without DropdownMenu.Portal. - Adjusted rendering logic to support new ContentWrapper. - Added documentation for DropdownMenu usage within Dialogs, Drawers, and Popovers. --- .../dropdown-menu/dropdown-menu.stories.tsx | 94 ++++++++++++---- .../dropdown-menu/dropdown-menu.tsx | 98 +++++++++-------- .../dropdown-menu/dropdown-types.ts | 7 ++ src/components/select/select-atom.stories.tsx | 102 ++++++++++-------- 4 files changed, 195 insertions(+), 106 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.stories.tsx b/src/components/dropdown-menu/dropdown-menu.stories.tsx index 3e888d20..0c09e1a4 100644 --- a/src/components/dropdown-menu/dropdown-menu.stories.tsx +++ b/src/components/dropdown-menu/dropdown-menu.stories.tsx @@ -10,6 +10,7 @@ const meta: Meta = { subcomponents: { 'DropdownMenu.Trigger': DropdownMenu.Trigger, 'DropdownMenu.Content': DropdownMenu.Content, + 'DropdownMenu.ContentWrapper': DropdownMenu.ContentWrapper, 'DropdownMenu.List': DropdownMenu.List, 'DropdownMenu.Item': DropdownMenu.Item, 'DropdownMenu.Separator': DropdownMenu.Separator, @@ -24,18 +25,25 @@ const meta: Meta = { control: false, }, }, + decorators: [ + ( Story ) => ( +
+ +
+ ), + ], }; export default meta; type Story = StoryFn; -export const ButtonTrigger: Story = ( args ) => ( +export const DropdownWithOutPortal: Story = ( args ) => ( - + Menu Item 1 @@ -45,6 +53,44 @@ export const ButtonTrigger: Story = ( args ) => ( Menu Item 5 + + +); +DropdownWithOutPortal.parameters = { + docs: { + description: { + story: `If you want to use the dropdown menu inside a **Dialog**, **Drawer** or **Popover**, you can omit using the \`DropdownMenu.Portal\` component. Using the portal is not required in this case and it might cause issues like the \`z-index\` problem and the dropdown menu not being visible. + +If you really need to use the portal and if you face \`z-index\` issues, in that case you can update the \`z-index\` of the dropdown menu to a higher value. + +#### When to use the \`DropdownMenu.Portal\` component? + +Portal helps to render a floating element into a given container element. By default, outside of the app root and into the body. This is necessary to ensure the floating element can appear outside any potential parent containers that cause clipping (such as overflow: hidden), while retaining its location in the React tree. + +- When the dropdown is being cut off by parent elements. Ex. Parent container has \`overflow: hidden\` property. +- When you need to render the dropdown menu into a different part of the DOM except the parent container. +`, + }, + }, +}; + +export const ButtonTrigger: Story = ( args ) => ( + + + + + + + + + Menu Item 1 + Menu Item 2 + Menu Item 3 + Menu Item 4 + Menu Item 5 + + + ); @@ -56,35 +102,41 @@ export const AvatarTrigger: Story = ( args ) => ( Open Menu - - - Menu Item 1 - Menu Item 2 - Menu Item 3 - Menu Item 4 - Menu Item 5 - - + + + + Menu Item 1 + Menu Item 2 + Menu Item 3 + Menu Item 4 + Menu Item 5 + + + ); -export const IconTrigger: Story = ( args ) => ( +const IconTriggerTemplate: Story = ( args ) => ( Open Menu - - - Menu Item 1 - Menu Item 2 - Menu Item 3 - Menu Item 4 - Menu Item 5 - - + + + + Menu Item 1 + Menu Item 2 + Menu Item 3 + Menu Item 4 + Menu Item 5 + + + ); +export const IconTrigger = IconTriggerTemplate.bind( {} ); + diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index b1c6b0cf..badbdb4c 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -27,6 +27,7 @@ import Menu from '../menu-item/menu-item'; import { AdditionalProps, DropdownCommonProps, + DropdownMenuContentWrapperProps, DropdownMenuItemProps, DropdownMenuListProps, DropdownMenuProps, @@ -51,7 +52,7 @@ export const DropdownMenu = ( { open: isOpen, onOpenChange: setIsOpen, placement, - strategy: 'absolute', + strategy: 'fixed', middleware: [ offset( offsetValue ), flip( { boundary } ), @@ -111,19 +112,19 @@ export const DropdownMenu = ( { return null; } ) } - { React.Children.map( children, ( child ) => { - if ( - React.isValidElement( child ) && - ( - child as ReactElement & { - type: { displayName: string }; - } - )?.type?.displayName === 'DropdownMenu.Portal' - ) { - return child; - } - return null; - } ) } + { React.Children.toArray( children ) + .filter( + ( child ): child is React.ReactElement => + React.isValidElement( child ) && + [ + 'DropdownMenu.Portal', + 'DropdownMenu.ContentWrapper', + ].includes( + ( child.type as { displayName?: string } ) + .displayName || '' + ) + ) + .map( ( child ) => child ) }
); @@ -131,46 +132,56 @@ export const DropdownMenu = ( { DropdownMenu.displayName = 'DropdownMenu'; -export const DropdownMenuPortal = ( { +export const DropdownMenuContentWrapper = ( { children, className, - root, - id, -}: DropdownPortalProps ) => { +}: DropdownMenuContentWrapperProps ) => { const { refs, floatingStyles, getFloatingProps, isMounted, styles } = useDropdownMenuContext() as { refs: UseFloatingReturn['refs']; floatingStyles: UseFloatingReturn['floatingStyles']; getFloatingProps: UseInteractionsReturn['getFloatingProps']; isMounted: boolean; - styles: React.CSSProperties; - }; + styles: React.CSSProperties; + }; + return isMounted && ( +
+ { React.Children.map( children, ( child ) => { + if ( + ( + child as ReactElement & { + type?: { displayName: string }; + } + )?.type?.displayName === 'DropdownMenu.Content' + ) { + return child; + } + return null; + } ) } +
+ ); +}; + +DropdownMenuContentWrapper.displayName = 'DropdownMenu.ContentWrapper'; + +export const DropdownMenuPortal = ( { + children, + root, + id, +}: DropdownPortalProps ) => { return ( - isMounted && ( + ( -
- { React.Children.map( children, ( child ) => { - if ( - ( - child as ReactElement & { - type?: { displayName: string }; - } - )?.type?.displayName === 'DropdownMenu.Content' - ) { - return child; - } - return null; - } ) } -
+ { children }
) ); @@ -274,5 +285,6 @@ DropdownMenu.List = DropdownMenuList; DropdownMenu.Item = DropdownMenuItem; DropdownMenu.Separator = DropdownMenuSeparator; DropdownMenu.Portal = DropdownMenuPortal; +DropdownMenu.ContentWrapper = DropdownMenuContentWrapper; export default DropdownMenu; diff --git a/src/components/dropdown-menu/dropdown-types.ts b/src/components/dropdown-menu/dropdown-types.ts index 753d2719..0f4e5968 100644 --- a/src/components/dropdown-menu/dropdown-types.ts +++ b/src/components/dropdown-menu/dropdown-types.ts @@ -74,3 +74,10 @@ export interface DropdownPortalProps extends DropdownCommonProps { export type DropdownMenuSeparatorProps = MenuSeparatorProps; export type DropdownMenuListProps = MenuListProps; + +export type DropdownMenuContentWrapperProps = { + /** Children of the component */ + children: ReactNode; + /** Additional class name */ + className?: string; +}; diff --git a/src/components/select/select-atom.stories.tsx b/src/components/select/select-atom.stories.tsx index 93656a9f..7d4ecdb5 100644 --- a/src/components/select/select-atom.stories.tsx +++ b/src/components/select/select-atom.stories.tsx @@ -65,6 +65,66 @@ export default meta; type Story = StoryFn; +// Single Select without portal +const SelectWithoutPortalTemplate: Story = ( { + size, + multiple, + combobox, + disabled, +} ) => ( +
+ +
+); + +export const SingleSelectWithoutPortal = SelectWithoutPortalTemplate.bind( {} ); +SingleSelectWithoutPortal.parameters = { + docs: { + description: { + story: `If you want to use the Select component inside a **Dialog**, **Drawer** or **Popover**, you can omit using the \`Select.Portal\` component. Using the portal is not required in this case and it might cause issues like the \`z-index\` problem and the dropdown menu not being visible. + +If you really need to use the portal and if you face \`z-index\` issues, in that case you can update the \`z-index\` of the Select dropdown to a higher value. + +#### When to use the \`Select.Portal\` component? + +Portal helps to render a floating element into a given container element. By default, outside of the app root and into the body. This is necessary to ensure the floating element can appear outside any potential parent containers that cause clipping (such as overflow: hidden), while retaining its location in the React tree. + +- When the dropdown is being cut off by parent elements. Ex. Parent container has \`overflow: hidden\` property. +- When you need to render the dropdown menu into a different part of the DOM except the parent container. +`, + }, + }, +}; +SingleSelectWithoutPortal.args = { + size: 'md', + multiple: false, + combobox: false, + disabled: false, +}; + // Single Select Story export const SingleSelect: Story = ( { size, multiple, combobox, disabled } ) => { const [ selected, setSelected ] = useState( null ); @@ -130,48 +190,6 @@ SingleSelect.play = async ( { canvasElement } ) => { expect( selectButton ).toHaveTextContent( 'Red' ); }; -const SelectWithoutPortalTemplate: Story = ( { - size, - multiple, - combobox, - disabled, -} ) => ( -
- -
-); - -export const SingleSelectWithoutPortal = SelectWithoutPortalTemplate.bind( {} ); -SingleSelectWithoutPortal.args = { - size: 'md', - multiple: false, - combobox: false, - disabled: false, -}; - // Multi-select Story export const MultiSelect: Story = ( { size, multiple, combobox, disabled } ) => { const [ selected, setSelected ] = useState( [] ); From c3c37ba29b562a4cd7e1309777dccf311915da4b Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 16:39:52 +0600 Subject: [PATCH 3/6] Update the changelog --- changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.txt b/changelog.txt index 1ee0c122..6e06574b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +Version 1.7.0 - 2nd May, 2025 +- Improvement - Molecule - DropdownMenu: Added `Dropdown.ContentWrapper` sub-component to render dropdown content inside Dialog or Drawer contexts without requiring `Dropdown.Portal`. +- Improvement - Atom - Select: Refined dropdown positioning and enhanced the Storybook documentation to clearly illustrate when to apply `Select.Portal`, especially within Dialog or Drawer contexts. + Version 1.6.3 - 23rd April, 2025 - Fix - Atom - Accordion: Resolved an issue where the Accordion component's content was not properly visible when expanded due to overflow hidden. From daf53d34e4f7d541b9495e29ca38da1849c6a78b Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 16:56:20 +0600 Subject: [PATCH 4/6] Delete duplicate Select story - Select component should be inside the Atom not molecule. --- src/components/select/select.stories.tsx | 106 ----------------------- 1 file changed, 106 deletions(-) delete mode 100644 src/components/select/select.stories.tsx diff --git a/src/components/select/select.stories.tsx b/src/components/select/select.stories.tsx deleted file mode 100644 index f501658d..00000000 --- a/src/components/select/select.stories.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import type { Meta, StoryFn } from '@storybook/react'; -import { startTransition, useLayoutEffect, useState } from 'react'; -import Select from './select'; -import Label from '../label'; - -const meta: Meta = { - title: 'Molecules/Select', - component: Select, - subcomponents: { - 'Select.Button': Select.Button, - 'Select.Portal': Select.Portal, - 'Select.OptionGroup': Select.OptionGroup, - 'Select.Options': Select.Options, - 'Select.Option': Select.Option, - } as Record>, - parameters: { - layout: 'centered', - }, - tags: [ 'autodocs' ], - argTypes: { - children: { control: false }, - size: { - control: { type: 'select' }, - }, - }, -} satisfies Meta; - -export default meta; - -type Story = StoryFn; - -const options = [ - 'Select Item 1', - 'Select Item 2', - 'Select Item 3', - 'Select Item 4', - 'Select Item 5', -]; - -const Template: Story = ( args ) => { - const [ selected, setSelected ] = useState(); - - const handleSelect = ( value: unknown ) => { - setSelected( value as unknown as string | [] ); - }; - - // Reset selected value when multiple prop changes. - useLayoutEffect( () => { - if ( args?.multiple ) { - startTransition( () => { - setSelected( [] ); - } ); - return; - } - startTransition( () => { - setSelected( '' ); - } ); - }, [ args ] ); - - return ( -
- - -
- ); -}; - -export const BasicSelect = Template.bind( {} ); -BasicSelect.args = { - size: 'md', - multiple: false, -}; - -export const Combobox = Template.bind( {} ); -Combobox.args = { - size: 'md', - combobox: true, - multiple: false, -}; - -export const Multiselect = Template.bind( {} ); -Multiselect.args = { - size: 'md', - multiple: true, -}; - -export const MultiselectCombobox = Template.bind( {} ); -MultiselectCombobox.args = { - size: 'md', - multiple: true, - combobox: true, -}; From 743871ec2cb207bcedea7ba244b409652a7900c8 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 16:57:06 +0600 Subject: [PATCH 5/6] chore: Lint --- .../dropdown-menu/dropdown-menu.stories.tsx | 1 - .../dropdown-menu/dropdown-menu.tsx | 58 +++++++++---------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.stories.tsx b/src/components/dropdown-menu/dropdown-menu.stories.tsx index 0c09e1a4..378cf0e5 100644 --- a/src/components/dropdown-menu/dropdown-menu.stories.tsx +++ b/src/components/dropdown-menu/dropdown-menu.stories.tsx @@ -139,4 +139,3 @@ const IconTriggerTemplate: Story = ( args ) => ( ); export const IconTrigger = IconTriggerTemplate.bind( {} ); - diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index badbdb4c..7460f1e1 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -142,32 +142,34 @@ export const DropdownMenuContentWrapper = ( { floatingStyles: UseFloatingReturn['floatingStyles']; getFloatingProps: UseInteractionsReturn['getFloatingProps']; isMounted: boolean; - styles: React.CSSProperties; - }; + styles: React.CSSProperties; + }; - return isMounted && ( -
- { React.Children.map( children, ( child ) => { - if ( - ( - child as ReactElement & { - type?: { displayName: string }; + return ( + isMounted && ( +
+ { React.Children.map( children, ( child ) => { + if ( + ( + child as ReactElement & { + type?: { displayName: string }; + } + )?.type?.displayName === 'DropdownMenu.Content' + ) { + return child; } - )?.type?.displayName === 'DropdownMenu.Content' - ) { - return child; - } - return null; - } ) } -
+ return null; + } ) } +
+ ) ); }; @@ -179,11 +181,9 @@ export const DropdownMenuPortal = ( { id, }: DropdownPortalProps ) => { return ( - ( - - { children } - - ) + + { children } + ); }; From f976bfad0918883002ec4816898bfa8d924b7c80 Mon Sep 17 00:00:00 2001 From: Jaied Al Sabid <87969327+jaieds@users.noreply.github.com> Date: Fri, 2 May 2025 16:59:29 +0600 Subject: [PATCH 6/6] Update the change log --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 6e06574b..ff4d6615 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,4 @@ -Version 1.7.0 - 2nd May, 2025 +Version 1.7.0 - xx May, 2025 - Improvement - Molecule - DropdownMenu: Added `Dropdown.ContentWrapper` sub-component to render dropdown content inside Dialog or Drawer contexts without requiring `Dropdown.Portal`. - Improvement - Atom - Select: Refined dropdown positioning and enhanced the Storybook documentation to clearly illustrate when to apply `Select.Portal`, especially within Dialog or Drawer contexts.