From 647469a479daec7bec22da3d69116d0290454aab Mon Sep 17 00:00:00 2001 From: Bruce B Date: Tue, 19 Apr 2022 04:34:15 -0700 Subject: [PATCH] fix(a11y): beta RWD feedback for code check (#45579) * fix: beta RWD feedback for code check * cleaned up comments * move colon on hint heading to CSS * fix: keep status message box size consistent --- .../templates/Challenges/classic/editor.css | 25 ++++++ .../templates/Challenges/classic/editor.tsx | 86 ++++++++++++------- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/client/src/templates/Challenges/classic/editor.css b/client/src/templates/Challenges/classic/editor.css index e12fc72db56..868d98dcaef 100644 --- a/client/src/templates/Challenges/classic/editor.css +++ b/client/src/templates/Challenges/classic/editor.css @@ -62,6 +62,10 @@ animation-delay: 0.5s; } +.action-row-container button[aria-hidden='true'] { + display: none; +} + @keyframes example { from { background-color: var(--highlight-background); @@ -80,6 +84,27 @@ padding-top: 5px; } +#test-status .status { + font-family: revert; +} + +#test-status p { + margin: 0.5rem 0 0; +} + +#test-status h2.hint { + font-size: 1rem; + line-height: 1.5; + padding-right: 0.5rem; + /* using float instead of inline display so screen readers recognize h2 as a block element */ + float: left; + margin: 0; +} + +#test-status h2.hint::after { + content: ':'; +} + .myEditableLineDecoration { background-color: var(--gray-45); width: 15px !important; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 459587be6ce..66319364f4c 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -405,6 +405,7 @@ const Editor = (props: EditorProps): JSX.Element => { if (challengeIsComplete()) { props.submitChallenge(); } else { + clearTestFeedback(); props.executeChallenge(); } } else { @@ -593,25 +594,40 @@ const Editor = (props: EditorProps): JSX.Element => { return domNode; } + function clearTestFeedback() { + const testStatus = document.getElementById('test-status'); + if (testStatus && testStatus.innerHTML) { + const currentHeight = `${testStatus.offsetHeight}px`; + // Height will be cleared after status message has been displayed + testStatus.style.height = currentHeight; + testStatus.innerHTML = ''; + } + } + function createOutputNode(editor: editor.IStandaloneCodeEditor) { if (dataRef.current.outputNode) return dataRef.current.outputNode; const outputNode = document.createElement('div'); const statusNode = document.createElement('div'); - const hintNode = document.createElement('div'); const editorActionRow = document.createElement('div'); editorActionRow.classList.add('action-row-container'); outputNode.classList.add('editor-lower-jaw'); outputNode.appendChild(editorActionRow); - hintNode.setAttribute('id', 'test-output'); statusNode.setAttribute('id', 'test-status'); + statusNode.setAttribute('aria-live', 'assertive'); const button = document.createElement('button'); button.setAttribute('id', 'test-button'); button.classList.add('btn-block'); button.innerHTML = 'Check Your Code (Ctrl + Enter)'; + const submitButton = document.createElement('button'); + submitButton.setAttribute('id', 'submit-button'); + submitButton.setAttribute('aria-hidden', 'true'); + submitButton.innerHTML = 'Submit your code (Ctrl + Enter)'; + submitButton.classList.add('btn-block'); editorActionRow.appendChild(button); + editorActionRow.appendChild(submitButton); editorActionRow.appendChild(statusNode); - editorActionRow.appendChild(hintNode); button.onclick = () => { + clearTestFeedback(); const { executeChallenge } = props; executeChallenge(); }; @@ -630,17 +646,12 @@ const Editor = (props: EditorProps): JSX.Element => { if (testButton) { testButton.innerHTML = 'Check Your Code (Ctrl + Enter)'; testButton.onclick = () => { + clearTestFeedback(); props.executeChallenge(); }; } - const testStatus = document.getElementById('test-status'); - if (testStatus) { - testStatus.innerHTML = ''; - } - const testOutput = document.getElementById('test-output'); - if (testOutput) { - testOutput.innerHTML = ''; - } + + clearTestFeedback(); // Resetting margin decorations const range = model?.getDecorationRange(insideEditDecId); @@ -1019,18 +1030,32 @@ const Editor = (props: EditorProps): JSX.Element => { const { output } = props; const { model, insideEditDecId } = dataRef.current; const editableRegion = getEditableRegionFromRedux(); + const isEditorInFocus = document.activeElement?.tagName === 'TEXTAREA'; if (editableRegion.length === 2) { - const testOutput = document.getElementById('test-output'); const testStatus = document.getElementById('test-status'); if (challengeIsComplete()) { const testButton = document.getElementById('test-button'); - if (testButton) { - testButton.innerHTML = - 'Submit your code and go to next challenge (Ctrl + Enter)'; - testButton.onclick = () => { + // In case test button has focus, only visually hide it for now so we + // don't lose the focus before we set it on submit button. + testButton?.classList.add('sr-only'); + const submitButton = document.getElementById('submit-button'); + if (submitButton) { + submitButton.removeAttribute('aria-hidden'); + submitButton.onclick = () => { + clearTestFeedback(); const { submitChallenge } = props; submitChallenge(); }; + // Delay setting focus on submit button to ensure aria-live status + // message is announced first by screen reader. + setTimeout(() => { + // Must set focus on submit button before removing test button from + // accessibility API since test button might have focus. + if (!isEditorInFocus) { + submitButton.focus(); + } + testButton?.setAttribute('aria-hidden', 'true'); + }, 500); } const range = model?.getDecorationRange(insideEditDecId); @@ -1044,23 +1069,26 @@ const Editor = (props: EditorProps): JSX.Element => { ); } - if (testOutput && testStatus) { - testOutput.innerHTML = ''; - testStatus.innerHTML = '✅ Step completed.'; + const submitKeyboardInstructions = isEditorInFocus + ? 'Use Ctrl + Enter to submit.' + : ''; + + if (testStatus) { + testStatus.innerHTML = `

Congratulations, your code passes. Submit your code to complete this step and move on to the next one. ${submitKeyboardInstructions}

`; + testStatus.style.height = ''; } - } else if (challengeHasErrors() && testStatus && testOutput) { + } else if (challengeHasErrors() && testStatus) { const wordsArray = [ - "Not quite. Here's a hint:", - 'Try again. This might help:', - 'Keep trying. A quick hint for you:', - "You're getting there. This may help:", - "Hang in there. You'll get there. A hint:", - "Don't give up. Here's a hint to get you thinking:" + 'Try again.', + 'Keep trying.', + "You're getting there.", + 'Hang in there.', + "Don't give up." ]; - testStatus.innerHTML = `✖️ ${ + testStatus.innerHTML = `

Sorry, your code does not pass. ${ wordsArray[Math.floor(Math.random() * wordsArray.length)] - }`; - testOutput.innerHTML = `${output[1]}`; + }

Hint

${output[1]}
`; + testStatus.style.height = ''; } } // eslint-disable-next-line react-hooks/exhaustive-deps