diff --git a/apps/web/content/docs/forms/phone-input.mdx b/apps/web/content/docs/forms/phone-input.mdx new file mode 100644 index 000000000..7e9677259 --- /dev/null +++ b/apps/web/content/docs/forms/phone-input.mdx @@ -0,0 +1,38 @@ +--- +title: React Phone Input - Flowbite +description: Use the phone number input component from Flowbite to set a phone number inside a form field and use sizes +--- + +The phone number input component from Flowbite React, leveraging the native type="tel" attribute, simplifies entering phone numbers in form fields. + +The examples are built with the utility classes from Tailwind CSS, and they are fully responsive, dark mode compatible, and support RTL layouts, making them suitable for any type of web project. + +To start using the component, make sure that you have imported it from Flowbite React: + +```jsx +import { PhoneInput } from "flowbite-react"; +``` + +## Default phone input + +Get started with the following phone input example with default type as `type="tel"`. + + + +## Example with Helper Text + + + +## Example with Icon on the right + + + +## Theme + +For detailed instructions on customizing component appearances, refer to the [Theme documentation](/docs/customize/theme). + + + +## References + +- [Flowbite Phone Input](https://flowbite.com/docs/forms/phone-input/) diff --git a/apps/web/data/docs-sidebar.ts b/apps/web/data/docs-sidebar.ts index 60c49560b..984e89fac 100644 --- a/apps/web/data/docs-sidebar.ts +++ b/apps/web/data/docs-sidebar.ts @@ -90,6 +90,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [ items: [ { title: "File Input", href: "/docs/forms/file-input" }, { title: "Floating Label", href: "/docs/forms/floating-label", isNew: true }, + { title: "Phone Input", href: "/docs/forms/phone-input", isNew: true }, ], }, { diff --git a/apps/web/examples/index.ts b/apps/web/examples/index.ts index 7992d8e29..c9202217d 100644 --- a/apps/web/examples/index.ts +++ b/apps/web/examples/index.ts @@ -21,6 +21,7 @@ export * as listGroup from "./listGroup"; export * as modal from "./modal"; export * as navbar from "./navbar"; export * as pagination from "./pagination"; +export * as phoneInput from "./phoneInput"; export * as popover from "./popover"; export * as progress from "./progress"; export * as rating from "./rating"; diff --git a/apps/web/examples/phoneInput/index.ts b/apps/web/examples/phoneInput/index.ts new file mode 100644 index 000000000..41b776d02 --- /dev/null +++ b/apps/web/examples/phoneInput/index.ts @@ -0,0 +1,3 @@ +export { root } from "./phoneInput.root"; +export { withHelperText } from "./phoneInput.withHelperText"; +export { withRightIcon } from "./phoneInput.withRightIcon"; diff --git a/apps/web/examples/phoneInput/phoneInput.root.tsx b/apps/web/examples/phoneInput/phoneInput.root.tsx new file mode 100644 index 000000000..d836c254a --- /dev/null +++ b/apps/web/examples/phoneInput/phoneInput.root.tsx @@ -0,0 +1,52 @@ +import { PhoneInput } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { PhoneInput } from "flowbite-react"; + +function Component() { + return ( +
+ + + ) +} +`; + +const codeRSC = ` +function Component() { + return ( +
+ + + ) +} +`; + +function Component() { + return ( +
+ + + ); +} + +export const root: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "/phoneInput/phoneInput.root.tsx", + component: , +}; diff --git a/apps/web/examples/phoneInput/phoneInput.withHelperText.tsx b/apps/web/examples/phoneInput/phoneInput.withHelperText.tsx new file mode 100644 index 000000000..2ebdc5200 --- /dev/null +++ b/apps/web/examples/phoneInput/phoneInput.withHelperText.tsx @@ -0,0 +1,57 @@ +import { PhoneInput } from "flowbite-react"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { PhoneInput } from "flowbite-react"; + +function Component() { + return ( +
+ + + ) +} +`; + +const codeRSC = ` +function Component() { + return ( +
+ + + ) +} +`; + +function Component() { + return ( +
+ + + ); +} + +export const withHelperText: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "/phoneInput/phoneInput.withHelperText.tsx", + component: , +}; diff --git a/apps/web/examples/phoneInput/phoneInput.withRightIcon.tsx b/apps/web/examples/phoneInput/phoneInput.withRightIcon.tsx new file mode 100644 index 000000000..ae864afd1 --- /dev/null +++ b/apps/web/examples/phoneInput/phoneInput.withRightIcon.tsx @@ -0,0 +1,59 @@ +import { PhoneInput } from "flowbite-react"; +import { FaPhoneAlt } from "react-icons/fa"; +import type { CodeData } from "~/components/code-demo"; + +const code = ` +"use client"; + +import { PhoneInput } from "flowbite-react"; + +function Component() { + return ( +
+ + + ) +} +`; + +const codeRSC = ` +function Component() { + return ( +
+ + + ) +} +`; + +function Component() { + return ( +
+ + + ); +} + +export const withRightIcon: CodeData = { + type: "single", + code: [ + { + fileName: "client", + language: "tsx", + code, + }, + { + fileName: "server", + language: "tsx", + code: codeRSC, + }, + ], + githubSlug: "/phoneInput/phoneInput.withRightIcon.tsx", + component: , +}; diff --git a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts index a0ce75db5..310a95785 100644 --- a/packages/ui/src/components/Flowbite/FlowbiteTheme.ts +++ b/packages/ui/src/components/Flowbite/FlowbiteTheme.ts @@ -23,6 +23,7 @@ import type { FlowbiteListGroupTheme } from "../ListGroup"; import type { FlowbiteModalTheme } from "../Modal"; import type { FlowbiteNavbarTheme } from "../Navbar"; import type { FlowbitePaginationTheme } from "../Pagination"; +import type { FlowbitePhoneInputTheme } from "../PhoneInput/PhoneInput"; import type { FlowbitePopoverTheme } from "../Popover"; import type { FlowbiteProgressTheme } from "../Progress"; import type { FlowbiteRadioTheme } from "../Radio"; @@ -68,6 +69,7 @@ export interface FlowbiteTheme { modal: FlowbiteModalTheme; navbar: FlowbiteNavbarTheme; pagination: FlowbitePaginationTheme; + phoneInput: FlowbitePhoneInputTheme; popover: FlowbitePopoverTheme; progress: FlowbiteProgressTheme; radio: FlowbiteRadioTheme; diff --git a/packages/ui/src/components/PhoneInput/PhoneInput.spec.tsx b/packages/ui/src/components/PhoneInput/PhoneInput.spec.tsx new file mode 100644 index 000000000..2d85df5e0 --- /dev/null +++ b/packages/ui/src/components/PhoneInput/PhoneInput.spec.tsx @@ -0,0 +1,30 @@ +import { render } from "@testing-library/react"; +import { FaPhoneAlt } from "react-icons/fa"; +import { describe, expect, it } from "vitest"; +import { PhoneInput } from "./PhoneInput"; + +describe.concurrent("Components / PhoneInput", () => { + describe.concurrent("A11y", () => { + it('should have `role="textbox"` by default', () => { + const textInput = render().getByRole("textbox"); + + expect(textInput).toBeInTheDocument(); + }); + + it("should have Left Icon", () => { + const leftPage = render().getAllByTestId("left-icon"); + + leftPage.forEach((leftIcon) => { + expect(leftIcon).toBeInTheDocument(); + }); + }); + + it("should have Right Icon", () => { + const rightPage = render().getAllByTestId("right-icon"); + + rightPage.forEach((rightIcon) => { + expect(rightIcon).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/packages/ui/src/components/PhoneInput/PhoneInput.stories.tsx b/packages/ui/src/components/PhoneInput/PhoneInput.stories.tsx new file mode 100644 index 000000000..03d4c677a --- /dev/null +++ b/packages/ui/src/components/PhoneInput/PhoneInput.stories.tsx @@ -0,0 +1,43 @@ +import type { Meta, StoryFn } from "@storybook/react"; +import { FaPhoneAlt } from "react-icons/fa"; +import type { PhoneInputProps } from "./PhoneInput"; +import { PhoneInput } from "./PhoneInput"; + +export default { + title: "Components/PhoneInput", + component: PhoneInput, +} as Meta; + +const Template: StoryFn = (args) => { + return ( +
+ + + ); +}; + +export const Default = Template.bind({}); +Default.storyName = "Phone Input"; +Default.args = { + placeholder: "123-456-7890", + pattern: "[0-9]{3}-[0-9]{3}-[0-9]{4}", + required: true, +}; + +export const WithRightIcon = Template.bind({}); +WithRightIcon.storyName = "Phone Input with Right Icon"; +WithRightIcon.args = { + placeholder: "123-456-7890", + pattern: "[0-9]{3}-[0-9]{3}-[0-9]{4}", + required: true, + rightIcon: FaPhoneAlt, +}; + +export const WithHelperText = Template.bind({}); +WithHelperText.storyName = "Phone Input with Helper Text"; +WithHelperText.args = { + placeholder: "123-456-7890", + pattern: "[0-9]{3}-[0-9]{3}-[0-9]{4}", + required: true, + helperText: "Select a phone number that matches the format.", +}; diff --git a/packages/ui/src/components/PhoneInput/PhoneInput.tsx b/packages/ui/src/components/PhoneInput/PhoneInput.tsx new file mode 100644 index 000000000..18158ba6d --- /dev/null +++ b/packages/ui/src/components/PhoneInput/PhoneInput.tsx @@ -0,0 +1,78 @@ +import { forwardRef, type ComponentProps, type FC, type ReactNode } from "react"; +import { FaPhoneAlt } from "react-icons/fa"; +import { twMerge } from "tailwind-merge"; +import { mergeDeep } from "../../helpers/merge-deep"; +import { getTheme } from "../../theme-store"; +import type { DeepPartial } from "../../types"; +import type { FlowbiteSizes } from "../Flowbite"; +import { HelperText } from "../HelperText"; + +export interface FlowbitePhoneInputTheme { + root: { + base: string; + input: { + base: string; + sizes: FlowbitePhoneInputSizes; + startIcon: { + base: string; + icon: string; + }; + rightIcon: { + base: string; + icon: string; + }; + }; + }; +} + +export interface FlowbitePhoneInputSizes extends Pick { + [key: string]: string; +} + +export interface PhoneInputProps extends Omit, "ref"> { + helperText?: ReactNode; + icon?: FC>; + rightIcon?: FC>; + sizing?: keyof FlowbitePhoneInputSizes; + theme?: DeepPartial; +} + +export const PhoneInput = forwardRef( + ( + { helperText, icon: LeftIcon, rightIcon: RightIcon, sizing = "md", theme: customTheme = {}, className, ...props }, + ref, + ) => { + const theme = mergeDeep(getTheme().phoneInput.root, customTheme); + + const StartIcon = LeftIcon ? LeftIcon : FaPhoneAlt; + + return ( + <> +
+
+ +
+
+ {RightIcon && ( +
+ +
+ )} +
+ +
+ {helperText && {helperText}} + + ); + }, +); + +PhoneInput.displayName = "PhoneInput"; diff --git a/packages/ui/src/components/PhoneInput/index.ts b/packages/ui/src/components/PhoneInput/index.ts new file mode 100644 index 000000000..66911750c --- /dev/null +++ b/packages/ui/src/components/PhoneInput/index.ts @@ -0,0 +1,2 @@ +export { PhoneInput } from "./PhoneInput"; +export type { FlowbitePhoneInputSizes, FlowbitePhoneInputTheme, PhoneInputProps } from "./PhoneInput"; diff --git a/packages/ui/src/components/PhoneInput/theme.ts b/packages/ui/src/components/PhoneInput/theme.ts new file mode 100644 index 000000000..a571cc544 --- /dev/null +++ b/packages/ui/src/components/PhoneInput/theme.ts @@ -0,0 +1,24 @@ +import { createTheme } from "../../helpers/create-theme"; +import type { FlowbitePhoneInputTheme } from "./PhoneInput"; + +export const phoneInputTheme: FlowbitePhoneInputTheme = createTheme({ + root: { + base: "relative", + input: { + base: "block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 ps-10 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500", + startIcon: { + base: "pointer-events-none absolute inset-y-0 start-0 top-0 flex items-center ps-3.5", + icon: "h-4 w-4 text-gray-500 dark:text-gray-400", + }, + rightIcon: { + base: "pointer-events-none absolute inset-y-0 end-3 top-0 flex items-center ps-3.5", + icon: "h-4 w-4 text-gray-500 dark:text-gray-400", + }, + sizes: { + sm: "p-2 sm:text-xs", + md: "p-2.5 text-sm", + lg: "p-4 sm:text-base", + }, + }, + }, +}); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index d4ac43525..06c6f0c95 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -25,6 +25,7 @@ export * from "./components/ListGroup"; export * from "./components/Modal"; export * from "./components/Navbar"; export * from "./components/Pagination"; +export * from "./components/PhoneInput"; export * from "./components/Popover"; export * from "./components/Progress"; export * from "./components/Radio"; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index a5c64cb0b..67d48c671 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -23,6 +23,7 @@ import { listGroupTheme } from "./components/ListGroup/theme"; import { modalTheme } from "./components/Modal/theme"; import { navbarTheme } from "./components/Navbar/theme"; import { paginationTheme } from "./components/Pagination/theme"; +import { phoneInputTheme } from "./components/PhoneInput/theme"; import { popoverTheme } from "./components/Popover/theme"; import { progressTheme } from "./components/Progress/theme"; import { radioTheme } from "./components/Radio/theme"; @@ -66,6 +67,7 @@ export const theme: FlowbiteTheme = { modal: modalTheme, navbar: navbarTheme, pagination: paginationTheme, + phoneInput: phoneInputTheme, popover: popoverTheme, progress: progressTheme, radio: radioTheme,