From f4119df0aa3b223030e98caf315210b2a4d687b4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Feb 2022 15:13:13 -0500 Subject: [PATCH] Add basic dropdown styling using headless ui --- package.json | 1 + .../admin/users/UsersContainerV2.tsx | 53 +++++----------- .../components/elements/dropdown/Dropdown.tsx | 62 +++++++++++++++++++ .../elements/dropdown/DropdownButton.tsx | 24 +++++++ .../elements/dropdown/DropdownItem.tsx | 33 ++++++++++ .../components/elements/dropdown/index.ts | 2 + .../elements/dropdown/styles.module.css | 54 ++++++++++++++++ resources/scripts/globals.d.ts | 1 + webpack.config.js | 5 ++ yarn.lock | 8 +++ 10 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 resources/scripts/components/elements/dropdown/Dropdown.tsx create mode 100644 resources/scripts/components/elements/dropdown/DropdownButton.tsx create mode 100644 resources/scripts/components/elements/dropdown/DropdownItem.tsx create mode 100644 resources/scripts/components/elements/dropdown/index.ts create mode 100644 resources/scripts/components/elements/dropdown/styles.module.css diff --git a/package.json b/package.json index ef66f6e0a..bdeea5211 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/scripts/components/admin/users/UsersContainerV2.tsx b/resources/scripts/components/admin/users/UsersContainerV2.tsx index 91d0fe5d0..4010d301f 100644 --- a/resources/scripts/components/admin/users/UsersContainerV2.tsx +++ b/resources/scripts/components/admin/users/UsersContainerV2.tsx @@ -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([]); @@ -20,8 +20,8 @@ const UsersContainerV2 = () => { }, []); return ( -
- +
+
))} diff --git a/resources/scripts/components/elements/dropdown/Dropdown.tsx b/resources/scripts/components/elements/dropdown/Dropdown.tsx new file mode 100644 index 000000000..8e43ccb7f --- /dev/null +++ b/resources/scripts/components/elements/dropdown/Dropdown.tsx @@ -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 }) => ( +
+); + +type TypedChild = (React.ReactChild | React.ReactFragment | React.ReactPortal) & { + type?: JSX.Element; +} + +const Dropdown = forwardRef(({ 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 component without a child .'); + } + + return ( + + {Button} + + +
+ {items} +
+
+
+
+ ); +}); + +const _Dropdown = Object.assign(Dropdown, { + Button: DropdownButton, + Item: DropdownItem, + Gap: DropdownGap, +}); + +export { _Dropdown as default }; diff --git a/resources/scripts/components/elements/dropdown/DropdownButton.tsx b/resources/scripts/components/elements/dropdown/DropdownButton.tsx new file mode 100644 index 000000000..87e3af288 --- /dev/null +++ b/resources/scripts/components/elements/dropdown/DropdownButton.tsx @@ -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) => ( + + {typeof children === 'string' ? + <> + {children} + + + : + children + } + +); diff --git a/resources/scripts/components/elements/dropdown/DropdownItem.tsx b/resources/scripts/components/elements/dropdown/DropdownItem.tsx new file mode 100644 index 000000000..ca1f39173 --- /dev/null +++ b/resources/scripts/components/elements/dropdown/DropdownItem.tsx @@ -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(({ disabled, danger, className, onClick, children, icon: IconComponent }, ref) => { + return ( + + {(args) => ( + + {IconComponent} + {typeof children === 'function' ? children(args) : children} + + )} + + ); +}); + +export default DropdownItem; diff --git a/resources/scripts/components/elements/dropdown/index.ts b/resources/scripts/components/elements/dropdown/index.ts new file mode 100644 index 000000000..97067f43e --- /dev/null +++ b/resources/scripts/components/elements/dropdown/index.ts @@ -0,0 +1,2 @@ +export { default as Dropdown } from './Dropdown'; +export * as styles from './styles.module.css'; diff --git a/resources/scripts/components/elements/dropdown/styles.module.css b/resources/scripts/components/elements/dropdown/styles.module.css new file mode 100644 index 000000000..f4025c90c --- /dev/null +++ b/resources/scripts/components/elements/dropdown/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; + } + } + } +} diff --git a/resources/scripts/globals.d.ts b/resources/scripts/globals.d.ts index bb38b7421..b08b72719 100644 --- a/resources/scripts/globals.d.ts +++ b/resources/scripts/globals.d.ts @@ -1,3 +1,4 @@ declare module '*.jpg'; declare module '*.png'; declare module '*.svg'; +declare module '*.css'; diff --git a/webpack.config.js b/webpack.config.js index a7d7effb2..22cd3c18d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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, }, diff --git a/yarn.lock b/yarn.lock index c5b13323a..d79ca6f68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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
@@ -57,41 +57,16 @@ const UsersContainerV2 = () => { - - - Options - - - -
- - {() => ( - - - Reset Password - - )} - - - {() => ( - Delete - )} - - - - Resend Invite - - -
-
-
+ + + + + }>Edit + }>Reset Password + }>Suspend + + } danger>Delete Account +