Skip to content

Commit d2b91ef

Browse files
committed
✨ Add Copy component
1 parent 1754ccb commit d2b91ef

File tree

13 files changed

+343
-0
lines changed

13 files changed

+343
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ import { Accordion } from 'webcoreui/react'
229229
- [Checkbox](https://github.com/Frontendland/webcoreui/tree/main/src/components/Checkbox)
230230
- [Collapsible](https://github.com/Frontendland/webcoreui/tree/main/src/components/Collapsible)
231231
- [ConditionalWrapper](https://github.com/Frontendland/webcoreui/tree/main/src/components/ConditionalWrapper)
232+
- [Copy](https://github.com/Frontendland/webcoreui/tree/main/src/components/Copy)
232233
- [DataTable](https://github.com/Frontendland/webcoreui/tree/main/src/components/DataTable)
233234
- [Footer](https://github.com/Frontendland/webcoreui/tree/main/src/components/Footer)
234235
- [Group](https://github.com/Frontendland/webcoreui/tree/main/src/components/Group)

src/components/Copy/Copy.astro

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
import type { CopyProps } from './copy'
3+
4+
import Badge from '../Badge/Badge.astro'
5+
import Icon from '../Icon/Icon.astro'
6+
7+
import { classNames } from '../../utils/classNames'
8+
9+
import styles from './copy.module.scss'
10+
11+
import type { IconProps } from '../Icon/icon'
12+
13+
interface Props extends CopyProps {}
14+
15+
const {
16+
tooltip,
17+
tooltipPosition,
18+
copyIcon = 'copy',
19+
copiedIcon = 'circle-check',
20+
className,
21+
...rest
22+
} = Astro.props
23+
24+
const classes = classNames([
25+
styles.copy,
26+
className
27+
])
28+
---
29+
30+
<Badge
31+
{...rest}
32+
className={classes}
33+
data-tooltip={tooltip}
34+
data-position={tooltipPosition}
35+
>
36+
<slot />
37+
<div class={styles.icons}>
38+
<button
39+
class={styles['copy-icon']}
40+
data-id="w-copy"
41+
>
42+
{copyIcon?.startsWith('<svg')
43+
? <Fragment set:html={copyIcon} />
44+
: <Icon type={copyIcon as IconProps['type']} />
45+
}
46+
</button>
47+
<span class={styles.copied}>
48+
{copiedIcon?.startsWith('<svg')
49+
? <Fragment set:html={copiedIcon} />
50+
: <Icon type={copiedIcon as IconProps['type']} />
51+
}
52+
</span>
53+
</div>
54+
</Badge>
55+
56+
<script>
57+
import { on } from '../../utils/DOMUtils'
58+
59+
on('[data-id="w-copy"]', 'click', (event: Event) => {
60+
const copy = event.currentTarget as HTMLButtonElement
61+
const copied = copy.nextElementSibling as HTMLSpanElement
62+
const badge = copy.parentElement?.parentElement as HTMLElement
63+
64+
const text = copy.parentElement?.previousSibling?.textContent?.trim()
65+
66+
copy.style.opacity = '0'
67+
copy.style.pointerEvents = 'none'
68+
69+
copied.style.opacity = '1'
70+
badge.removeAttribute('data-tooltip')
71+
72+
navigator.clipboard.writeText(text as string)
73+
}, true)
74+
</script>

src/components/Copy/Copy.svelte

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<script lang="ts">
2+
import type { CopyProps } from './copy'
3+
4+
import Badge from '../Badge/Badge.svelte'
5+
6+
import { classNames } from '../../utils/classNames'
7+
8+
import circleCheck from '../../icons/circle-check.svg?raw'
9+
import copy from '../../icons/copy.svg?raw'
10+
11+
import styles from './copy.module.scss'
12+
13+
export let tooltip: CopyProps['tooltip'] = ''
14+
export let tooltipPosition: CopyProps['tooltipPosition'] = null
15+
export let copyIcon: CopyProps['copyIcon'] = ''
16+
export let copiedIcon: CopyProps['copiedIcon'] = ''
17+
export let className: CopyProps['className'] = ''
18+
19+
const classes = classNames([
20+
styles.copy,
21+
className
22+
])
23+
24+
let copyButton: HTMLButtonElement
25+
let copiedButton: HTMLSpanElement
26+
27+
const copyText = () => {
28+
const text = copyButton.parentElement?.previousSibling?.textContent?.trim()
29+
|| (copyButton.parentElement?.previousSibling as Text)?.wholeText?.trim()
30+
|| copyButton.parentElement?.previousElementSibling?.textContent?.trim()
31+
32+
copyButton.style.opacity = '0'
33+
copyButton.style.pointerEvents = 'none'
34+
35+
copiedButton.style.opacity = '1'
36+
tooltip = ''
37+
38+
navigator.clipboard.writeText(text as string)
39+
}
40+
</script>
41+
42+
<Badge
43+
{...$$restProps}
44+
className={classes}
45+
data-tooltip={tooltip || undefined}
46+
data-position={tooltipPosition}
47+
>
48+
<slot />
49+
<div class={styles.icons}>
50+
<button
51+
class={styles['copy-icon']}
52+
bind:this={copyButton}
53+
on:click={copyText}
54+
>
55+
{@html copyIcon || copy}
56+
</button>
57+
<span class={styles.copied} bind:this={copiedButton}>
58+
{@html copiedIcon || circleCheck}
59+
</span>
60+
</div>
61+
</Badge>

src/components/Copy/Copy.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React, { useRef, useState } from 'react'
2+
import type { ReactCopyProps } from './copy'
3+
4+
import Badge from '../Badge/Badge.tsx'
5+
6+
import { classNames } from '../../utils/classNames'
7+
8+
import circleCheck from '../../icons/circle-check.svg?raw'
9+
import copy from '../../icons/copy.svg?raw'
10+
11+
import styles from './copy.module.scss'
12+
13+
const Copy = ({
14+
tooltip,
15+
tooltipPosition,
16+
copyIcon,
17+
copiedIcon,
18+
className,
19+
children,
20+
...rest
21+
}: ReactCopyProps) => {
22+
const copyButton = useRef<HTMLButtonElement>(null)
23+
const copiedButton = useRef<HTMLSpanElement>(null)
24+
const [tooltipText, setTooltipText] = useState(tooltip)
25+
26+
const classes = classNames([
27+
styles.copy,
28+
className
29+
])
30+
31+
const copyText = () => {
32+
const copyButtonElement = copyButton.current as HTMLButtonElement
33+
const copiedButtonElement = copiedButton.current as HTMLSpanElement
34+
35+
const text = copyButtonElement.parentElement?.previousSibling?.textContent?.trim()
36+
|| copyButtonElement.parentElement?.previousElementSibling?.textContent?.trim()
37+
38+
copyButtonElement.style.opacity = '0'
39+
copyButtonElement.style.pointerEvents = 'none'
40+
41+
copiedButtonElement.style.opacity = '1'
42+
43+
setTooltipText('')
44+
45+
navigator.clipboard.writeText(text as string)
46+
}
47+
48+
return (
49+
<Badge
50+
{...rest}
51+
className={classes}
52+
data-tooltip={tooltipText || undefined}
53+
data-position={tooltipPosition}
54+
>
55+
{children}
56+
<div className={styles.icons}>
57+
<button
58+
className={styles['copy-icon']}
59+
ref={copyButton}
60+
onClick={copyText}
61+
dangerouslySetInnerHTML={{ __html: copyIcon || copy }}
62+
/>
63+
<span
64+
className={styles.copied}
65+
ref={copiedButton}
66+
dangerouslySetInnerHTML={{ __html: copiedIcon || circleCheck }}
67+
/>
68+
</div>
69+
</Badge>
70+
)
71+
}
72+
73+
export default Copy

src/components/Copy/copy.module.scss

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@use '../../scss/config.scss' as *;
2+
3+
.copy {
4+
@include typography(md);
5+
6+
svg {
7+
@include size(18px);
8+
9+
cursor: pointer;
10+
}
11+
12+
.icons {
13+
@include position(relative);
14+
@include size(18px);
15+
}
16+
17+
.copy-icon,
18+
.copied {
19+
@include position(absolute, t0, l0);
20+
@include transition(opacity);
21+
}
22+
23+
.copy-icon {
24+
@include background(transparent);
25+
@include border(0);
26+
@include spacing(p0);
27+
28+
color: inherit;
29+
}
30+
31+
.copied {
32+
@include visibility(0);
33+
34+
pointer-events: none;
35+
cursor: auto;
36+
}
37+
}

src/components/Copy/copy.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { type BadgeProps } from '../Badge/badge'
2+
import { type IconProps } from '../Icon/icon'
3+
4+
export type CopyProps = {
5+
tooltip?: string
6+
tooltipPosition?: 'left' | 'right' | 'bottom' | null
7+
copyIcon?: IconProps['type'] | string
8+
copiedIcon?: IconProps['type'] | string
9+
className?: string
10+
} & BadgeProps
11+
12+
export type ReactCopyProps = {
13+
children: React.ReactNode
14+
} & CopyProps

src/components/Icon/icon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type IconProps = {
88
| 'circle-close'
99
| 'close'
1010
| 'components'
11+
| 'copy'
1112
| 'file'
1213
| 'github'
1314
| 'home'

src/components/Icon/map.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import CircleCheck from '../../icons/circle-check.svg?raw'
77
import CircleClose from '../../icons/circle-close.svg?raw'
88
import Close from '../../icons/close.svg?raw'
99
import Components from '../../icons/components.svg?raw'
10+
import Copy from '../../icons/copy.svg?raw'
1011
import File from '../../icons/file.svg?raw'
1112
import Github from '../../icons/github.svg?raw'
1213
import Home from '../../icons/home.svg?raw'
@@ -28,6 +29,7 @@ const iconMap = {
2829
'circle-close': CircleClose,
2930
'close': Close,
3031
'components': Components,
32+
'copy': Copy,
3133
'file': File,
3234
'github': Github,
3335
'home': Home,

src/icons/copy.svg

Lines changed: 3 additions & 0 deletions
Loading

src/pages/components/copy.astro

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
import ComponentWrapper from '@static/ComponentWrapper.astro'
3+
import Layout from '@static/Layout.astro'
4+
5+
import AstroCopy from '@components/Copy/Copy.astro'
6+
import SvelteCopy from '@components/Copy/Copy.svelte'
7+
import ReactCopy from '@components/Copy/Copy.tsx'
8+
9+
import checkIcon from '../../icons/check.svg?raw'
10+
import fileIcon from '../../icons/file.svg?raw'
11+
12+
import { getSections } from '@helpers'
13+
14+
const sections = getSections({
15+
title: 'copies',
16+
components: [AstroCopy, SvelteCopy, ReactCopy],
17+
showSubTitle: true
18+
})
19+
---
20+
21+
<Layout>
22+
<h1>Copy</h1>
23+
<div class="grid md-2 lg-3">
24+
<ComponentWrapper type="Astro">
25+
<AstroCopy tooltip="Click icon to copy">Copy Astro</AstroCopy>
26+
</ComponentWrapper>
27+
28+
<ComponentWrapper type="Svelte">
29+
<SvelteCopy theme="alert" tooltip="Click icon to copy" client:load>Copy Svelte</SvelteCopy>
30+
</ComponentWrapper>
31+
32+
<ComponentWrapper type="React">
33+
<ReactCopy theme="info" tooltip="Click icon to copy" client:load>Copy React</ReactCopy>
34+
</ComponentWrapper>
35+
</div>
36+
37+
{sections.map(section => (
38+
<h1>{section.title}</h1>
39+
<Fragment>
40+
{section.subTitle && <h2 set:html={section.subTitle} />}
41+
</Fragment>
42+
<div class="flex wrap sm">
43+
<section.component>Default</section.component>
44+
<section.component theme="secondary">Secondary</section.component>
45+
<section.component theme="outline">Outline</section.component>
46+
<section.component theme="flat">Flat</section.component>
47+
<section.component theme="info">Info</section.component>
48+
<section.component theme="success">Success</section.component>
49+
<section.component theme="warning">Warning</section.component>
50+
<section.component theme="alert">Alert</section.component>
51+
<section.component tooltip="Click icon to copy">With tooltip</section.component>
52+
<section.component tooltip="Click icon to copy" tooltipPosition="bottom">
53+
With tooltip position
54+
</section.component>
55+
<section.component copyIcon={fileIcon} copiedIcon={checkIcon}>
56+
Custom Icons
57+
</section.component>
58+
<section.component small={true} rounded={true}>
59+
Small & rounded
60+
</section.component>
61+
</div>
62+
))}
63+
</Layout>

src/pages/index.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Button from '@components/Button/Button.astro'
1515
import Carousel from '@components/Carousel/Carousel.astro'
1616
import Checkbox from '@components/Checkbox/Checkbox.astro'
1717
import Collapsible from '@components/Collapsible/Collapsible.astro'
18+
import Copy from '@components/Copy/Copy.astro'
1819
import DataTable from '@components/DataTable/DataTable.astro'
1920
import Footer from '@components/Footer/Footer.astro'
2021
import Group from '@components/Group/Group.astro'
@@ -158,6 +159,9 @@ const tabItems = [{
158159
<Badge slot="on">Expand</Badge>
159160
</Collapsible>
160161
</CardWrapper>
162+
<CardWrapper title="Copy" href="/components/copy">
163+
<Copy>Click to copy</Copy>
164+
</CardWrapper>
161165
<CardWrapper title="DataTable" href="/components/data-table">
162166
<DataTable
163167
headings={[

0 commit comments

Comments
 (0)