diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index 69e239bc9de..5d289062c55 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -849,7 +849,8 @@ "opens-new-window": "Opens in new window", "rsa-checkbox": "I have tried the Read-Search-Ask method", "similar-questions-checkbox": "I have searched for similar questions that have already been answered on the forum", - "edit-my-profile": "Edit my profile" + "edit-my-profile": "Edit my profile", + "terminal-output": "Terminal output" }, "flash": { "no-email-in-userinfo": "We could not retrieve an email from your chosen provider. Please try another provider or use the 'Continue with Email' option.", diff --git a/client/src/templates/Challenges/classic/xterm.tsx b/client/src/templates/Challenges/classic/xterm.tsx index da988c72a65..42a815dc5b4 100644 --- a/client/src/templates/Challenges/classic/xterm.tsx +++ b/client/src/templates/Challenges/classic/xterm.tsx @@ -1,6 +1,7 @@ import React, { MutableRefObject, useEffect, useRef } from 'react'; import type { IDisposable, Terminal } from 'xterm'; import type { FitAddon } from 'xterm-addon-fit'; +import { useTranslation } from 'react-i18next'; import { registerTerminal } from '../utils/python-worker-handler'; @@ -23,6 +24,7 @@ export const XtermTerminal = ({ dimensions?: { height: number; width: number }; }) => { const termContainerRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { void registerServiceWorker(); @@ -43,7 +45,25 @@ export const XtermTerminal = ({ if (termContainerRef.current) term.open(termContainerRef.current); fitAddon.fit(); - const print = (text?: string) => term?.writeln(`${text ?? ''}`); + // xterm does provide a11y support via the `screenReaderMode` option. + // However, the mode only works best if the user interacts with the terminal directly. + // Since we feed the content to xterm, it's better to control the output a11y ourselves. + const termContainerDiv = + termContainerRef.current?.querySelector('.xterm'); + const outputForScreenReader = document.createElement('div'); + + outputForScreenReader.setAttribute('role', 'region'); + outputForScreenReader.setAttribute( + 'aria-label', + t('aria.terminal-output') + ); + outputForScreenReader.classList.add('sr-only'); + termContainerDiv?.appendChild(outputForScreenReader); + + const print = (text?: string) => { + term?.writeln(`${text ?? ''}`); + outputForScreenReader.textContent = text ?? ''; + }; // TODO: prevent user from moving cursor outside the current input line and // handle insertion and deletion properly. While backspace and delete don't @@ -94,6 +114,8 @@ export const XtermTerminal = ({ term?.write('\x1bc'); disposables.forEach(disposable => disposable.dispose()); disposables.length = 0; + + outputForScreenReader.textContent = ''; }; registerTerminal({ print, input, reset }); } @@ -103,7 +125,7 @@ export const XtermTerminal = ({ return () => { term?.dispose(); }; - }, [xtermFitRef]); + }, [xtermFitRef, t]); useEffect(() => { if (xtermFitRef.current) xtermFitRef.current.fit();