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
22 changes: 22 additions & 0 deletions src/components/ui/DropdownMenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import DropdownMenuRoot from './fragments/DropdownMenuRoot';
import DropdownMenuTrigger from './fragments/DropdownMenuTrigger';
import DropdownMenuContent from './fragments/DropdownMenuContent';
import DropdownMenuPortal from './fragments/DropdownMenuPortal';
import DropdownMenuItem from './fragments/DropdownMenuItem';
import DropdownMenuSub from './fragments/DropdownMenuSub';
import DropdownMenuSubTrigger from './fragments/DropdownMenuSubTrigger';

const DropdownMenu = () => {
console.warn('Direct usage of DropdownMenu is not supported. Please use DropdownMenu.Root, DropdownMenu.Item instead.');
return null;
};

DropdownMenu.Root = DropdownMenuRoot;
DropdownMenu.Trigger = DropdownMenuTrigger;
DropdownMenu.Content = DropdownMenuContent;
DropdownMenu.Portal = DropdownMenuPortal;
DropdownMenu.Item = DropdownMenuItem;
DropdownMenu.Sub = DropdownMenuSub;
DropdownMenu.SubTrigger = DropdownMenuSubTrigger;

export default DropdownMenu;
11 changes: 11 additions & 0 deletions src/components/ui/DropdownMenu/contexts/DropdownMenuContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

import React from 'react';

export interface DropdownMenuContextProps {
rootClass: string
}

const DropdownMenuContext = React.createContext<DropdownMenuContextProps|null>(null);

export default DropdownMenuContext;
25 changes: 25 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';
import clsx from 'clsx';

export type DropdownMenuContentProps = {
children: React.ReactNode;
className?: string;
} & MenuPrimitiveProps.Content;

const DropdownMenuContent = ({ children, className }:DropdownMenuContentProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuContent should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Content className={clsx(`${rootClass}-content`, className)}>
{children}
</MenuPrimitive.Content>
);
};

export default DropdownMenuContent;
26 changes: 26 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';
import clsx from 'clsx';

export type DropdownMenuItemProps = {
children: React.ReactNode;
className?: string;
label?: string;
} & MenuPrimitiveProps.Item;

const DropdownMenuItem = ({ children, className, label }:DropdownMenuItemProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuItem should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Item className={clsx(`${rootClass}-item`, className)} label={label}>
{children}
</MenuPrimitive.Item>
);
};

export default DropdownMenuItem;
23 changes: 23 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import MenuPrimitive from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';

export type DropdownMenuPortalProps = {
children: React.ReactNode;
}

const DropdownMenuPortal = ({ children }:DropdownMenuPortalProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuPortal should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;

Check warning on line 15 in src/components/ui/DropdownMenu/fragments/DropdownMenuPortal.tsx

View workflow job for this annotation

GitHub Actions / lint

'rootClass' is assigned a value but never used
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused variable.

The rootClass variable is extracted from context but never used in the component.

Apply this diff to remove the unused variable:

     const { rootClass } = context;
-    const { rootClass } = context;

Or if the rootClass will be used in the future, consider adding a comment explaining its intended purpose.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { rootClass } = context;
🧰 Tools
🪛 GitHub Check: lint

[warning] 15-15:
'rootClass' is assigned a value but never used

🤖 Prompt for AI Agents
In src/components/ui/DropdownMenu/fragments/DropdownMenuPortal.tsx at line 15,
the variable rootClass is destructured from context but never used in the
component. Remove the declaration of rootClass to clean up unused code. If
rootClass is intended for future use, add a comment explaining its purpose
instead of removing it.

return (
<MenuPrimitive.Portal>
{children}
</MenuPrimitive.Portal>
);
};

export default DropdownMenuPortal;
26 changes: 26 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuRoot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import { customClassSwitcher } from '~/core';
import clsx from 'clsx';
import DropdownMenuContext from '../contexts/DropdownMenuContext';

export type DropdownMenuRootProps = {
children: React.ReactNode;
customRootClass?: string;
className?: string;
} & MenuPrimitiveProps.Root;

const COMPONENT_NAME = 'DropdownMenu';

const DropdownMenuRoot = ({ children, customRootClass, className }:DropdownMenuRootProps) => {
const rootClass = customClassSwitcher(customRootClass, COMPONENT_NAME);
return (
<DropdownMenuContext.Provider value={{ rootClass }} >
<MenuPrimitive.Root className={clsx(`${rootClass}-root`, className)}>
{children}
</MenuPrimitive.Root>
</DropdownMenuContext.Provider>
);
};

export default DropdownMenuRoot;
25 changes: 25 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuSub.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';
import clsx from 'clsx';

export type DropdownMenuSubProps = {
children: React.ReactNode;
className?: string;
} & MenuPrimitiveProps.Sub;

const DropdownMenuSub = ({ children, className }:DropdownMenuSubProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuSub should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Sub className={clsx(`${rootClass}-sub`, className)}>
{children}
</MenuPrimitive.Sub>
);
};

export default DropdownMenuSub;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';
import clsx from 'clsx';

export type DropdownMenuSubTriggerProps = {
children: React.ReactNode;
className?: string;
} & MenuPrimitiveProps.Trigger;

const DropdownMenuSubTrigger = ({ children, className }:DropdownMenuSubTriggerProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuSubTrigger should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Trigger className={clsx(`${rootClass}-sub-trigger`, className)}>
{children}
</MenuPrimitive.Trigger>
);
Comment on lines +11 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add prop forwarding for complete API support.

The component extends MenuPrimitiveProps.Trigger but doesn't forward additional props to the underlying MenuPrimitive.Trigger, which may limit its functionality.

Apply this diff to add proper prop forwarding:

-const DropdownMenuSubTrigger = ({ children, className }:DropdownMenuSubTriggerProps) => {
+const DropdownMenuSubTrigger = ({ children, className, ...props }:DropdownMenuSubTriggerProps) => {
     const context = React.useContext(DropdownMenuContext);
     if (!context) {
         console.log('DropdownMenuSubTrigger should be used in the DropdownMenuRoot');
         return null;
     }
     const { rootClass } = context;
     return (
-        <MenuPrimitive.Trigger className={clsx(`${rootClass}-sub-trigger`, className)}>
+        <MenuPrimitive.Trigger className={clsx(`${rootClass}-sub-trigger`, className)} {...props}>
             {children}
         </MenuPrimitive.Trigger>
     );

This ensures all trigger-related props are properly forwarded to the underlying primitive.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DropdownMenuSubTrigger = ({ children, className }:DropdownMenuSubTriggerProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuSubTrigger should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Trigger className={clsx(`${rootClass}-sub-trigger`, className)}>
{children}
</MenuPrimitive.Trigger>
);
const DropdownMenuSubTrigger = ({ children, className, ...props }: DropdownMenuSubTriggerProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuSubTrigger should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Trigger
className={clsx(`${rootClass}-sub-trigger`, className)}
{...props}
>
{children}
</MenuPrimitive.Trigger>
);
};
🤖 Prompt for AI Agents
In src/components/ui/DropdownMenu/fragments/DropdownMenuSubTrigger.tsx around
lines 11 to 22, the DropdownMenuSubTrigger component does not forward additional
props to the underlying MenuPrimitive.Trigger, limiting its API support. To fix
this, update the component to accept and forward all extra props to
MenuPrimitive.Trigger by spreading the remaining props onto it. This will ensure
full compatibility with the MenuPrimitive.Trigger API.

};

export default DropdownMenuSubTrigger;
25 changes: 25 additions & 0 deletions src/components/ui/DropdownMenu/fragments/DropdownMenuTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import MenuPrimitive, { MenuPrimitiveProps } from '~/core/primitives/Menu/MenuPrimitive';
import DropdownMenuContext from '../contexts/DropdownMenuContext';
import clsx from 'clsx';

export type DropdownMenuTriggerProps = {
children: React.ReactNode;
className?: string;
} & MenuPrimitiveProps.Trigger;

const DropdownMenuTrigger = ({ children, className }:DropdownMenuTriggerProps) => {
const context = React.useContext(DropdownMenuContext);
if (!context) {
console.log('DropdownMenuTrigger should be used in the DropdownMenuRoot');
return null;
}
const { rootClass } = context;
return (
<MenuPrimitive.Trigger className={clsx(`${rootClass}-trigger`, className)}>
{children}
</MenuPrimitive.Trigger>
);
};

export default DropdownMenuTrigger;
51 changes: 51 additions & 0 deletions src/components/ui/DropdownMenu/stories/DropdownMenu.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import type { StoryObj } from '@storybook/react';
import DropdownMenu from '../DropdownMenu';
import SandboxEditor from '~/components/tools/SandboxEditor/SandboxEditor';

type Story = StoryObj<typeof DropdownMenu>;

export default {
title: 'WIP/DropdownMenu',
component: DropdownMenu
};

const ChevronRight =() => <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.1584 3.13508C6.35985 2.94621 6.67627 2.95642 6.86514 3.15788L10.6151 7.15788C10.7954 7.3502 10.7954 7.64949 10.6151 7.84182L6.86514 11.8418C6.67627 12.0433 6.35985 12.0535 6.1584 11.8646C5.95694 11.6757 5.94673 11.3593 6.1356 11.1579L9.565 7.49985L6.1356 3.84182C5.94673 3.64036 5.95694 3.32394 6.1584 3.13508Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

const Hamburger = () => <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
export const Basic: Story = {
render: () => (
<SandboxEditor>
<DropdownMenu.Root customRootClass="" >
<DropdownMenu.Trigger ><Hamburger /></DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content >
<DropdownMenu.Item label="Profile">Profile</DropdownMenu.Item>
<DropdownMenu.Item label="Settings">Settings</DropdownMenu.Item>
<DropdownMenu.Item label="Notifications">Notifications</DropdownMenu.Item>
<DropdownMenu.Sub >
<DropdownMenu.SubTrigger >More Options <ChevronRight /></DropdownMenu.SubTrigger>
<DropdownMenu.Content >
<DropdownMenu.Item label="Help Center">Help Center</DropdownMenu.Item>
<DropdownMenu.Item label="Feedback">Feedback</DropdownMenu.Item>
<DropdownMenu.Item label="About">About</DropdownMenu.Item>
<DropdownMenu.Sub >
<DropdownMenu.SubTrigger >Legal <ChevronRight /></DropdownMenu.SubTrigger>
<DropdownMenu.Content >
<DropdownMenu.Item label="Terms of Service">Terms of Service</DropdownMenu.Item>
<DropdownMenu.Item label="Privacy Policy">Privacy Policy</DropdownMenu.Item>
<DropdownMenu.Item label="Licenses">Licenses</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Sub>
<DropdownMenu.Item label="Contact">Contact</DropdownMenu.Item>
<DropdownMenu.Item label="Support">Support</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Sub>
<DropdownMenu.Item label="Logout">Logout</DropdownMenu.Item>
<DropdownMenu.Item label="Switch Account">Switch Account</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</SandboxEditor>
)
};
14 changes: 13 additions & 1 deletion src/core/primitives/Floater/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { FloatingOverlay, FloatingPortal, FloatingFocusManager, useFloating, FloatingArrow, arrow, useRole, useInteractions, useDismiss, useHover, useFocus, flip, shift, hide, offset, useMergeRefs } from '@floating-ui/react';
import { FloatingOverlay, FloatingPortal, FloatingNode, safePolygon, FloatingTree, FloatingList, useClick, useTypeahead, useListNavigation, useFloatingParentNodeId, useListItem, autoUpdate, useFloatingTree, FloatingFocusManager, useFloating, useFloatingNodeId, FloatingArrow, arrow, useRole, useInteractions, useDismiss, useHover, useFocus, flip, shift, hide, offset, useMergeRefs } from '@floating-ui/react';

const Floater = {
Portal: FloatingPortal,
Overlay: FloatingOverlay,
FocusManager: FloatingFocusManager,
useFloating,
Arrow: FloatingArrow,
useFloatingNodeId,
useListItem,
useTypeahead,
safePolygon,
useListNavigation,
useFloatingParentNodeId,
useFloatingTree,
FloatingNode,
FloatingTree,
FloatingList,
autoUpdate,
useClick,
arrow,
useRole,
useFocus,
Expand Down
28 changes: 28 additions & 0 deletions src/core/primitives/Menu/MenuPrimitive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import MenuPrimitiveRoot, { MenuPrimitiveRootProps } from './fragments/MenuPrimitiveRoot';
import MenuPrimitiveItem, { MenuPrimitiveItemProps } from './fragments/MenuPrimitiveItem';
import MenuPrimitiveTrigger, { MenuPrimitiveTriggerProps } from './fragments/MenuPrimitiveTrigger';
import MenuPrimitiveContent, { MenuPrimitiveContentProps } from './fragments/MenuPrimitiveContent';
import MenuPrimitiveSub, { MenuPrimitiveSubProps } from './fragments/MenuPrimitiveSub';
import MenuPrimitivePortal from './fragments/MenuPrimitivePortal';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing props type for MenuPrimitivePortal

The MenuPrimitivePortal component is imported without its corresponding props type, creating an inconsistency with the other imports. This also means there's no Portal type in the MenuPrimitiveProps namespace.

If MenuPrimitivePortal has props, import them:

-import MenuPrimitivePortal from './fragments/MenuPrimitivePortal';
+import MenuPrimitivePortal, { MenuPrimitivePortalProps } from './fragments/MenuPrimitivePortal';

And add to the namespace:

 export namespace MenuPrimitiveProps {
     export type Root = MenuPrimitiveRootProps;
     export type Item = MenuPrimitiveItemProps;
     export type Trigger = MenuPrimitiveTriggerProps;
     export type Content = MenuPrimitiveContentProps;
     export type Sub = MenuPrimitiveSubProps;
+    export type Portal = MenuPrimitivePortalProps;
 }

If MenuPrimitivePortal doesn't have props, consider documenting this or using a type alias for consistency.

Also applies to: 20-26

🤖 Prompt for AI Agents
In src/core/primitives/Menu/MenuPrimitive.tsx around line 6 and also lines 20 to
26, the import of MenuPrimitivePortal lacks its corresponding props type,
causing inconsistency with other imports and missing the Portal type in the
MenuPrimitiveProps namespace. To fix this, check if MenuPrimitivePortal has
defined props; if yes, import its props type alongside the component and add
that type to the MenuPrimitiveProps namespace. If it does not have props, add a
type alias or document this explicitly for consistency with other components.


const MenuPrimitive = () => {
console.warn('Direct usage of MenuPrimitive is not supported. Please use MenuPrimitive.Root, MenuPrimitive.Item instead.');
return null;
};

MenuPrimitive.Root = MenuPrimitiveRoot;
MenuPrimitive.Item = MenuPrimitiveItem;
MenuPrimitive.Trigger = MenuPrimitiveTrigger;
MenuPrimitive.Content = MenuPrimitiveContent;
MenuPrimitive.Sub = MenuPrimitiveSub;
MenuPrimitive.Portal = MenuPrimitivePortal;

export namespace MenuPrimitiveProps {
export type Root = MenuPrimitiveRootProps;
export type Item = MenuPrimitiveItemProps;
export type Trigger = MenuPrimitiveTriggerProps;
export type Content = MenuPrimitiveContentProps;
export type Sub = MenuPrimitiveSubProps;
}

export default MenuPrimitive;
33 changes: 33 additions & 0 deletions src/core/primitives/Menu/contexts/MenuPrimitiveRootContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client';

import React from 'react';

export interface MenuPrimitiveRootPrimitiveContextProps {
isOpen: boolean;
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
refs: {
reference: React.MutableRefObject<Element | null>;
floating: React.MutableRefObject<HTMLElement | null>;
domReference: React.MutableRefObject<Element | null>;
setReference(node: Element | null): void;
setFloating(node: HTMLElement | null): void;
setPositionReference(node: Element): void;
};
floatingStyles: React.CSSProperties;
getReferenceProps: (userProps?: any) => any;
getFloatingProps: (userProps?: any) => any;
getItemProps: (userProps?: any) => any;
activeIndex: number | null;
setActiveIndex: React.Dispatch<React.SetStateAction<number | null>>;
listRef: React.MutableRefObject<any[]>;
elementsRef: React.MutableRefObject<any[]>;
labelsRef: React.MutableRefObject<any[]>;
virtualItemRef: React.MutableRefObject<any>;
nodeId: any;
isNested: boolean;
floatingContext: any;
}

const MenuPrimitiveRootPrimitiveContext = React.createContext<MenuPrimitiveRootPrimitiveContextProps|null>(null);

export default MenuPrimitiveRootPrimitiveContext;
38 changes: 38 additions & 0 deletions src/core/primitives/Menu/fragments/MenuPrimitiveContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useContext } from 'react';

import Floater from '~/core/primitives/Floater';
import MenuPrimitiveRootContext from '../contexts/MenuPrimitiveRootContext';

export type MenuPrimitiveContentProps = {
children: React.ReactNode;
className?: string;
};

const MenuPrimitiveContent = ({ children, className }: MenuPrimitiveContentProps) => {
const context = useContext(MenuPrimitiveRootContext);
if (!context || !context.isOpen) return null;
const { isOpen, refs, floatingStyles, getFloatingProps, elementsRef, labelsRef, nodeId, isNested, floatingContext } = context;

return (

<Floater.FloatingList elementsRef={elementsRef} labelsRef={labelsRef}>
<Floater.FocusManager
context={floatingContext}
modal={false}
initialFocus={isNested ? -1 : 0}
returnFocus={!isNested}
>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className={className}
>
{children}
</div>
</Floater.FocusManager>
</Floater.FloatingList>

);
};
export default MenuPrimitiveContent;
Loading
Loading