Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 72 additions & 21 deletions src/components/dropdown-menu/dropdown-menu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const meta: Meta<typeof DropdownMenu> = {
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,
Expand All @@ -24,18 +25,25 @@ const meta: Meta<typeof DropdownMenu> = {
control: false,
},
},
decorators: [
( Story ) => (
<div className="h-56">
<Story />
</div>
),
],
};

export default meta;

type Story = StoryFn<typeof DropdownMenu>;

export const ButtonTrigger: Story = ( args ) => (
export const DropdownWithOutPortal: Story = ( args ) => (
<DropdownMenu { ...args }>
<DropdownMenu.Trigger>
<Button>Dropdown</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.ContentWrapper>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
Expand All @@ -45,6 +53,44 @@ export const ButtonTrigger: Story = ( args ) => (
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
</DropdownMenu.ContentWrapper>
</DropdownMenu>
);
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 ) => (
<DropdownMenu { ...args }>
<DropdownMenu.Trigger>
<Button>Dropdown</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.ContentWrapper>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 3</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 4</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
</DropdownMenu.ContentWrapper>
</DropdownMenu.Portal>
</DropdownMenu>
);
Expand All @@ -56,35 +102,40 @@ export const AvatarTrigger: Story = ( args ) => (
<span className="sr-only">Open Menu</span>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 3</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 4</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
<DropdownMenu.ContentWrapper>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 3</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 4</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
</DropdownMenu.ContentWrapper>
</DropdownMenu.Portal>
</DropdownMenu>
);

export const IconTrigger: Story = ( args ) => (
const IconTriggerTemplate: Story = ( args ) => (
<DropdownMenu { ...args } placement="bottom-end">
<DropdownMenu.Trigger>
<House />
<span className="sr-only">Open Menu</span>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 3</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 4</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
<DropdownMenu.ContentWrapper>
<DropdownMenu.Content className="w-60">
<DropdownMenu.List>
<DropdownMenu.Item>Menu Item 1</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 2</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 3</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 4</DropdownMenu.Item>
<DropdownMenu.Item>Menu Item 5</DropdownMenu.Item>
</DropdownMenu.List>
</DropdownMenu.Content>
</DropdownMenu.ContentWrapper>
</DropdownMenu.Portal>
</DropdownMenu>
);
export const IconTrigger = IconTriggerTemplate.bind( {} );
96 changes: 54 additions & 42 deletions src/components/dropdown-menu/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import Menu from '../menu-item/menu-item';
import {
AdditionalProps,
DropdownCommonProps,
DropdownMenuContentWrapperProps,
DropdownMenuItemProps,
DropdownMenuListProps,
DropdownMenuProps,
Expand All @@ -51,7 +52,7 @@ export const DropdownMenu = ( {
open: isOpen,
onOpenChange: setIsOpen,
placement,
strategy: 'absolute',
strategy: 'fixed',
middleware: [
offset( offsetValue ),
flip( { boundary } ),
Expand Down Expand Up @@ -111,32 +112,30 @@ 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 ) }
</div>
</DropdownMenuContext.Provider>
);
};

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'];
Expand All @@ -148,34 +147,46 @@ export const DropdownMenuPortal = ( {

return (
isMounted && (
<FloatingPortal id={ id } root={ root }>
<div
ref={ refs.setFloating }
className={ className }
style={ {
...floatingStyles!,
...styles!,
} }
{ ...getFloatingProps() }
>
{ React.Children.map( children, ( child ) => {
if (
(
child as ReactElement & {
type?: { displayName: string };
}
)?.type?.displayName === 'DropdownMenu.Content'
) {
return child;
}
return null;
} ) }
</div>
</FloatingPortal>
<div
ref={ refs.setFloating }
className={ className }
style={ {
...floatingStyles!,
...styles!,
} }
{ ...getFloatingProps() }
>
{ React.Children.map( children, ( child ) => {
if (
(
child as ReactElement & {
type?: { displayName: string };
}
)?.type?.displayName === 'DropdownMenu.Content'
) {
return child;
}
return null;
} ) }
</div>
)
);
};

DropdownMenuContentWrapper.displayName = 'DropdownMenu.ContentWrapper';

export const DropdownMenuPortal = ( {
children,
root,
id,
}: DropdownPortalProps ) => {
return (
<FloatingPortal id={ id } root={ root }>
{ children }
</FloatingPortal>
);
};

DropdownMenuPortal.displayName = 'DropdownMenu.Portal';

export const DropdownMenuTrigger = React.forwardRef<
Expand Down Expand Up @@ -274,5 +285,6 @@ DropdownMenu.List = DropdownMenuList;
DropdownMenu.Item = DropdownMenuItem;
DropdownMenu.Separator = DropdownMenuSeparator;
DropdownMenu.Portal = DropdownMenuPortal;
DropdownMenu.ContentWrapper = DropdownMenuContentWrapper;

export default DropdownMenu;
7 changes: 7 additions & 0 deletions src/components/dropdown-menu/dropdown-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Loading
Loading