Skip to content

Commit a1d635e

Browse files
authored
Add button to copy icon URLs (#2992)
* Add button to copy icon URLs * Only render button when async clipboard API is supported * Add success and error toasts
1 parent fea6e1d commit a1d635e

File tree

2 files changed

+154
-95
lines changed

2 files changed

+154
-95
lines changed

.storybook/components/Icons.module.css

+14-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
.wrapper {
4646
position: relative;
4747
width: 7.5rem;
48-
margin-top: var(--cui-spacings-giga);
49-
margin-bottom: var(--cui-spacings-giga);
48+
padding-bottom: var(--cui-spacings-tera);
49+
margin-top: var(--cui-spacings-mega);
5050
text-align: center;
5151
}
5252

@@ -94,3 +94,15 @@
9494
left: 50%;
9595
transform: translate(-50%, -50%) rotate(-30deg);
9696
}
97+
98+
.copy {
99+
position: absolute;
100+
right: 50%;
101+
bottom: 0;
102+
display: none;
103+
transform: translateX(50%);
104+
}
105+
106+
.wrapper:hover .copy {
107+
display: block;
108+
}

.storybook/components/Icons.tsx

+140-93
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import { SearchInput } from '../../packages/circuit-ui/components/SearchInput/Se
3131
import { Select } from '../../packages/circuit-ui/components/Select/Select.js';
3232
import { SelectorGroup } from '../../packages/circuit-ui/components/SelectorGroup/SelectorGroup.js';
3333
import { Tooltip } from '../../packages/circuit-ui/components/Tooltip/Tooltip.js';
34+
import { IconButton } from '../../packages/circuit-ui/components/Button/IconButton.js';
35+
import { ToastProvider } from '../../packages/circuit-ui/components/ToastContext/ToastContext.js';
36+
import { useNotificationToast } from '../../packages/circuit-ui/components/NotificationToast/NotificationToast.js';
3437
import { clsx } from '../../packages/circuit-ui/styles/clsx.js';
3538
import { utilClasses } from '../../packages/circuit-ui/styles/utility.js';
3639
import { slugify } from '../slugify.js';
@@ -77,7 +80,7 @@ export function Icons() {
7780

7881
const handleChange =
7982
(setState: Dispatch<SetStateAction<string>>) =>
80-
(event: ChangeEvent<any>) => {
83+
(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
8184
setState(event.target.value);
8285
};
8386

@@ -113,100 +116,144 @@ export function Icons() {
113116

114117
return (
115118
<Unstyled>
116-
<fieldset className={classes.filters}>
117-
<legend className={utilClasses.hideVisually}>Icon filters</legend>
118-
<SearchInput
119-
label="Search by name or keyword"
120-
placeholder="Search..."
121-
value={search}
122-
onChange={handleChange(setSearch)}
123-
onClear={() => setSearch('')}
124-
clearLabel="Clear"
125-
/>
126-
<Select
127-
label="Size"
128-
options={sizeOptions}
129-
value={size}
130-
onChange={handleChange(setSize)}
131-
/>
132-
<Select
133-
label="Color"
134-
options={colorOptions}
135-
value={color}
136-
onChange={handleChange(setColor)}
119+
<ToastProvider>
120+
<fieldset className={classes.filters}>
121+
<legend className={utilClasses.hideVisually}>Icon filters</legend>
122+
<SearchInput
123+
label="Search by name or keyword"
124+
placeholder="Search..."
125+
value={search}
126+
onChange={handleChange(setSearch)}
127+
onClear={() => setSearch('')}
128+
clearLabel="Clear"
129+
/>
130+
<Select
131+
label="Size"
132+
options={sizeOptions}
133+
value={size}
134+
onChange={handleChange(setSize)}
135+
/>
136+
<Select
137+
label="Color"
138+
options={colorOptions}
139+
value={color}
140+
onChange={handleChange(setColor)}
141+
/>
142+
<SelectorGroup
143+
label="Scale"
144+
options={scaleOptions}
145+
value={scale}
146+
onChange={handleChange(setScale)}
147+
/>
148+
</fieldset>
149+
150+
{activeIcons.length <= 0 ? (
151+
<Body>No icons found</Body>
152+
) : (
153+
Object.entries<IconsManifest['icons']>(
154+
groupBy(activeIcons, 'category'),
155+
).map(([category, items]) => (
156+
<section key={category} className={classes.category}>
157+
<Headline as="h2" size="m" id={slugify(category)}>
158+
{category}
159+
</Headline>
160+
<div className={classes.list}>
161+
{sortBy(items, 'name').map((icon) => (
162+
<Icon
163+
key={`${icon.name}-${icon.size}`}
164+
icon={icon}
165+
scale={scale}
166+
color={color}
167+
/>
168+
))}
169+
</div>
170+
</section>
171+
))
172+
)}
173+
</ToastProvider>
174+
</Unstyled>
175+
);
176+
}
177+
178+
function Icon({
179+
icon,
180+
scale,
181+
color,
182+
}: { icon: IconsManifest['icons'][number]; scale: string; color: string }) {
183+
const { setToast } = useNotificationToast();
184+
185+
const id = `${icon.name}-${icon.size}`;
186+
const componentName = getComponentName(
187+
icon.name,
188+
) as keyof typeof iconComponents;
189+
const Icon = iconComponents[componentName] as IconComponentType;
190+
191+
const copyIconURL = () => {
192+
const iconURL = `https://circuit.sumup.com/icons/v2/${icon.name}_${icon.size}.svg`;
193+
navigator.clipboard
194+
.writeText(iconURL)
195+
.then(() => {
196+
setToast({
197+
variant: 'success',
198+
body: `Copied the ${componentName} (${icon.size}) icon URL to the clipboard.`,
199+
});
200+
})
201+
.catch((error) => {
202+
console.error(error);
203+
setToast({
204+
variant: 'danger',
205+
body: `Failed to copy the ${componentName} (${icon.size}) icon URL to the clipboard.`,
206+
});
207+
});
208+
};
209+
210+
return (
211+
<div className={classes.wrapper}>
212+
<div className={clsx(classes['icon-wrapper'], classes[scale])}>
213+
<Icon
214+
aria-labelledby={id}
215+
size={icon.size}
216+
className={classes.icon}
217+
style={{
218+
color,
219+
backgroundColor:
220+
color === 'var(--cui-fg-on-strong)'
221+
? 'var(--cui-bg-strong)'
222+
: 'var(--cui-bg-normal)',
223+
}}
137224
/>
138-
<SelectorGroup
139-
label="Scale"
140-
options={scaleOptions}
141-
value={scale}
142-
onChange={handleChange(setScale)}
225+
</div>
226+
<span id={id} className={classes.label}>
227+
{componentName}
228+
<span className={classes.size}>{icon.size}</span>
229+
</span>
230+
{icon.deprecation && (
231+
<Tooltip
232+
type="description"
233+
label={icon.deprecation}
234+
component={(props) => (
235+
<Badge
236+
{...props}
237+
tabIndex={0}
238+
variant="warning"
239+
className={classes.badge}
240+
>
241+
Deprecated
242+
</Badge>
243+
)}
143244
/>
144-
</fieldset>
145-
146-
{activeIcons.length <= 0 ? (
147-
<Body>No icons found</Body>
148-
) : (
149-
Object.entries<IconsManifest['icons']>(
150-
groupBy(activeIcons, 'category'),
151-
).map(([category, items]) => (
152-
<section key={category} className={classes.category}>
153-
<Headline as="h2" size="m" id={slugify(category)}>
154-
{category}
155-
</Headline>
156-
<div className={classes.list}>
157-
{sortBy(items, 'name').map((icon) => {
158-
const id = `${icon.name}-${icon.size}`;
159-
const componentName = getComponentName(
160-
icon.name,
161-
) as keyof typeof iconComponents;
162-
const Icon = iconComponents[componentName] as IconComponentType;
163-
return (
164-
<div key={id} className={classes.wrapper}>
165-
<div
166-
className={clsx(classes['icon-wrapper'], classes[scale])}
167-
>
168-
<Icon
169-
aria-labelledby={id}
170-
size={icon.size}
171-
className={classes.icon}
172-
style={{
173-
color,
174-
backgroundColor:
175-
color === 'var(--cui-fg-on-strong)'
176-
? 'var(--cui-bg-strong)'
177-
: 'var(--cui-bg-normal)',
178-
}}
179-
/>
180-
</div>
181-
<span id={id} className={classes.label}>
182-
{componentName}
183-
{size === 'all' && (
184-
<span className={classes.size}>{icon.size}</span>
185-
)}
186-
</span>
187-
{icon.deprecation && (
188-
<Tooltip
189-
type="description"
190-
label={icon.deprecation}
191-
component={(props) => (
192-
<Badge
193-
{...props}
194-
tabIndex={0}
195-
variant="warning"
196-
className={classes.badge}
197-
>
198-
Deprecated
199-
</Badge>
200-
)}
201-
/>
202-
)}
203-
</div>
204-
);
205-
})}
206-
</div>
207-
</section>
208-
))
209245
)}
210-
</Unstyled>
246+
{navigator.clipboard && (
247+
<IconButton
248+
variant="tertiary"
249+
size="s"
250+
icon={iconComponents.Link}
251+
className={classes.copy}
252+
onClick={copyIconURL}
253+
>
254+
Copy URL
255+
</IconButton>
256+
)}
257+
</div>
211258
);
212259
}

0 commit comments

Comments
 (0)