From b6c6245da1f29107d45c2221284926f4580dae36 Mon Sep 17 00:00:00 2001 From: Michael Thomas Date: Tue, 30 Jul 2024 11:30:21 -0400 Subject: [PATCH] feat(dropdown): add new shared Dropdown component Adds a shared component for plain dropdown menus, based on the headlessui Menu component. Updates the `ButtonWithDropdown` component to use the same inner components, ensuring that the only difference between the two components is the trigger button, and both use the same components for the actual dropdown menu. --- .../Common/ButtonWithDropdown/index.tsx | 132 ++++------------- src/components/Common/Dropdown/index.tsx | 133 ++++++++++++++++++ 2 files changed, 159 insertions(+), 106 deletions(-) create mode 100644 src/components/Common/Dropdown/index.tsx diff --git a/src/components/Common/ButtonWithDropdown/index.tsx b/src/components/Common/ButtonWithDropdown/index.tsx index bf98cdae..36c79364 100644 --- a/src/components/Common/ButtonWithDropdown/index.tsx +++ b/src/components/Common/ButtonWithDropdown/index.tsx @@ -1,77 +1,29 @@ -import useClickOutside from '@app/hooks/useClickOutside'; +import Dropdown from '@app/components/Common/Dropdown'; import { withProperties } from '@app/utils/typeHelpers'; -import { Transition } from '@headlessui/react'; +import { Menu } from '@headlessui/react'; import { ChevronDownIcon } from '@heroicons/react/24/solid'; -import type { - AnchorHTMLAttributes, - ButtonHTMLAttributes, - RefObject, -} from 'react'; -import { Fragment, useRef, useState } from 'react'; +import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react'; -interface DropdownItemProps extends AnchorHTMLAttributes { - buttonType?: 'primary' | 'ghost'; -} - -const DropdownItem = ({ - children, - buttonType = 'primary', - ...props -}: DropdownItemProps) => { - let styleClass = 'button-md text-white'; - - switch (buttonType) { - case 'ghost': - styleClass += - ' bg-transparent rounded hover:bg-gradient-to-br from-indigo-600 to-purple-600 text-white focus:border-gray-500 focus:text-white'; - break; - default: - styleClass += - ' bg-indigo-600 rounded hover:bg-indigo-500 focus:border-indigo-700 focus:text-white'; - } - return ( - - {children} - - ); -}; - -interface ButtonWithDropdownProps { +type ButtonWithDropdownProps = { text: React.ReactNode; dropdownIcon?: React.ReactNode; buttonType?: 'primary' | 'ghost'; -} -interface ButtonProps - extends ButtonHTMLAttributes, - ButtonWithDropdownProps { - as?: 'button'; -} -interface AnchorProps - extends AnchorHTMLAttributes, - ButtonWithDropdownProps { - as: 'a'; -} +} & ( + | ({ as?: 'button' } & ButtonHTMLAttributes) + | ({ as: 'a' } & AnchorHTMLAttributes) +); const ButtonWithDropdown = ({ - as, text, children, dropdownIcon, className, buttonType = 'primary', ...props -}: ButtonProps | AnchorProps) => { - const [isOpen, setIsOpen] = useState(false); - const buttonRef = useRef(null); - useClickOutside(buttonRef, () => setIsOpen(false)); - +}: ButtonWithDropdownProps) => { const styleClasses = { mainButtonClasses: 'button-md text-white border', dropdownSideButtonClasses: 'button-md border', - dropdownClasses: 'button-md', }; switch (buttonType) { @@ -79,72 +31,40 @@ const ButtonWithDropdown = ({ styleClasses.mainButtonClasses += ' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100'; styleClasses.dropdownSideButtonClasses = styleClasses.mainButtonClasses; - styleClasses.dropdownClasses += - ' bg-gray-800 border border-gray-700 bg-opacity-80 p-1 backdrop-blur'; break; default: styleClasses.mainButtonClasses += ' bg-indigo-600 border-indigo-500 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue'; styleClasses.dropdownSideButtonClasses += ' bg-indigo-600 bg-opacity-80 border-indigo-500 hover:bg-opacity-100 active:bg-opacity-100 focus:ring-blue'; - styleClasses.dropdownClasses += ' bg-indigo-600 p-1'; } + const TriggerElement = props.as ?? 'button'; + return ( - - {as === 'a' ? ( - } - {...(props as AnchorHTMLAttributes)} - > - {text} - - ) : ( - - )} + + )} + > + {text} + {children && ( - - -
-
-
{children}
-
-
-
+ + {children}
)} - +
); }; -export default withProperties(ButtonWithDropdown, { Item: DropdownItem }); +export default withProperties(ButtonWithDropdown, { Item: Dropdown.Item }); diff --git a/src/components/Common/Dropdown/index.tsx b/src/components/Common/Dropdown/index.tsx new file mode 100644 index 00000000..2d052398 --- /dev/null +++ b/src/components/Common/Dropdown/index.tsx @@ -0,0 +1,133 @@ +import { withProperties } from '@app/utils/typeHelpers'; +import { Menu, Transition } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/solid'; +import { + Fragment, + useRef, + type AnchorHTMLAttributes, + type ButtonHTMLAttributes, + type HTMLAttributes, +} from 'react'; + +interface DropdownItemProps extends AnchorHTMLAttributes { + buttonType?: 'primary' | 'ghost'; +} + +const DropdownItem = ({ + children, + buttonType = 'primary', + ...props +}: DropdownItemProps) => { + let styleClass = 'button-md text-white'; + + switch (buttonType) { + case 'ghost': + styleClass += + ' bg-transparent rounded hover:bg-gradient-to-br from-indigo-600 to-purple-600 text-white focus:border-gray-500 focus:text-white'; + break; + default: + styleClass += + ' bg-indigo-600 rounded hover:bg-indigo-500 focus:border-indigo-700 focus:text-white'; + } + return ( + + + {children} + + + ); +}; + +type DropdownItemsProps = HTMLAttributes & { + dropdownType: 'primary' | 'ghost'; +}; + +const DropdownItems = ({ + children, + className, + dropdownType, + ...props +}: DropdownItemsProps) => { + let dropdownClasses: string; + + switch (dropdownType) { + case 'ghost': + dropdownClasses = + 'bg-gray-800 border border-gray-700 bg-opacity-80 p-1 backdrop-blur'; + break; + default: + dropdownClasses = 'bg-indigo-600 p-1'; + } + + return ( + + +
{children}
+
+
+ ); +}; + +interface DropdownProps extends ButtonHTMLAttributes { + text: React.ReactNode; + dropdownIcon?: React.ReactNode; + buttonType?: 'primary' | 'ghost'; +} + +const Dropdown = ({ + text, + children, + dropdownIcon, + className, + buttonType = 'primary', + ...props +}: DropdownProps) => { + const buttonRef = useRef(null); + let dropdownButtonClasses = 'button-md text-white border'; + + switch (buttonType) { + case 'ghost': + dropdownButtonClasses += + ' bg-transparent border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100'; + break; + default: + dropdownButtonClasses += + ' bg-indigo-600 border-indigo-500 bg-opacity-80 hover:bg-opacity-100 hover:border-indigo-500 active:bg-indigo-700 active:border-indigo-700 focus:ring-blue'; + } + + return ( + + + {text} + {children && (dropdownIcon ? dropdownIcon : )} + + {children && ( + {children} + )} + + ); +}; +export default withProperties(Dropdown, { + Item: DropdownItem, + Items: DropdownItems, +});