Skip to content

Commit 8105fa0

Browse files
committed
Copy to clipboard theme path in color helper
1 parent 0c974cf commit 8105fa0

File tree

4 files changed

+216
-69
lines changed

4 files changed

+216
-69
lines changed

stories/ColorHelper/ColorDecisionCard.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useColors } from "../../dist/useColors";
44
import { fr } from "../../dist/fr";
55
import Tooltip from "@mui/material/Tooltip";
66
import { useStyles } from "./makeStyles";
7+
import { CopyToClipboardButton } from "./CopyToClipboardButton";
78

89
export function ColorDecisionCard(
910
props: { className?: string } & ColorDecisionAndCorrespondingOption
@@ -38,7 +39,7 @@ export function ColorDecisionCard(
3839
>
3940
CSS variable:{" "}
4041
</span>
41-
&nbsp;{colorDecisionName}
42+
&nbsp;<code>{colorDecisionName}</code>
4243
</p>
4344
<p>
4445
<span
@@ -66,6 +67,9 @@ export function ColorDecisionCard(
6667
<code>
6768
theme.decisions.<strong>{themePath.join(".")}</strong>
6869
</code>
70+
<CopyToClipboardButton
71+
textToCopy={["theme", "decisions", ...themePath].join(".")}
72+
/>
6973
</p>
7074
<h6>Corresponding color option:</h6>
7175
<p>

stories/ColorHelper/ColorHelper.tsx

Lines changed: 120 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,6 @@ import { ColorDecisionCard } from "./ColorDecisionCard";
1414
import type { Props as SearchProps } from "./Search";
1515
import { useEffectOnValueChange } from "powerhooks/useEffectOnValueChange";
1616

17-
const colors = Array.from(
18-
new Set(
19-
colorDecisionAndCorrespondingOption.map(
20-
({ parsedColorDecisionName }) => parsedColorDecisionName.colorName
21-
)
22-
)
23-
);
24-
25-
const contextes = Array.from(
26-
new Set(
27-
colorDecisionAndCorrespondingOption.map(
28-
({ parsedColorDecisionName }) => parsedColorDecisionName.context
29-
)
30-
)
31-
);
32-
33-
const usages = Array.from(
34-
new Set(
35-
colorDecisionAndCorrespondingOption.map(
36-
({ parsedColorDecisionName }) => parsedColorDecisionName.usage
37-
)
38-
)
39-
);
40-
41-
const { useDebounce } = createUseDebounce({ "delay": 400 });
42-
43-
const fzf = new Fzf<readonly ColorDecisionAndCorrespondingOption[]>(
44-
colorDecisionAndCorrespondingOption,
45-
{
46-
"selector": ({
47-
colorDecisionName,
48-
themePath,
49-
colorOption: { colorOptionName, themePath: optionThemePath, color }
50-
}) =>
51-
`${colorDecisionName} ${["theme", "decisions", ...themePath].join(
52-
"."
53-
)} ${colorOptionName} ${["theme", "options", ...optionThemePath].join(".")} ${
54-
typeof color === "string" ? color : `${color.light} ${color.dark}`
55-
}`
56-
}
57-
);
58-
5917
export function ColorHelper() {
6018
const [search, setSearch] = useState("");
6119

@@ -70,33 +28,33 @@ export function ColorHelper() {
7028
const [color, setColor] = useState<SearchProps["color"]>(undefined);
7129
const [usage, setUsage] = useState<SearchProps["usage"]>(undefined);
7230

73-
const updateSearch = () => {
74-
setFilteredColorDecisionAndCorrespondingOption(
75-
fzf
76-
.find(search)
77-
.map(
78-
({ item: colorDecisionAndCorrespondingOption }) =>
79-
colorDecisionAndCorrespondingOption
80-
)
81-
.filter(({ parsedColorDecisionName }) =>
82-
context === undefined ? true : parsedColorDecisionName.context === context
83-
)
84-
.filter(({ parsedColorDecisionName }) =>
85-
color === undefined ? true : parsedColorDecisionName.colorName === color
86-
)
87-
.filter(({ parsedColorDecisionName }) =>
88-
usage === undefined ? true : parsedColorDecisionName.usage === usage
89-
)
90-
);
91-
};
92-
93-
useDebounce(updateSearch, [search]);
31+
useDebounce(
32+
() =>
33+
setFilteredColorDecisionAndCorrespondingOption(
34+
filterColorDecisionAndCorrespondingOption({
35+
search,
36+
context,
37+
color,
38+
usage
39+
})
40+
),
41+
[search]
42+
);
9443

9544
{
9645
const [, startTransition] = useTransition();
9746

9847
useEffectOnValueChange(() => {
99-
startTransition(() => updateSearch());
48+
startTransition(() =>
49+
setFilteredColorDecisionAndCorrespondingOption(
50+
filterColorDecisionAndCorrespondingOption({
51+
search,
52+
context,
53+
color,
54+
usage
55+
})
56+
)
57+
);
10058
}, [context, color, usage]);
10159
}
10260

@@ -127,13 +85,37 @@ export function ColorHelper() {
12785
evtAction={evtSearchAction}
12886
onSearchChange={search => setSearch(search)}
12987
search={search}
130-
contextes={contextes}
88+
contextes={contextes.filter(
89+
context =>
90+
filterColorDecisionAndCorrespondingOption({
91+
search,
92+
context,
93+
color,
94+
usage
95+
}).length !== 0
96+
)}
13197
context={context}
13298
onContextChange={setContext}
133-
colors={colors}
99+
colors={colors.filter(
100+
color =>
101+
filterColorDecisionAndCorrespondingOption({
102+
search,
103+
context,
104+
color,
105+
usage
106+
}).length !== 0
107+
)}
134108
color={color}
135109
onColorChange={setColor}
136-
usages={usages}
110+
usages={usages.filter(
111+
usage =>
112+
filterColorDecisionAndCorrespondingOption({
113+
search,
114+
context,
115+
color,
116+
usage
117+
}).length !== 0
118+
)}
137119
usage={usage}
138120
onUsageChange={setUsage}
139121
/>
@@ -155,3 +137,74 @@ export function ColorHelper() {
155137
</MuiDsfrThemeProvider>
156138
);
157139
}
140+
141+
const colors = Array.from(
142+
new Set(
143+
colorDecisionAndCorrespondingOption.map(
144+
({ parsedColorDecisionName }) => parsedColorDecisionName.colorName
145+
)
146+
)
147+
);
148+
149+
const contextes = Array.from(
150+
new Set(
151+
colorDecisionAndCorrespondingOption.map(
152+
({ parsedColorDecisionName }) => parsedColorDecisionName.context
153+
)
154+
)
155+
);
156+
157+
const usages = Array.from(
158+
new Set(
159+
colorDecisionAndCorrespondingOption.map(
160+
({ parsedColorDecisionName }) => parsedColorDecisionName.usage
161+
)
162+
)
163+
);
164+
165+
const { useDebounce } = createUseDebounce({ "delay": 400 });
166+
167+
const { filterColorDecisionAndCorrespondingOption } = (() => {
168+
const fzf = new Fzf<readonly ColorDecisionAndCorrespondingOption[]>(
169+
colorDecisionAndCorrespondingOption,
170+
{
171+
"selector": ({
172+
colorDecisionName,
173+
themePath,
174+
colorOption: { colorOptionName, themePath: optionThemePath, color }
175+
}) =>
176+
`${colorDecisionName} ${["theme", "decisions", ...themePath].join(
177+
"."
178+
)} ${colorOptionName} ${["theme", "options", ...optionThemePath].join(".")} ${
179+
typeof color === "string" ? color : `${color.light} ${color.dark}`
180+
}`
181+
}
182+
);
183+
184+
function filterColorDecisionAndCorrespondingOption(params: {
185+
search: string;
186+
context: SearchProps["context"] | undefined;
187+
color: SearchProps["color"] | undefined;
188+
usage: SearchProps["usage"] | undefined;
189+
}) {
190+
const { search, context, color, usage } = params;
191+
192+
return fzf
193+
.find(search)
194+
.map(
195+
({ item: colorDecisionAndCorrespondingOption }) =>
196+
colorDecisionAndCorrespondingOption
197+
)
198+
.filter(({ parsedColorDecisionName }) =>
199+
context === undefined ? true : parsedColorDecisionName.context === context
200+
)
201+
.filter(({ parsedColorDecisionName }) =>
202+
color === undefined ? true : parsedColorDecisionName.colorName === color
203+
)
204+
.filter(({ parsedColorDecisionName }) =>
205+
usage === undefined ? true : parsedColorDecisionName.usage === usage
206+
);
207+
}
208+
209+
return { filterColorDecisionAndCorrespondingOption };
210+
})();
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React, { useState, useEffect } from "react";
2+
import { Button } from "../../dist/Button";
3+
import { useStyles } from "./makeStyles";
4+
import { fr } from "../../dist";
5+
import { assert } from "tsafe/assert";
6+
7+
type Props = {
8+
className?: string;
9+
textToCopy: string;
10+
};
11+
12+
export function CopyToClipboardButton(props: Props) {
13+
const { className, textToCopy } = props;
14+
15+
const { css, cx, theme } = useStyles();
16+
17+
const [isCopiedFeedbackShown, setIsCopiedFeedbackShown] = useState(false);
18+
19+
useEffect(() => {
20+
if (!isCopiedFeedbackShown) {
21+
return;
22+
}
23+
24+
const timer = setTimeout(() => {
25+
setIsCopiedFeedbackShown(false);
26+
}, 1000);
27+
28+
return () => {
29+
clearTimeout(timer);
30+
};
31+
}, [isCopiedFeedbackShown]);
32+
33+
return (
34+
<div
35+
className={cx(
36+
css({
37+
"position": "relative",
38+
"display": "inline-block",
39+
"marginLeft": fr.spacing("4v")
40+
}),
41+
className
42+
)}
43+
>
44+
{isCopiedFeedbackShown ? (
45+
<p className={css({ "margin": 0 })}>
46+
<i
47+
className={cx(
48+
fr.cx("fr-icon-check-line"),
49+
css({ "color": theme.decisions.text.default.success.default })
50+
)}
51+
/>
52+
&nbsp; Copied to clipboard!
53+
</p>
54+
) : (
55+
<Button
56+
className={css({
57+
"position": "absolute",
58+
"top": -25
59+
})}
60+
iconId={"ri-clipboard-line"}
61+
priority="tertiary no outline"
62+
title="Copy to clipboard"
63+
onClick={() => {
64+
copyToClipboard(textToCopy);
65+
setIsCopiedFeedbackShown(true);
66+
}}
67+
/>
68+
)}
69+
</div>
70+
);
71+
}
72+
73+
const copyToClipboard = (str: string) => {
74+
Promise.resolve().then(() => {
75+
const textArea = document.createElement("textarea");
76+
textArea.value = str;
77+
textArea.style.opacity = "0";
78+
document.body.appendChild(textArea);
79+
//textArea.focus();
80+
//textArea.select();
81+
try {
82+
const successful = document.execCommand("copy");
83+
assert(!!successful);
84+
} catch (err) {
85+
alert("Unable to copy value , error : " + (err as Error).message);
86+
}
87+
88+
document.body.removeChild(textArea);
89+
});
90+
};

stories/ColorHelper/Search.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ const useStyles = makeStyles<{ filterWrapperMaxHeight: number }>({ "name": { Sea
173173
"maxHeight": filterWrapperMaxHeight,
174174
"overflow": "hidden",
175175
"display": "flex",
176-
"marginTop": fr.spacing("3v"),
176+
"marginTop": fr.spacing("4v"),
177177
"& > *": {
178178
"flex": 1,
179179
...fr.spacing("padding", {

0 commit comments

Comments
 (0)