diff --git a/changelog.txt b/changelog.txt index 1ee0c122..ff4d6615 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,7 @@ +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. + 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. 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/dropdown-menu/dropdown-menu.stories.tsx b/src/components/dropdown-menu/dropdown-menu.stories.tsx index 3e888d20..378cf0e5 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,40 @@ 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..7460f1e1 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,12 +132,10 @@ 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']; @@ -148,34 +147,46 @@ export const DropdownMenuPortal = ( { return ( isMounted && ( - -
- { React.Children.map( children, ( child ) => { - if ( - ( - child as ReactElement & { - type?: { displayName: string }; - } - )?.type?.displayName === 'DropdownMenu.Content' - ) { - return child; - } - return null; - } ) } -
-
+
+ { 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 ( + + { children } + + ); +}; + DropdownMenuPortal.displayName = 'DropdownMenu.Portal'; export const DropdownMenuTrigger = React.forwardRef< @@ -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( [] ); 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, -}; 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,