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 }; +}