mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-01-09 06:04:17 -05:00
fix(a11y): improve keyboard accessibility in tablist (#45866)
* chore: resolve conflicts * fix: focus outline on console pane * refactor: focus indicator on console pane * chore: remove commented code * chore: resolve conflicts * chore: add newline to end of file * chore: fixed for prettier's sake
This commit is contained in:
@@ -110,3 +110,13 @@
|
||||
flex: 1;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
/* Focus indicator for tab panel */
|
||||
.nav-tabs [role='tab']:focus {
|
||||
outline: 2px solid var(--blue-mid);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.nav-tabs [role='tab']:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -89,8 +89,10 @@ interface EditorProps {
|
||||
initialExt: string;
|
||||
initTests: (tests: Test[]) => void;
|
||||
initialTests: Test[];
|
||||
isMobileLayout: boolean;
|
||||
isResetting: boolean;
|
||||
isSignedIn: boolean;
|
||||
isUsingKeyboardInTablist: boolean;
|
||||
openHelpModal: () => void;
|
||||
openResetModal: () => void;
|
||||
output: string[];
|
||||
@@ -373,6 +375,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: typeof monacoEditor
|
||||
) => {
|
||||
const { isMobileLayout, isUsingKeyboardInTablist } = props;
|
||||
// TODO this should *probably* be set on focus
|
||||
editorRef.current = editor;
|
||||
dataRef.current.editor = editor;
|
||||
@@ -405,11 +408,16 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
editor.updateOptions({
|
||||
accessibilitySupport: accessibilityMode ? 'on' : 'auto'
|
||||
});
|
||||
// Users who are using screen readers should not have to move focus from
|
||||
// the editor to the description every time they open a challenge.
|
||||
if (props.canFocus && !accessibilityMode) {
|
||||
focusIfTargetEditor();
|
||||
} else focusOnHotkeys();
|
||||
|
||||
// Focus should not automatically leave the 'Code' tab when using a keyboard
|
||||
// to navigate the tablist.
|
||||
if (!isMobileLayout || !isUsingKeyboardInTablist) {
|
||||
// Users who are using screen readers should not have to move focus from
|
||||
// the editor to the description every time they open a challenge.
|
||||
if (props.canFocus && !accessibilityMode) {
|
||||
focusIfTargetEditor();
|
||||
} else focusOnHotkeys();
|
||||
}
|
||||
// Removes keybind for intellisense
|
||||
// Private method - hopefully changes with future version
|
||||
// ref: https://github.com/microsoft/monaco-editor/issues/102
|
||||
|
||||
@@ -14,6 +14,7 @@ interface MobileLayoutProps {
|
||||
instructions: JSX.Element;
|
||||
notes: ReactElement;
|
||||
preview: JSX.Element;
|
||||
updateUsingKeyboardInTablist: (arg0: boolean) => void;
|
||||
testOutput: JSX.Element;
|
||||
videoUrl: string;
|
||||
usesMultifileEditor: boolean;
|
||||
@@ -44,6 +45,10 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyDown = () => this.props.updateUsingKeyboardInTablist(true);
|
||||
|
||||
handleClick = () => this.props.updateUsingKeyboardInTablist(false);
|
||||
|
||||
render() {
|
||||
const { currentTab } = this.state;
|
||||
const {
|
||||
@@ -74,7 +79,10 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
|
||||
activeKey={currentTab}
|
||||
defaultActiveKey={currentTab}
|
||||
id='mobile-layout'
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onMouseDown={this.handleClick}
|
||||
onSelect={this.switchTab}
|
||||
onTouchStart={this.handleClick}
|
||||
>
|
||||
{!hasEditableBoundaries && (
|
||||
<TabPane
|
||||
@@ -86,6 +94,7 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
|
||||
)}
|
||||
<TabPane
|
||||
eventKey={Tab.Editor}
|
||||
tabIndex='0'
|
||||
title={i18next.t('learn.editor-tabs.code')}
|
||||
{...editorTabPaneProps}
|
||||
>
|
||||
|
||||
@@ -38,6 +38,8 @@ interface MultifileEditorProps {
|
||||
initialEditorContent?: string;
|
||||
initialExt?: string;
|
||||
initialTests: Test[];
|
||||
isMobileLayout: boolean;
|
||||
isUsingKeyboardInTablist: boolean;
|
||||
output?: string[];
|
||||
resizeProps: ResizeProps;
|
||||
title: string;
|
||||
@@ -77,6 +79,8 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
||||
description,
|
||||
editorRef,
|
||||
initialTests,
|
||||
isMobileLayout,
|
||||
isUsingKeyboardInTablist,
|
||||
resizeProps,
|
||||
title,
|
||||
visibleEditors: { stylescss, indexhtml, scriptjs, indexjsx },
|
||||
@@ -143,6 +147,8 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
||||
editorRef={editorRef}
|
||||
fileKey={key as FileKey}
|
||||
initialTests={initialTests}
|
||||
isMobileLayout={isMobileLayout}
|
||||
isUsingKeyboardInTablist={isUsingKeyboardInTablist}
|
||||
resizeProps={resizeProps}
|
||||
contents={props.contents ?? ''}
|
||||
dimensions={props.dimensions ?? { height: 0, width: 0 }}
|
||||
|
||||
@@ -118,6 +118,7 @@ interface ShowClassicProps {
|
||||
interface ShowClassicState {
|
||||
layout: ReflexLayout;
|
||||
resizing: boolean;
|
||||
usingKeyboardInTablist: boolean;
|
||||
}
|
||||
|
||||
interface ReflexLayout {
|
||||
@@ -129,6 +130,11 @@ interface ReflexLayout {
|
||||
testsPane: { flex: number };
|
||||
}
|
||||
|
||||
interface RenderEditorArgs {
|
||||
isMobileLayout: boolean;
|
||||
isUsingKeyboardInTablist: boolean;
|
||||
}
|
||||
|
||||
const REFLEX_LAYOUT = 'challenge-layout';
|
||||
const BASE_LAYOUT = {
|
||||
codePane: { flex: 1 },
|
||||
@@ -167,12 +173,20 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
// layout: Holds the information of the panes sizes for desktop view
|
||||
this.state = {
|
||||
layout: this.getLayoutState(),
|
||||
resizing: false
|
||||
resizing: false,
|
||||
usingKeyboardInTablist: false
|
||||
};
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
this.editorRef = React.createRef();
|
||||
this.instructionsPanelRef = React.createRef();
|
||||
|
||||
this.updateUsingKeyboardInTablist =
|
||||
this.updateUsingKeyboardInTablist.bind(this);
|
||||
}
|
||||
|
||||
updateUsingKeyboardInTablist(usingKeyboardInTablist: boolean): void {
|
||||
this.setState({ usingKeyboardInTablist });
|
||||
}
|
||||
|
||||
getLayoutState(): ReflexLayout {
|
||||
@@ -389,7 +403,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
);
|
||||
}
|
||||
|
||||
renderEditor() {
|
||||
renderEditor({ isMobileLayout, isUsingKeyboardInTablist }: RenderEditorArgs) {
|
||||
const {
|
||||
pageContext: {
|
||||
projectPreview: { showProjectPreview }
|
||||
@@ -416,6 +430,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
this.editorRef as MutableRefObject<editor.IStandaloneCodeEditor>
|
||||
}
|
||||
initialTests={tests}
|
||||
isMobileLayout={isMobileLayout}
|
||||
isUsingKeyboardInTablist={isUsingKeyboardInTablist}
|
||||
resizeProps={this.resizeProps}
|
||||
title={title}
|
||||
usesMultifileEditor={usesMultifileEditor}
|
||||
@@ -494,7 +510,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<Helmet title={windowTitle} />
|
||||
<Media maxWidth={MAX_MOBILE_WIDTH}>
|
||||
<MobileLayout
|
||||
editor={this.renderEditor()}
|
||||
editor={this.renderEditor({
|
||||
isMobileLayout: true,
|
||||
isUsingKeyboardInTablist: this.state.usingKeyboardInTablist
|
||||
})}
|
||||
guideUrl={getGuideUrl({ forumTopicId, title })}
|
||||
hasEditableBoundaries={hasEditableBoundaries}
|
||||
hasNotes={!!notes}
|
||||
@@ -505,6 +524,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
notes={this.renderNotes(notes)}
|
||||
preview={this.renderPreview()}
|
||||
testOutput={this.renderTestOutput()}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
updateUsingKeyboardInTablist={this.updateUsingKeyboardInTablist}
|
||||
usesMultifileEditor={usesMultifileEditor}
|
||||
videoUrl={this.getVideoUrl()}
|
||||
/>
|
||||
@@ -514,7 +535,10 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
block={block}
|
||||
challengeFiles={challengeFiles}
|
||||
challengeType={challengeType}
|
||||
editor={this.renderEditor()}
|
||||
editor={this.renderEditor({
|
||||
isMobileLayout: false,
|
||||
isUsingKeyboardInTablist: this.state.usingKeyboardInTablist
|
||||
})}
|
||||
hasEditableBoundaries={hasEditableBoundaries}
|
||||
hasNotes={!!notes}
|
||||
hasPreview={this.hasPreview()}
|
||||
|
||||
Reference in New Issue
Block a user