Skip to content

Commit 26401bc

Browse files
authored
feat(components): add Clipboard (#1413)
* 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
1 parent c8dba76 commit 26401bc

18 files changed

+531
-0
lines changed

.changeset/poor-tools-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"flowbite-react": minor
3+
---
4+
5+
feat(components): add "Clipboard"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
title: React Clipboard - Flowbite
3+
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
4+
---
5+
6+
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.
7+
8+
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.
9+
10+
Import the component from `flowbite-react` to use the clipboard element:
11+
12+
```jsx
13+
import { Clipboard } from "flowbite-react";
14+
```
15+
16+
## Default copy to clipboard
17+
18+
Use this example to copy the content of an input text field by clicking on a button and update the button text.
19+
20+
<Example name="clipboard.root" />
21+
22+
## Input with copy button
23+
24+
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.
25+
26+
<Example name="clipboard.withIcon" />
27+
28+
## Copy button with text
29+
30+
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.
31+
32+
<Example name="clipboard.withIconText" />
33+
34+
## Theme
35+
36+
To learn more about how to customize the appearance of components, please see the [Theme docs](/docs/customize/theme).
37+
38+
<Theme name="clipboard" />
39+
40+
## References
41+
42+
- [Flowbite Datepicker](https://flowbite.com/docs/components/clipboard/)

apps/web/data/docs-sidebar.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const DOCS_SIDEBAR: DocsSidebarSection[] = [
6363
{ title: "Button group", href: "/docs/components/button-group" },
6464
{ title: "Card", href: "/docs/components/card" },
6565
{ title: "Carousel", href: "/docs/components/carousel" },
66+
{ title: "Clipboard", href: "/docs/components/clipboard", isNew: true },
6667
{ title: "Datepicker", href: "/docs/components/datepicker", isNew: true },
6768
{ title: "Drawer", href: "/docs/components/drawer", isNew: true },
6869
{ title: "Dropdown", href: "/docs/components/dropdown" },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"use client";
2+
3+
import { Clipboard } from "flowbite-react";
4+
import type { CodeData } from "~/components/code-demo";
5+
6+
const code = `
7+
"use client";
8+
9+
import { Clipboard } from "flowbite-react"
10+
11+
export function Component() {
12+
return (
13+
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
14+
<label htmlFor="npm-install" className="sr-only">
15+
Label
16+
</label>
17+
<input id="npm-install" type="text"
18+
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"
19+
value="npm install flowbite-react"
20+
disabled
21+
readOnly
22+
/>
23+
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
24+
</div>
25+
)
26+
}
27+
`;
28+
29+
export function Component() {
30+
return (
31+
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
32+
<label htmlFor="npm-install" className="sr-only">
33+
Label
34+
</label>
35+
<input
36+
id="npm-install"
37+
type="text"
38+
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"
39+
value="npm install flowbite-react"
40+
disabled
41+
readOnly
42+
/>
43+
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
44+
</div>
45+
);
46+
}
47+
48+
export const root: CodeData = {
49+
type: "single",
50+
code: [
51+
{
52+
fileName: "client",
53+
language: "tsx",
54+
code,
55+
},
56+
],
57+
githubSlug: "clipboard/clipboard.root.tsx",
58+
component: <Component />,
59+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use client";
2+
3+
import { Clipboard } from "flowbite-react";
4+
import type { CodeData } from "~/components/code-demo";
5+
6+
const code = `
7+
"use client";
8+
9+
import { Clipboard } from "flowbite-react"
10+
11+
export function Component() {
12+
return (
13+
<div className="grid w-full max-w-64">
14+
<div className="relative">
15+
<label htmlFor="npm-install" className="sr-only">
16+
Label
17+
</label>
18+
<input
19+
id="npm-install"
20+
type="text"
21+
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"
22+
value="npm install flowbite-react"
23+
disabled
24+
readOnly
25+
/>
26+
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
27+
</div>
28+
</div>
29+
)
30+
}
31+
`;
32+
33+
export function Component() {
34+
return (
35+
<div className="grid w-full max-w-64">
36+
<div className="relative">
37+
<label htmlFor="npm-install" className="sr-only">
38+
Label
39+
</label>
40+
<input
41+
id="npm-install"
42+
type="text"
43+
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"
44+
value="npm install flowbite-react"
45+
disabled
46+
readOnly
47+
/>
48+
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
49+
</div>
50+
</div>
51+
);
52+
}
53+
54+
export const withIcon: CodeData = {
55+
type: "single",
56+
code: [
57+
{
58+
fileName: "client",
59+
language: "tsx",
60+
code,
61+
},
62+
],
63+
githubSlug: "clipboard/clipboard.withIcon.tsx",
64+
component: <Component />,
65+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"use client";
2+
3+
import { Clipboard } from "flowbite-react";
4+
import type { CodeData } from "~/components/code-demo";
5+
6+
const code = `
7+
"use client";
8+
9+
import { Clipboard } from "flowbite-react"
10+
11+
export function Component() {
12+
return (
13+
<div className="grid w-full max-w-80">
14+
<div className="relative">
15+
<label htmlFor="npm-install" className="sr-only">
16+
Label
17+
</label>
18+
<input
19+
id="npm-install"
20+
type="text"
21+
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"
22+
value="npm install flowbite-react"
23+
disabled
24+
readOnly
25+
/>
26+
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
27+
</div>
28+
</div>
29+
)
30+
}
31+
`;
32+
33+
export function Component() {
34+
return (
35+
<div className="grid w-full max-w-80">
36+
<div className="relative">
37+
<label htmlFor="npm-install" className="sr-only">
38+
Label
39+
</label>
40+
<input
41+
id="npm-install"
42+
type="text"
43+
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"
44+
value="npm install flowbite-react"
45+
disabled
46+
readOnly
47+
/>
48+
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
49+
</div>
50+
</div>
51+
);
52+
}
53+
54+
export const withIconText: CodeData = {
55+
type: "single",
56+
code: [
57+
{
58+
fileName: "client",
59+
language: "tsx",
60+
code,
61+
},
62+
],
63+
githubSlug: "clipboard/clipboard.withIconText.tsx",
64+
component: <Component />,
65+
};

apps/web/examples/clipboard/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { root } from "./clipboard.root";
2+
export { withIcon } from "./clipboard.withIcon";
3+
export { withIconText } from "./clipboard.withIconText";

apps/web/examples/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * as button from "./button";
99
export * as buttonGroup from "./buttonGroup";
1010
export * as card from "./card";
1111
export * as carousel from "./carousel";
12+
export * as clipboard from "./clipboard";
1213
export * as datepicker from "./datepicker";
1314
export * as drawer from "./drawer";
1415
export * as dropdown from "./dropdown";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Meta, StoryFn } from "@storybook/react";
2+
// import { FaClipboardList } from "react-icons/fa6";
3+
import type { ClipboardProps } from "./Clipboard";
4+
import { Clipboard } from "./Clipboard";
5+
import type { ClipboardWithIconProps } from "./ClipboardWithIcon";
6+
import type { ClipboardWithIconTextProps } from "./ClipboardWithIconText";
7+
8+
export default {
9+
title: "Components/Clipboard",
10+
component: Clipboard,
11+
} as Meta;
12+
13+
const DefaultTemplate: StoryFn<ClipboardProps> = () => (
14+
<div className="grid w-full max-w-[23rem] grid-cols-8 gap-2">
15+
<label htmlFor="npm-install" className="sr-only">
16+
Label
17+
</label>
18+
<input
19+
id="npm-install"
20+
type="text"
21+
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"
22+
value="npm install flowbite-react"
23+
disabled
24+
readOnly
25+
/>
26+
<Clipboard valueToCopy="npm install flowbite-react" label="Copy" />
27+
</div>
28+
);
29+
30+
export const Default = DefaultTemplate.bind({});
31+
32+
const CopyIconTemplate: StoryFn<ClipboardWithIconProps> = () => (
33+
<div className="grid w-full max-w-64">
34+
<div className="relative">
35+
<label htmlFor="npm-install" className="sr-only">
36+
Label
37+
</label>
38+
<input
39+
id="npm-install"
40+
type="text"
41+
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"
42+
value="npm install flowbite-react"
43+
disabled
44+
readOnly
45+
/>
46+
<Clipboard.WithIcon valueToCopy="npm install flowbite-react" />
47+
</div>
48+
</div>
49+
);
50+
51+
export const CopyIcon = CopyIconTemplate.bind({});
52+
53+
const CopyIconTextTemplate: StoryFn<ClipboardWithIconTextProps> = () => (
54+
<div className="grid w-full max-w-80">
55+
<div className="relative">
56+
<label htmlFor="npm-install" className="sr-only">
57+
Label
58+
</label>
59+
<input
60+
id="npm-install"
61+
type="text"
62+
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"
63+
value="npm install flowbite-react"
64+
disabled
65+
readOnly
66+
/>
67+
<Clipboard.WithIconText valueToCopy="npm install flowbite-react" />
68+
</div>
69+
</div>
70+
);
71+
72+
export const CopyIconText = CopyIconTextTemplate.bind({});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import { forwardRef, useState, type ComponentProps, type ReactNode } from "react";
4+
import { twMerge } from "tailwind-merge";
5+
import { mergeDeep } from "../../helpers/merge-deep";
6+
import { getTheme } from "../../theme-store";
7+
import type { DeepPartial } from "../../types";
8+
import { Tooltip } from "../Tooltip";
9+
import { ClipboardWithIcon } from "./ClipboardWithIcon";
10+
import type { FlowbiteClipboardWithIconTheme } from "./ClipboardWithIcon";
11+
import { ClipboardWithIconText } from "./ClipboardWithIconText";
12+
import type { FlowbiteClipboardWithIconTextTheme } from "./ClipboardWithIconText";
13+
import { copyToClipboard } from "./helpers";
14+
15+
export interface FlowbiteClipboardTheme {
16+
button: {
17+
base: string;
18+
label: string;
19+
};
20+
withIcon: FlowbiteClipboardWithIconTheme;
21+
withIconText: FlowbiteClipboardWithIconTextTheme;
22+
}
23+
24+
export interface ClipboardProps extends ComponentProps<"button"> {
25+
valueToCopy: string;
26+
label?: ReactNode;
27+
theme?: DeepPartial<FlowbiteClipboardTheme>;
28+
}
29+
30+
const ClipboardComponent = forwardRef<HTMLButtonElement, ClipboardProps>(
31+
({ className, valueToCopy, label, theme: customTheme = {}, ...rest }, ref) => {
32+
const [isJustCopied, setIsJustCopied] = useState(false);
33+
34+
const theme = mergeDeep(getTheme().clipboard.button, customTheme);
35+
36+
return (
37+
<Tooltip content={isJustCopied ? "Copied" : "Copy to clipboard"} className="[&_*]:cursor-pointer">
38+
<button
39+
className={twMerge(theme.base, className)}
40+
onClick={() => copyToClipboard(valueToCopy, setIsJustCopied)}
41+
{...rest}
42+
ref={ref}
43+
>
44+
<span className={theme.label}>{label}</span>
45+
</button>
46+
</Tooltip>
47+
);
48+
},
49+
);
50+
51+
ClipboardComponent.displayName = "Clipboard";
52+
ClipboardWithIcon.displayName = "Clipboard.WithIcon";
53+
ClipboardWithIconText.displayName = "Clipboard.WithIconText";
54+
55+
export const Clipboard = Object.assign(ClipboardComponent, {
56+
WithIcon: ClipboardWithIcon,
57+
WithIconText: ClipboardWithIconText,
58+
});

0 commit comments

Comments
 (0)