Skip to content

Commit

Permalink
feat(DropdownMenu): extract <DropdownMenu.Item/> (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
ogonkov authored Dec 5, 2022
1 parent a70b248 commit 1307f87
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 29 deletions.
41 changes: 25 additions & 16 deletions src/components/DropdownMenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, {ReactNode, MouseEventHandler, useRef, useCallback, useEffect} from 'react';
import React, {ReactNode, MouseEventHandler, useRef, useCallback, useEffect, useMemo} from 'react';
import {block} from '../utils/cn';
import {PopupPlacement} from '../Popup';
import {Button, ButtonProps} from '../Button';
import {Icon} from '../Icon';
import {DotsIcon} from '../icons/DotsIcon';
import {useStateWithCallback} from '../utils/useStateWithCallback';
import {DropdownMenuContext} from './DropdownMenuContext';
import type {
DropdownMenuSize,
DropdownMenuItem,
DropdownMenuItemMixed,
DropdownMenuItemAction,
} from './types';
import {DropdownMenuPopup} from './DropdownMenuPopup';
import {DropdownMenuItem as DropdownMenuItemComponent} from './DropdownMenuItem';
import {MenuProps} from '../Menu';
import './DropdownMenu.scss';

Expand Down Expand Up @@ -66,7 +68,7 @@ export type DropdownMenuProps<T> = {
children?: ReactNode;
};

export const DropdownMenu = <T,>({
const DropdownMenu = <T,>({
items = [],
size = 'm',
icon = <Icon data={DotsIcon} />,
Expand Down Expand Up @@ -96,17 +98,6 @@ export const DropdownMenu = <T,>({
setPopupShown((value) => !value);
};

const handleMenuItemClick = useCallback(
(
event: React.MouseEvent<HTMLElement, MouseEvent>,
action: DropdownMenuItemAction<T> | undefined,
) => {
action?.(event, data);
setPopupShown(false);
},
[data, setPopupShown],
);

const handleClose = useCallback(() => {
setPopupShown(false);
}, [setPopupShown]);
Expand Down Expand Up @@ -138,8 +129,24 @@ export const DropdownMenu = <T,>({
}
}, [disabled, isPopupShown, setPopupShown]);

const contextValue = useMemo(
() => ({
toggle(open?: boolean) {
setPopupShown((popupShown) => {
if (typeof open === 'boolean') {
return open;
}

return !popupShown;
});
},
data,
}),
[data, setPopupShown],
);

return (
<>
<DropdownMenuContext.Provider value={contextValue}>
<div
ref={anchorRef}
className={b('switcher-wrapper', switcherWrapperClassName)}
Expand All @@ -165,13 +172,15 @@ export const DropdownMenu = <T,>({
placement={popupPlacement}
menuProps={menuProps}
anchorRef={anchorRef}
onMenuItemClick={handleMenuItemClick}
onClose={handleClose}
>
{children}
</DropdownMenuPopup>
</>
</DropdownMenuContext.Provider>
);
};

const DropdownMenuExport = Object.assign(DropdownMenu, {Item: DropdownMenuItemComponent});
export {DropdownMenuExport as DropdownMenu};

export type {DropdownMenuItem, DropdownMenuItemMixed, DropdownMenuItemAction};
13 changes: 13 additions & 0 deletions src/components/DropdownMenu/DropdownMenuContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {createContext} from 'react';

interface Context<T> {
toggle(open?: boolean): void;
data: T | undefined;
}

export const DropdownMenuContext = createContext<Context<unknown>>({
toggle() {},
data: undefined,
});

DropdownMenuContext.displayName = 'DropdownMenu.Context';
25 changes: 25 additions & 0 deletions src/components/DropdownMenu/DropdownMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, {MouseEventHandler, useCallback, useContext} from 'react';
import {Menu} from '../Menu';
import {DropdownMenuContext} from './DropdownMenuContext';
import type {DropdownMenuItem as DropdownMenuItemType} from './types';

type Props<T> = DropdownMenuItemType<T>;

export const DropdownMenuItem = <T,>({text, action, ...props}: Props<T>) => {
const {toggle, data} = useContext(DropdownMenuContext);
const onClick: MouseEventHandler<HTMLElement> = useCallback(
(event) => {
action?.(event, data as unknown as T);
toggle(false);
},
[action, data, toggle],
);

return (
<Menu.Item onClick={onClick} {...props}>
{text}
</Menu.Item>
);
};

DropdownMenuItem.displayName = 'DropdownMenu.Item';
20 changes: 7 additions & 13 deletions src/components/DropdownMenu/DropdownMenuPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React, {ReactNode, RefObject, useMemo} from 'react';
import {block} from '../utils/cn';
import {Popup, PopupPlacement} from '../Popup';
import {Menu, MenuProps} from '../Menu';
import type {DropdownMenuItem, DropdownMenuItemAction, DropdownMenuSize} from './types';
import type {DropdownMenuItem, DropdownMenuSize} from './types';
import {toItemList} from './toItemList';
import {DropdownMenuItem as DropdownMenuItemComponent} from './DropdownMenuItem';

const b = block('dropdown-menu');
const SEPARATOR: DropdownMenuItem = {text: '', action: () => {}};
Expand All @@ -12,10 +13,6 @@ export type DropdownMenuPopupProps<T> = {
items: (DropdownMenuItem<T> | DropdownMenuItem<T>[])[];
open: boolean;
anchorRef?: RefObject<HTMLDivElement>;
onMenuItemClick: (
event: React.MouseEvent<HTMLElement, MouseEvent>,
action: DropdownMenuItemAction<T> | undefined,
) => void;
onClose: () => void;
popupClassName?: string;
placement?: PopupPlacement;
Expand All @@ -28,7 +25,6 @@ export const DropdownMenuPopup = <T,>({
items,
open,
anchorRef,
onMenuItemClick,
onClose,
popupClassName,
placement,
Expand All @@ -41,26 +37,24 @@ export const DropdownMenuPopup = <T,>({
children || (
<Menu className={b('menu')} size={size} {...menuProps}>
{toItemList(items, SEPARATOR).map((item, index) => {
const {text, action, className, ...itemProps} = item;
const {className, ...itemProps} = item;

return (
<Menu.Item
<DropdownMenuItemComponent
key={index}
className={b(
'menu-item',
{separator: item === SEPARATOR},
className,
)}
onClick={(event) => onMenuItemClick(event, action)}
{...itemProps}
>
{text}
</Menu.Item>
/>
);
})}
</Menu>
)
);
}, [children, size, menuProps, items, onMenuItemClick]);
}, [children, size, menuProps, items]);

return (
<Popup
Expand Down
30 changes: 30 additions & 0 deletions src/components/DropdownMenu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,33 @@ This type describes an individual dropdown menu item.
]}
/>
```

Usage with custom `<Menu/>`

```tsx
import {DropdownMenu, Menu} from './DropdownMenu';

const dropdown = (
<DropdownMenu>
<Menu>
<DropdownMenu.Item icon={<Icon data={openIcon} />} action={() => console.log('Open')}>
Open
</DropdownMenu.Item>
<Menu.Group>
<DropdownMenu.Item icon={<Icon data={trashIcon} />} action={() => console.log('Remove')}>
Remove
</DropdownMenu.Item>
<DropdownMenu.Item icon={<Icon data={renameIcon} />} action={() => console.log('Rename')}>
Rename
</DropdownMenu.Item>
</Menu.Group>
<DropdownMenu.Item
icon={<Icon data={propertiesIcon} />}
action={() => console.log('Properties')}
>
Properties
</DropdownMenu.Item>
</Menu>
</DropdownMenu>
);
```

0 comments on commit 1307f87

Please sign in to comment.