Update react, add some V2 components for V1 usage

This commit is contained in:
DaneEveritt 2022-06-05 14:56:42 -04:00
parent 921da09a63
commit 1a5465dc34
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
21 changed files with 564 additions and 43 deletions

View file

@ -0,0 +1,62 @@
import React, { ElementType, forwardRef, useMemo } from 'react';
import { Menu, Transition } from '@headlessui/react';
import styles from './style.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/style.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,43 @@
import React, { forwardRef } from 'react';
import { Menu } from '@headlessui/react';
import styles from './style.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}>
{({ disabled, active }) => (
<a
ref={ref}
href={'#'}
className={classNames(styles.menu_item, {
[styles.danger]: danger,
[styles.disabled]: disabled,
}, className)}
onClick={onClick}
>
{IconComponent}
{typeof children === 'function' ? children({ disabled, active }) : children}
</a>
)}
</Menu.Item>
);
});
export default DropdownItem;

View file

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

View file

@ -0,0 +1,58 @@
.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 {
@apply bg-blue-500 text-blue-50;
& svg {
@apply text-blue-50;
}
}
&.danger {
&:hover, &:focus {
@apply bg-red-500 text-red-50;
& svg {
@apply text-red-50;
}
}
}
&.disabled {
@apply cursor-not-allowed hover:bg-neutral-800 opacity-30 focus:bg-transparent focus:hover:bg-neutral-800;
}
}