Skip to content

Commit

Permalink
feat: adapt the DropdownMenu to accept 'as' prop to specify an elemen…
Browse files Browse the repository at this point in the history
…t to be a wrapper (#476)
  • Loading branch information
hamed-musallam authored Apr 28, 2023
1 parent 05bdfc5 commit 824fd23
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 41 deletions.
102 changes: 61 additions & 41 deletions src/components/dropdown-menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import styled from '@emotion/styled';
import { Menu } from '@headlessui/react';
import type { Placement } from '@popperjs/core';
import { ReactNode, useRef } from 'react';
import { ReactNode, useRef, ElementType, ComponentProps } from 'react';

import { useModifiedPopper } from '../hooks/useModifiedPopper';
import { useOnClickOutside } from '../hooks/useOnClickOutside';
Expand All @@ -21,6 +20,10 @@ interface DropdownMenuBaseProps<T> {
onSelect: (selected: MenuOption<T>) => void;
}

type ElementProps<E = unknown> = E extends ElementType
? ComponentProps<E>
: never;

interface DropdownMenuClickProps<T> extends DropdownMenuBaseProps<T> {
/**
* Node to be inside the Button
Expand All @@ -29,33 +32,42 @@ interface DropdownMenuClickProps<T> extends DropdownMenuBaseProps<T> {
trigger: 'click';
}

interface DropdownMenuContextProps<T> extends DropdownMenuBaseProps<T> {
interface DropdownMenuContextProps<T, E> extends DropdownMenuBaseProps<T> {
trigger: 'contextMenu';
children: ReactNode;
as?: E;
}

export type DropdownMenuProps<T> =
| DropdownMenuContextProps<T>
export type DropdownMenuProps<T, E> =
| DropdownMenuContextProps<T, E>
| DropdownMenuClickProps<T>;

export function DropdownMenu<T>(props: DropdownMenuProps<T>) {
export function DropdownMenu<T = unknown, E extends ElementType = 'div'>(
props: DropdownMenuProps<T, E> &
Omit<ElementProps<E>, keyof DropdownMenuProps<T, E>>,
) {
const { trigger, ...otherProps } = props;

if (trigger === 'contextMenu') {
return <DropdownContextMenu {...otherProps} />;
return <DropdownContextMenu<T, E> {...props} />;
}

return (
<DropdownClickMenu {...otherProps}>{props.children}</DropdownClickMenu>
);
}

const HandleMenuContextDiv = styled.div`
display: contents;
`;

function DropdownContextMenu<T>(props: Omit<DropdownMenuProps<T>, 'trigger'>) {
const { children, onSelect, ...otherProps } = props;
function DropdownContextMenu<T, E extends ElementType = 'div'>(
props: Omit<DropdownMenuContextProps<T, E>, 'trigger'> &
Omit<ElementProps<E>, keyof DropdownMenuProps<T, E>>,
) {
const {
children,
onSelect,
as: Wrapper = 'div',
placement = 'right-start',
options,
...otherProps
} = props;

const {
isPopperElementOpen,
Expand All @@ -64,43 +76,51 @@ function DropdownContextMenu<T>(props: Omit<DropdownMenuProps<T>, 'trigger'>) {
setPopperElement,
styles,
attributes,
} = useContextMenuPlacement(otherProps.placement || 'right-start');
} = useContextMenuPlacement(placement);

const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, closePopperElement);

const { style = {}, ...otherWrapperProps } = otherProps as ElementProps<E>;

return (
<HandleMenuContextDiv onContextMenu={handleContextMenu}>
{isPopperElementOpen && (
<Portal>
<div ref={ref}>
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<Menu>
<MenuItems
itemsStatic
onSelect={(selected) => {
closePopperElement();
onSelect(selected);
}}
{...otherProps}
/>
</Menu>
<Wrapper
style={{ ...(!props?.as && { display: 'contents' }), ...style }}
{...otherWrapperProps}
onContextMenu={handleContextMenu}
>
<>
{isPopperElementOpen && (
<Portal>
<div ref={ref}>
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<Menu>
<MenuItems
itemsStatic
onSelect={(selected) => {
closePopperElement();
onSelect(selected);
}}
options={options}
/>
</Menu>
</div>
</div>
</div>
</Portal>
)}
</Portal>
)}

{children}
</HandleMenuContextDiv>
{children}
</>
</Wrapper>
);
}

function DropdownClickMenu<T>(
props: Omit<DropdownMenuProps<T>, 'trigger'> & { children: ReactNode },
function DropdownClickMenu<T, E>(
props: Omit<DropdownMenuProps<T, E>, 'trigger'> & { children: ReactNode },
) {
const { placement = 'bottom-start', onSelect, ...otherProps } = props;

Expand Down
89 changes: 89 additions & 0 deletions stories/components/dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,92 @@ function ColumnWithDropdownMenu({
</td>
);
}

const TableWithContext = styled.table`
border: 0.55px solid gray;
th,
td {
border: 0.55px solid gray;
padding: 0.4em;
}
`;

export function TableWithContextMenu() {
const headerOptions = useMemo<MenuOptions<string>>(() => {
return [
{ label: 'Copy', type: 'option', icon: <FaAddressBook /> },
{
label: 'Past',
type: 'option',
disabled: true,
icon: <FaAccessibleIcon />,
},
];
}, []);
const options = useMemo<MenuOptions<string>>(() => {
return [
{ label: 'Back', type: 'option', icon: <FaAddressBook /> },
{
label: 'Forward',
type: 'option',
disabled: true,
icon: <FaAccessibleIcon />,
},
{ label: 'Refresh', type: 'option', icon: <Fa500Px /> },
{ type: 'divider' },
{ label: 'Save as', type: 'option', icon: <FaAccusoft /> },
{ label: 'Print', type: 'option', icon: <FaAcquisitionsIncorporated /> },
{ label: 'Cast media to device', type: 'option', icon: <FaAd /> },
{ type: 'divider' },
{
label: 'Send page to your devices',
type: 'option',
icon: <FaAddressCard />,
},
{
label: 'Create QR Code for this page',
type: 'option',
icon: <FaAdjust />,
},
];
}, []);

return (
<TableWithContext>
<thead>
<DropdownMenu
as="tr"
trigger="contextMenu"
onSelect={noop}
options={headerOptions}
>
<th>id</th>
<th>name</th>
<th>rn</th>
<th>mw</th>
<th>em</th>
<th>isExpensive</th>
</DropdownMenu>
</thead>
<tbody>
{data.slice(0, 2).map(({ id, name, rn, mw, em, isExpensive }) => (
<DropdownMenu
key={id}
trigger="contextMenu"
as="tr"
onSelect={noop}
options={options}
style={{ height: '50px' }}
>
<td>{id}</td>
<td>{name}</td>
<td>{rn}</td>
<td>{mw}</td>
<td>{em}</td>
<td>{isExpensive}</td>
</DropdownMenu>
))}
</tbody>
</TableWithContext>
);
}

0 comments on commit 824fd23

Please sign in to comment.