Add permission check around clipboard access (#22781)

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2023-08-29 13:50:38 +02:00 committed by GitHub
parent 8e649b05c9
commit 82a94cb12b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 22 deletions

View file

@ -317,6 +317,7 @@
"copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.",
"copySuccess": "Successfully copied to clipboard!",
"clipboardCopyError": "Error copying to clipboard.",
"clipboardCopyDenied": "Your browser is blocking access to the clipboard.",
"copyToClipboard": "Copy to clipboard",
"clientRegistration": "Client registration",
"anonymousAccessPolicies": "Anonymous access polices",

View file

@ -6,6 +6,7 @@ import {
} from "@patternfly/react-core";
import useSetTimeout from "../../utils/useSetTimeout";
import useQueryPermission from "../../utils/useQueryPermission";
enum CopyState {
Ready,
@ -27,32 +28,39 @@ export const CopyToClipboardButton = ({
}: CopyToClipboardButtonProps) => {
const { t } = useTranslation("clients");
const setTimeout = useSetTimeout();
const permission = useQueryPermission("clipboard-write" as PermissionName);
const permissionDenied = permission?.state === "denied";
const [copyState, setCopyState] = useState(CopyState.Ready);
const [copy, setCopy] = useState(CopyState.Ready);
// Determine the message to use for the copy button.
const copyMessageKey = useMemo(() => {
if (permissionDenied) {
return "clipboardCopyDenied";
}
const copyMessage = useMemo(() => {
switch (copy) {
switch (copyState) {
case CopyState.Ready:
return t("copyToClipboard");
return "copyToClipboard";
case CopyState.Copied:
return t("copySuccess");
return "copySuccess";
case CopyState.Error:
return t("clipboardCopyError");
return "clipboardCopyError";
}
}, [copy]);
}, [permissionDenied, copyState]);
// Reset the message of the copy button after copying to the clipboard.
useEffect(() => {
if (copy !== CopyState.Ready) {
return setTimeout(() => setCopy(CopyState.Ready), 1000);
if (copyState !== CopyState.Ready) {
return setTimeout(() => setCopyState(CopyState.Ready), 1000);
}
}, [copy]);
}, [copyState, setTimeout]);
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopy(CopyState.Copied);
setCopyState(CopyState.Copied);
} catch (error) {
setCopy(CopyState.Error);
setCopyState(CopyState.Error);
}
};
@ -65,7 +73,7 @@ export const CopyToClipboardButton = ({
exitDelay={600}
variant={variant}
>
{copyMessage}
{t(copyMessageKey)}
</ClipboardCopyButton>
);
};

View file

@ -0,0 +1,54 @@
import { useState, useEffect } from "react";
/** A 'plain' object version of the permission status. */
export type PlainPermissionStatus = {
readonly name: string;
readonly state: PermissionState;
};
export default function useQueryPermission(
name: PermissionName,
): PlainPermissionStatus | null {
const [status, setStatus] = useState<PermissionStatus | null>(null);
const [plainStatus, setPlainStatus] = useState<PlainPermissionStatus | null>(
null,
);
function updatePlainStatus(newStatus: PermissionStatus) {
setPlainStatus({
name: newStatus.name,
state: newStatus.state,
});
}
// Query the permission status when the name changes.
useEffect(() => {
setStatus(null);
setPlainStatus(null);
navigator.permissions.query({ name }).then((newStatus) => {
setStatus(newStatus);
updatePlainStatus(newStatus);
});
}, [name]);
// Update the 'plain' status when the permission status changes.
useEffect(() => {
if (!status) {
return;
}
function onStatusChange() {
if (!status) {
return;
}
updatePlainStatus(status);
}
status.addEventListener("change", onStatusChange);
return () => status.removeEventListener("change", onStatusChange);
}, [status]);
return plainStatus;
}

View file

@ -1,8 +1,8 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useCallback } from "react";
export default function useSetTimeout() {
const didUnmountRef = useRef(false);
const { current: scheduledTimers } = useRef(new Set<number>());
const scheduledTimersRef = useRef(new Set<number>());
useEffect(() => {
didUnmountRef.current = false;
@ -14,27 +14,27 @@ export default function useSetTimeout() {
}, []);
function clearAll() {
scheduledTimers.forEach((timer) => clearTimeout(timer));
scheduledTimers.clear();
scheduledTimersRef.current.forEach((timer) => clearTimeout(timer));
scheduledTimersRef.current.clear();
}
return function scheduleTimeout(callback: () => void, delay: number) {
return useCallback((callback: () => void, delay: number) => {
if (didUnmountRef.current) {
throw new Error("Can't schedule a timeout on an unmounted component.");
}
const timer = Number(setTimeout(handleCallback, delay));
scheduledTimers.add(timer);
scheduledTimersRef.current.add(timer);
function handleCallback() {
scheduledTimers.delete(timer);
scheduledTimersRef.current.delete(timer);
callback();
}
return function cancelTimeout() {
clearTimeout(timer);
scheduledTimers.delete(timer);
scheduledTimersRef.current.delete(timer);
};
};
}, []);
}