fix(client): preview button with screenreader text (#63061)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
Anna
2026-04-13 06:57:29 -04:00
committed by GitHub
parent 02b12ac880
commit 6f059e8259
6 changed files with 87 additions and 27 deletions
@@ -584,9 +584,9 @@
"instructions": "Instructions",
"notes": "Notes",
"preview": "Preview",
"terminal": "Terminal",
"editor": "Editor",
"interactive-editor": "Interactive Editor"
"interactive-editor": "Interactive Editor",
"terminal": "Terminal"
},
"editor-alerts": {
"tab-trapped": "Pressing tab will now insert the tab character",
@@ -1034,6 +1034,10 @@
"terminal-output": "Terminal output",
"not-available": "Not available",
"interactive-editor-desc": "Turn static code examples into interactive editors. This allows you to edit and run the code directly on the page.",
"hide-terminal": "Hide the terminal",
"move-terminal-to-new-window": "Move the terminal to a new window and focus it",
"move-terminal-to-main-window": "Move the terminal to this window and close the external terminal window",
"close-external-terminal-window": "Close the external terminal window",
"pinyin-to-hanzi-input-desc": "This task uses Pinyin-to-Hanzi inputs. Type pinyin with tone numbers (1 to 5). When you enter a correct syllable, it will turn into a Chinese character. If you press backspace after a Chinese character, it will change back to pinyin and remove the last thing you typed: if it's a tone number, the tone is removed; if it's a letter, the letter is removed.",
"pinyin-tone-input-desc": "This task uses Pinyin Tone inputs. Type pinyin with tone numbers (1 to 5). When you enter a tone number, it will be converted to a tone mark. If you press backspace, the last thing you typed is removed: if it's a tone number, the tone is removed; if it's a letter, the letter is removed."
},
@@ -4,7 +4,6 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import store from 'store';
import { DailyCodingChallengeLanguages } from '../../../redux/prop-types';
import { challengeTypes } from '@freecodecamp/shared/config/challenge-types';
import EditorTabs from './editor-tabs';
interface ClassicLayoutProps {
@@ -21,9 +20,9 @@ interface ClassicLayoutProps {
showInstructions: boolean;
showPreviewPane: boolean;
showPreviewPortal: boolean;
challengeType: number;
togglePane: (pane: string) => void;
hasInteractiveEditor?: never;
usesTerminal: boolean;
hasContentOutline?: never;
}
@@ -108,7 +107,7 @@ const ActionRow = (props: ActionRowProps): JSX.Element => {
isDailyCodingChallenge,
dailyCodingChallengeLanguage,
setDailyCodingChallengeLanguage,
challengeType
usesTerminal
} = props;
// sets screen reader text for the two preview buttons
@@ -119,35 +118,39 @@ const ActionRow = (props: ActionRowProps): JSX.Element => {
portal: t('aria.open-preview-in-new-window')
};
// preview open in main window
// open in main window
if (showPreviewPane && !showPreviewPortal) {
previewBtnsSrText.pane = t('aria.hide-preview');
previewBtnsSrText.portal = t('aria.move-preview-to-new-window');
// preview open in external window
if (usesTerminal) {
previewBtnsSrText.pane = t('aria.hide-terminal');
previewBtnsSrText.portal = t('aria.move-terminal-to-new-window');
} else {
previewBtnsSrText.pane = t('aria.hide-preview');
previewBtnsSrText.portal = t('aria.move-preview-to-new-window');
}
// open in external window
} else if (showPreviewPortal && !showPreviewPane) {
previewBtnsSrText.pane = t('aria.move-preview-to-main-window');
previewBtnsSrText.portal = t('aria.close-external-preview-window');
if (usesTerminal) {
previewBtnsSrText.pane = t('aria.move-terminal-to-main-window');
previewBtnsSrText.portal = t('aria.close-external-terminal-window');
} else {
previewBtnsSrText.pane = t('aria.move-preview-to-main-window');
previewBtnsSrText.portal = t('aria.close-external-preview-window');
}
}
return previewBtnsSrText;
}
const isPythonChallenge =
challengeType === challengeTypes.python ||
challengeType === challengeTypes.multifilePythonCertProject ||
challengeType === challengeTypes.pyLab ||
challengeType === challengeTypes.dailyChallengePy;
const previewButtonText = isPythonChallenge
? t('learn.editor-tabs.terminal')
: t('learn.editor-tabs.preview');
const handleLanguageChange = (language: DailyCodingChallengeLanguages) => {
store.set('dailyCodingChallengeLanguage', language);
setDailyCodingChallengeLanguage(language);
};
const previewPaneButtonText =
usesTerminal == false
? 'learn.editor-tabs.preview'
: 'learn.editor-tabs.terminal';
return (
<div className='action-row' data-playwright-test-label='action-row'>
<div className='tabs-row' data-playwright-test-label='tabs-row'>
@@ -207,7 +210,7 @@ const ActionRow = (props: ActionRowProps): JSX.Element => {
onClick={() => togglePane('showPreviewPane')}
>
<span className='sr-only'>{getPreviewBtnsSrText().pane}</span>
<span aria-hidden='true'>{previewButtonText}</span>
<span aria-hidden='true'>{t(previewPaneButtonText)}</span>
</button>
<button
aria-expanded={!!showPreviewPortal}
@@ -271,6 +271,12 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
const editorPaneFlex =
!displayPreviewConsole && !displayPreviewPane ? 1 : editorPane.flex;
const usesTerminal =
challengeType === challengeTypes.python ||
challengeType === challengeTypes.multifilePythonCertProject ||
challengeType === challengeTypes.pyLab ||
challengeType === challengeTypes.dailyChallengePy;
return (
<div className='desktop-layout' data-playwright-test-label='desktop-layout'>
{isProjectStyle && (
@@ -287,7 +293,7 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
showPreviewPane={showPreviewPane}
showPreviewPortal={showPreviewPortal}
togglePane={togglePane}
challengeType={challengeType}
usesTerminal={usesTerminal}
data-playwright-test-label='action-row'
/>
)}
@@ -40,6 +40,7 @@ interface MobileLayoutProps {
updateUsingKeyboardInTablist: (arg0: boolean) => void;
testOutput: JSX.Element;
usesMultifileEditor: boolean;
usesTerminal: boolean;
}
const tabs = {
@@ -164,7 +165,8 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
setShowPreviewPortal,
portalWindow,
windowTitle,
usesMultifileEditor
usesMultifileEditor,
usesTerminal
} = this.props;
const displayPreviewPane = hasPreview && showPreviewPane;
@@ -206,9 +208,13 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
return portalBtnSrText;
}
const previewTriggerText =
usesTerminal == false
? 'learn.editor-tabs.preview'
: 'learn.editor-tabs.terminal';
// Unlike the desktop layout the mobile version does not have an ActionRow,
// but still needs a way to switch between the different tabs.
return (
<>
<Tabs
@@ -240,7 +246,7 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
</TabsTrigger>
{hasPreview && (
<TabsTrigger value={tabs.preview}>
{i18next.t('learn.editor-tabs.preview')}
{i18next.t(previewTriggerText)}
</TabsTrigger>
)}
</TabsList>
@@ -465,6 +465,12 @@ function ShowClassic({
);
};
const usesTerminal =
challengeType === challengeTypes.python ||
challengeType === challengeTypes.multifilePythonCertProject ||
challengeType === challengeTypes.pyLab ||
challengeType === challengeTypes.dailyChallengePy;
return (
<Hotkeys
challengeType={challengeType}
@@ -507,6 +513,7 @@ function ShowClassic({
}
updateUsingKeyboardInTablist={updateUsingKeyboardInTablist}
usesMultifileEditor={usesMultifileEditor}
usesTerminal={usesTerminal}
/>
) : (
<DesktopLayout
+34
View File
@@ -28,6 +28,40 @@ test.describe('Desktop view', () => {
await expect(previewPortalButton).toBeVisible();
});
test('Preview button is visible during HTML/CSS/JS challenges', async ({
page
}) => {
const previewPaneButton = page.getByTestId('preview-pane-button');
const previewPortalButton = page.getByRole('button', {
name: translations.aria['move-preview-to-new-window']
});
expect(previewPortalButton).toBeDefined();
const hidePreviewText = translations.aria['hide-preview'];
const previewText = translations.learn['editor-tabs']['preview'];
await expect(previewPaneButton).toHaveText(hidePreviewText + previewText);
});
test('Terminal button is visible during Python challenges', async ({
page
}) => {
await page.goto('/learn/python-v9/workshop-caesar-cipher/step-1');
const terminalPaneButton = page.getByTestId('preview-pane-button');
const terminalPortalButton = page.getByRole('button', {
name: translations.aria['move-terminal-to-new-window']
});
const hideTerminalText = translations.aria['hide-terminal'];
const terminalText = translations.learn['editor-tabs']['terminal'];
await expect(terminalPaneButton).toHaveText(
hideTerminalText + terminalText
);
expect(terminalPortalButton).toBeDefined();
});
test('Clicking instructions button hides instructions panel, but not any buttons', async ({
page
}) => {