diff --git a/src/components/CodeEmbed/index.jsx b/src/components/CodeEmbed/index.jsx
index 1ea1502e91..d0b39fe557 100644
--- a/src/components/CodeEmbed/index.jsx
+++ b/src/components/CodeEmbed/index.jsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "preact/hooks";
+import { useLiveRegion } from '../hooks/useLiveRegion';
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { cdnLibraryUrl, cdnSoundUrl } from "@/src/globals/globals";
@@ -25,6 +26,7 @@ import { Icon } from "../Icon";
* }
*/
export const CodeEmbed = (props) => {
+ const { ref: liveRegionRef, announce } = useLiveRegion();
const [rendered, setRendered] = useState(false);
const initialCode = props.initialValue ?? "";
// Source code from Google Docs sometimes uses a unicode non-breaking space
@@ -59,6 +61,7 @@ export const CodeEmbed = (props) => {
} else {
setPreviewCodeString(codeString);
}
+ announce("Sketch is running");
};
const [previewCodeString, setPreviewCodeString] = useState(codeString);
@@ -108,6 +111,7 @@ export const CodeEmbed = (props) => {
className="bg-bg-gray-40"
onClick={() => {
setPreviewCodeString("");
+ announce("Sketch stopped");
}}
ariaLabel="Stop sketch"
>
@@ -148,6 +152,7 @@ export const CodeEmbed = (props) => {
onClick={() => {
setCodeString(initialCode);
setPreviewCodeString(initialCode);
+ announce("Code reset to initial value.");
}}
ariaLabel="Reset code to initial value"
className="bg-white text-black"
@@ -156,6 +161,7 @@ export const CodeEmbed = (props) => {
+
);
};
diff --git a/src/components/CopyCodeButton/index.tsx b/src/components/CopyCodeButton/index.tsx
index d9566fb7b2..7bc144d3c8 100644
--- a/src/components/CopyCodeButton/index.tsx
+++ b/src/components/CopyCodeButton/index.tsx
@@ -1,13 +1,20 @@
import { useState } from 'preact/hooks';
+import { useLiveRegion } from '../hooks/useLiveRegion';
import CircleButton from "../CircleButton";
interface CopyCodeButtonProps {
textToCopy: string;
+ announceOnCopy?: string;
}
-export const CopyCodeButton = ({ textToCopy }: CopyCodeButtonProps) => {
+export const CopyCodeButton = ({
+ textToCopy,
+ announceOnCopy = 'Code copied to clipboard'
+}: CopyCodeButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
+ const { ref: liveRegionRef, announce } = useLiveRegion();
+
const copyTextToClipboard = async () => {
console.log('Copy button clicked');
console.log('Text to copy:', textToCopy);
@@ -16,6 +23,9 @@ export const CopyCodeButton = ({ textToCopy }: CopyCodeButtonProps) => {
console.log('Using Clipboard API');
await navigator.clipboard.writeText(textToCopy);
console.log('Text copied successfully');
+
+ announce(announceOnCopy);
+
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
@@ -29,52 +39,56 @@ export const CopyCodeButton = ({ textToCopy }: CopyCodeButtonProps) => {
console.log('Component rendered, isCopied:', isCopied);
return (
- {
- console.log('CircleButton clicked');
- copyTextToClipboard();
- }}
- ariaLabel="Copy code to clipboard"
- className={`bg-white ${isCopied ? 'text-green-600' : 'text-black'} transition-colors duration-200`}
- >
- {isCopied ? (
-
- ) : (
-
- )}
-
+ <>
+ {
+ console.log('CircleButton clicked');
+ copyTextToClipboard();
+ }}
+ ariaLabel="Copy code to clipboard"
+ className={`bg-white ${isCopied ? 'text-green-600' : 'text-black'} transition-colors duration-200`}
+ >
+ {isCopied ? (
+
+ ) : (
+
+ )}
+
+ {/* Visually hidden live region for accessibility announcements */}
+
+ >
);
};
\ No newline at end of file
diff --git a/src/components/hooks/useLiveRegion.ts b/src/components/hooks/useLiveRegion.ts
new file mode 100644
index 0000000000..b55e10f0c3
--- /dev/null
+++ b/src/components/hooks/useLiveRegion.ts
@@ -0,0 +1,30 @@
+import { useRef, useEffect } from 'preact/hooks';
+
+export function useLiveRegion() {
+ const ref = useRef(null);
+ const timerRef = useRef | null>(null);
+
+ /** Clear any existing timer */
+ const clearTimer = () => {
+ if (timerRef.current !== null) {
+ clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ if (ref.current) ref.current.textContent = '';
+ };
+
+ const announce = (message: string, clearMessage = 1000) => {
+ const node = ref.current;
+ if (!node) return;
+ clearTimer();
+ node.textContent = message;
+ timerRef.current = setTimeout(() => {
+ if (node) node.textContent = '';
+ timerRef.current = null;
+ }, clearMessage);
+ };
+
+ useEffect(() => clearTimer, []);
+
+ return { ref, announce };
+}