Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb0a855c

Browse files
authored
fix: improve click UX and styling for Auth Token page (#11863)
* wip: commit progress for clipboard update* wip: push more progress* chore: finish initial version of useClipboard revamp* refactor: update API query to use newer RQ patterns* fix: update importers of useClipboard* fix: increase clickable area of CodeExample* fix: update styles for CliAuthPageView* fix: resolve issue with ref re-routing* docs: update comments for clarity* wip: commit progress on clipboard tests* chore: add extra test case for referential stability* wip: disable test stub to avoid breaking CI* wip: add test case for tab-switching* feat: finish changes* fix: improve styling for strong text* fix: make sure period doesn't break onto separate line* fix: make center styling more friendly to screen readers* refactor: clean up mocking implementation* fix: resolve security concern for clipboard text* fix: update CodeExample to obscure text when appropriate* fix: apply secret changes to relevant code examples* refactor: simplify code for obfuscating text* fix: partially revert clipboard changes* fix: clean up page styling further* fix: remove duplicate property identifier* refactor: rename variables for clarity* fix: simplify/revert CopyButton component design* fix: update how dummy input is hidden from page* fix: remove unused onClick handler prop* fix: resolve unused import* fix: opt code examples out of secret behavior
1 parentc7f51a9 commitb0a855c

File tree

14 files changed

+217
-83
lines changed

14 files changed

+217
-83
lines changed

‎site/src/api/queries/users.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
UpdateUserAppearanceSettingsRequest,
1414
UsersRequest,
1515
User,
16+
GenerateAPIKeyResponse,
1617
}from"api/typesGenerated";
1718
import{getAuthorizationKey}from"./authCheck";
1819
import{getMetadataAsJSON}from"utils/metadata";
@@ -134,6 +135,13 @@ export const me = (): UseQueryOptions<User> & {
134135
};
135136
};
136137

138+
exportfunctionapiKey():UseQueryOptions<GenerateAPIKeyResponse>{
139+
return{
140+
queryKey:[...meKey,"apiKey"],
141+
queryFn:()=>API.getApiKey(),
142+
};
143+
}
144+
137145
exportconsthasFirstUser=()=>{
138146
return{
139147
queryKey:["hasFirstUser"],

‎site/src/components/CodeExample/CodeExample.stories.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ const meta: Meta<typeof CodeExample> = {
55
title:"components/CodeExample",
66
component:CodeExample,
77
args:{
8+
secret:false,
89
code:`echo "hello, friend!"`,
910
},
1011
};
1112

1213
exportdefaultmeta;
1314
typeStory=StoryObj<typeofCodeExample>;
1415

15-
exportconstExample:Story={};
16+
exportconstExample:Story={
17+
args:{
18+
secret:false,
19+
},
20+
};
1621

1722
exportconstSecret:Story={
1823
args:{
@@ -22,6 +27,7 @@ export const Secret: Story = {
2227

2328
exportconstLongCode:Story={
2429
args:{
30+
secret:false,
2531
code:"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
2632
},
2733
};

‎site/src/components/CodeExample/CodeExample.tsx‎

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import{typeFC}from"react";
1+
import{typeFC,typeKeyboardEvent,typeMouseEvent,useRef}from"react";
22
import{typeInterpolation,typeTheme}from"@emotion/react";
33
import{MONOSPACE_FONT_FAMILY}from"theme/constants";
44
import{CopyButton}from"../CopyButton/CopyButton";
5+
import{visuallyHidden}from"@mui/utils";
56

67
exportinterfaceCodeExampleProps{
78
code:string;
@@ -14,19 +15,72 @@ export interface CodeExampleProps {
1415
*/
1516
exportconstCodeExample:FC<CodeExampleProps>=({
1617
code,
17-
secret,
1818
className,
19+
20+
// Defaulting to true to be on the safe side; you should have to opt out of
21+
// the secure option, not remember to opt in
22+
secret=true,
1923
})=>{
24+
constbuttonRef=useRef<HTMLButtonElement>(null);
25+
consttriggerButton=(event:KeyboardEvent|MouseEvent)=>{
26+
if(event.target!==buttonRef.current){
27+
buttonRef.current?.click();
28+
}
29+
};
30+
2031
return(
21-
<divcss={styles.container}className={className}>
22-
<codecss={[styles.code,secret&&styles.secret]}>{code}</code>
23-
<CopyButtontext={code}/>
32+
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions --
33+
Expanding clickable area of CodeExample for better ergonomics, but don't
34+
want to change the semantics of the HTML elements being rendered
35+
*/
36+
<div
37+
css={styles.container}
38+
className={className}
39+
onClick={triggerButton}
40+
onKeyDown={(event)=>{
41+
if(event.key==="Enter"){
42+
triggerButton(event);
43+
}
44+
}}
45+
onKeyUp={(event)=>{
46+
if(event.key===" "){
47+
triggerButton(event);
48+
}
49+
}}
50+
>
51+
<codecss={[styles.code,secret&&styles.secret]}>
52+
{secret ?(
53+
<>
54+
{/*
55+
* Obfuscating text even though we have the characters replaced with
56+
* discs in the CSS for two reasons:
57+
* 1. The CSS property is non-standard and won't work everywhere;
58+
* MDN warns you not to rely on it alone in production
59+
* 2. Even with it turned on and supported, the plaintext is still
60+
* readily available in the HTML itself
61+
*/}
62+
<spanaria-hidden>{obfuscateText(code)}</span>
63+
<spancss={{ ...visuallyHidden}}>
64+
Encrypted text. Please access via the copy button.
65+
</span>
66+
</>
67+
) :(
68+
<>{code}</>
69+
)}
70+
</code>
71+
72+
<CopyButtonref={buttonRef}text={code}/>
2473
</div>
2574
);
2675
};
2776

77+
functionobfuscateText(text:string):string{
78+
returnnewArray(text.length).fill("*").join("");
79+
}
80+
2881
conststyles={
2982
container:(theme)=>({
83+
cursor:"pointer",
3084
display:"flex",
3185
flexDirection:"row",
3286
alignItems:"center",
@@ -37,6 +91,10 @@ const styles = {
3791
padding:8,
3892
lineHeight:"150%",
3993
border:`1px solid${theme.experimental.l1.outline}`,
94+
95+
"&:hover":{
96+
backgroundColor:theme.experimental.l2.hover.background,
97+
},
4098
}),
4199

42100
code:{

‎site/src/components/CopyButton/CopyButton.tsx‎

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import IconButton from "@mui/material/Button";
22
importTooltipfrom"@mui/material/Tooltip";
33
importCheckfrom"@mui/icons-material/Check";
44
import{css,typeInterpolation,typeTheme}from"@emotion/react";
5-
import{typeFC,typeReactNode}from"react";
5+
import{forwardRef,typeReactNode}from"react";
66
import{useClipboard}from"hooks/useClipboard";
77
import{FileCopyIcon}from"../Icons/FileCopyIcon";
88

@@ -23,36 +23,40 @@ export const Language = {
2323
/**
2424
* Copy button used inside the CodeBlock component internally
2525
*/
26-
exportconstCopyButton:FC<CopyButtonProps>=({
27-
text,
28-
ctaCopy,
29-
wrapperStyles,
30-
buttonStyles,
31-
tooltipTitle=Language.tooltipTitle,
32-
})=>{
33-
const{ isCopied,copy:copyToClipboard}=useClipboard(text);
26+
exportconstCopyButton=forwardRef<HTMLButtonElement,CopyButtonProps>(
27+
(props,ref)=>{
28+
const{
29+
text,
30+
ctaCopy,
31+
wrapperStyles,
32+
buttonStyles,
33+
tooltipTitle=Language.tooltipTitle,
34+
}=props;
35+
const{ isCopied, copyToClipboard}=useClipboard(text);
3436

35-
return(
36-
<Tooltiptitle={tooltipTitle}placement="top">
37-
<divcss={[{display:"flex"},wrapperStyles]}>
38-
<IconButton
39-
css={[styles.button,buttonStyles]}
40-
onClick={copyToClipboard}
41-
size="small"
42-
aria-label={Language.ariaLabel}
43-
variant="text"
44-
>
45-
{isCopied ?(
46-
<Checkcss={styles.copyIcon}/>
47-
) :(
48-
<FileCopyIconcss={styles.copyIcon}/>
49-
)}
50-
{ctaCopy&&<divcss={{marginLeft:8}}>{ctaCopy}</div>}
51-
</IconButton>
52-
</div>
53-
</Tooltip>
54-
);
55-
};
37+
return(
38+
<Tooltiptitle={tooltipTitle}placement="top">
39+
<divcss={[{display:"flex"},wrapperStyles]}>
40+
<IconButton
41+
ref={ref}
42+
css={[styles.button,buttonStyles]}
43+
size="small"
44+
aria-label={Language.ariaLabel}
45+
variant="text"
46+
onClick={copyToClipboard}
47+
>
48+
{isCopied ?(
49+
<Checkcss={styles.copyIcon}/>
50+
) :(
51+
<FileCopyIconcss={styles.copyIcon}/>
52+
)}
53+
{ctaCopy&&<divcss={{marginLeft:8}}>{ctaCopy}</div>}
54+
</IconButton>
55+
</div>
56+
</Tooltip>
57+
);
58+
},
59+
);
5660

5761
conststyles={
5862
button:(theme)=>css`

‎site/src/components/CopyableValue/CopyableValue.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export const CopyableValue: FC<CopyableValueProps> = ({
1616
children,
1717
...attrs
1818
})=>{
19-
const{ isCopied,copy}=useClipboard(value);
20-
constclickableProps=useClickable<HTMLSpanElement>(copy);
19+
const{ isCopied,copyToClipboard}=useClipboard(value);
20+
constclickableProps=useClickable<HTMLSpanElement>(copyToClipboard);
2121

2222
return(
2323
<Tooltip

‎site/src/hooks/useClipboard.ts‎

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
1-
import{useState}from"react";
1+
import{useEffect,useRef,useState}from"react";
22

3-
exportconstuseClipboard=(
4-
text:string,
5-
):{isCopied:boolean;copy:()=>Promise<void>}=>{
6-
const[isCopied,setIsCopied]=useState<boolean>(false);
3+
typeUseClipboardResult=Readonly<{
4+
isCopied:boolean;
5+
copyToClipboard:()=>Promise<void>;
6+
}>;
77

8-
constcopy=async():Promise<void>=>{
8+
exportconstuseClipboard=(textToCopy:string):UseClipboardResult=>{
9+
const[isCopied,setIsCopied]=useState(false);
10+
consttimeoutIdRef=useRef<number|undefined>();
11+
12+
useEffect(()=>{
13+
constclearIdsOnUnmount=()=>window.clearTimeout(timeoutIdRef.current);
14+
returnclearIdsOnUnmount;
15+
},[]);
16+
17+
constcopyToClipboard=async()=>{
918
try{
10-
awaitwindow.navigator.clipboard.writeText(text);
19+
awaitwindow.navigator.clipboard.writeText(textToCopy);
1120
setIsCopied(true);
12-
window.setTimeout(()=>{
21+
timeoutIdRef.current=window.setTimeout(()=>{
1322
setIsCopied(false);
1423
},1000);
1524
}catch(err){
16-
constinput=document.createElement("input");
17-
input.value=text;
18-
document.body.appendChild(input);
19-
input.focus();
20-
input.select();
21-
constresult=document.execCommand("copy");
22-
document.body.removeChild(input);
23-
if(result){
25+
constisCopied=simulateClipboardWrite();
26+
if(isCopied){
2427
setIsCopied(true);
25-
window.setTimeout(()=>{
28+
timeoutIdRef.current=window.setTimeout(()=>{
2629
setIsCopied(false);
2730
},1000);
2831
}else{
@@ -37,8 +40,45 @@ export const useClipboard = (
3740
}
3841
};
3942

40-
return{
41-
isCopied,
42-
copy,
43-
};
43+
return{ isCopied, copyToClipboard};
4444
};
45+
46+
/**
47+
* It feels silly that you have to make a whole dummy input just to simulate a
48+
* clipboard, but that's really the recommended approach for older browsers.
49+
*
50+
*@see {@link https://web.dev/patterns/clipboard/copy-text?hl=en}
51+
*/
52+
functionsimulateClipboardWrite():boolean{
53+
constpreviousFocusTarget=document.activeElement;
54+
constdummyInput=document.createElement("input");
55+
56+
// Using visually-hidden styling to ensure that inserting the element doesn't
57+
// cause any content reflows on the page (removes any risk of UI flickers).
58+
// Can't use visibility:hidden or display:none, because then the elements
59+
// can't receive focus, which is needed for the execCommand method to work
60+
conststyle=dummyInput.style;
61+
style.display="inline-block";
62+
style.position="absolute";
63+
style.overflow="hidden";
64+
style.clip="rect(0 0 0 0)";
65+
style.clipPath="rect(0 0 0 0)";
66+
style.height="1px";
67+
style.width="1px";
68+
style.margin="-1px";
69+
style.padding="0";
70+
style.border="0";
71+
72+
document.body.appendChild(dummyInput);
73+
dummyInput.focus();
74+
dummyInput.select();
75+
76+
constisCopied=document.execCommand("copy");
77+
dummyInput.remove();
78+
79+
if(previousFocusTargetinstanceofHTMLElement){
80+
previousFocusTarget.focus();
81+
}
82+
83+
returnisCopied;
84+
}

‎site/src/modules/resources/SSHButton/SSHButton.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const SSHButton: FC<SSHButtonProps> = ({
5757
Configure SSH hosts on machine:
5858
</strong>
5959
</HelpTooltipText>
60-
<CodeExamplecode="coder config-ssh"/>
60+
<CodeExamplesecret={false}code="coder config-ssh"/>
6161
</div>
6262

6363
<div>
@@ -67,6 +67,7 @@ export const SSHButton: FC<SSHButtonProps> = ({
6767
</strong>
6868
</HelpTooltipText>
6969
<CodeExample
70+
secret={false}
7071
code={`ssh${sshPrefix}${workspaceName}.${agentName}`}
7172
/>
7273
</div>

‎site/src/pages/CliAuthPage/CliAuthPage.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import{typeFC}from"react";
22
import{Helmet}from"react-helmet-async";
33
import{useQuery}from"react-query";
4-
import{getApiKey}from"api/api";
54
import{pageTitle}from"utils/page";
65
import{CliAuthPageView}from"./CliAuthPageView";
6+
import{apiKey}from"api/queries/users";
77

88
exportconstCliAuthenticationPage:FC=()=>{
9-
const{ data}=useQuery({
10-
queryFn:()=>getApiKey(),
11-
});
9+
const{ data}=useQuery(apiKey());
1210

1311
return(
1412
<>

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp