From 3394c8aad2bfa171e50d40ea671afd60ba091cb0 Mon Sep 17 00:00:00 2001 From: Bruce Blaser Date: Fri, 23 Sep 2022 09:30:20 -0700 Subject: [PATCH] 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 --- .../templates/Challenges/classic/classic.css | 10 ++++++ .../templates/Challenges/classic/editor.tsx | 18 ++++++++--- .../Challenges/classic/mobile-layout.tsx | 9 ++++++ .../Challenges/classic/multifile-editor.tsx | 6 ++++ .../src/templates/Challenges/classic/show.tsx | 32 ++++++++++++++++--- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/client/src/templates/Challenges/classic/classic.css b/client/src/templates/Challenges/classic/classic.css index 89a3ef80e25..2c7922f5336 100644 --- a/client/src/templates/Challenges/classic/classic.css +++ b/client/src/templates/Challenges/classic/classic.css @@ -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; +} diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 01d440b02f4..06c243fe1b4 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -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 diff --git a/client/src/templates/Challenges/classic/mobile-layout.tsx b/client/src/templates/Challenges/classic/mobile-layout.tsx index f38830b6726..aee4feac001 100644 --- a/client/src/templates/Challenges/classic/mobile-layout.tsx +++ b/client/src/templates/Challenges/classic/mobile-layout.tsx @@ -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 { }); }; + handleKeyDown = () => this.props.updateUsingKeyboardInTablist(true); + + handleClick = () => this.props.updateUsingKeyboardInTablist(false); + render() { const { currentTab } = this.state; const { @@ -74,7 +79,10 @@ class MobileLayout extends Component { activeKey={currentTab} defaultActiveKey={currentTab} id='mobile-layout' + onKeyDown={this.handleKeyDown} + onMouseDown={this.handleClick} onSelect={this.switchTab} + onTouchStart={this.handleClick} > {!hasEditableBoundaries && ( { )} diff --git a/client/src/templates/Challenges/classic/multifile-editor.tsx b/client/src/templates/Challenges/classic/multifile-editor.tsx index 08b431a54a3..5c94a8e6caf 100644 --- a/client/src/templates/Challenges/classic/multifile-editor.tsx +++ b/client/src/templates/Challenges/classic/multifile-editor.tsx @@ -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 }} diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index 845a256be6a..7ab20924c7a 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -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 { // 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 { ); } - renderEditor() { + renderEditor({ isMobileLayout, isUsingKeyboardInTablist }: RenderEditorArgs) { const { pageContext: { projectPreview: { showProjectPreview } @@ -416,6 +430,8 @@ class ShowClassic extends Component { this.editorRef as MutableRefObject } initialTests={tests} + isMobileLayout={isMobileLayout} + isUsingKeyboardInTablist={isUsingKeyboardInTablist} resizeProps={this.resizeProps} title={title} usesMultifileEditor={usesMultifileEditor} @@ -494,7 +510,10 @@ class ShowClassic extends Component { { 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 { 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()}