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

fix: improve click UX and styling for Auth Token page#11863

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Parkreiner merged 32 commits intomainfrommes/clipboard-fix
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
32 commits
Select commitHold shift + click to select a range
7e4d964
wip: commit progress for clipboard update
ParkreinerJan 26, 2024
ece3f6d
wip: push more progress
ParkreinerJan 26, 2024
bf6e73a
chore: finish initial version of useClipboard revamp
ParkreinerJan 26, 2024
697bdf0
refactor: update API query to use newer RQ patterns
ParkreinerJan 26, 2024
9b881c0
fix: update importers of useClipboard
ParkreinerJan 26, 2024
ccdca08
fix: increase clickable area of CodeExample
ParkreinerJan 26, 2024
a7c7cc7
fix: update styles for CliAuthPageView
ParkreinerJan 26, 2024
163db92
fix: resolve issue with ref re-routing
ParkreinerJan 26, 2024
38b80a8
docs: update comments for clarity
ParkreinerJan 27, 2024
92f94d7
wip: commit progress on clipboard tests
ParkreinerJan 27, 2024
e0e5e6f
chore: add extra test case for referential stability
ParkreinerJan 27, 2024
0ca1558
wip: disable test stub to avoid breaking CI
ParkreinerJan 27, 2024
88b96df
wip: add test case for tab-switching
ParkreinerJan 27, 2024
15feb14
feat: finish changes
ParkreinerJan 27, 2024
e3feffc
fix: improve styling for strong text
ParkreinerJan 27, 2024
3ec5196
fix: make sure period doesn't break onto separate line
ParkreinerJan 27, 2024
cf2d179
fix: make center styling more friendly to screen readers
ParkreinerJan 27, 2024
98bd1af
refactor: clean up mocking implementation
ParkreinerJan 28, 2024
400e07c
fix: resolve security concern for clipboard text
ParkreinerJan 28, 2024
3307432
fix: update CodeExample to obscure text when appropriate
ParkreinerJan 28, 2024
f10134f
fix: apply secret changes to relevant code examples
ParkreinerJan 28, 2024
c4469f3
refactor: simplify code for obfuscating text
ParkreinerJan 28, 2024
d8b6727
fix: partially revert clipboard changes
ParkreinerJan 29, 2024
6157049
fix: clean up page styling further
ParkreinerJan 29, 2024
89c74da
fix: remove duplicate property identifier
ParkreinerJan 29, 2024
b95cfd6
refactor: rename variables for clarity
ParkreinerFeb 1, 2024
4ab6326
fix: simplify/revert CopyButton component design
ParkreinerFeb 1, 2024
778560f
fix: update how dummy input is hidden from page
ParkreinerFeb 1, 2024
cf53114
fix: remove unused onClick handler prop
ParkreinerFeb 1, 2024
97fed6f
fix: resolve unused import
ParkreinerFeb 1, 2024
83febe2
Merge branch 'main' into mes/clipboard-fix
ParkreinerFeb 1, 2024
fb0f4b7
fix: opt code examples out of secret behavior
ParkreinerFeb 1, 2024
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
8 changes: 8 additions & 0 deletionssite/src/api/queries/users.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ import type {
UpdateUserAppearanceSettingsRequest,
UsersRequest,
User,
GenerateAPIKeyResponse,
} from "api/typesGenerated";
import { getAuthorizationKey } from "./authCheck";
import { getMetadataAsJSON } from "utils/metadata";
Expand DownExpand Up@@ -134,6 +135,13 @@ export const me = (): UseQueryOptions<User> & {
};
};

export function apiKey(): UseQueryOptions<GenerateAPIKeyResponse> {
return {
queryKey: [...meKey, "apiKey"],
queryFn: () => API.getApiKey(),
};
}

export const hasFirstUser = () => {
return {
queryKey: ["hasFirstUser"],
Expand Down
8 changes: 7 additions & 1 deletionsite/src/components/CodeExample/CodeExample.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,14 +5,19 @@ const meta: Meta<typeof CodeExample> = {
title: "components/CodeExample",
component: CodeExample,
args: {
secret: false,
code: `echo "hello, friend!"`,
},
};

export default meta;
type Story = StoryObj<typeof CodeExample>;

export const Example: Story = {};
export const Example: Story = {
args: {
secret: false,
},
};

export const Secret: Story = {
args: {
Expand All@@ -22,6 +27,7 @@ export const Secret: Story = {

export const LongCode: Story = {
args: {
secret: false,
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
},
};
68 changes: 63 additions & 5 deletionssite/src/components/CodeExample/CodeExample.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
import { type FC } from "react";
import { type FC, type KeyboardEvent, type MouseEvent, useRef } from "react";
import { type Interpolation, type Theme } from "@emotion/react";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { CopyButton } from "../CopyButton/CopyButton";
import { visuallyHidden } from "@mui/utils";

export interface CodeExampleProps {
code: string;
Expand All@@ -14,19 +15,72 @@ export interface CodeExampleProps {
*/
export const CodeExample: FC<CodeExampleProps> = ({
code,
secret,
className,

// Defaulting to true to be on the safe side; you should have to opt out of
// the secure option, not remember to opt in
secret = true,
}) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const triggerButton = (event: KeyboardEvent | MouseEvent) => {
if (event.target !== buttonRef.current) {
buttonRef.current?.click();
}
};

return (
<div css={styles.container} className={className}>
<code css={[styles.code, secret && styles.secret]}>{code}</code>
<CopyButton text={code} />
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions --
Expanding clickable area of CodeExample for better ergonomics, but don't
want to change the semantics of the HTML elements being rendered
*/
<div
css={styles.container}
className={className}
onClick={triggerButton}
onKeyDown={(event) => {
if (event.key === "Enter") {
triggerButton(event);
}
}}
onKeyUp={(event) => {
if (event.key === " ") {
triggerButton(event);
}
}}
>
<code css={[styles.code, secret && styles.secret]}>
{secret ? (
<>
{/*
* Obfuscating text even though we have the characters replaced with
* discs in the CSS for two reasons:
* 1. The CSS property is non-standard and won't work everywhere;
* MDN warns you not to rely on it alone in production
* 2. Even with it turned on and supported, the plaintext is still
* readily available in the HTML itself
*/}
<span aria-hidden>{obfuscateText(code)}</span>
<span css={{ ...visuallyHidden }}>
Encrypted text. Please access via the copy button.
</span>
</>
) : (
<>{code}</>
)}
</code>

<CopyButton ref={buttonRef} text={code} />
</div>
);
};

function obfuscateText(text: string): string {
return new Array(text.length).fill("*").join("");
}

const styles = {
container: (theme) => ({
cursor: "pointer",
display: "flex",
flexDirection: "row",
alignItems: "center",
Expand All@@ -37,6 +91,10 @@ const styles = {
padding: 8,
lineHeight: "150%",
border: `1px solid ${theme.experimental.l1.outline}`,

"&:hover": {
backgroundColor: theme.experimental.l2.hover.background,
},
}),

code: {
Expand Down
64 changes: 34 additions & 30 deletionssite/src/components/CopyButton/CopyButton.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@ import IconButton from "@mui/material/Button";
import Tooltip from "@mui/material/Tooltip";
import Check from "@mui/icons-material/Check";
import { css, type Interpolation, type Theme } from "@emotion/react";
import {type FC, type ReactNode } from "react";
import {forwardRef, type ReactNode } from "react";
import { useClipboard } from "hooks/useClipboard";
import { FileCopyIcon } from "../Icons/FileCopyIcon";

Expand All@@ -23,36 +23,40 @@ export const Language = {
/**
* Copy button used inside the CodeBlock component internally
*/
export const CopyButton: FC<CopyButtonProps> = ({
text,
ctaCopy,
wrapperStyles,
buttonStyles,
tooltipTitle = Language.tooltipTitle,
}) => {
const { isCopied, copy: copyToClipboard } = useClipboard(text);
export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
(props, ref) => {
const {
text,
ctaCopy,
wrapperStyles,
buttonStyles,
tooltipTitle = Language.tooltipTitle,
} = props;
const { isCopied, copyToClipboard } = useClipboard(text);

return (
<Tooltip title={tooltipTitle} placement="top">
<div css={[{ display: "flex" }, wrapperStyles]}>
<IconButton
css={[styles.button, buttonStyles]}
onClick={copyToClipboard}
size="small"
aria-label={Language.ariaLabel}
variant="text"
>
{isCopied ? (
<Check css={styles.copyIcon} />
) : (
<FileCopyIcon css={styles.copyIcon} />
)}
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
</IconButton>
</div>
</Tooltip>
);
};
return (
<Tooltip title={tooltipTitle} placement="top">
<div css={[{ display: "flex" }, wrapperStyles]}>
<IconButton
ref={ref}
css={[styles.button, buttonStyles]}
size="small"
aria-label={Language.ariaLabel}
variant="text"
onClick={copyToClipboard}
>
{isCopied ? (
<Check css={styles.copyIcon} />
) : (
<FileCopyIcon css={styles.copyIcon} />
)}
{ctaCopy && <div css={{ marginLeft: 8 }}>{ctaCopy}</div>}
</IconButton>
</div>
</Tooltip>
);
},
);

const styles = {
button: (theme) => css`
Expand Down
4 changes: 2 additions & 2 deletionssite/src/components/CopyableValue/CopyableValue.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,8 +16,8 @@ export const CopyableValue: FC<CopyableValueProps> = ({
children,
...attrs
}) => {
const { isCopied,copy } = useClipboard(value);
const clickableProps = useClickable<HTMLSpanElement>(copy);
const { isCopied,copyToClipboard } = useClipboard(value);
const clickableProps = useClickable<HTMLSpanElement>(copyToClipboard);

return (
<Tooltip
Expand Down
82 changes: 61 additions & 21 deletionssite/src/hooks/useClipboard.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
import { useState } from "react";
import {useEffect, useRef,useState } from "react";

export const useClipboard = (
text: string,
): { isCopied: boolean; copy:() => Promise<void> } => {
const [isCopied, setIsCopied] = useState<boolean>(false);
type UseClipboardResult = Readonly<{
isCopied: boolean;
copyToClipboard:() => Promise<void>;
}>;

const copy = async (): Promise<void> => {
export const useClipboard = (textToCopy: string): UseClipboardResult => {
const [isCopied, setIsCopied] = useState(false);
const timeoutIdRef = useRef<number | undefined>();

useEffect(() => {
const clearIdsOnUnmount = () => window.clearTimeout(timeoutIdRef.current);
return clearIdsOnUnmount;
}, []);

const copyToClipboard = async () => {
try {
await window.navigator.clipboard.writeText(text);
await window.navigator.clipboard.writeText(textToCopy);
setIsCopied(true);
window.setTimeout(() => {
timeoutIdRef.current =window.setTimeout(() => {
setIsCopied(false);
}, 1000);
} catch (err) {
const input = document.createElement("input");
input.value = text;
document.body.appendChild(input);
input.focus();
input.select();
const result = document.execCommand("copy");
document.body.removeChild(input);
if (result) {
const isCopied = simulateClipboardWrite();
if (isCopied) {
setIsCopied(true);
window.setTimeout(() => {
timeoutIdRef.current =window.setTimeout(() => {
setIsCopied(false);
}, 1000);
} else {
Expand All@@ -37,8 +40,45 @@ export const useClipboard = (
}
};

return {
isCopied,
copy,
};
return { isCopied, copyToClipboard };
};

/**
* It feels silly that you have to make a whole dummy input just to simulate a
* clipboard, but that's really the recommended approach for older browsers.
*
* @see {@link https://web.dev/patterns/clipboard/copy-text?hl=en}
*/
function simulateClipboardWrite(): boolean {
const previousFocusTarget = document.activeElement;
const dummyInput = document.createElement("input");

// Using visually-hidden styling to ensure that inserting the element doesn't
// cause any content reflows on the page (removes any risk of UI flickers).
// Can't use visibility:hidden or display:none, because then the elements
// can't receive focus, which is needed for the execCommand method to work
const style = dummyInput.style;
style.display = "inline-block";
style.position = "absolute";
style.overflow = "hidden";
style.clip = "rect(0 0 0 0)";
style.clipPath = "rect(0 0 0 0)";
style.height = "1px";
style.width = "1px";
style.margin = "-1px";
style.padding = "0";
style.border = "0";

document.body.appendChild(dummyInput);
dummyInput.focus();
dummyInput.select();

const isCopied = document.execCommand("copy");
dummyInput.remove();

if (previousFocusTarget instanceof HTMLElement) {
previousFocusTarget.focus();
}

return isCopied;
}
3 changes: 2 additions & 1 deletionsite/src/modules/resources/SSHButton/SSHButton.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,7 @@ export const SSHButton: FC<SSHButtonProps> = ({
Configure SSH hosts on machine:
</strong>
</HelpTooltipText>
<CodeExample code="coder config-ssh" />
<CodeExamplesecret={false}code="coder config-ssh" />
</div>

<div>
Expand All@@ -67,6 +67,7 @@ export const SSHButton: FC<SSHButtonProps> = ({
</strong>
</HelpTooltipText>
<CodeExample
secret={false}
code={`ssh ${sshPrefix}${workspaceName}.${agentName}`}
/>
</div>
Expand Down
6 changes: 2 additions & 4 deletionssite/src/pages/CliAuthPage/CliAuthPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
import { type FC } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { getApiKey } from "api/api";
import { pageTitle } from "utils/page";
import { CliAuthPageView } from "./CliAuthPageView";
import { apiKey } from "api/queries/users";

export const CliAuthenticationPage: FC = () => {
const { data } = useQuery({
queryFn: () => getApiKey(),
});
const { data } = useQuery(apiKey());

return (
<>
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp