Add basic dropdown styling using headless ui
This commit is contained in:
parent
eb56be8021
commit
f4119df0aa
|
@ -47,6 +47,7 @@
|
||||||
"@hot-loader/react-dom": "^16.14.0",
|
"@hot-loader/react-dom": "^16.14.0",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React, { useEffect, useState } from 'react';
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
import { User } from '@/api/admin/user';
|
import { User } from '@/api/admin/user';
|
||||||
import { AdminTransformers } from '@/api/admin/transformers';
|
import { AdminTransformers } from '@/api/admin/transformers';
|
||||||
import { Menu } from '@headlessui/react';
|
import { Dropdown } from '@/components/elements/dropdown';
|
||||||
import { ChevronDownIcon, LockClosedIcon } from '@heroicons/react/solid';
|
import { DotsVerticalIcon, LockClosedIcon, PaperAirplaneIcon, PencilIcon, TrashIcon } from '@heroicons/react/solid';
|
||||||
|
|
||||||
const UsersContainerV2 = () => {
|
const UsersContainerV2 = () => {
|
||||||
const [ users, setUsers ] = useState<User[]>([]);
|
const [ users, setUsers ] = useState<User[]>([]);
|
||||||
|
@ -20,8 +20,8 @@ const UsersContainerV2 = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'overflow-hidden rounded bg-neutral-700'}>
|
<div className={'bg-neutral-700'}>
|
||||||
<table className={'min-w-full'}>
|
<table className={'min-w-full rounded'}>
|
||||||
<thead className={'bg-neutral-900'}>
|
<thead className={'bg-neutral-900'}>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope={'col'} className={'w-8'}/>
|
<th scope={'col'} className={'w-8'}/>
|
||||||
|
@ -57,41 +57,16 @@ const UsersContainerV2 = () => {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={'px-6 py-4 whitespace-nowrap'}>
|
<td className={'px-6 py-4 whitespace-nowrap'}>
|
||||||
<Menu as={'div'} className={'relative inline-block text-left'}>
|
<Dropdown>
|
||||||
<Menu.Button
|
<Dropdown.Button className={'px-2'}>
|
||||||
className={'inline-flex justify-center w-full px-4 py-2 font-medium text-white rounded-md'}
|
<DotsVerticalIcon />
|
||||||
>
|
</Dropdown.Button>
|
||||||
Options
|
<Dropdown.Item icon={<PencilIcon />}>Edit</Dropdown.Item>
|
||||||
<ChevronDownIcon
|
<Dropdown.Item icon={<PaperAirplaneIcon />}>Reset Password</Dropdown.Item>
|
||||||
aria-hidden={'true'}
|
<Dropdown.Item icon={<LockClosedIcon />}>Suspend</Dropdown.Item>
|
||||||
className={'w-5 h-5 -mr-1 ml-2 text-neutral-100'}
|
<Dropdown.Gap />
|
||||||
/>
|
<Dropdown.Item icon={<TrashIcon />} danger>Delete Account</Dropdown.Item>
|
||||||
</Menu.Button>
|
</Dropdown>
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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 };
|
|
@ -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>
|
||||||
|
);
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Dropdown } from './Dropdown';
|
||||||
|
export * as styles from './styles.module.css';
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
declare module '*.jpg';
|
declare module '*.jpg';
|
||||||
declare module '*.png';
|
declare module '*.png';
|
||||||
declare module '*.svg';
|
declare module '*.svg';
|
||||||
|
declare module '*.css';
|
||||||
|
|
|
@ -36,6 +36,11 @@ module.exports = {
|
||||||
{
|
{
|
||||||
loader: 'css-loader',
|
loader: 'css-loader',
|
||||||
options: {
|
options: {
|
||||||
|
modules: {
|
||||||
|
auto: true,
|
||||||
|
localIdentName: isProduction ? '[name]_[hash:base64:8]' : '[path][name]__[local]',
|
||||||
|
localIdentContext: path.join(__dirname, "resources/scripts/components"),
|
||||||
|
},
|
||||||
sourceMap: !isProduction,
|
sourceMap: !isProduction,
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -4650,6 +4650,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"clean-set@npm:^1.1.1":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "clean-set@npm: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
|
babel-plugin-styled-components: ^2.0.3
|
||||||
browserslist: ^4.17.6
|
browserslist: ^4.17.6
|
||||||
chart.js: ^2.9.4
|
chart.js: ^2.9.4
|
||||||
|
classnames: ^2.3.1
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
css-loader: ^5.2.7
|
css-loader: ^5.2.7
|
||||||
date-fns: ^2.25.0
|
date-fns: ^2.25.0
|
||||||
|
|
Loading…
Reference in New Issue