Skip to content

Commit

Permalink
feat(components): add Clipboard (#1413)
Browse files Browse the repository at this point in the history
* feat(components): add Clipboard

* PR Build error fixed

* docs typo mistake

* docs typo mistake

* added missing use client in example

* export naming fix

* small change

* Example updated

* moved copyToClipboard function to helpers for the reusability (DRY)

* error handling added to helper function
  • Loading branch information
dhavalveera authored Jun 19, 2024
1 parent c8dba76 commit 26401bc
Show file tree
Hide file tree
Showing 18 changed files with 531 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-tools-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"flowbite-react": minor
---

feat(components): add "Clipboard"
42 changes: 42 additions & 0 deletions apps/web/content/docs/components/clipboard.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: React Clipboard - Flowbite
description: Use the clipboard component to copy text, data or lines of code to the clipboard with a single click based on various styles and examples coded with Tailwind CSS and Flowbite
---

The copy to clipboard component allows you to copy text, lines of code, contact details or any other data to the clipboard with a single click on a trigger element such as a button. This component can be used to copy text from an input field, textarea, code block or even address fields in a form element.

These components are built with Tailwind CSS and Flowbite React and can be found on the internet on websites such as Bitly, Cloudflare, Amazon AWS and almost all open-source projects and documentations.

Import the component from `flowbite-react` to use the clipboard element:

```jsx
import { Clipboard } from "flowbite-react";
```

## Default copy to clipboard

Use this example to copy the content of an input text field by clicking on a button and update the button text.

<Example name="clipboard.root" />

## Input with copy button

This example can be used to copy the content of an input field by clicking on a button with an icon positioned inside the form element and also show a tooltip with a message when the text has been copied.

<Example name="clipboard.withIcon" />

## Copy button with text

Use this example to show a copy button inside the input field with a text label and icon that updates to a success state when the text has been copied.

<Example name="clipboard.withIconText" />

## Theme

To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme).

<Theme name="clipboard" />

## References

- [Flowbite Datepicker](https://flowbite.com/docs/components/clipboard/)
1 change: 1 addition & 0 deletions apps/web/data/docs-sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [
{ title: "Button group", href: "/docs/components/button-group" },
{ title: "Card", href: "/docs/components/card" },
{ title: "Carousel", href: "/docs/components/carousel" },
{ title: "Clipboard", href: "/docs/components/clipboard", isNew: true },
{ title: "Datepicker", href: "/docs/components/datepicker", isNew: true },
{ title: "Drawer", href: "/docs/components/drawer", isNew: true },
{ title: "Dropdown", href: "/docs/components/dropdown" },
Expand Down
59 changes: 59 additions & 0 deletions apps/web/examples/clipboard/clipboard.root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { Clipboard } from "flowbite-react";
import type { CodeData } from "~/components/code-demo";

const code = `
"use client";
import { Clipboard } from "flowbite-react"
export function Component() {
return (
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input id="npm-install" type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
</div>
)
}
`;

export function Component() {
return (
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
</div>
);
}

export const root: CodeData = {
type: "single",
code: [
{
fileName: "client",
language: "tsx",
code,
},
],
githubSlug: "clipboard/clipboard.root.tsx",
component: <Component />,
};
65 changes: 65 additions & 0 deletions apps/web/examples/clipboard/clipboard.withIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { Clipboard } from "flowbite-react";
import type { CodeData } from "~/components/code-demo";

const code = `
"use client";
import { Clipboard } from "flowbite-react"
export function Component() {
return (
<div className="grid w-full max-w-64">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
</div>
</div>
)
}
`;

export function Component() {
return (
<div className="grid w-full max-w-64">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
</div>
</div>
);
}

export const withIcon: CodeData = {
type: "single",
code: [
{
fileName: "client",
language: "tsx",
code,
},
],
githubSlug: "clipboard/clipboard.withIcon.tsx",
component: <Component />,
};
65 changes: 65 additions & 0 deletions apps/web/examples/clipboard/clipboard.withIconText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { Clipboard } from "flowbite-react";
import type { CodeData } from "~/components/code-demo";

const code = `
"use client";
import { Clipboard } from "flowbite-react"
export function Component() {
return (
<div className="grid w-full max-w-80">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-4 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
</div>
</div>
)
}
`;

export function Component() {
return (
<div className="grid w-full max-w-80">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-4 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
</div>
</div>
);
}

export const withIconText: CodeData = {
type: "single",
code: [
{
fileName: "client",
language: "tsx",
code,
},
],
githubSlug: "clipboard/clipboard.withIconText.tsx",
component: <Component />,
};
3 changes: 3 additions & 0 deletions apps/web/examples/clipboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { root } from "./clipboard.root";
export { withIcon } from "./clipboard.withIcon";
export { withIconText } from "./clipboard.withIconText";
1 change: 1 addition & 0 deletions apps/web/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * as button from "./button";
export * as buttonGroup from "./buttonGroup";
export * as card from "./card";
export * as carousel from "./carousel";
export * as clipboard from "./clipboard";
export * as datepicker from "./datepicker";
export * as drawer from "./drawer";
export * as dropdown from "./dropdown";
Expand Down
72 changes: 72 additions & 0 deletions packages/ui/src/components/Clipboard/Clipboard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { Meta, StoryFn } from "@storybook/react";
// import { FaClipboardList } from "react-icons/fa6";
import type { ClipboardProps } from "./Clipboard";
import { Clipboard } from "./Clipboard";
import type { ClipboardWithIconProps } from "./ClipboardWithIcon";
import type { ClipboardWithIconTextProps } from "./ClipboardWithIconText";

export default {
title: "Components/Clipboard",
component: Clipboard,
} as Meta;

const DefaultTemplate: StoryFn<ClipboardProps> = () => (
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
</div>
);

export const Default = DefaultTemplate.bind({});

const CopyIconTemplate: StoryFn<ClipboardWithIconProps> = () => (
<div className="grid w-full max-w-64">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
</div>
</div>
);

export const CopyIcon = CopyIconTemplate.bind({});

const CopyIconTextTemplate: StoryFn<ClipboardWithIconTextProps> = () => (
<div className="grid w-full max-w-80">
<div className="relative">
<label htmlFor="npm-install" className="sr-only">
Label
</label>
<input
id="npm-install"
type="text"
className="col-span-6 block w-full rounded-lg border border-gray-300 bg-gray-50 px-2.5 py-4 text-sm text-gray-500 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400 dark:placeholder:text-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500"
value="npm install flowbite-react"
disabled
readOnly
/>
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
</div>
</div>
);

export const CopyIconText = CopyIconTextTemplate.bind({});
58 changes: 58 additions & 0 deletions packages/ui/src/components/Clipboard/Clipboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import { forwardRef, useState, type ComponentProps, type ReactNode } from "react";
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";
import { ClipboardWithIcon } from "./ClipboardWithIcon";
import type { FlowbiteClipboardWithIconTheme } from "./ClipboardWithIcon";
import { ClipboardWithIconText } from "./ClipboardWithIconText";
import type { FlowbiteClipboardWithIconTextTheme } from "./ClipboardWithIconText";
import { copyToClipboard } from "./helpers";

export interface FlowbiteClipboardTheme {
button: {
base: string;
label: string;
};
withIcon: FlowbiteClipboardWithIconTheme;
withIconText: FlowbiteClipboardWithIconTextTheme;
}

export interface ClipboardProps extends ComponentProps<"button"> {
valueToCopy: string;
label?: ReactNode;
theme?: DeepPartial<FlowbiteClipboardTheme>;
}

const ClipboardComponent = forwardRef<HTMLButtonElement, ClipboardProps>(
({ className, valueToCopy, label, theme: customTheme = {}, ...rest }, ref) => {
const [isJustCopied, setIsJustCopied] = useState(false);

const theme = mergeDeep(getTheme().clipboard.button, customTheme);

return (
<Tooltip content={isJustCopied ? "Copied" : "Copy to clipboard"} className="[&_*]:cursor-pointer">
<button
className={twMerge(theme.base, className)}
onClick={() => copyToClipboard(valueToCopy, setIsJustCopied)}
{...rest}
ref={ref}
>
<span className={theme.label}>{label}</span>
</button>
</Tooltip>
);
},
);

ClipboardComponent.displayName = "Clipboard";
ClipboardWithIcon.displayName = "Clipboard.WithIcon";
ClipboardWithIconText.displayName = "Clipboard.WithIconText";

export const Clipboard = Object.assign(ClipboardComponent, {
WithIcon: ClipboardWithIcon,
WithIconText: ClipboardWithIconText,
});
Loading

0 comments on commit 26401bc

Please sign in to comment.