Add basic dropdown styling using headless ui
This commit is contained in:
parent
eb56be8021
commit
f4119df0aa
10 changed files with 204 additions and 39 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
62
resources/scripts/components/elements/dropdown/Dropdown.tsx
Normal file
62
resources/scripts/components/elements/dropdown/Dropdown.tsx
Normal 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 };
|
|
@ -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;
|
2
resources/scripts/components/elements/dropdown/index.ts
Normal file
2
resources/scripts/components/elements/dropdown/index.ts
Normal file
|
@ -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
resources/scripts/globals.d.ts
vendored
1
resources/scripts/globals.d.ts
vendored
|
@ -1,3 +1,4 @@
|
|||
declare module '*.jpg';
|
||||
declare module '*.png';
|
||||
declare module '*.svg';
|
||||
declare module '*.css';
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue