diff --git a/apps/web/content/docs/components/bottom-navigation.mdx b/apps/web/content/docs/components/bottom-navigation.mdx new file mode 100644 index 000000000..017fe36c9 --- /dev/null +++ b/apps/web/content/docs/components/bottom-navigation.mdx @@ -0,0 +1,36 @@ +--- +title: React Bottom Navigation - Flowbite +description: Use the bottom navigation bar component to allow users to navigate through your website or create a control bar using a menu that is positioned at the bottom of the page +--- + +The bottom bar component can be used to allow menu items and certain control actions to be performed by the user through the use of a fixed bar positioning on the bottom side of your page. + +Check out multiple examples of the bottom navigation component based on various styles, controls, sizes, content and leverage the utility classes from Tailwind CSS to integrate into your own project. + +## Default bottom navigation + +Use the default bottom navigation bar example to show a list of menu items as buttons to allow the user to navigate through your website based on a fixed position. You can also use anchor tags instead of buttons. + + + +## Menu items with border + +This example can be used to show a border between the menu items inside the bottom navigation bar. + + + +## Example with Tooltip + +This example can be used to show a Tooltip on the hover on the menu items. + + + +## Theme + +To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme). + + + +## References + +- [Flowbite Bottom Navigation](https://flowbite.com/docs/components/bottom-navigation/) diff --git a/apps/web/data/components.tsx b/apps/web/data/components.tsx index 29e7e8619..60e64a825 100644 --- a/apps/web/data/components.tsx +++ b/apps/web/data/components.tsx @@ -35,6 +35,13 @@ export const COMPONENTS_DATA: Component[] = [ link: "/docs/components/badge", classes: "w-28", }, + { + name: "Bottom Navigation", + image: "/images/components/bottom-bar.svg", + imageDark: "/images/components/bottom-bar-dark.svg", + link: `/docs/components/bottom-navigation`, + classes: "w-28", + }, { name: "Breadcrumbs", image: "/images/components/breadcrumbs.svg", diff --git a/apps/web/data/docs-sidebar.ts b/apps/web/data/docs-sidebar.ts index 60c49560b..845bdf3ed 100644 --- a/apps/web/data/docs-sidebar.ts +++ b/apps/web/data/docs-sidebar.ts @@ -58,6 +58,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [ { title: "Avatar", href: "/docs/components/avatar" }, { title: "Badge", href: "/docs/components/badge" }, { title: "Banner", href: "/docs/components/banner", isNew: true }, + { title: "Bottom Navigation", href: "/docs/components/bottom-navigation", isNew: true }, { title: "Breadcrumb", href: "/docs/components/breadcrumb" }, { title: "Button", href: "/docs/components/button" }, { title: "Button group", href: "/docs/components/button-group" }, diff --git a/apps/web/examples/bottomNavigation/bottomNavigation.root.tsx b/apps/web/examples/bottomNavigation/bottomNavigation.root.tsx new file mode 100644 index 000000000..410f116e3 --- /dev/null +++ b/apps/web/examples/bottomNavigation/bottomNavigation.root.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { BottomNavigation } from "flowbite-react"; +import { AiFillHome } from "react-icons/ai"; +import { CgProfile } from "react-icons/cg"; +import { GiSettingsKnobs, GiWallet } from "react-icons/gi"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { BottomNavigation } from "flowbite-react"; + +function Component() { + return ( + + + + + + + ) +} +`; + +function Component() { + return ( +
+ + + + + + +
+ ); +} + +export const root: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "bottomNavigation/bottomNavigation.root.tsx", + component: , +}; diff --git a/apps/web/examples/bottomNavigation/bottomNavigation.withBorder.tsx b/apps/web/examples/bottomNavigation/bottomNavigation.withBorder.tsx new file mode 100644 index 000000000..2123a7dc0 --- /dev/null +++ b/apps/web/examples/bottomNavigation/bottomNavigation.withBorder.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { BottomNavigation } from "flowbite-react"; +import { AiFillHome } from "react-icons/ai"; +import { CgProfile } from "react-icons/cg"; +import { GiSettingsKnobs, GiWallet } from "react-icons/gi"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { BottomNavigation } from "flowbite-react"; + +function Component() { + return ( + + + + + + + ) +} +`; + +function Component() { + return ( +
+ + + + + + +
+ ); +} + +export const withBorder: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "bottomNavigation/bottomNavigation.withBorder.tsx", + component: , +}; diff --git a/apps/web/examples/bottomNavigation/bottomNavigation.withTooltip.tsx b/apps/web/examples/bottomNavigation/bottomNavigation.withTooltip.tsx new file mode 100644 index 000000000..814e73cab --- /dev/null +++ b/apps/web/examples/bottomNavigation/bottomNavigation.withTooltip.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { BottomNavigation } from "flowbite-react"; +import { AiFillHome } from "react-icons/ai"; +import { CgProfile } from "react-icons/cg"; +import { GiSettingsKnobs, GiWallet } from "react-icons/gi"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { BottomNavigation } from "flowbite-react"; + +function Component() { + return ( + + + + + + + ) +} +`; + +function Component() { + return ( +
+ + + + + + +
+ ); +} + +export const withTooltip: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + ], + githubSlug: "bottomNavigation/bottomNavigation.withTooltip.tsx", + component: , +}; diff --git a/apps/web/examples/bottomNavigation/index.ts b/apps/web/examples/bottomNavigation/index.ts new file mode 100644 index 000000000..90c719c40 --- /dev/null +++ b/apps/web/examples/bottomNavigation/index.ts @@ -0,0 +1,3 @@ +export { root } from "./bottomNavigation.root"; +export { withBorder } from "./bottomNavigation.withBorder"; +export { withTooltip } from "./bottomNavigation.withTooltip"; diff --git a/apps/web/examples/index.ts b/apps/web/examples/index.ts index 7992d8e29..9ab67830f 100644 --- a/apps/web/examples/index.ts +++ b/apps/web/examples/index.ts @@ -4,6 +4,7 @@ export * as avatar from "./avatar"; export * as badge from "./badge"; export * as banner from "./banner"; export * as blockquote from "./blockquote"; +export * as bottomNavigation from "./bottomNavigation"; export * as breadcrumb from "./breadcrumb"; export * as button from "./button"; export * as buttonGroup from "./buttonGroup"; diff --git a/apps/web/public/images/components/bottom-bar-dark.svg b/apps/web/public/images/components/bottom-bar-dark.svg new file mode 100644 index 000000000..0a0f67895 --- /dev/null +++ b/apps/web/public/images/components/bottom-bar-dark.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/images/components/bottom-bar.svg b/apps/web/public/images/components/bottom-bar.svg new file mode 100644 index 000000000..9e0dfc0bb --- /dev/null +++ b/apps/web/public/images/components/bottom-bar.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/components/BottomNavigation/BottomNavigation.spec.tsx b/packages/ui/src/components/BottomNavigation/BottomNavigation.spec.tsx new file mode 100644 index 000000000..7438925ba --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/BottomNavigation.spec.tsx @@ -0,0 +1,134 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { BottomNavigation } from "./BottomNavigation"; + +describe.concurrent("BottomNavigation", () => { + it('BottomNavigation should have "data-testid=flowbite-bottom-navigation" in the document', async () => { + render(); + + bottomNavTestId().forEach((bottomNavTestId) => { + expect(bottomNavTestId).toBeInTheDocument(); + }); + }); + + it('BottomNavigation.Item should have "data-testid=flowbite-bottom-nav-item" in the document', async () => { + render(); + + bottomNavItemTestId().forEach((bottomNavItemTest) => { + expect(bottomNavItemTest).toBeInTheDocument(); + }); + }); + + it('BottomNavigation.Item should have "button" in the document', async () => { + render(); + + bottomNavItemButton().forEach((button) => { + expect(button).toBeInTheDocument(); + }); + }); + + it('BottomNavigation.Item should have "Home" in the document', async () => { + render(); + + homeTextInButton().forEach((homeText) => { + expect(homeText).toBeInTheDocument(); + }); + }); + + it('should have "attribute of type = button" in the document', async () => { + render(); + + bottomNavItemTestId().forEach((buttonType) => { + expect(buttonType).toHaveAttribute("type", "button"); + }); + }); + + it("tooltip in document", async () => { + render(); + + expect(tooltips()).toBeInTheDocument(); + }); + + it('svg icons in the document by "data-testid=flowbite-bottom-nav-icon"', async () => { + render(); + + imgByTestId().forEach((imgTestId) => { + expect(imgTestId).toBeInTheDocument(); + }); + }); + + it("first svg Icon in the Tooltip", async () => { + render(); + + expect(imgByTestId()[0]).toBeInTheDocument(); + }); + + it("second svg Icon in the Tooltip", async () => { + render(); + + expect(imgByTestId()[1]).toBeInTheDocument(); + }); + + it("third svg Icon outside of Tooltip", async () => { + render(); + + expect(imgByTestId()[2] as HTMLElement).toBeInTheDocument(); + }); + + it("all svg Icon should have className of w-5", async () => { + render(); + + imgByTestId().forEach((imgByTest) => { + expect(imgByTest).toHaveClass("w-5"); + }); + }); + + it('first svg element selector using "document.querySelector=svg"', async () => { + render(); + + const svgImg = document.querySelectorAll("svg")[0] as SVGElement; + + expect(svgImg).toBeInTheDocument(); + }); + + it('second svg element selector using "document.querySelector=svg"', async () => { + render(); + + const svgImg = document.querySelectorAll("svg")[1] as SVGElement; + + expect(svgImg).toBeInTheDocument(); + }); + + it('third svg element selector using "document.querySelector=svg"', async () => { + render(); + + const svgImg = document.querySelectorAll("svg")[2] as SVGElement; + + expect(svgImg).toBeInTheDocument(); + }); +}); + +const bottomNavTestId = () => screen.getAllByTestId("flowbite-bottom-navigation"); +const bottomNavItemTestId = () => screen.getAllByTestId("flowbite-bottom-nav-item"); +const bottomNavItemButton = () => screen.getAllByRole("button"); +const homeTextInButton = () => screen.getAllByText("Home"); +const tooltips = () => screen.getByTestId("flowbite-tooltip"); +const imgByTestId = () => screen.getAllByTestId("flowbite-bottom-nav-icon"); + +const TestBottomNavigation = (): JSX.Element => { + return ( +
+ + + + + + + + + + + +
+ ); +}; diff --git a/packages/ui/src/components/BottomNavigation/BottomNavigation.stories.tsx b/packages/ui/src/components/BottomNavigation/BottomNavigation.stories.tsx new file mode 100644 index 000000000..71fa1e496 --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/BottomNavigation.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryFn } from "@storybook/react"; +import { AiFillHome } from "react-icons/ai"; +import { GiWallet } from "react-icons/gi"; +import { BottomNavigation, type BottomNavigationProps } from "./BottomNavigation"; + +export default { + title: "Components/BottomNavigation", + component: BottomNavigation, +} as Meta; + +const Template: StoryFn = (args) => ( +
+ +
+); + +export const DefaultBottomNav = Template.bind({}); +DefaultBottomNav.storyName = "Default"; +DefaultBottomNav.args = { + children: ( + <> + + + + ), +}; + +export const WithBorder = Template.bind({}); +WithBorder.args = { + children: ( + <> + + + + ), + bordered: true, +}; + +export const WithTooltip = Template.bind({}); +WithTooltip.args = { + children: ( + <> + + + + ), +}; diff --git a/packages/ui/src/components/BottomNavigation/BottomNavigation.tsx b/packages/ui/src/components/BottomNavigation/BottomNavigation.tsx new file mode 100644 index 000000000..638f963c2 --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/BottomNavigation.tsx @@ -0,0 +1,45 @@ +"use client"; + +import type { ComponentProps, FC } from "react"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import type { FlowbiteBoolean } from "../Flowbite"; +import { BottomNavigationItem, type FlowbiteBottomNavigationItemTheme } from "./BottomNavigationItem"; + +export interface FlowbiteBottomNavigationTheme { + root: { + base: string; + inner: string; + }; + border: FlowbiteBoolean; + item: FlowbiteBottomNavigationItemTheme; +} + +export interface BottomNavigationProps extends ComponentProps<"div"> { + bordered?: boolean; + theme?: DeepPartial; +} + +const BottomNavigationComponent: FC = ({ + children, + bordered: isBordered = false, + theme: customTheme = {}, + className, + ...props +}) => { + const theme = mergeDeep(getTheme().bottomNavigation, customTheme); + + return ( +
+
{children}
+
+ ); +}; + +BottomNavigationComponent.displayName = "BottomNavigation"; + +export const BottomNavigation = Object.assign(BottomNavigationComponent, { + Item: BottomNavigationItem, +}); diff --git a/packages/ui/src/components/BottomNavigation/BottomNavigationItem.tsx b/packages/ui/src/components/BottomNavigation/BottomNavigationItem.tsx new file mode 100644 index 000000000..aa8bc4920 --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/BottomNavigationItem.tsx @@ -0,0 +1,104 @@ +"use client"; + +import type { ComponentProps, FC } from "react"; +import { useId } from "react"; +import { IoMdHome } from "react-icons/io"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import { Tooltip } from "../Tooltip"; + +export interface FlowbiteBottomNavigationItemTheme { + base: string; + icon: { + base: string; + }; + label: string; +} + +export interface BottomNavigationItemProps extends Omit, "ref">, Record { + icon?: FC>; + label: string; + theme?: DeepPartial; + showTooltip?: boolean; + showLabel?: boolean; +} + +interface BottomNavItemBtnProps { + reactId: string; + customClassName: string; + icon: FC>; + theme: DeepPartial; + label: string; + showLabel: boolean; +} + +const BottomNavItemBtn: FC = (props) => { + const { icon: Icon, customClassName = "", reactId, label, showLabel, theme: customTheme = {}, ...rest } = props; + + const theme = mergeDeep(getTheme().bottomNavigation.item, customTheme); + + return ( + + ); +}; + +export const BottomNavigationItem: FC = ({ + icon: Icon, + label, + theme: customTheme = {}, + showTooltip = false, + showLabel = true, + className, + ...props +}) => { + const id = useId(); + + return ( + <> + {showTooltip ? ( + + {label} + + } + placement="top" + > + + + ) : ( + + )} + + ); +}; + +BottomNavigationItem.displayName = "BottomNavigation.Item"; diff --git a/packages/ui/src/components/BottomNavigation/index.ts b/packages/ui/src/components/BottomNavigation/index.ts new file mode 100644 index 000000000..6c9e2daf0 --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/index.ts @@ -0,0 +1,5 @@ +export { BottomNavigation } from "./BottomNavigation"; +export type { FlowbiteBottomNavigationTheme, BottomNavigationProps } from "./BottomNavigation"; + +export { BottomNavigationItem } from "./BottomNavigationItem"; +export type { BottomNavigationItemProps, FlowbiteBottomNavigationItemTheme } from "./BottomNavigationItem"; diff --git a/packages/ui/src/components/BottomNavigation/theme.ts b/packages/ui/src/components/BottomNavigation/theme.ts new file mode 100644 index 000000000..136761ab8 --- /dev/null +++ b/packages/ui/src/components/BottomNavigation/theme.ts @@ -0,0 +1,19 @@ +import type { FlowbiteBottomNavigationTheme } from "./BottomNavigation"; + +export const bottomNavigationTheme: FlowbiteBottomNavigationTheme = { + root: { + base: "fixed bottom-0 left-0 z-50 w-full h-16 bg-white border-t border-gray-200 dark:bg-gray-700 dark:border-gray-600", + inner: "grid h-full max-w-lg grid-cols-4 mx-auto font-medium w-fit", + }, + border: { + on: "border-gray-200 [&_button]:border", + off: "", + }, + item: { + base: "inline-flex flex-col items-center justify-center px-5 py-3 hover:bg-gray-50 dark:hover:bg-gray-800 group", + icon: { + base: "w-5 h-5 mb-2 text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500", + }, + label: "text-sm text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500", + }, +}; diff --git a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts index a0ce75db5..0f197643a 100644 --- a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts +++ b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts @@ -4,6 +4,7 @@ import type { FlowbiteAlertTheme } from "../Alert"; import type { FlowbiteAvatarTheme } from "../Avatar"; import type { FlowbiteBadgeTheme } from "../Badge"; import type { FlowbiteBlockquoteTheme } from "../Blockquote"; +import type { FlowbiteBottomNavigationTheme } from "../BottomNavigation"; import type { FlowbiteBreadcrumbTheme } from "../Breadcrumb"; import type { FlowbiteButtonGroupTheme, FlowbiteButtonTheme } from "../Button"; import type { FlowbiteCardTheme } from "../Card"; @@ -48,6 +49,7 @@ export interface FlowbiteTheme { avatar: FlowbiteAvatarTheme; badge: FlowbiteBadgeTheme; blockquote: FlowbiteBlockquoteTheme; + bottomNavigation: FlowbiteBottomNavigationTheme; breadcrumb: FlowbiteBreadcrumbTheme; button: FlowbiteButtonTheme; buttonGroup: FlowbiteButtonGroupTheme; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d4ac43525..73cb39798 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -5,6 +5,7 @@ export * from "./components/Avatar"; export * from "./components/Badge"; export * from "./components/Banner"; export * from "./components/Blockquote"; +export * from "./components/BottomNavigation"; export * from "./components/Breadcrumb"; export * from "./components/Button"; export * from "./components/Card"; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index a5c64cb0b..3fa0b8251 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -4,6 +4,7 @@ import { alertTheme } from "./components/Alert/theme"; import { avatarTheme } from "./components/Avatar/theme"; import { badgeTheme } from "./components/Badge/theme"; import { blockquoteTheme } from "./components/Blockquote/theme"; +import { bottomNavigationTheme } from "./components/BottomNavigation/theme"; import { breadcrumbTheme } from "./components/Breadcrumb/theme"; import { buttonGroupTheme, buttonTheme } from "./components/Button/theme"; import { cardTheme } from "./components/Card/theme"; @@ -46,6 +47,7 @@ export const theme: FlowbiteTheme = { avatar: avatarTheme, badge: badgeTheme, blockquote: blockquoteTheme, + bottomNavigation: bottomNavigationTheme, breadcrumb: breadcrumbTheme, button: buttonTheme, buttonGroup: buttonGroupTheme,