Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DropDown menu #283

Merged
merged 19 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
51 changes: 51 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@
},
"dependencies": {
"@emotion/react": "^11.10.4",
"@headlessui/react": "^1.7.3",
"@lukeed/uuid": "^2.0.0",
"@popperjs/core": "^2.11.6",
"@tanstack/react-table": "^8.5.15",
"biologic-converter": "^0.3.2",
"cheminfo-types": "^1.4.0",
Expand All @@ -60,6 +62,7 @@
"netcdfjs": "^2.0.2",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^3.1.4",
"react-popper": "^2.3.0",
"react-shadow": "^19.0.3",
"spc-parser": "^0.7.1",
"tinycolor2": "^1.4.2",
Expand Down
109 changes: 109 additions & 0 deletions src/components/menu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ClassNames } from '@emotion/react';
import { Menu } from '@headlessui/react';
import type { Placement } from '@popperjs/core';
import { ReactNode, useState } from 'react';
import { usePopper } from 'react-popper';

import { Portal } from '../Portal';

import { MenuItems, MenuOption, MenuOptions } from './MenuItems';

interface DropdownMenuBaseProps<T> {
/**
* Placement for react-popper
*/
placement?: Placement;

options: MenuOptions<T>;
onSelect: (selected: MenuOption<T>) => void;
}

interface DropdownMenuClickProps<T> extends DropdownMenuBaseProps<T> {
/**
* Node to be inside the Button
*/
children: ReactNode;

/**
* [STILL IN PROGRESS]
* [click]: the User have to define children (this will be the ref to popper)
*/
trigger: 'click';
}

interface DropdownMenuContextProps<T> extends DropdownMenuBaseProps<T> {
/**
* [STILL IN PROGRESS]
* [contextMenu]: Children's will act as the Zone wich respond to the context menu event
*/
trigger: 'contextMenu';
}

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

const styles = {
button: {
boxShadow: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
color: 'white',
fontWeight: 600,
fontSize: '0.875rem',
lineHeight: '1.25rem',
paddingTop: '0.5rem',
paddingBottom: '0.5rem',
paddingLeft: '1rem',
paddingRight: '1rem',
backgroundColor: 'rgb(107 114 128)',
borderColor: 'transparent',
borderRadius: '0.375rem',
borderWidth: 1,
},
};

export default function DropdownMenu<T>(props: DropdownMenuProps<T>) {
const { trigger, placement, ...otherProps } = props;

const [targetRef, setTargetRef] = useState<HTMLButtonElement | null>(null);
const [contentRef, setContentRef] = useState<HTMLDivElement | null>(null);
const { styles: popperStyles, attributes: popperAttribues } = usePopper(
targetRef,
contentRef,
{
placement,
modifiers: [
{
name: 'offset',
options: {
offset: [0, 6],
},
},
],
},
);

if (trigger === 'contextMenu') {
return null;
}

return (
<ClassNames>
{({ css }) => (
<Menu>
<Menu.Button ref={setTargetRef} className={css(styles.button)}>
{props.children}
</Menu.Button>
<Portal>
<div
ref={setContentRef}
style={popperStyles.popper}
{...popperAttribues.popper}
>
<MenuItems {...otherProps} />
</div>
</Portal>
</Menu>
)}
</ClassNames>
);
}
152 changes: 152 additions & 0 deletions src/components/menu/MenuItems.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { ClassNames } from '@emotion/react';
import { Menu } from '@headlessui/react';
import type { ReactNode } from 'react';

export interface MenuOption<T> {
type: 'option';
label: ReactNode;
icon?: ReactNode;
disabled?: boolean;
data?: T;
}

export interface MenuDivider {
type: 'divider';
}

export type MenuOptions<T> = Array<MenuOption<T> | MenuDivider>;

export interface MenuItemsProps<T> {
options: MenuOptions<T>;
onSelect: (selected: MenuOption<T>) => void;
itemsStatic?: boolean;
}

interface ItemProps<T> {
option: MenuOptions<T>[number];
onSelect: MenuItemsProps<T>['onSelect'];
}

interface ItemOptionProps<T> {
option: MenuOption<T>;
onSelect: MenuItemsProps<T>['onSelect'];
active: boolean;
}

const spacing = {
width: 180,
};

const styles = {
items: {
width: spacing.width,
borderRadius: 6,
backgroundColor: 'white',
boxShadow:
'rgba(0, 0, 0, 0.3) 0px 19px 38px, rgba(0, 0, 0, 0.22) 0px 5px 12px',
paddingTop: 5,
paddingBottom: 5,
},
item: {
color: 'black',
minWidth: spacing.width,
maxWidth: spacing.width,
width: 'fit-content',
fontSize: '0.875rem',
paddingTop: 2,
paddingBottom: 2,
paddingLeft: '1rem',
paddingRight: '1rem',
},
divider: {
width: '100%',
color: 'rgb(229, 229, 229)',
marginTop: 5,
marginBottom: 5,
},
};

export function MenuItems<T>(props: MenuItemsProps<T>) {
const { options, onSelect, itemsStatic } = props;

return (
<ClassNames>
{({ css }) => (
<Menu.Items static={itemsStatic} className={css(styles.items)}>
{options.map((option, index) => (
// eslint-disable-next-line react/no-array-index-key
<Item key={index} onSelect={onSelect} option={option} />
))}
</Menu.Items>
)}
</ClassNames>
);
}

function Item<T>(props: ItemProps<T>) {
const { option, onSelect } = props;
const isDivider = option.type === 'divider';

return (
<Menu.Item disabled={!isDivider && option.disabled}>
Sebastien-Ahkrin marked this conversation as resolved.
Show resolved Hide resolved
{({ active }) => (
<>
{isDivider ? (
<ItemDivider />
) : (
<ItemOption onSelect={onSelect} option={option} active={active} />
)}
</>
)}
</Menu.Item>
);
}

function ItemOption<T>(props: ItemOptionProps<T>) {
const { onSelect, option, active } = props;
const { disabled } = option;

return (
<ClassNames>
{({ css }) => (
Sebastien-Ahkrin marked this conversation as resolved.
Show resolved Hide resolved
<div
onClick={() => onSelect(option)}
className={css([
{
...styles.item,
display: 'flex',
Sebastien-Ahkrin marked this conversation as resolved.
Show resolved Hide resolved
flexDirection: 'row',
gap: 15,
alignItems: 'center',
cursor: 'pointer',
},
disabled && {
color: 'rgb(163, 163, 163)',
cursor: 'default',
},
!disabled && {
'&:hover': {
backgroundColor: 'rgb(243, 244, 246)',
},
},
!disabled &&
active && {
backgroundColor: 'rgb(243, 244, 246)',
},
])}
>
{option.icon && <span>{option.icon}</span>}
<span>{option.label}</span>
</div>
)}
</ClassNames>
);
}

function ItemDivider() {
return (
<ClassNames>
{({ css }) => <hr className={css(styles.divider)} />}
</ClassNames>
);
}
Loading