diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index df9b4692c17..d2e7e9bd019 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -503,7 +503,8 @@ "open-preview-in-new-window": "Open the preview in a new window and focus it", "step": "Step", "steps": "Steps", - "steps-for": "Steps for {{blockTitle}}" + "steps-for": "Steps for {{blockTitle}}", + "code-example": "{{codeName}} code example" }, "flash": { "honest-first": "To claim a certification, you must first accept our academic honesty policy", diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index ec1e0092d1d..7ba75d038af 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -7,7 +7,7 @@ import type { editor // eslint-disable-next-line import/no-duplicates } from 'monaco-editor/esm/vs/editor/editor.api'; -import { highlightAllUnder } from 'prismjs'; +import Prism from 'prismjs'; import React, { useEffect, Suspense, @@ -66,6 +66,7 @@ import { isChallengeCompletedSelector } from '../redux/selectors'; import GreenPass from '../../../assets/icons/green-pass'; +import { enhancePrismAccessibility } from '../utils/index'; import LowerJaw from './lower-jaw'; import './editor.css'; @@ -725,7 +726,8 @@ const Editor = (props: EditorProps): JSX.Element => { descContainer.appendChild(jawHeading); descContainer.appendChild(desc); desc.innerHTML = description; - highlightAllUnder(desc); + Prism.hooks.add('complete', enhancePrismAccessibility); + Prism.highlightAllUnder(desc); domNode.style.userSelect = 'text'; diff --git a/client/src/templates/Challenges/components/prism-formatted.tsx b/client/src/templates/Challenges/components/prism-formatted.tsx index b71e92603eb..298c1509ae8 100644 --- a/client/src/templates/Challenges/components/prism-formatted.tsx +++ b/client/src/templates/Challenges/components/prism-formatted.tsx @@ -1,5 +1,6 @@ import Prism from 'prismjs'; import React, { useRef, useEffect } from 'react'; +import { enhancePrismAccessibility } from '../utils'; interface PrismFormattedProps { className?: string; @@ -12,6 +13,7 @@ function PrismFormatted({ className, text }: PrismFormattedProps): JSX.Element { useEffect(() => { // Just in case 'current' has not been created, though it should have been. if (instructionsRef.current) { + Prism.hooks.add('complete', enhancePrismAccessibility); Prism.highlightAllUnder(instructionsRef.current); } }, []); diff --git a/client/src/templates/Challenges/utils/index.ts b/client/src/templates/Challenges/utils/index.ts index 2d6e96dee8b..81f0fb5146b 100644 --- a/client/src/templates/Challenges/utils/index.ts +++ b/client/src/templates/Challenges/utils/index.ts @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import envData from '../../../../../config/env.json'; const { forumLocation } = envData; @@ -31,3 +32,37 @@ export function transformEditorLink(url: string): string { '//glitch.com/edit/#!/$' ); } + +export function enhancePrismAccessibility( + prismEnv: Prism.hooks.ElementHighlightedEnvironment +) { + const langs: { [key: string]: string } = { + js: 'JavaScript', + javascript: 'JavaScript', + css: 'CSS', + html: 'HTML', + python: 'python', + py: 'python', + xml: 'XML', + jsx: 'JSX', + scss: 'SCSS', + sql: 'SQL', + http: 'HTTP', + json: 'JSON', + pug: 'pug' + }; + const parent = prismEnv?.element?.parentElement; + if (parent && parent.nodeName === 'PRE' && parent.tabIndex === 0) { + parent.setAttribute('role', 'region'); + const codeType = prismEnv.element?.className + .replace(/language-(.*)/, '$1') + .toLowerCase(); + const codeName = langs[codeType] || ''; + parent.setAttribute( + 'aria-label', + i18next.t('aria.code-example', { + codeName + }) + ); + } +}