Add basic dropdown styling using headless ui

This commit is contained in:
Dane Everitt 2022-02-26 15:13:13 -05:00
parent eb56be8021
commit f4119df0aa
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
10 changed files with 204 additions and 39 deletions

View file

@ -47,6 +47,7 @@
"@hot-loader/react-dom": "^16.14.0",
"axios": "^0.21.4",
"chart.js": "^2.9.4",
"classnames": "^2.3.1",
"date-fns": "^2.25.0",
"debounce": "^1.2.1",
"deepmerge": "^4.2.2",

View file

@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react';
import http from '@/api/http';
import { User } from '@/api/admin/user';
import { AdminTransformers } from '@/api/admin/transformers';
import { Menu } from '@headlessui/react';
import { ChevronDownIcon, LockClosedIcon } from '@heroicons/react/solid';
import { Dropdown } from '@/components/elements/dropdown';
import { DotsVerticalIcon, LockClosedIcon, PaperAirplaneIcon, PencilIcon, TrashIcon } from '@heroicons/react/solid';
const UsersContainerV2 = () => {
const [ users, setUsers ] = useState<User[]>([]);
@ -20,8 +20,8 @@ const UsersContainerV2 = () => {
}, []);
return (
<div className={'overflow-hidden rounded bg-neutral-700'}>
<table className={'min-w-full'}>
<div className={'bg-neutral-700'}>
<table className={'min-w-full rounded'}>
<thead className={'bg-neutral-900'}>
<tr>
<th scope={'col'} className={'w-8'}/>
@ -57,41 +57,16 @@ const UsersContainerV2 = () => {
</div>
</td>
<td className={'px-6 py-4 whitespace-nowrap'}>
<Menu as={'div'} className={'relative inline-block text-left'}>
<Menu.Button
className={'inline-flex justify-center w-full px-4 py-2 font-medium text-white rounded-md'}
>
Options
<ChevronDownIcon
aria-hidden={'true'}
className={'w-5 h-5 -mr-1 ml-2 text-neutral-100'}
/>
</Menu.Button>
<Menu.Items className={'absolute right-0 mt-2 origin-top-right bg-neutral-900 z-10 w-56'}>
<div className={'px-1 py-1'}>
<Menu.Item>
{() => (
<a href={'#'} className={'group flex rounded items-center w-full px-2 py-2 hover:bg-neutral-800'}>
<LockClosedIcon className={'w-5 h-5 mr-2'} />
<span>Reset Password</span>
</a>
)}
</Menu.Item>
<Menu.Item>
{() => (
<a href={'#'} className={'group flex rounded items-center w-full px-2 py-2'}>Delete</a>
)}
</Menu.Item>
<Menu.Item
disabled
>
<span className={'group flex rounded items-center w-full px-2 py-2 opacity-75'}>
Resend Invite
</span>
</Menu.Item>
</div>
</Menu.Items>
</Menu>
<Dropdown>
<Dropdown.Button className={'px-2'}>
<DotsVerticalIcon />
</Dropdown.Button>
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item>
<Dropdown.Item icon={<PaperAirplaneIcon />}>Reset Password</Dropdown.Item>
<Dropdown.Item icon={<LockClosedIcon />}>Suspend</Dropdown.Item>
<Dropdown.Gap />
<Dropdown.Item icon={<TrashIcon />} danger>Delete Account</Dropdown.Item>
</Dropdown>
</td>
</tr>
))}

View file

@ -0,0 +1,62 @@
import React, { ElementType, forwardRef, useMemo } from 'react';
import { Menu, Transition } from '@headlessui/react';
import styles from './styles.module.css';
import classNames from 'classnames';
import DropdownItem from '@/components/elements/dropdown/DropdownItem';
import DropdownButton from '@/components/elements/dropdown/DropdownButton';
interface Props {
as?: ElementType;
children: React.ReactNode;
}
const DropdownGap = ({ invisible }: { invisible?: boolean }) => (
<div className={classNames('border m-2', { 'border-neutral-700': !invisible, 'border-transparent': invisible })}/>
);
type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & {
type?: JSX.Element;
}
const Dropdown = forwardRef<typeof Menu, Props>(({ as, children }, ref) => {
const [ Button, items ] = useMemo(() => {
const list = React.Children.toArray(children) as unknown as TypedChild[];
return [
list.filter(child => child.type === DropdownButton),
list.filter(child => child.type !== DropdownButton),
];
}, [ children ]);
if (!Button) {
throw new Error('Cannot mount <Dropdown /> component without a child <Dropdown.Button />.');
}
return (
<Menu as={as || 'div'} className={styles.menu} ref={ref}>
{Button}
<Transition
enter={'transition duration-100 ease-out'}
enterFrom={'transition scale-95 opacity-0'}
enterTo={'transform scale-100 opacity-100'}
leave={'transition duration-75 ease-out'}
leaveFrom={'transform scale-100 opacity-100'}
leaveTo={'transform scale-95 opacity-0'}
>
<Menu.Items className={classNames(styles.items_container, 'w-56')}>
<div className={'px-1 py-1'}>
{items}
</div>
</Menu.Items>
</Transition>
</Menu>
);
});
const _Dropdown = Object.assign(Dropdown, {
Button: DropdownButton,
Item: DropdownItem,
Gap: DropdownGap,
});
export { _Dropdown as default };

View file

@ -0,0 +1,24 @@
import classNames from 'classnames';
import styles from '@/components/elements/dropdown/styles.module.css';
import { ChevronDownIcon } from '@heroicons/react/solid';
import { Menu } from '@headlessui/react';
import React from 'react';
interface Props {
className?: string;
animate?: boolean;
children: React.ReactNode;
}
export default ({ className, animate = true, children }: Props) => (
<Menu.Button className={classNames(styles.button, className || 'px-4')}>
{typeof children === 'string' ?
<>
<span className={'mr-2'}>{children}</span>
<ChevronDownIcon aria-hidden={'true'} data-animated={animate.toString()}/>
</>
:
children
}
</Menu.Button>
);

View file

@ -0,0 +1,33 @@
import React, { forwardRef } from 'react';
import { Menu } from '@headlessui/react';
import styles from './styles.module.css';
import classNames from 'classnames';
interface Props {
children: React.ReactNode | ((opts: { active: boolean; disabled: boolean }) => JSX.Element);
danger?: boolean;
disabled?: boolean;
className?: string;
icon?: JSX.Element;
onClick?: (e: React.MouseEvent) => void;
}
const DropdownItem = forwardRef<HTMLAnchorElement, Props>(({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => {
return (
<Menu.Item disabled={disabled}>
{(args) => (
<a
ref={ref}
href={'#'}
className={classNames(styles.menu_item, { [styles.danger]: danger }, className)}
onClick={onClick}
>
{IconComponent}
{typeof children === 'function' ? children(args) : children}
</a>
)}
</Menu.Item>
);
});
export default DropdownItem;

View file

@ -0,0 +1,2 @@
export { default as Dropdown } from './Dropdown';
export * as styles from './styles.module.css';

View file

@ -0,0 +1,54 @@
.menu {
@apply relative inline-block text-left;
& .button {
@apply inline-flex justify-center items-center w-full py-2 text-neutral-100 rounded-md;
@apply transition-all duration-100;
&:hover, &[aria-expanded="true"] {
@apply bg-neutral-600 text-white;
}
&:focus, &:focus-within, &:active {
@apply ring-2 ring-opacity-50 ring-neutral-300 text-white;
}
& svg {
@apply w-5 h-5 transition-transform duration-75;
}
&[aria-expanded="true"] svg[data-animated="true"] {
@apply rotate-180;
}
}
& .items_container {
@apply absolute right-0 mt-2 origin-top-right bg-neutral-900 rounded z-10;
}
}
.menu_item {
@apply flex items-center rounded w-full px-2 py-2;
& svg {
@apply w-4 h-4 mr-4 text-neutral-300;
}
&:hover, &:focus, &:focus-within {
@apply bg-primary-600 text-primary-50;
& svg {
@apply text-primary-50;
}
}
&.danger {
&:hover, &:focus, &:focus-within {
@apply bg-red-500 text-red-50;
& svg {
@apply text-red-50;
}
}
}
}

View file

@ -1,3 +1,4 @@
declare module '*.jpg';
declare module '*.png';
declare module '*.svg';
declare module '*.css';

View file

@ -36,6 +36,11 @@ module.exports = {
{
loader: 'css-loader',
options: {
modules: {
auto: true,
localIdentName: isProduction ? '[name]_[hash:base64:8]' : '[path][name]__[local]',
localIdentContext: path.join(__dirname, "resources/scripts/components"),
},
sourceMap: !isProduction,
importLoaders: 1,
},

View file

@ -4650,6 +4650,13 @@ __metadata:
languageName: node
linkType: hard
"classnames@npm:^2.3.1":
version: 2.3.1
resolution: "classnames@npm:2.3.1"
checksum: 14db8889d56c267a591f08b0834989fe542d47fac659af5a539e110cc4266694e8de86e4e3bbd271157dbd831361310a8293e0167141e80b0f03a0f175c80960
languageName: node
linkType: hard
"clean-set@npm:^1.1.1":
version: 1.1.2
resolution: "clean-set@npm:1.1.2"
@ -10976,6 +10983,7 @@ fsevents@^1.2.7:
babel-plugin-styled-components: ^2.0.3
browserslist: ^4.17.6
chart.js: ^2.9.4
classnames: ^2.3.1
cross-env: ^7.0.3
css-loader: ^5.2.7
date-fns: ^2.25.0