From 169b46e3bc59629a25f6badc1e6e8df0786b7ede Mon Sep 17 00:00:00 2001 From: Nasreddine Bac Ali Date: Tue, 15 Mar 2022 21:12:20 +0100 Subject: [PATCH] feat: add Dropdown component --- public/images/dropdown-dark.svg | 40 ++++++ public/images/dropdown-light.svg | 40 ++++++ src/Root.tsx | 35 +++-- src/components/Tooltip.tsx | 11 +- src/components/dropdown/Dropdown.tsx | 71 ++++++++++ src/components/dropdown/DropdownDivider.tsx | 3 + src/components/dropdown/DropdownHeader.tsx | 9 ++ src/components/dropdown/DropdownItem.tsx | 14 ++ src/components/index.ts | 1 + src/pages/DashboardPage.tsx | 6 + src/pages/DropdownPage.tsx | 140 ++++++++++++++++++++ 11 files changed, 354 insertions(+), 16 deletions(-) create mode 100644 public/images/dropdown-dark.svg create mode 100644 public/images/dropdown-light.svg create mode 100644 src/components/dropdown/Dropdown.tsx create mode 100644 src/components/dropdown/DropdownDivider.tsx create mode 100644 src/components/dropdown/DropdownHeader.tsx create mode 100644 src/components/dropdown/DropdownItem.tsx create mode 100644 src/pages/DropdownPage.tsx diff --git a/public/images/dropdown-dark.svg b/public/images/dropdown-dark.svg new file mode 100644 index 000000000..42212e175 --- /dev/null +++ b/public/images/dropdown-dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/dropdown-light.svg b/public/images/dropdown-light.svg new file mode 100644 index 000000000..0b8c159ec --- /dev/null +++ b/public/images/dropdown-light.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Root.tsx b/src/Root.tsx index 60b5609dc..543752208 100644 --- a/src/Root.tsx +++ b/src/Root.tsx @@ -1,6 +1,7 @@ -import { FC, lazy, Suspense, useState } from 'react'; +import { FC, Suspense, useState } from 'react'; import { HiAnnotation, + HiArrowCircleDown, HiBadgeCheck, HiBell, HiChevronDoubleRight, @@ -17,18 +18,19 @@ import { Route, Routes } from 'react-router-dom'; import { DarkThemeToggle, Navbar, Sidebar, SidebarItem, Spinner } from './components'; -const DashboardPage = lazy(() => import('./pages/DashboardPage')); -const AlertsPage = lazy(() => import('./pages/AlertsPage')); -const AccordionPage = lazy(() => import('./pages/AccordionPage')); -const BadgesPage = lazy(() => import('./pages/BadgesPage')); -const BreadcrumbPage = lazy(() => import('./pages/BreadcrumbPage')); -const ButtonsPage = lazy(() => import('./pages/ButtonsPage')); -const ButtonGroupPage = lazy(() => import('./pages/ButtonGroupPage')); -const CardPage = lazy(() => import('./pages/CardPage')); -const CarouselPage = lazy(() => import('./pages/CarouselPage')); -const ListGroupPage = lazy(() => import('./pages/ListGroupPage')); -const SpinnersPage = lazy(() => import('./pages/SpinnersPage')); -const TooltipsPage = lazy(() => import('./pages/TooltipsPage')); +import DashboardPage from './pages/DashboardPage'; +import AlertsPage from './pages/AlertsPage'; +import AccordionPage from './pages/AccordionPage'; +import BadgesPage from './pages/BadgesPage'; +import BreadcrumbPage from './pages/BreadcrumbPage'; +import ButtonsPage from './pages/ButtonsPage'; +import ButtonGroupPage from './pages/ButtonGroupPage'; +import CardPage from './pages/CardPage'; +import CarouselPage from './pages/CarouselPage'; +import DropdownPage from './pages/DropdownPage'; +import ListGroupPage from './pages/ListGroupPage'; +import SpinnersPage from './pages/SpinnersPage'; +import TooltipsPage from './pages/TooltipsPage'; export const Root: FC = () => { const [collapsed, setCollapsed] = useState(false); @@ -88,6 +90,12 @@ export const Root: FC = () => { title: 'Carousel', href: '/carousel', }, + { + group: false, + icon: HiArrowCircleDown, + title: 'Dropdown', + href: '/dropdown', + }, { group: false, icon: HiClipboardList, @@ -151,6 +159,7 @@ export const Root: FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/Tooltip.tsx b/src/components/Tooltip.tsx index 28faa75b5..7075e0683 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/Tooltip.tsx @@ -3,16 +3,18 @@ import { FC, ReactNode, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; export type TooltipProps = { + className?: string; content: ReactNode; placement?: Placement; trigger?: 'hover' | 'click'; - style?: 'dark' | 'light'; - animation?: false | `duration-${string}`; + style?: 'dark' | 'light' | 'auto'; + animation?: false | `duration-${number}`; arrow?: boolean; }; export const Tooltip: FC = ({ children, + className, content, placement = 'top', trigger = 'hover', @@ -47,7 +49,7 @@ export const Tooltip: FC = ({ popperInstance.current?.update(); }; - const hide = () => setVisible(false); + const hide = () => setTimeout(() => setVisible(false), 100); return ( <> @@ -59,7 +61,10 @@ export const Tooltip: FC = ({ 'invisible opacity-0': !visible, 'bg-gray-900 text-white dark:bg-gray-700': style === 'dark', 'border border-gray-200 bg-white text-gray-900': style === 'light', + 'border border-gray-200 bg-white text-gray-900 dark:border-none dark:bg-gray-700 dark:text-white': + style === 'auto', }, + className, )} ref={tooltipRef} role="tooltip" diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx new file mode 100644 index 000000000..57ed03c7e --- /dev/null +++ b/src/components/dropdown/Dropdown.tsx @@ -0,0 +1,71 @@ +import { ComponentProps, FC, PropsWithChildren, ReactNode, useMemo } from 'react'; +import { HiOutlineChevronDown, HiOutlineChevronLeft, HiOutlineChevronRight, HiOutlineChevronUp } from 'react-icons/hi'; +import classNames from 'classnames'; + +import { Button, ButtonProps } from '../Button'; +import { Tooltip, TooltipProps } from '../Tooltip'; +import { DropdownItem } from './DropdownItem'; +import { DropdownDivider } from './DropdownDivider'; +import { DropdownHeader } from './DropdownHeader'; + +export type DropdownProps = ButtonProps & + Omit & { + className?: string; + label: ReactNode; + inline?: boolean; + }; + +const icons: Record>> = { + top: HiOutlineChevronUp, + right: HiOutlineChevronRight, + bottom: HiOutlineChevronDown, + left: HiOutlineChevronLeft, +}; + +const DropdownComponent: FC = (props) => { + const { children, className, label, inline, ...restProps } = props; + const { + placement = inline ? 'bottom-start' : 'bottom', + arrow = false, + trigger = 'click', + ...buttonProps + } = restProps; + + const Icon = useMemo(() => { + const [p] = placement.split('-'); + + return icons[p] ?? HiOutlineChevronDown; + }, [placement]); + const content = useMemo(() =>
    {children}
, [children]); + + const TriggerWrapper = ({ children }: PropsWithChildren) => + inline ? : ; + + return ( + + + {label} + + + + ); +}; + +DropdownComponent.displayName = 'Dropdown'; +DropdownItem.displayName = 'Dropdown.Item'; +DropdownHeader.displayName = 'Dropdown.Header'; +DropdownDivider.displayName = 'Dropdown.Divider'; + +export const Dropdown = Object.assign(DropdownComponent, { + Item: DropdownItem, + Header: DropdownHeader, + Divider: DropdownDivider, +}); diff --git a/src/components/dropdown/DropdownDivider.tsx b/src/components/dropdown/DropdownDivider.tsx new file mode 100644 index 000000000..7d23a5ed8 --- /dev/null +++ b/src/components/dropdown/DropdownDivider.tsx @@ -0,0 +1,3 @@ +import { FC } from 'react'; + +export const DropdownDivider: FC = () =>
; diff --git a/src/components/dropdown/DropdownHeader.tsx b/src/components/dropdown/DropdownHeader.tsx new file mode 100644 index 000000000..48ffef9f3 --- /dev/null +++ b/src/components/dropdown/DropdownHeader.tsx @@ -0,0 +1,9 @@ +import { FC } from 'react'; +import { DropdownDivider } from './DropdownDivider'; + +export const DropdownHeader: FC = ({ children }) => ( + <> +
{children}
+ + +); diff --git a/src/components/dropdown/DropdownItem.tsx b/src/components/dropdown/DropdownItem.tsx new file mode 100644 index 000000000..d3a4c5942 --- /dev/null +++ b/src/components/dropdown/DropdownItem.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; + +export type DropdownItemProps = { + onClick?: () => void; +}; + +export const DropdownItem: FC = ({ children, onClick }) => ( +
  • + {children} +
  • +); diff --git a/src/components/index.ts b/src/components/index.ts index 5811d229b..5e7dd878c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,6 +6,7 @@ export * from './Button'; export * from './ButtonGroup'; export * from './Card'; export * from './Carousel'; +export * from './dropdown/Dropdown'; export * from './DarkThemeToggle'; export * from './Navbar'; export * from './Sidebar'; diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index bad0ea45e..5525d6143 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -55,6 +55,12 @@ const DashboardPage: FC = () => { className: 'w-36', images: { light: 'card-light.svg', dark: 'card-dark.svg' }, }, + { + title: 'Dropdown', + href: '/dropdown', + className: 'w-28', + images: { light: 'dropdown-light.svg', dark: 'dropdown-dark.svg' }, + }, { title: 'List group', href: '/list-group', diff --git a/src/pages/DropdownPage.tsx b/src/pages/DropdownPage.tsx new file mode 100644 index 000000000..1bebb1706 --- /dev/null +++ b/src/pages/DropdownPage.tsx @@ -0,0 +1,140 @@ +import { FC } from 'react'; + +import { CodeExample, DemoPage } from './DemoPage'; +import { Dropdown } from '../components'; + +const DropdownPage: FC = () => { + const examples: CodeExample[] = [ + { + title: 'Dropdown example', + code: ( + + Dashboard + Settings + Earnings + Sign out + + ), + }, + { + title: 'Dropdown divider', + code: ( + + Dashboard + Settings + Earnings + + Separated link + + ), + }, + { + title: 'Dropdown header', + code: ( + + + Bonnie Green + name@flowbite.com + + Dashboard + Settings + Earnings + + Sign out + + ), + }, + { + title: 'Inline dropdown', + code: ( + + Dashboard + Settings + Earnings + Sign out + + ), + }, + { + title: 'Dropdown item on click handler', + code: ( + + alert('Dashboard!')}>Dashboard + alert('Settings!')}>Settings + alert('Earnings!')}>Earnings + alert('Sign out!')}>Sign out + + ), + }, + { + title: 'Sizing', + code: ( +
    + + Dashboard + Settings + Earnings + Sign out + + + Dashboard + Settings + Earnings + Sign out + +
    + ), + }, + { + title: 'Placement', + code: ( +
    +
    + + Dashboard + Settings + Earnings + Sign out + + + Dashboard + Settings + Earnings + Sign out + + + Dashboard + Settings + Earnings + Sign out + + + Dashboard + Settings + Earnings + Sign out + +
    +
    + + Dashboard + Settings + Earnings + Sign out + + + Dashboard + Settings + Earnings + Sign out + +
    +
    + ), + }, + ]; + + return ; +}; + +export default DropdownPage;