Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): add Clipboard #1413

Merged
merged 10 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading