From 005d78ac281ab14876b3b528cf3f51976d4fa349 Mon Sep 17 00:00:00 2001 From: Conner Davis Date: Mon, 20 Feb 2023 01:23:01 -0800 Subject: [PATCH] feat(theme): add `theme={}` attribute to components that need it (#611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(theme): adding theme prop to components * feat(/theme/default): finish migrating components to use a `root` theme Follow up to #500 BREAKING CHANGE: Like in #500, this version permanently changes the `FlowbiteTheme` for numerous components. The philosophy is that themes will more clearly reflect the component's structure. For example, an `` can contain any number of `` or `` sections. The theme used to look like: ```js accordion: { base: "..", content: "..", flush: "..", title: "..", } ``` And now, the theme for an `` looks like: ``` js accordion: { root: { base: "..", flush: "..", }, content: "..", title: "..", } ``` So now the options in the theme which apply to the `` itself will always be found under `root`. Likewise, `` can be themed via the `content` subsection. This ultimately will apply to all components. * ci(eslint): remove `prettier` plugins for `eslint` Instead, use `prettier-plugin-tailwindcss`, which is sufficient. * refactor(/lib/*): use `yarn prettier` with `prettier-plugin-tailwindcss` * fix(/lib/components/*.spec): resolve test errors caused by migrating theme * feat(/lib/components/*): add `theme={}` attribute to components that need it See notes in #566 * fix(/lib/components/accordion): fix `` to not use `

` You can cast the component to a component of your own, or a generic HTML element, e.g., ``. resolve #594 * fix(/lib/components/modal): fix `Modal` expects `document` to exist So, I originally fixed this issue across every component in #124, but the bug was reintroduced by resolve #609 * ci(.github/workflows/build): upgrade `codecov-action` -> `v3` --------- Co-authored-by: Ricardo Lüders --- .eslintignore | 6 - .eslintrc.js | 36 - .github/workflows/build.yaml | 3 +- package.json | 41 +- src/docs/pages/AlertsPage.tsx | 2 +- src/docs/pages/DropdownPage.tsx | 10 +- src/docs/pages/PaginationPage.tsx | 4 +- src/docs/pages/TabsPage.tsx | 6 +- src/docs/pages/ThemePage.tsx | 258 ++++++- src/index.tsx | 6 +- .../components/Accordion/Accordion.spec.tsx | 4 +- src/lib/components/Accordion/Accordion.tsx | 14 +- .../components/Accordion/AccordionContent.tsx | 4 +- .../components/Accordion/AccordionPanel.tsx | 2 +- .../components/Accordion/AccordionTitle.tsx | 9 +- src/lib/components/Alert/Alert.spec.tsx | 34 +- src/lib/components/Alert/Alert.tsx | 32 +- src/lib/components/Avatar/Avatar.tsx | 51 +- src/lib/components/Avatar/AvatarGroup.tsx | 10 +- .../components/Avatar/AvatarGroupCounter.tsx | 23 +- src/lib/components/Badge/Badge.tsx | 12 +- .../components/Breadcrumb/Breadcrumb.spec.tsx | 2 +- .../Breadcrumb/Breadcrumb.stories.tsx | 2 +- src/lib/components/Breadcrumb/Breadcrumb.tsx | 5 +- .../components/Breadcrumb/BreadcrumbItem.tsx | 13 +- src/lib/components/Button/Button.tsx | 46 +- src/lib/components/Button/ButtonGroup.tsx | 16 +- src/lib/components/Card/Card.stories.tsx | 2 +- src/lib/components/Card/Card.tsx | 21 +- src/lib/components/Carousel/Carousel.tsx | 111 +-- src/lib/components/Checkbox/Checkbox.tsx | 8 +- .../DarkThemeToggle/DarkThemeToggle.tsx | 23 +- src/lib/components/Dropdown/Dropdown.spec.tsx | 4 +- .../components/Dropdown/Dropdown.stories.tsx | 2 +- src/lib/components/Dropdown/Dropdown.tsx | 88 ++- .../components/Dropdown/DropdownDivider.tsx | 11 +- .../components/Dropdown/DropdownHeader.tsx | 10 +- src/lib/components/Dropdown/DropdownItem.tsx | 27 +- src/lib/components/FileInput/FileInput.tsx | 36 +- src/lib/components/Floating/Floating.tsx | 47 +- src/lib/components/Flowbite/Flowbite.spec.tsx | 2 +- src/lib/components/Flowbite/FlowbiteTheme.ts | 74 +- src/lib/components/Flowbite/ThemeContext.tsx | 29 +- src/lib/components/Footer/Footer.spec.tsx | 22 +- src/lib/components/Footer/Footer.stories.tsx | 12 +- src/lib/components/Footer/Footer.tsx | 61 +- src/lib/components/Footer/FooterBrand.tsx | 36 +- src/lib/components/Footer/FooterCopyright.tsx | 48 +- src/lib/components/Footer/FooterDivider.tsx | 16 +- src/lib/components/Footer/FooterIcon.tsx | 25 +- src/lib/components/Footer/FooterLink.tsx | 28 +- src/lib/components/Footer/FooterLinkGroup.tsx | 24 +- src/lib/components/Footer/FooterTitle.tsx | 24 +- src/lib/components/HelperText/HelperText.tsx | 21 +- src/lib/components/Label/Label.tsx | 22 +- .../components/ListGroup/ListGroup.spec.tsx | 29 +- src/lib/components/ListGroup/ListGroup.tsx | 26 +- .../components/ListGroup/ListGroupItem.tsx | 46 +- src/lib/components/Modal/Modal.stories.tsx | 8 +- src/lib/components/Modal/Modal.tsx | 74 +- src/lib/components/Modal/ModalBody.tsx | 15 +- src/lib/components/Modal/ModalFooter.tsx | 15 +- src/lib/components/Modal/ModalHeader.tsx | 31 +- src/lib/components/Navbar/Navbar.spec.tsx | 2 +- src/lib/components/Navbar/Navbar.stories.tsx | 14 +- src/lib/components/Navbar/Navbar.tsx | 24 +- src/lib/components/Navbar/NavbarBrand.tsx | 8 +- src/lib/components/Navbar/NavbarCollapse.tsx | 14 +- src/lib/components/Navbar/NavbarLink.tsx | 10 +- src/lib/components/Navbar/NavbarToggle.tsx | 11 +- .../Pagination/Pagination.stories.tsx | 7 +- src/lib/components/Pagination/Pagination.tsx | 108 +-- .../Pagination/PaginationButton.tsx | 30 +- .../components/Progress/Progress.stories.tsx | 2 +- src/lib/components/Progress/Progress.tsx | 16 +- src/lib/components/Radio/Radio.tsx | 11 +- .../RangeSlider/RangeSlider.spec.tsx | 8 +- .../RangeSlider/RangeSlider.stories.tsx | 2 +- .../components/RangeSlider/RangeSlider.tsx | 30 +- src/lib/components/Rating/Rating.tsx | 34 +- src/lib/components/Rating/RatingAdvanced.tsx | 25 +- src/lib/components/Rating/RatingContext.tsx | 4 +- src/lib/components/Rating/RatingStar.tsx | 30 +- src/lib/components/Select/Select.tsx | 35 +- src/lib/components/Sidebar/Sidebar.spec.tsx | 18 +- .../components/Sidebar/Sidebar.stories.tsx | 1 - src/lib/components/Sidebar/Sidebar.tsx | 69 +- src/lib/components/Sidebar/SidebarCTA.tsx | 26 +- .../components/Sidebar/SidebarCollapse.tsx | 40 +- src/lib/components/Sidebar/SidebarItem.tsx | 51 +- .../components/Sidebar/SidebarItemGroup.tsx | 6 +- src/lib/components/Sidebar/SidebarItems.tsx | 4 +- src/lib/components/Sidebar/SidebarLogo.tsx | 29 +- src/lib/components/Spinner/Spinner.tsx | 18 +- src/lib/components/Tab/TabItem.tsx | 9 +- src/lib/components/Tab/Tabs.spec.tsx | 10 +- src/lib/components/Tab/Tabs.tsx | 40 +- src/lib/components/Table/Table.spec.tsx | 10 +- src/lib/components/Table/Table.stories.tsx | 10 +- src/lib/components/Table/Table.tsx | 47 +- src/lib/components/Table/TableCell.tsx | 14 +- src/lib/components/Table/TableHead.tsx | 16 +- src/lib/components/Table/TableHeadCell.tsx | 15 +- src/lib/components/Table/TableRow.tsx | 17 +- src/lib/components/TextInput/TextInput.tsx | 12 +- src/lib/components/Textarea/Textarea.tsx | 12 +- src/lib/components/Timeline/Timeline.spec.tsx | 3 +- src/lib/components/Timeline/Timeline.tsx | 58 +- src/lib/components/Timeline/TimelineBody.tsx | 20 +- .../components/Timeline/TimelineContent.tsx | 33 +- src/lib/components/Timeline/TimelineItem.tsx | 27 +- src/lib/components/Timeline/TimelinePoint.tsx | 42 +- src/lib/components/Timeline/TimelineTime.tsx | 21 +- src/lib/components/Timeline/TimelineTitle.tsx | 29 +- src/lib/components/Toast/Toast.stories.tsx | 4 +- src/lib/components/Toast/Toast.tsx | 21 +- src/lib/components/Toast/ToastToggle.tsx | 31 +- .../ToggleSwitch/ToggleSwitch.spec.tsx | 3 +- .../ToggleSwitch/ToggleSwitch.stories.tsx | 4 +- .../components/ToggleSwitch/ToggleSwitch.tsx | 12 +- src/lib/components/Tooltip/Tooltip.tsx | 27 +- src/lib/helpers/mergeDeep.ts | 2 +- src/lib/hooks/useKeyDown.ts | 13 +- src/lib/theme/default.ts | 687 +++++++++--------- tsconfig.json | 5 +- yarn.lock | 42 +- 126 files changed, 2225 insertions(+), 1487 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 17b0db15e..000000000 --- a/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -*.md -.gitignore -build/ -coverage/ -lib/ -docs/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 4bbbf4249..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = { - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - 'plugin:react-hooks/recommended', - 'plugin:storybook/recommended', - 'plugin:tailwindcss/recommended', - ], - ignorePatterns: [ - '.eslintrc.js', - 'config-overrides.js', - 'lint-staged.js', - 'postcss.config.js', - 'tailwind.config.js', - 'commitlint.config.js', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - project: ['tsconfig.json', 'tsconfig.lib.json', 'cypress/tsconfig.json'], - }, - plugins: ['@typescript-eslint', 'prettier', 'react-hooks', 'storybook', 'tailwindcss'], - root: true, - rules: { - '@typescript-eslint/consistent-type-imports': 'warn', - 'tailwindcss/classnames-order': [ - 'warn', - { - officialSorting: true, - }, - ], - }, -}; diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ba3174870..26096ddfa 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -45,11 +45,10 @@ jobs: run: yarn build - name: 📊 Upload coverage to Codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: fail_ci_if_error: false files: coverage/coverage-final.json - token: ${{ secrets.CODECOV_TOKEN }} verbose: false - name: 📖 Build Storybook diff --git a/package.json b/package.json index 403d46e54..df1078c63 100644 --- a/package.json +++ b/package.json @@ -103,12 +103,9 @@ "cypress-axe": "^1.0.0", "cz-conventional-changelog": "3.3.0", "eslint": "^8.21.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.30.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-storybook": "^0.6.4", - "eslint-plugin-tailwindcss": "^3.6.0", "flowbite": "^1.5.4", "husky": "^8.0.1", "jsdom": "^20.0.1", @@ -116,6 +113,7 @@ "postcss": "^8.4.18", "prettier": "^2.7.1", "prettier-plugin-organize-imports": "^3.0.3", + "prettier-plugin-tailwindcss": "^0.2.2", "react": "^18.2.0", "react-app-rewired": "^2.2.1", "react-dom": "^18.2.0", @@ -165,5 +163,42 @@ "react": "^18.2.0", "react-dom": "^18.2.0" } + }, + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:storybook/recommended" + ], + "ignorePatterns": [ + "/build", + "/docs", + "/lib", + "commitlint.config.js", + "config-overrides.js", + "lint-staged.js", + "postcss.config.js", + "tailwind.config.js" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "project": [ + "tsconfig.json", + "cypress/tsconfig.json" + ] + }, + "plugins": [ + "@typescript-eslint", + "react-hooks", + "storybook" + ], + "root": true, + "rules": { + "@typescript-eslint/consistent-type-imports": "warn" + } } } diff --git a/src/docs/pages/AlertsPage.tsx b/src/docs/pages/AlertsPage.tsx index 7cf3768c9..ada3b77b3 100644 --- a/src/docs/pages/AlertsPage.tsx +++ b/src/docs/pages/AlertsPage.tsx @@ -36,7 +36,7 @@ const AlertsPage: FC = () => { { title: 'Rounded', code: ( - + {alertText} ), diff --git a/src/docs/pages/DropdownPage.tsx b/src/docs/pages/DropdownPage.tsx index 6b5412c21..fba055649 100644 --- a/src/docs/pages/DropdownPage.tsx +++ b/src/docs/pages/DropdownPage.tsx @@ -35,7 +35,7 @@ const DropdownPage: FC = () => { Bonnie Green - bonnie@flowbite.com + bonnie@flowbite.com Dashboard Settings @@ -62,7 +62,7 @@ const DropdownPage: FC = () => { Bonnie Green - bonnie@flowbite.com + bonnie@flowbite.com Dashboard Settings @@ -86,7 +86,7 @@ const DropdownPage: FC = () => { { title: 'Sizing', code: ( -
+
Dashboard Settings @@ -106,7 +106,7 @@ const DropdownPage: FC = () => { title: 'Placement', code: (
-
+
Dashboard Settings @@ -132,7 +132,7 @@ const DropdownPage: FC = () => { Sign out
-
+
Dashboard Settings diff --git a/src/docs/pages/PaginationPage.tsx b/src/docs/pages/PaginationPage.tsx index 7a12b8437..8f8d87c7c 100644 --- a/src/docs/pages/PaginationPage.tsx +++ b/src/docs/pages/PaginationPage.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react'; import { useState } from 'react'; -import { Pagination } from '../../lib/components/Pagination'; +import { Pagination } from '../../lib'; import type { CodeExample } from './DemoPage'; import { DemoPage } from './DemoPage'; -const PaginationPage: FC = (): JSX.Element => { +const PaginationPage: FC = () => { const [currentPage, setCurrentPage] = useState(1); const onPageChange = (page: number) => { diff --git a/src/docs/pages/TabsPage.tsx b/src/docs/pages/TabsPage.tsx index d933ef127..16ec605b6 100644 --- a/src/docs/pages/TabsPage.tsx +++ b/src/docs/pages/TabsPage.tsx @@ -1,7 +1,9 @@ -import { FC, useRef, useState } from 'react'; +import type { FC } from 'react'; +import { useRef, useState } from 'react'; import { HiAdjustments, HiClipboardList, HiUserCircle } from 'react-icons/hi'; import { MdDashboard } from 'react-icons/md'; -import { Button, Tabs, TabsRef } from '../../lib'; +import type { TabsRef } from '../../lib'; +import { Button, Tabs } from '../../lib'; import type { CodeExample } from './DemoPage'; import { DemoPage } from './DemoPage'; diff --git a/src/docs/pages/ThemePage.tsx b/src/docs/pages/ThemePage.tsx index 767f21f4f..c3c75d223 100644 --- a/src/docs/pages/ThemePage.tsx +++ b/src/docs/pages/ThemePage.tsx @@ -1,46 +1,215 @@ -import type { FC } from 'react'; +import type { FC, PropsWithChildren } from 'react'; import reactElementToJSXString from 'react-element-to-jsx-string'; +import { FaMinus, FaPlus } from 'react-icons/fa'; import { HiInformationCircle } from 'react-icons/hi'; import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { Alert, Card, DarkThemeToggle } from '../../lib'; -import { Flowbite } from '../../lib/components'; -import type { CustomFlowbiteTheme } from '../../lib/components/Flowbite/FlowbiteTheme'; +import { Flowbite, Table } from '../../lib/components'; const ThemePage: FC = () => { - const theme: CustomFlowbiteTheme = { alert: { root: { color: { info: 'bg-primary' } } } }; + return ( +
+

Theme

+ + This feature is highly experimental. In the future, it could be deprecated or even suffer several changes. + + + + +
+ ); +}; +const CustomizeFlowbiteComponentsSection: FC = () => { return ( -
-
- Theme -
- - This feature is highly experimental. In the future, it could be deprecated or even suffer several changes. - -

- Sometimes you want to give your web application a little more personality and customize some aspects of - Flowbite. This is possible thanks to the support we offer for themes. To use our theme support, your - application needs to be surrounded by the Flowbite component, and you must tell this component which theme - you want to load for your application. -

-
- - - {reactElementToJSXString(..., { - showFunctions: true, - functionValue: (fn) => fn.name, - sortProps: false, - useBooleanShorthandSyntax: false, - useFragmentShortSyntax: false, - })} - - -
- Switch to dark theme -

+

+
+

Customize Flowbite components using Tailwind CSS

+
+

+ You want to customize Flowbite. Specifically, you would like to remove/add Tailwind CSS classes to one or more + components. +

+

+ You have a few options. They each have benefits and drawbacks, and you can combine them how you want. +

+ + +
+ ); +}; + +const FlowbiteCustomizationOptionsTable: FC = () => { + return ( + + + Option + Example + + + + Custom theme + + + {`const theme: CustomFlowbiteTheme = { + accordion: { + root: { + base: 'bg-primary', + }, + }, +}; + +...`} + + + + + + Custom component with className={} + + + + {` + My accordion + Contains +`} + + + + + + Custom component with theme={} + + + + {`const accordionTheme: CustomFlowbiteTheme = { + accordion: { + root: { + base: 'bg-primary', + }, + }, +} + + + My accordion + Contains +`} + + + + +
+ ); +}; + +const BenefitsAndDrawbacks: FC = () => { + return ( +
+

+ Benefits & drawbacks of custom themes +

+
    + You can customize every component, one time, in one place + Changes will apply to every usage of the component in your app + + + You get the best performance + + See disclaimer + * + +  compared to other options + + + Customizations can quickly become complex and hard to maintain in one large JSON file +
+

+ Benefits & drawbacks of  + custom components with className={} +

+
    + You can customize with very little effort and code + You don't need to learn how to use the theme API + + + Some components have nested elements, and you can't customize all of them with one  + className + + + + You need to customize every usage of a component individually, or create and remember to use a custom + component of your own + +
+

+ Benefits & drawbacks of  + custom components with theme={} +

+
    + You can customize one usage of a component that has nested elements + + You can still create a custom component of your own to reuse the customizations rather than repeating them + + You add further complexity and indirection to your app + + + Your app will probably perform worse at scale + + See disclaimer + * + + + +
+

+ + Disclaimer: + * + +  We haven't tested performance at any scale. The theme={} attribute merges the + necessary part of the global theme with what is provided in the attribute, which is a deep object merge — + and it isn't fast. It is safe to assume that theme={} attribute will degrade + performance with enough components using that technique. It is safe to assume performance won't degrade + meaningfully at scale if you just use a global theme and/or className={} attributes. +

+
+ ); +}; + +const Benefit: FC = ({ children }) => { + return ( +
  • + + + Benefit: + + {children} +
  • + ); +}; + +const Drawback: FC = ({ children }) => { + return ( +
  • + + + Drawback: + + {children} +
  • + ); +}; + +const SwitchToDarkModeSection: FC = () => { + return ( +
    +
    +

    Switch to dark theme

    +
    +

    Since the Flowbite component creates and context to manage the theme, it also enables your application to use - the DarkThemeToggle component. + the <DarkThemeToggle/> component.

    @@ -58,20 +227,29 @@ const ThemePage: FC = () => { )} - Get the theme -

    - For more customizations, there is the possibility to get the theme with the useTheme hook and - its mode with the useThemeMode hook. +

    + ); +}; + +const ReadTheThemeSection: FC = () => { + return ( +
    +
    +

    Read the theme

    +
    +

    + You can obtain active Tailwind CSS Classes in the theme via useTheme as well as the status of + light/dark mode via useThemeMode.

    - const theme = useTheme().theme.button; + {`const theme = useTheme().theme.button; // -> { base: "..", color: { ... }, ... }`} - const [mode, setMode, toggleMode] = useThemeMode(usePreferences); + {`const [mode, setMode, toggleMode] = useThemeMode(usePreferences); // -> ["light", ..]`} -
    + ); }; diff --git a/src/index.tsx b/src/index.tsx index 34523d5ab..c449fed0e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,8 +10,10 @@ if (container) { const root = createRoot(container); const theme = { sidebar: { - base: 'h-full bg-inherit', - inner: 'h-full overflow-y-auto overflow-x-hidden rounded bg-inherit py-4 px-3', + root: { + base: 'h-full bg-inherit', + inner: 'h-full overflow-y-auto overflow-x-hidden rounded bg-inherit py-4 px-3', + }, }, }; diff --git a/src/lib/components/Accordion/Accordion.spec.tsx b/src/lib/components/Accordion/Accordion.spec.tsx index a3919d44c..ee123f0de 100644 --- a/src/lib/components/Accordion/Accordion.spec.tsx +++ b/src/lib/components/Accordion/Accordion.spec.tsx @@ -1,6 +1,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { FC } from 'react'; +import type { FC } from 'react'; import { HiOutlineArrowCircleDown } from 'react-icons/hi'; import { describe, expect, it } from 'vitest'; import { Flowbite } from '../Flowbite'; @@ -48,6 +48,7 @@ describe('Components / Accordion', () => { const user = userEvent.setup(); render(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _ of titles()) { await user.tab(); } @@ -61,6 +62,7 @@ describe('Components / Accordion', () => { const user = userEvent.setup(); render(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _ of titles()) { await user.tab(); } diff --git a/src/lib/components/Accordion/Accordion.tsx b/src/lib/components/Accordion/Accordion.tsx index a0d7f17d8..8a11fe5c8 100644 --- a/src/lib/components/Accordion/Accordion.tsx +++ b/src/lib/components/Accordion/Accordion.tsx @@ -2,14 +2,16 @@ import classNames from 'classnames'; import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react'; import { Children, cloneElement, useMemo, useState } from 'react'; import { HiChevronDown } from 'react-icons/hi'; -import { DeepPartial } from '..'; +import type { DeepPartial } from '..'; import { mergeDeep } from '../../helpers/mergeDeep'; -import { FlowbiteBoolean } from '../Flowbite/FlowbiteTheme'; +import type { FlowbiteBoolean } from '../Flowbite/FlowbiteTheme'; import { useTheme } from '../Flowbite/ThemeContext'; -import { AccordionContent, FlowbiteAccordionComponentTheme } from './AccordionContent'; +import type { FlowbiteAccordionComponentTheme } from './AccordionContent'; +import { AccordionContent } from './AccordionContent'; import type { AccordionPanelProps } from './AccordionPanel'; import { AccordionPanel } from './AccordionPanel'; -import { AccordionTitle, FlowbiteAccordionTitleTheme } from './AccordionTitle'; +import type { FlowbiteAccordionTitleTheme } from './AccordionTitle'; +import { AccordionTitle } from './AccordionTitle'; export interface FlowbiteAccordionTheme { root: FlowbiteAccordionRootTheme; @@ -28,7 +30,7 @@ export interface AccordionProps extends PropsWithChildren> children: ReactElement | ReactElement[]; flush?: boolean; collapseAll?: boolean; - theme?: DeepPartial; + theme?: DeepPartial; } const AccordionComponent: FC = ({ @@ -40,7 +42,7 @@ const AccordionComponent: FC = ({ className, theme: customTheme = {}, ...props -}): JSX.Element => { +}) => { const [isOpen, setOpen] = useState(collapseAll ? -1 : 0); const panels = useMemo( () => diff --git a/src/lib/components/Accordion/AccordionContent.tsx b/src/lib/components/Accordion/AccordionContent.tsx index 4b80c7e91..5e7339240 100644 --- a/src/lib/components/Accordion/AccordionContent.tsx +++ b/src/lib/components/Accordion/AccordionContent.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import type { ComponentProps, FC, PropsWithChildren } from 'react'; -import { DeepPartial } from '..'; +import type { DeepPartial } from '..'; import { mergeDeep } from '../../helpers/mergeDeep'; import { useTheme } from '../Flowbite/ThemeContext'; import { useAccordionContext } from './AccordionPanelContext'; @@ -18,7 +18,7 @@ export const AccordionContent: FC = ({ className, theme: customTheme = {}, ...props -}): JSX.Element => { +}) => { const { isOpen } = useAccordionContext(); const theme = mergeDeep(useTheme().theme.accordion.content, customTheme); diff --git a/src/lib/components/Accordion/AccordionPanel.tsx b/src/lib/components/Accordion/AccordionPanel.tsx index dde85b1e2..809afbf2a 100644 --- a/src/lib/components/Accordion/AccordionPanel.tsx +++ b/src/lib/components/Accordion/AccordionPanel.tsx @@ -8,7 +8,7 @@ export interface AccordionPanelProps extends PropsWithChildren { setOpen?: () => void; } -export const AccordionPanel: FC = ({ children, ...props }): JSX.Element => { +export const AccordionPanel: FC = ({ children, ...props }) => { const { alwaysOpen } = props; const [isOpen, setOpen] = useState(props.isOpen); diff --git a/src/lib/components/Accordion/AccordionTitle.tsx b/src/lib/components/Accordion/AccordionTitle.tsx index d89c223d6..6145bfb90 100644 --- a/src/lib/components/Accordion/AccordionTitle.tsx +++ b/src/lib/components/Accordion/AccordionTitle.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import type { ComponentProps, FC } from 'react'; -import { DeepPartial } from '..'; +import type { DeepPartial } from '..'; import { mergeDeep } from '../../helpers/mergeDeep'; import type { FlowbiteBoolean, FlowbiteHeadingLevel } from '../Flowbite/FlowbiteTheme'; import { useTheme } from '../Flowbite/ThemeContext'; @@ -9,10 +9,7 @@ import { useAccordionContext } from './AccordionPanelContext'; export interface FlowbiteAccordionTitleTheme { arrow: { base: string; - open: { - off: string; - on: string; - }; + open: FlowbiteBoolean; }; base: string; flush: FlowbiteBoolean; @@ -32,7 +29,7 @@ export const AccordionTitle: FC = ({ className, theme: customTheme = {}, ...props -}): JSX.Element => { +}) => { const { arrowIcon: ArrowIcon, flush, isOpen, setOpen } = useAccordionContext(); const onClick = () => typeof setOpen !== 'undefined' && setOpen(); diff --git a/src/lib/components/Alert/Alert.spec.tsx b/src/lib/components/Alert/Alert.spec.tsx index a195123ca..f91a31d54 100644 --- a/src/lib/components/Alert/Alert.spec.tsx +++ b/src/lib/components/Alert/Alert.spec.tsx @@ -1,11 +1,13 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { FC, useState } from 'react'; +import type { FC } from 'react'; +import { useState } from 'react'; import { HiEye, HiHeart, HiInformationCircle } from 'react-icons/hi'; import { describe, expect, it, vi } from 'vitest'; import { Flowbite } from '../Flowbite'; -import { Alert, AlertProps } from './Alert'; +import type { AlertProps } from './Alert'; +import { Alert } from './Alert'; describe.concurrent('Components / Alert', () => { describe.concurrent('A11y', () => { @@ -19,9 +21,7 @@ describe.concurrent('Components / Alert', () => { it('should use custom `base` classes', () => { const theme = { alert: { - root: { - base: 'text-purple-100', - }, + base: 'text-purple-100', }, }; render( @@ -36,9 +36,7 @@ describe.concurrent('Components / Alert', () => { it('should use custom `borderAccent` classes', () => { const theme = { alert: { - root: { - borderAccent: 'border-t-4 border-purple-500', - }, + borderAccent: 'border-t-4 border-purple-500', }, }; render( @@ -53,9 +51,7 @@ describe.concurrent('Components / Alert', () => { it('should use custom `wrapper` classes', () => { const theme = { alert: { - root: { - wrapper: 'flex items-center', - }, + wrapper: 'flex items-center', }, }; render( @@ -70,16 +66,14 @@ describe.concurrent('Components / Alert', () => { it('should use custom `color` classes', () => { const theme = { alert: { - root: { - color: { - info: 'text-purple-700 bg-purple-100 border-purple-500 dark:bg-purple-200 dark:text-purple-800', - }, - }, closeButton: { color: { info: 'text-purple-500 hover:bg-purple-200 dark:text-purple-600 dark:hover:text-purple-300', }, }, + color: { + info: 'text-purple-700 bg-purple-100 border-purple-500 dark:bg-purple-200 dark:text-purple-800', + }, }, }; render( @@ -99,9 +93,7 @@ describe.concurrent('Components / Alert', () => { it('should use custom `icon`', () => { const theme = { alert: { - root: { - icon: 'alert-custom-icon', - }, + icon: 'alert-custom-icon', }, }; render( @@ -116,9 +108,7 @@ describe.concurrent('Components / Alert', () => { it('should show custom `rounded` class', () => { const theme = { alert: { - root: { - rounded: 'rounded', - }, + rounded: 'rounded', }, }; render( diff --git a/src/lib/components/Alert/Alert.tsx b/src/lib/components/Alert/Alert.tsx index cf000602f..9897bc255 100644 --- a/src/lib/components/Alert/Alert.tsx +++ b/src/lib/components/Alert/Alert.tsx @@ -1,29 +1,25 @@ import classNames from 'classnames'; import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react'; import { HiX } from 'react-icons/hi'; -import { DeepPartial } from '..'; +import type { DeepPartial } from '..'; import { mergeDeep } from '../../helpers/mergeDeep'; import type { FlowbiteColors } from '../Flowbite/FlowbiteTheme'; import { useTheme } from '../Flowbite/ThemeContext'; export interface FlowbiteAlertTheme { - root: FlowbiteAlertRootTheme; - closeButton: FlowbiteAlertCloseButtonTheme; -} - -export interface FlowbiteAlertRootTheme { base: string; borderAccent: string; - wrapper: string; + closeButton: FlowbiteAlertCloseButtonTheme; color: AlertColors; icon: string; rounded: string; + wrapper: string; } export interface FlowbiteAlertCloseButtonTheme { base: string; - icon: string; color: AlertColors; + icon: string; } export interface AlertColors extends Pick { @@ -36,36 +32,38 @@ export interface AlertProps extends PropsWithChildren icon?: FC>; onDismiss?: boolean | (() => void); rounded?: boolean; - withBorderAccent?: boolean; theme?: DeepPartial; + withBorderAccent?: boolean; } export const Alert: FC = ({ additionalContent, children, + className, color = 'info', icon: Icon, onDismiss, rounded = true, - withBorderAccent, - className, theme: customTheme = {}, + withBorderAccent, + ...props }) => { const theme = mergeDeep(useTheme().theme.alert, customTheme); return (
    -
    - {Icon && } +
    + {Icon && }
    {children}
    {typeof onDismiss === 'function' && (
    )} {items && ( <> -
    +
    -
    +
    ); diff --git a/src/lib/components/Dropdown/Dropdown.spec.tsx b/src/lib/components/Dropdown/Dropdown.spec.tsx index 18131c42d..2b17dfa08 100644 --- a/src/lib/components/Dropdown/Dropdown.spec.tsx +++ b/src/lib/components/Dropdown/Dropdown.spec.tsx @@ -1,6 +1,6 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { FC } from 'react'; +import type { FC } from 'react'; import { describe, expect, it } from 'vitest'; import { Dropdown } from './Dropdown'; @@ -61,7 +61,7 @@ const TestDropdown: FC<{ dismissOnClick?: boolean }> = ({ dismissOnClick = true Bonnie Green - name@flowbite.com + name@flowbite.com Dashboard Settings diff --git a/src/lib/components/Dropdown/Dropdown.stories.tsx b/src/lib/components/Dropdown/Dropdown.stories.tsx index 81e0c33a3..4b4808a6c 100644 --- a/src/lib/components/Dropdown/Dropdown.stories.tsx +++ b/src/lib/components/Dropdown/Dropdown.stories.tsx @@ -48,7 +48,7 @@ WithHeader.args = { <> Bonnie Green - name@flowbite.com + name@flowbite.com Dashboard Settings diff --git a/src/lib/components/Dropdown/Dropdown.tsx b/src/lib/components/Dropdown/Dropdown.tsx index 11950c0e2..a059f55c8 100644 --- a/src/lib/components/Dropdown/Dropdown.tsx +++ b/src/lib/components/Dropdown/Dropdown.tsx @@ -1,23 +1,27 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import type { ComponentProps, FC, PropsWithChildren, ReactElement, ReactNode } from 'react'; -import React, { Children, useMemo, useState } from 'react'; +import React, { Children, useCallback, useMemo, useState } from 'react'; import { HiOutlineChevronDown, HiOutlineChevronLeft, HiOutlineChevronRight, HiOutlineChevronUp } from 'react-icons/hi'; +import type { DeepPartial } from '..'; +import { mergeDeep } from '../../helpers/mergeDeep'; import { uuid } from '../../helpers/uuid'; import type { ButtonProps } from '../Button'; import { Button } from '../Button'; import type { FloatingProps, FlowbiteFloatingTheme } from '../Floating'; import { Floating } from '../Floating'; import { useTheme } from '../Flowbite/ThemeContext'; +import type { FlowbiteDropdownDividerTheme } from './DropdownDivider'; import { DropdownDivider } from './DropdownDivider'; +import type { FlowbiteDropdownHeaderTheme } from './DropdownHeader'; import { DropdownHeader } from './DropdownHeader'; +import type { FlowbiteDropdownItemTheme } from './DropdownItem'; import { DropdownItem } from './DropdownItem'; -export interface FlowbiteDropdownFloatingTheme extends FlowbiteFloatingTheme { - header: string; - item: { - base: string; - icon: string; - }; - divider: string; +export interface FlowbiteDropdownFloatingTheme + extends FlowbiteFloatingTheme, + FlowbiteDropdownDividerTheme, + FlowbiteDropdownHeaderTheme { + item: FlowbiteDropdownItemTheme; } export interface FlowbiteDropdownTheme { @@ -27,12 +31,16 @@ export interface FlowbiteDropdownTheme { arrowIcon: string; } -export interface DropdownProps extends PropsWithChildren>, ButtonProps { - label: ReactNode; - inline?: boolean; - floatingArrow?: boolean; +export interface DropdownProps + extends PropsWithChildren, + Pick, + Omit { arrowIcon?: boolean; dismissOnClick?: boolean; + floatingArrow?: boolean; + inline?: boolean; + label: ReactNode; + theme?: DeepPartial; } const icons: Record>> = { @@ -42,9 +50,15 @@ const icons: Record>> = { left: HiOutlineChevronLeft, }; -const DropdownComponent: FC = ({ children, className, dismissOnClick = true, ...props }) => { - const theme = useTheme().theme.dropdown; - const theirProps = props as DropdownProps; +const DropdownComponent: FC = ({ + children, + className, + dismissOnClick = true, + theme: customTheme = {}, + ...props +}) => { + const theme = mergeDeep(useTheme().theme.dropdown, customTheme); + const theirProps = props as Omit; const { placement = props.inline ? 'bottom-start' : 'bottom', trigger = 'click', @@ -63,30 +77,35 @@ const DropdownComponent: FC = ({ children, className, dismissOnCl const [closeRequestKey, setCloseRequestKey] = useState(undefined); // Extends DropdownItem's onClick to trigger a close request to the Floating component - const attachCloseListener: any = (node: ReactNode) => { - if (!React.isValidElement(node)) return node; - if ((node as ReactElement).type === DropdownItem) - return React.cloneElement(node, { - onClick: () => { - node.props.onClick?.(); - dismissOnClick && setCloseRequestKey(uuid()); - }, - } as any); - if (node.props.children && typeof node.props.children === 'object') { - return React.cloneElement(node, { - // @ts-ignore - children: Children.map(node.props.children, attachCloseListener), - }); - } - return node; - }; + const attachCloseListener = useCallback( + // @ts-ignore TODO: Rewrite Dropdown + (node: ReactNode) => { + if (!React.isValidElement(node)) return node; + if ((node as ReactElement).type === DropdownItem) + return React.cloneElement(node, { + // @ts-ignore TODO: Rewrite Dropdown + onClick: () => { + node.props.onClick?.(); + dismissOnClick && setCloseRequestKey(uuid()); + }, + }); + if (node.props.children && typeof node.props.children === 'object') { + return React.cloneElement(node, { + // @ts-ignore TODO: Rewrite Dropdown + children: Children.map(node.props.children, attachCloseListener), + }); + } + return node; + }, + [dismissOnClick], + ); const content = useMemo( () =>
      {Children.map(children, attachCloseListener)}
    , - [children, theme], + [attachCloseListener, children, theme.content], ); - const TriggerWrapper: FC = ({ children }): JSX.Element => + const TriggerWrapper: FC = ({ children }) => inline ? : ; return ( @@ -109,7 +128,6 @@ const DropdownComponent: FC = ({ children, className, dismissOnCl ); }; -DropdownComponent.displayName = 'Dropdown'; DropdownItem.displayName = 'Dropdown.Item'; DropdownHeader.displayName = 'Dropdown.Header'; DropdownDivider.displayName = 'Dropdown.Divider'; diff --git a/src/lib/components/Dropdown/DropdownDivider.tsx b/src/lib/components/Dropdown/DropdownDivider.tsx index fcd48ca5a..25f442253 100644 --- a/src/lib/components/Dropdown/DropdownDivider.tsx +++ b/src/lib/components/Dropdown/DropdownDivider.tsx @@ -1,8 +1,13 @@ -import type { FC } from 'react'; +import classNames from 'classnames'; +import type { ComponentProps, FC } from 'react'; import { useTheme } from '../Flowbite/ThemeContext'; -export const DropdownDivider: FC = () => { +export interface FlowbiteDropdownDividerTheme { + divider: string; +} + +export const DropdownDivider: FC> = ({ className, ...props }) => { const theme = useTheme().theme.dropdown.floating.divider; - return
    ; + return
    ; }; diff --git a/src/lib/components/Dropdown/DropdownHeader.tsx b/src/lib/components/Dropdown/DropdownHeader.tsx index 520763348..59e60db44 100644 --- a/src/lib/components/Dropdown/DropdownHeader.tsx +++ b/src/lib/components/Dropdown/DropdownHeader.tsx @@ -3,11 +3,11 @@ import type { ComponentProps, FC, PropsWithChildren } from 'react'; import { useTheme } from '../Flowbite/ThemeContext'; import { DropdownDivider } from './DropdownDivider'; -export const DropdownHeader: FC>> = ({ - children, - className, - ...props -}): JSX.Element => { +export interface FlowbiteDropdownHeaderTheme { + header: string; +} + +export const DropdownHeader: FC> = ({ children, className, ...props }) => { const theme = useTheme().theme.dropdown.floating.header; return ( diff --git a/src/lib/components/Dropdown/DropdownItem.tsx b/src/lib/components/Dropdown/DropdownItem.tsx index 6fc60aeb0..9e8f982c1 100644 --- a/src/lib/components/Dropdown/DropdownItem.tsx +++ b/src/lib/components/Dropdown/DropdownItem.tsx @@ -1,17 +1,32 @@ import classNames from 'classnames'; import type { ComponentProps, FC, PropsWithChildren } from 'react'; +import type { DeepPartial } from '..'; +import { mergeDeep } from '../../helpers/mergeDeep'; import { useTheme } from '../Flowbite/ThemeContext'; -export type DropdownItemProps = PropsWithChildren> & { - onClick?: () => void; +export interface FlowbiteDropdownItemTheme { + base: string; + icon: string; +} + +export interface DropdownItemProps extends PropsWithChildren, ComponentProps<'li'> { icon?: FC>; -}; + onClick?: () => void; + theme?: DeepPartial; +} -export const DropdownItem: FC = ({ children, className, onClick, icon: Icon }) => { - const theme = useTheme().theme.dropdown.floating.item; +export const DropdownItem: FC = ({ + children, + className, + icon: Icon, + onClick, + theme: customTheme = {}, + ...props +}) => { + const theme = mergeDeep(useTheme().theme.dropdown.floating.item, customTheme); return ( -
  • +
  • {Icon && } {children}
  • diff --git a/src/lib/components/FileInput/FileInput.tsx b/src/lib/components/FileInput/FileInput.tsx index bcc9ec151..896031b33 100644 --- a/src/lib/components/FileInput/FileInput.tsx +++ b/src/lib/components/FileInput/FileInput.tsx @@ -1,38 +1,46 @@ import classNames from 'classnames'; import type { ComponentProps, ReactNode } from 'react'; import { forwardRef } from 'react'; -import { DeepPartial } from '..'; +import type { DeepPartial } from '..'; import { mergeDeep } from '../../helpers/mergeDeep'; import { useTheme } from '../Flowbite/ThemeContext'; import { HelperText } from '../HelperText'; import type { TextInputColors, TextInputSizes } from '../TextInput'; export interface FlowbiteFileInputTheme { + root: FlowbiteFileInputRootTheme; + field: FlowbiteFileInputFieldTheme; +} + +export interface FlowbiteFileInputRootTheme { base: string; - field: { - base: string; - input: { - base: string; - sizes: TextInputSizes; - colors: TextInputColors; - }; - }; +} + +export interface FlowbiteFileInputFieldTheme { + base: string; + input: FlowbiteFileInputFieldInputTheme; +} + +export interface FlowbiteFileInputFieldInputTheme { + base: string; + colors: TextInputColors; + sizes: TextInputSizes; } export interface FileInputProps extends Omit, 'type' | 'ref' | 'color'> { - sizing?: keyof TextInputSizes; - helperText?: ReactNode; color?: keyof TextInputColors; + helperText?: ReactNode; + sizing?: keyof TextInputSizes; theme?: DeepPartial; } export const FileInput = forwardRef( - ({ theme: customTheme = {}, sizing = 'md', helperText, color = 'gray', className, ...props }, ref) => { + ({ className, color = 'gray', helperText, sizing = 'md', theme: customTheme = {}, ...props }, ref) => { const theme = mergeDeep(useTheme().theme.fileInput, customTheme); return ( <> -
    +
    ( ); }, ); - -FileInput.displayName = 'FileInput'; diff --git a/src/lib/components/Floating/Floating.tsx b/src/lib/components/Floating/Floating.tsx index 65a13cced..050b45c6c 100644 --- a/src/lib/components/Floating/Floating.tsx +++ b/src/lib/components/Floating/Floating.tsx @@ -15,52 +15,54 @@ import { useEffect, useRef, useState } from 'react'; import { getArrowPlacement, getMiddleware, getPlacement } from '../../helpers/floating'; export interface FlowbiteFloatingTheme { - target: string; - base: string; + arrow: FlowbiteFloatingArrowTheme; animation: string; + base: string; + content: string; hidden: string; style: { + auto: string; dark: string; light: string; - auto: string; }; - content: string; - arrow: { - base: string; - style: { - dark: string; - light: string; - auto: string; - }; - placement: string; + target: string; +} + +export interface FlowbiteFloatingArrowTheme { + base: string; + placement: string; + style: { + dark: string; + light: string; + auto: string; }; } export interface FloatingProps extends PropsWithChildren, 'style'>> { - content: ReactNode; - theme: FlowbiteFloatingTheme; - placement?: 'auto' | Placement; - trigger?: 'hover' | 'click'; - style?: 'dark' | 'light' | 'auto'; animation?: false | `duration-${number}`; arrow?: boolean; closeRequestKey?: string; + content: ReactNode; + placement?: 'auto' | Placement; + style?: 'dark' | 'light' | 'auto'; + theme: FlowbiteFloatingTheme; + trigger?: 'hover' | 'click'; } /** * @see https://floating-ui.com/docs/react-dom-interactions */ export const Floating: FC = ({ - children, - content, - theme, animation = 'duration-300', arrow = true, + children, + className, + closeRequestKey, + content, placement = 'top', style = 'dark', + theme, trigger = 'hover', - closeRequestKey, - className, ...props }) => { const arrowRef = useRef(null); @@ -72,6 +74,7 @@ export const Floating: FC = ({ open, placement: getPlacement({ placement }), }); + const { context, floating, diff --git a/src/lib/components/Flowbite/Flowbite.spec.tsx b/src/lib/components/Flowbite/Flowbite.spec.tsx index 36f56d96c..3c3f91541 100644 --- a/src/lib/components/Flowbite/Flowbite.spec.tsx +++ b/src/lib/components/Flowbite/Flowbite.spec.tsx @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { mergeDeep } from '../../helpers/mergeDeep'; import defaultTheme from '../../theme/default'; import { Flowbite, useTheme } from '../Flowbite'; -import { ThemeContextProps } from './ThemeContext'; +import type { ThemeContextProps } from './ThemeContext'; afterEach(() => { localStorage.removeItem('theme'); diff --git a/src/lib/components/Flowbite/FlowbiteTheme.ts b/src/lib/components/Flowbite/FlowbiteTheme.ts index 2c16f05db..42bb2aca6 100644 --- a/src/lib/components/Flowbite/FlowbiteTheme.ts +++ b/src/lib/components/Flowbite/FlowbiteTheme.ts @@ -1,47 +1,45 @@ -import { DeepPartial } from '..'; -import { FlowbiteAccordionTheme } from '../Accordion'; -import { FlowbiteAlertTheme } from '../Alert'; -import { FlowbiteAvatarGroupCounterTheme, FlowbiteAvatarGroupTheme, FlowbiteAvatarTheme } from '../Avatar'; -import { FlowbiteBadgeTheme } from '../Badge'; -import { FlowbiteBreadcrumbTheme } from '../Breadcrumb'; -import { FlowbiteButtonGroupTheme, FlowbiteButtonTheme } from '../Button'; -import { FlowbiteCardTheme } from '../Card'; -import { FlowbiteCarouselTheme } from '../Carousel'; -import { FlowbiteCheckboxTheme } from '../Checkbox'; -import { FlowbiteDarkThemeToggleTheme } from '../DarkThemeToggle'; -import { FlowbiteDropdownTheme } from '../Dropdown'; -import { FlowbiteFileInputTheme } from '../FileInput'; -import { FlowbiteFooterTheme } from '../Footer'; -import { FlowbiteHelperTextTheme } from '../HelperText'; -import { FlowbiteLabelTheme } from '../Label'; -import { FlowbiteListGroupTheme } from '../ListGroup'; -import { FlowbiteModalTheme } from '../Modal'; -import { FlowbiteNavbarTheme } from '../Navbar'; -import { FlowbitePaginationTheme } from '../Pagination'; -import { FlowbiteProgressTheme } from '../Progress'; -import { FlowbiteRadioTheme } from '../Radio'; -import { FlowbiteRangeSliderTheme } from '../RangeSlider'; -import { FlowbiteRatingTheme } from '../Rating'; -import { FlowbiteSelectTheme } from '../Select'; -import { FlowbiteSidebarTheme } from '../Sidebar'; -import { FlowbiteSpinnerTheme } from '../Spinner'; -import { FlowbiteTabTheme } from '../Tab'; -import { FlowbiteTableTheme } from '../Table'; -import { FlowbiteTextareaTheme } from '../Textarea'; -import { FlowbiteTextInputTheme } from '../TextInput'; -import { FlowbiteTimelineTheme } from '../Timeline'; -import { FlowbiteToastTheme } from '../Toast'; -import { FlowbiteToggleSwitchTheme } from '../ToggleSwitch'; -import { FlowbiteTooltipTheme } from '../Tooltip'; +import type { DeepPartial } from '..'; +import type { FlowbiteAccordionTheme } from '../Accordion'; +import type { FlowbiteAlertTheme } from '../Alert'; +import type { FlowbiteAvatarTheme } from '../Avatar'; +import type { FlowbiteBadgeTheme } from '../Badge'; +import type { FlowbiteBreadcrumbTheme } from '../Breadcrumb'; +import type { FlowbiteButtonGroupTheme, FlowbiteButtonTheme } from '../Button'; +import type { FlowbiteCardTheme } from '../Card'; +import type { FlowbiteCarouselTheme } from '../Carousel'; +import type { FlowbiteCheckboxTheme } from '../Checkbox'; +import type { FlowbiteDarkThemeToggleTheme } from '../DarkThemeToggle'; +import type { FlowbiteDropdownTheme } from '../Dropdown'; +import type { FlowbiteFileInputTheme } from '../FileInput'; +import type { FlowbiteFooterTheme } from '../Footer'; +import type { FlowbiteHelperTextTheme } from '../HelperText'; +import type { FlowbiteLabelTheme } from '../Label'; +import type { FlowbiteListGroupTheme } from '../ListGroup'; +import type { FlowbiteModalTheme } from '../Modal'; +import type { FlowbiteNavbarTheme } from '../Navbar'; +import type { FlowbitePaginationTheme } from '../Pagination'; +import type { FlowbiteProgressTheme } from '../Progress'; +import type { FlowbiteRadioTheme } from '../Radio'; +import type { FlowbiteRangeSliderTheme } from '../RangeSlider'; +import type { FlowbiteRatingTheme } from '../Rating'; +import type { FlowbiteSelectTheme } from '../Select'; +import type { FlowbiteSidebarTheme } from '../Sidebar'; +import type { FlowbiteSpinnerTheme } from '../Spinner'; +import type { FlowbiteTabTheme } from '../Tab'; +import type { FlowbiteTableTheme } from '../Table'; +import type { FlowbiteTextareaTheme } from '../Textarea'; +import type { FlowbiteTextInputTheme } from '../TextInput'; +import type { FlowbiteTimelineTheme } from '../Timeline'; +import type { FlowbiteToastTheme } from '../Toast'; +import type { FlowbiteToggleSwitchTheme } from '../ToggleSwitch'; +import type { FlowbiteTooltipTheme } from '../Tooltip'; export type CustomFlowbiteTheme = DeepPartial; -export interface FlowbiteTheme extends Record { +export interface FlowbiteTheme { accordion: FlowbiteAccordionTheme; alert: FlowbiteAlertTheme; avatar: FlowbiteAvatarTheme; - avatarGroupCounter: FlowbiteAvatarGroupCounterTheme; - avatarGroup: FlowbiteAvatarGroupTheme; badge: FlowbiteBadgeTheme; breadcrumb: FlowbiteBreadcrumbTheme; button: FlowbiteButtonTheme; diff --git a/src/lib/components/Flowbite/ThemeContext.tsx b/src/lib/components/Flowbite/ThemeContext.tsx index 92d2a7744..ddcead480 100644 --- a/src/lib/components/Flowbite/ThemeContext.tsx +++ b/src/lib/components/Flowbite/ThemeContext.tsx @@ -1,6 +1,5 @@ -/* eslint-disable react-hooks/rules-of-hooks */ import type { FC, ReactNode } from 'react'; -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import windowExists from '../../helpers/window-exists'; import defaultTheme from '../../theme/default'; import type { FlowbiteTheme } from './FlowbiteTheme'; @@ -34,16 +33,15 @@ export const useThemeMode = ( usePreferences: boolean, ): [Mode, React.Dispatch>, () => void] => { const [mode, setModeState] = useState('light'); + const savePreference = (mode: Mode) => localStorage.setItem('theme', mode); - const getPreference = (): Mode => (localStorage.getItem('theme') as Mode) || getPrefersColorScheme(); const userPreferenceIsDark = () => window.matchMedia?.('(prefers-color-scheme: dark)').matches; - const getPrefersColorScheme = (): Mode => (userPreferenceIsDark() ? 'dark' : 'light'); const toggleMode = () => { const newMode = mode === 'dark' ? 'light' : 'dark'; setMode(newMode); setModeState(newMode); }; - const setMode = (mode: Mode) => { + const setMode = useCallback((mode: Mode) => { savePreference(mode); if (!windowExists()) { return; @@ -55,11 +53,22 @@ export const useThemeMode = ( } document.documentElement.classList.remove('dark'); - }; - if (usePreferences) { - useEffect(() => setModeState(getPreference()), []); - useEffect(() => setMode(mode), [mode]); - } + }, []); + + useEffect(() => { + if (usePreferences) { + const getPreference = (): Mode => (localStorage.getItem('theme') as Mode) || getPrefersColorScheme(); + const getPrefersColorScheme = (): Mode => (userPreferenceIsDark() ? 'dark' : 'light'); + + setModeState(getPreference()); + } + }, [usePreferences]); + + useEffect(() => { + if (usePreferences) { + setMode(mode); + } + }, [mode, setMode, usePreferences]); return [mode, setModeState, toggleMode]; }; diff --git a/src/lib/components/Footer/Footer.spec.tsx b/src/lib/components/Footer/Footer.spec.tsx index 965169ad6..120acfec6 100644 --- a/src/lib/components/Footer/Footer.spec.tsx +++ b/src/lib/components/Footer/Footer.spec.tsx @@ -1,5 +1,5 @@ import { cleanup, render, screen } from '@testing-library/react'; -import { FC } from 'react'; +import type { FC } from 'react'; import { BsDribbble, BsFacebook, BsGithub, BsInstagram, BsTwitter } from 'react-icons/bs'; import { describe, expect, it } from 'vitest'; import { Flowbite } from '../Flowbite'; @@ -63,7 +63,9 @@ describe('Components / Footer', () => { it('should use `base` classes', () => { const theme = { footer: { - base: 'text-gray-100', + root: { + base: 'text-gray-100', + }, }, }; render( @@ -78,7 +80,9 @@ describe('Components / Footer', () => { it('should use `bgDark` classes', () => { const theme = { footer: { - bgDark: 'text-gray-100', + root: { + bgDark: 'text-gray-100', + }, }, }; render( @@ -93,7 +97,9 @@ describe('Components / Footer', () => { it('should use `container` classes', () => { const theme = { footer: { - container: 'text-gray-100', + root: { + container: 'text-gray-100', + }, }, }; render( @@ -183,10 +189,6 @@ describe('Components / Footer', () => { }); }); - it('should use `divider` classes', () => {}); - - it('should use `groupLink` classes', () => {}); - describe('`Footer.Icon`', () => { it('should use `icon` classes', () => { const theme = { @@ -244,7 +246,7 @@ describe('Components / Footer', () => { const TestFooter: FC = () => (