From 97d88a1281b1946db03464de6938203fb0b4e514 Mon Sep 17 00:00:00 2001 From: Tony Anziano Date: Tue, 17 Sep 2019 16:26:34 -0700 Subject: [PATCH] Added screen reader alerts for various actions throughout the app. --- CHANGELOG.md | 1 + .../src/ui/a11y/ariaAlertService.spec.ts | 60 +++++++++++++++++++ .../client/src/ui/a11y/ariaAlertService.ts | 57 ++++++++++++++++++ packages/app/client/src/ui/a11y/index.ts | 34 +++++++++++ .../botCreationDialog.spec.tsx | 27 ++++++++- .../botCreationDialog/botCreationDialog.tsx | 31 ++++++---- .../openBotDialog/openBotDialog.spec.tsx | 11 ++++ .../dialogs/openBotDialog/openBotDialog.tsx | 11 ++++ .../openBotDialog/openBotDialogContainer.ts | 4 ++ .../appSettingsEditor.spec.tsx | 5 ++ .../appSettingsEditor/appSettingsEditor.tsx | 3 +- .../appSettingsEditorContainer.ts | 4 ++ .../parts/inspector/inspector.spec.tsx | 14 ++++- .../emulator/parts/inspector/inspector.tsx | 6 ++ .../parts/inspector/inspectorContainer.ts | 4 ++ .../main/src/extensions/inspector-preload.js | 4 ++ .../json/src/windowHostReceiver.tsx | 9 ++- packages/sdk/client/src/extensions/host.ts | 3 + .../shared/src/types/ipc/extensionChannel.ts | 1 + 19 files changed, 273 insertions(+), 16 deletions(-) create mode 100644 packages/app/client/src/ui/a11y/ariaAlertService.spec.ts create mode 100644 packages/app/client/src/ui/a11y/ariaAlertService.ts create mode 100644 packages/app/client/src/ui/a11y/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3fc860e..8dd14ec95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [1867](https://github.com/microsoft/BotFramework-Emulator/pull/1867) - [1871](https://github.com/microsoft/BotFramework-Emulator/pull/1871) - [1872](https://github.com/microsoft/BotFramework-Emulator/pull/1872) + - [1873](https://github.com/microsoft/BotFramework-Emulator/pull/1873) - [client] Fixed an issue with the transcripts path input inside of the resource settings dialog in PR [1836](https://github.com/microsoft/BotFramework-Emulator/pull/1836) diff --git a/packages/app/client/src/ui/a11y/ariaAlertService.spec.ts b/packages/app/client/src/ui/a11y/ariaAlertService.spec.ts new file mode 100644 index 000000000..25e12a534 --- /dev/null +++ b/packages/app/client/src/ui/a11y/ariaAlertService.spec.ts @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +import { ariaAlertService } from './ariaAlertService'; + +describe('AriaAlertService', () => { + it('should create an aria alert and only one at a time', () => { + ariaAlertService.alert('I am an alert!'); + const alertElement = document.querySelector('span#alert-from-service') as HTMLSpanElement; + + expect(alertElement).toBeTruthy(); + expect(alertElement.innerText).toBe('I am an alert!'); + + ariaAlertService.alert('I am another alert!'); + + const alertElements = document.querySelectorAll('span#alert-from-service'); + + expect(alertElements.length).toBe(1); + }); + + it('should not create an aria alert if there is no message', () => { + // make sure there are no leftover alerts from previous test(s) + const preExistingAlerts = document.querySelectorAll('span#alert-from-service'); + preExistingAlerts.forEach(alert => alert.remove()); + ariaAlertService.alert(undefined); + const alertElement = document.querySelector('span#alert-from-service') as HTMLSpanElement; + + expect(alertElement).toBeFalsy(); + }); +}); diff --git a/packages/app/client/src/ui/a11y/ariaAlertService.ts b/packages/app/client/src/ui/a11y/ariaAlertService.ts new file mode 100644 index 000000000..6facbcb45 --- /dev/null +++ b/packages/app/client/src/ui/a11y/ariaAlertService.ts @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +let singleton: AriaAlertService; +class AriaAlertService { + constructor() { + singleton = this; + } + + /** Creates an alert and inserts it into the DOM */ + public alert(msg: string): void { + if (!msg) { + return; + } + const prevAlert = document.querySelector('span#alert-from-service'); + prevAlert && prevAlert.remove(); + const alert = document.createElement('span'); + alert.innerText = msg; + alert.setAttribute('id', 'alert-from-service'); + alert.setAttribute('role', 'alert'); + alert.setAttribute('style', 'position: absolute; top: -9999px; overflow: hidden;'); + document.body.appendChild(alert); + } +} + +/** Creates invisible alerts to be read by screen reader technologies */ +export const ariaAlertService = singleton || new AriaAlertService(); diff --git a/packages/app/client/src/ui/a11y/index.ts b/packages/app/client/src/ui/a11y/index.ts new file mode 100644 index 000000000..8a0646f18 --- /dev/null +++ b/packages/app/client/src/ui/a11y/index.ts @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. +// +// Microsoft Bot Framework: http://botframework.com +// +// Bot Framework Emulator Github: +// https://github.com/Microsoft/BotFramwork-Emulator +// +// Copyright (c) Microsoft Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +export * from './ariaAlertService'; diff --git a/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.spec.tsx b/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.spec.tsx index 6147fd2d1..e84990303 100644 --- a/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.spec.tsx +++ b/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.spec.tsx @@ -31,11 +31,12 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import * as React from 'react'; import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared'; import { ActiveBotHelper } from '../../helpers/activeBotHelper'; +import { ariaAlertService } from '../../a11y'; import { BotCreationDialog, BotCreationDialogState } from './botCreationDialog'; @@ -85,7 +86,7 @@ describe('BotCreationDialog tests', () => { commandService = descriptor.descriptor.get(); }); - let testWrapper; + let testWrapper: ReactWrapper; beforeEach(() => { testWrapper = mount(); }); @@ -141,9 +142,11 @@ describe('BotCreationDialog tests', () => { }); (window.document.getElementById as any) = mockGetElementById; window.document.execCommand = mockExec; + const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined); (testWrapper.instance() as any).onCopyClick(); expect(mockExec).toHaveBeenCalledWith('copy'); + expect(alertServiceSpy).toHaveBeenCalledWith('Secret copied to clipboard.'); // restore window functions window.document.execCommand = backupExec; @@ -238,4 +241,24 @@ describe('BotCreationDialog tests', () => { null ); }); + + it('should toggle the visibility of the secret', () => { + const spy = jest.spyOn(ariaAlertService, 'alert'); + testWrapper.setState({ encryptKey: true, revealSecret: false }); + testWrapper.instance().onRevealSecretClick(); + + expect(spy).toHaveBeenCalledWith('Secret showing.'); + expect(testWrapper.instance().state.revealSecret).toBe(true); + + testWrapper.instance().onRevealSecretClick(); + + expect(spy).toHaveBeenCalledWith('Secret hidden.'); + }); + + it('should not toggle the visibility of the secret if the encryption is disabled', () => { + testWrapper.setState({ encryptKey: false, revealSecret: false }); + testWrapper.instance().onRevealSecretClick(); + + expect(testWrapper.instance().state.revealSecret).toBe(false); + }); }); diff --git a/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.tsx b/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.tsx index 40d3b8376..1f020e8cd 100644 --- a/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.tsx +++ b/packages/app/client/src/ui/dialogs/botCreationDialog/botCreationDialog.tsx @@ -54,6 +54,7 @@ import { store } from '../../../state/store'; import { generateBotSecret } from '../../../utils'; import { ActiveBotHelper } from '../../helpers/activeBotHelper'; import { DialogService } from '../service'; +import { ariaAlertService } from '../../a11y'; import * as styles from './botCreationDialog.scss'; @@ -70,6 +71,8 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat @CommandServiceInstance() public commandService: CommandServiceImpl; + private secretInputRef: HTMLInputElement; + public constructor(props: {}, context: BotCreationDialogState) { super(props, context); @@ -171,12 +174,13 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
    @@ -261,21 +265,22 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat if (!this.state.encryptKey) { return null; } - this.setState({ revealSecret: !this.state.revealSecret }); + const revealSecret = !this.state.revealSecret; + ariaAlertService.alert(`Secret ${revealSecret ? 'showing' : 'hidden'}.`); + this.setState({ revealSecret }); }; private onCopyClick = (): void => { if (!this.state.encryptKey) { return null; } - const input: HTMLInputElement = window.document.getElementById('key-input') as HTMLInputElement; - input.removeAttribute('disabled'); - const { type } = input; - input.type = 'text'; - input.select(); + const { secretInputRef } = this; + const { type } = secretInputRef; + secretInputRef.type = 'text'; + secretInputRef.select(); window.document.execCommand('copy'); - input.type = type; - input.setAttribute('disabled', ''); + secretInputRef.type = type; + ariaAlertService.alert('Secret copied to clipboard.'); }; // TODO: Re-enable ability to re-generate secret after 4.1 @@ -364,4 +369,8 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat const controllerRegEx = /api\/messages\/?$/; return controllerRegEx.test(endpoint) ? '' : `Please include route if necessary: "/api/messages"`; } + + private setSecretInputRef = (ref: HTMLInputElement): void => { + this.secretInputRef = ref; + }; } diff --git a/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.spec.tsx b/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.spec.tsx index 2716a2fd1..68b2561f3 100644 --- a/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.spec.tsx +++ b/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.spec.tsx @@ -44,6 +44,7 @@ import { clientAwareSettingsChanged } from '../../../state/actions/clientAwareSe import { bot } from '../../../state/reducers/bot'; import { clientAwareSettings } from '../../../state/reducers/clientAwareSettings'; import { DialogService } from '../service'; +import { ariaAlertService } from '../../a11y'; import { OpenBotDialog } from './openBotDialog'; import { OpenBotDialogContainer } from './openBotDialogContainer'; @@ -243,4 +244,14 @@ describe('The OpenBotDialog', () => { expect(instance.state.botUrl).toBe('http://localhost:3978'); }); + + it('should announce any validation error messages', () => { + // make sure there are no leftover alerts from previous test(s) + const preExistingAlerts = document.querySelectorAll('body > span'); + preExistingAlerts.forEach(alert => alert.remove()); + const spy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined); + instance.announceErrorMessage('Invalid bot url.'); + + expect(spy).toHaveBeenCalledWith('Invalid bot url.'); + }); }); diff --git a/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.tsx b/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.tsx index 552ee70f9..8392c4459 100644 --- a/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.tsx +++ b/packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.tsx @@ -48,6 +48,7 @@ import { EmulatorMode } from '@bfemulator/sdk-shared'; import * as openBotStyles from './openBotDialog.scss'; export interface OpenBotDialogProps { + createAriaAlert?: (msg: string) => void; mode?: EmulatorMode; isDebug?: boolean; onDialogCancel?: () => void; @@ -127,6 +128,7 @@ export class OpenBotDialog extends Component void): OpenBotDialogProps => { return { + createAriaAlert: (msg: string) => { + ariaAlertService.alert(msg); + }, openBot: (componentState: OpenBotDialogState) => { DialogService.hideDialog(); const { appId = '', appPassword = '', botUrl = '', mode = 'livechat-url', isAzureGov } = componentState; diff --git a/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.spec.tsx b/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.spec.tsx index 5d7f8c4d6..96870b004 100644 --- a/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.spec.tsx +++ b/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.spec.tsx @@ -41,6 +41,7 @@ import * as EditorActions from '../../../state/actions/editorActions'; import { setFrameworkSettings, saveFrameworkSettings } from '../../../state/actions/frameworkSettingsActions'; import { getTabGroupForDocument } from '../../../state/helpers/editorHelpers'; import { framework } from '../../../state/reducers/framework'; +import { ariaAlertService } from '../../a11y'; import { AppSettingsEditor } from './appSettingsEditor'; import { AppSettingsEditorContainer } from './appSettingsEditorContainer'; @@ -163,6 +164,8 @@ describe('The AppSettingsEditorContainer', () => { }); it('should save the framework settings then get them again from main when the "onSaveClick" handler is called', async () => { + const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined); + await (instance as any).onSaveClick(); const keys = Object.keys(frameworkDefault).sort(); @@ -172,6 +175,8 @@ describe('The AppSettingsEditorContainer', () => { ...saveSettingsAction.payload, hash: jasmine.any(String), }; + expect(mockDispatch).toHaveBeenLastCalledWith(saveFrameworkSettings(savedSettings)); + expect(alertServiceSpy).toHaveBeenCalledWith('App settings saved.'); }); }); diff --git a/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.tsx b/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.tsx index 711932434..aea6661c6 100644 --- a/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.tsx +++ b/packages/app/client/src/ui/editor/appSettingsEditor/appSettingsEditor.tsx @@ -45,7 +45,7 @@ export interface AppSettingsEditorProps { documentId?: string; dirty?: boolean; framework?: FrameworkSettings; - + createAriaAlert?: (msg: string) => void; discardChanges?: () => void; openBrowseForNgrok: () => Promise; saveFrameworkSettings?: (framework: FrameworkSettings) => void; @@ -310,6 +310,7 @@ export class AppSettingsEditor extends React.Component }); const mapDispatchToProps = (dispatch: (action: Action) => void, ownProps: AppSettingsEditorProps) => ({ + createAriaAlert: (msg: string) => { + ariaAlertService.alert(msg); + }, discardChanges: () => dispatch(EditorActions.close(getTabGroupForDocument(ownProps.documentId), DOCUMENT_ID_APP_SETTINGS)), openBrowseForNgrok: async () => { diff --git a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx index bfa847774..a6d1d274a 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx +++ b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.spec.tsx @@ -53,6 +53,7 @@ import { theme } from '../../../../../state/reducers/theme'; import { ExtensionManager } from '../../../../../extensions'; import { logService } from '../../../../../platform/log/logService'; import { executeCommand } from '../../../../../state/actions/commandActions'; +import { ariaAlertService } from '../../../../a11y'; import { Inspector } from './inspector'; import { InspectorContainer } from './inspectorContainer'; @@ -444,6 +445,15 @@ describe('The Inspector component', () => { event = { channel: '', args: [1, 2] }; }); + it('"create-aria-alert"', () => { + event.channel = 'create-aria-alert'; + event.args = ['I am an alert!']; + const spy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined); + instance.ipcMessageEventHandler(event); + + expect(spy).toHaveBeenCalledWith('I am an alert!'); + }); + it('"enable-accessory"', () => { event.channel = 'enable-accessory'; const spy = jest.spyOn(instance, 'enableAccessory'); @@ -518,8 +528,9 @@ describe('The Inspector component', () => { event = { channel: '', currentTarget: { dataset: { currentState: 'default' }, name: '' } }; }); - it('"when the copy json button is clicked"', () => { + it('when the copy json button is clicked', () => { const spy = jest.spyOn(instance, 'accessoryClick'); + const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined); const clipboardSpy = jest.spyOn(Electron.clipboard, 'writeText'); event.channel = 'proxy'; @@ -527,6 +538,7 @@ describe('The Inspector component', () => { instance.accessoryClick(event); expect(spy).toHaveBeenCalledWith(event); expect(clipboardSpy).toHaveBeenCalledWith(JSON.stringify(instance.state.inspectObj, null, 2)); + expect(alertServiceSpy).toHaveBeenCalledWith('Activity JSON copied to clipboard.'); }); }); }); diff --git a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.tsx b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.tsx index 4e2d7e88b..7c47106e6 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.tsx +++ b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspector.tsx @@ -80,6 +80,7 @@ interface IpcMessageEvent extends Event { interface InspectorProps { appPath?: string; + createAriaAlert?: (msg: string) => void; document: ChatDocument; themeInfo: { themeName: string; themeComponents: string[] }; activeBot?: IBotConfiguration; @@ -371,6 +372,7 @@ export class Inspector extends React.Component { const id = event.currentTarget.name; if (id == 'copyJson') { + this.props.createAriaAlert('Activity JSON copied to clipboard.'); return clipboard.writeText(JSON.stringify(this.state.inspectObj, null, 2)); } @@ -397,6 +399,10 @@ export class Inspector extends React.Component { // TODO - localization const { channel } = event; switch (channel) { + case EmulatorChannel.CreateAriaAlert: + this.props.createAriaAlert(event.args[0]); + break; + case EmulatorChannel.EnableAccessory: this.enableAccessory(event.args[0], event.args[1]); break; diff --git a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspectorContainer.ts b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspectorContainer.ts index f36593fe3..137040d58 100644 --- a/packages/app/client/src/ui/editor/emulator/parts/inspector/inspectorContainer.ts +++ b/packages/app/client/src/ui/editor/emulator/parts/inspector/inspectorContainer.ts @@ -38,6 +38,7 @@ import { Activity } from 'botframework-schema'; import { RootState } from '../../../../../state/store'; import { executeCommand } from '../../../../../state/actions/commandActions'; import { setHighlightedObjects, setInspectorObjects } from '../../../../../state/actions/chatActions'; +import { ariaAlertService } from '../../../../a11y'; import { Inspector } from './inspector'; @@ -54,6 +55,9 @@ const mapStateToProps = (state: RootState, ownProps: any) => { const mapDispatchToProps = dispatch => { return { + createAriaAlert: (msg: string) => { + ariaAlertService.alert(msg); + }, trackEvent: (name: string, properties?: { [key: string]: any }) => { dispatch(executeCommand(true, SharedConstants.Commands.Telemetry.TrackEvent, null, name, properties)); }, diff --git a/packages/app/main/src/extensions/inspector-preload.js b/packages/app/main/src/extensions/inspector-preload.js index 10ce6e4ab..94ba0ce71 100644 --- a/packages/app/main/src/extensions/inspector-preload.js +++ b/packages/app/main/src/extensions/inspector-preload.js @@ -97,6 +97,10 @@ window.host = { }; }, + createAriaAlert: function(msg) { + ipcRenderer.sendToHost('create-aria-alert', msg); + }, + enableAccessory: function(id, enabled) { if (typeof id === 'string') { ipcRenderer.sendToHost('enable-accessory', id, !!enabled); diff --git a/packages/extensions/json/src/windowHostReceiver.tsx b/packages/extensions/json/src/windowHostReceiver.tsx index 0b43e624f..2ebbc52b2 100644 --- a/packages/extensions/json/src/windowHostReceiver.tsx +++ b/packages/extensions/json/src/windowHostReceiver.tsx @@ -69,8 +69,9 @@ interface JsonViewerExtensionAccessory { type AccessoryId = keyof JsonViewerExtensionAccessory; export function windowHostReceiver(WrappedComponent: ComponentClass): ComponentClass { - @IpcHost(['setAccessoryState', 'setHighlightedObjects', 'setInspectorObjects']) + @IpcHost(['createAriaAlert', 'setAccessoryState', 'setHighlightedObjects', 'setInspectorObjects']) class WindowHostReceiver extends Component<{}, WindowHostReceiverState> { + private createAriaAlert: (msg: string) => void; private setAccessoryState: (accessoryId: AccessoryId, state: string) => void; private setHighlightedObjects: (documentId: string, items: Activity | Activity[]) => void; private setInspectorObjects: (documentId: string, items: Activity | Activity[]) => void; @@ -160,8 +161,10 @@ export function windowHostReceiver(WrappedComponent: ComponentClass): Compo newStateFragment.selectedDiffItem = previousBotState; inspectorObjects.push(buildDiff(nextBotState, previousBotState)); highlightedObjects.push(previousBotState, nextBotState); + this.createAriaAlert('Showing bot state diff.'); } else { inspectorObjects.push(selectedItem as Activity); + this.createAriaAlert('Hiding bot state diff.'); } this.setAccessoryState('diff', newState); } @@ -176,8 +179,10 @@ export function windowHostReceiver(WrappedComponent: ComponentClass): Compo newStateFragment.selectedDiffItem = previousBotState; inspectorObjects.push(buildDiff(newSelectedItem, previousBotState)); highlightedObjects.push(previousBotState, newSelectedItem); + this.createAriaAlert('Showing previous bot state diff.'); } else if (!isDiff) { inspectorObjects.push(newSelectedItem || (selectedItem as Activity)); + this.createAriaAlert('Showing previous bot state.'); } else { return; } @@ -193,8 +198,10 @@ export function windowHostReceiver(WrappedComponent: ComponentClass): Compo newStateFragment.selectedDiffItem = previousBotState; inspectorObjects.push(buildDiff(newSelectedItem, previousBotState)); highlightedObjects.push(newSelectedItem, previousBotState); + this.createAriaAlert('Showing next bot state diff.'); } else if (!isDiff) { inspectorObjects.push(newSelectedItem || (selectedItem as Activity)); + this.createAriaAlert('Showing next bot state.'); } else { return; } diff --git a/packages/sdk/client/src/extensions/host.ts b/packages/sdk/client/src/extensions/host.ts index 141067682..ee2e8de4d 100644 --- a/packages/sdk/client/src/extensions/host.ts +++ b/packages/sdk/client/src/extensions/host.ts @@ -56,6 +56,9 @@ export interface InspectorHost { handler: (themeInfo: { themeName: string; themeComponents: string[] }) => void ): void; + // Creates an invisible ARIA alert to be announced by screen reader technology + createAriaAlert(msg: string): void; + // Enable/disable an accessory button enableAccessory(id: string, enabled: boolean): void; diff --git a/packages/sdk/shared/src/types/ipc/extensionChannel.ts b/packages/sdk/shared/src/types/ipc/extensionChannel.ts index 77e711987..7e9afdcbd 100644 --- a/packages/sdk/shared/src/types/ipc/extensionChannel.ts +++ b/packages/sdk/shared/src/types/ipc/extensionChannel.ts @@ -32,6 +32,7 @@ // // from the extension to Emulator export enum EmulatorChannel { + CreateAriaAlert = 'create-aria-alert', EnableAccessory = 'enable-accessory', Log = 'logger.log', LogError = 'logger.error',