From c5d4bedda5d4cbb751a5d170f3c83ec684dae70d Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Thu, 14 Nov 2019 09:18:08 +0100 Subject: [PATCH] feat: add accessibility toggle Users can now press ctrl/cmd+f1 to toggle the screen reader mode, in addition to using the accessibility tooltip. The mode now persists between challenges. If screen reader mode is on, the instructions are focused by default so the user is not required to navigate to them before reading starts. --- .../templates/Challenges/classic/Editor.js | 40 ++++++++++++++++++- .../src/templates/Challenges/redux/index.js | 11 ++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/client/src/templates/Challenges/classic/Editor.js b/client/src/templates/Challenges/classic/Editor.js index 29322e747d..44f4e5f6ef 100644 --- a/client/src/templates/Challenges/classic/Editor.js +++ b/client/src/templates/Challenges/classic/Editor.js @@ -6,7 +6,9 @@ import { createSelector } from 'reselect'; import { canFocusEditorSelector, executeChallenge, + inAccessibilityModeSelector, setEditorFocusability, + setAccessibilityMode, updateFile } from '../redux'; import { userSelector, isDonationModalOpenSelector } from '../../../redux'; @@ -22,6 +24,8 @@ const propTypes = { executeChallenge: PropTypes.func.isRequired, ext: PropTypes.string, fileKey: PropTypes.string, + inAccessibilityMode: PropTypes.bool.isRequired, + setAccessibilityMode: PropTypes.func.isRequired, setEditorFocusability: PropTypes.func, theme: PropTypes.string, updateFile: PropTypes.func.isRequired @@ -29,16 +33,19 @@ const propTypes = { const mapStateToProps = createSelector( canFocusEditorSelector, + inAccessibilityModeSelector, isDonationModalOpenSelector, userSelector, - (canFocus, open, { theme = 'default' }) => ({ + (canFocus, accessibilityMode, open, { theme = 'default' }) => ({ canFocus: open ? false : canFocus, + inAccessibilityMode: accessibilityMode, theme }) ); const mapDispatchToProps = { setEditorFocusability, + setAccessibilityMode, executeChallenge, updateFile }; @@ -114,7 +121,12 @@ class Editor extends Component { editorDidMount = (editor, monaco) => { this._editor = editor; - if (this.props.canFocus) { + this._editor.updateOptions({ + accessibilitySupport: this.props.inAccessibilityMode ? '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 (this.props.canFocus && !this.props.inAccessibilityMode) { this._editor.focus(); } else this.focusOnHotkeys(); this._editor.addAction({ @@ -135,9 +147,33 @@ class Editor extends Component { this.props.setEditorFocusability(false); } }); + this._editor.addAction({ + id: 'toggle-accessibility', + label: 'Toggle Accessibility Mode', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F1], + run: () => { + const currentAccessibility = this.props.inAccessibilityMode; + // The store needs to be updated first, as onDidChangeConfiguration is + // called before updateOptions returns + this.props.setAccessibilityMode(!currentAccessibility); + this._editor.updateOptions({ + accessibilitySupport: currentAccessibility ? 'auto' : 'on' + }); + } + }); this._editor.onDidFocusEditorWidget(() => this.props.setEditorFocusability(true) ); + // This is to persist changes caused by the accessibility tooltip. + // Unfortunately it relies on Monaco's implementation details + this._editor.onDidChangeConfiguration(() => { + if ( + this._editor.getConfiguration().accessibilitySupport === 2 && + !this.props.inAccessibilityMode + ) { + this.props.setAccessibilityMode(true); + } + }); }; focusOnHotkeys() { diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index a8c7a8435a..7385813d6b 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -29,6 +29,7 @@ const initialState = { }, challengeTests: [], consoleOut: '', + inAccessibilityMode: false, isCodeLocked: false, isBuildEnabled: true, modal: { @@ -78,7 +79,8 @@ export const types = createTypes( 'moveToTab', - 'setEditorFocusability' + 'setEditorFocusability', + 'setAccessibilityMode' ], ns ); @@ -152,6 +154,7 @@ export const submitChallenge = createAction(types.submitChallenge); export const moveToTab = createAction(types.moveToTab); export const setEditorFocusability = createAction(types.setEditorFocusability); +export const setAccessibilityMode = createAction(types.setAccessibilityMode); export const currentTabSelector = state => state[ns].currentTab; export const challengeFilesSelector = state => state[ns].challengeFiles; @@ -223,6 +226,8 @@ export const challengeDataSelector = state => { }; export const canFocusEditorSelector = state => state[ns].canFocusEditor; +export const inAccessibilityModeSelector = state => + state[ns].inAccessibilityMode; const MAX_LOGS_SIZE = 64 * 1024; @@ -359,6 +364,10 @@ export const reducer = handleActions( [types.setEditorFocusability]: (state, { payload }) => ({ ...state, canFocusEditor: payload + }), + [types.setAccessibilityMode]: (state, { payload }) => ({ + ...state, + inAccessibilityMode: payload }) }, initialState