From 751dc679f1ef9af02780e15b1a5eb7a92aee80a5 Mon Sep 17 00:00:00 2001 From: Ben Yackley <61990921+beyackle@users.noreply.github.com> Date: Wed, 22 Apr 2020 15:13:50 -0700 Subject: [PATCH] a11y: use useShell for global announcements (#2734) * start using useShell for announcements * fix crashing issue * Update ObjectArrayField.tsx * Update ObjectArrayField.tsx * fixes from PR Co-authored-by: Andy Brown Co-authored-by: Chris Whitten --- .../pages/language-generation/table-view.tsx | 17 +++------------- .../packages/client/src/shell/useShell.ts | 1 + .../packages/client/src/store/action/error.ts | 17 +++++++++++++++- .../components/fields/ObjectArrayField.tsx | 20 +++++++++---------- .../packages/lib/shared/src/types/shell.ts | 1 + 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/Composer/packages/client/src/pages/language-generation/table-view.tsx b/Composer/packages/client/src/pages/language-generation/table-view.tsx index f0f02e91f4..3b25736284 100644 --- a/Composer/packages/client/src/pages/language-generation/table-view.tsx +++ b/Composer/packages/client/src/pages/language-generation/table-view.tsx @@ -17,7 +17,6 @@ import formatMessage from 'format-message'; import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; import { RouteComponentProps } from '@reach/router'; import { LgTemplate } from '@bfc/shared'; -import { Async } from 'office-ui-fabric-react/lib/Utilities'; import { StoreContext } from '../../store'; import { increaseNameUtilNotExist } from '../../utils/lgUtil'; @@ -36,7 +35,6 @@ const TableView: React.FC = props => { const createLgTemplate = useRef(debounce(actions.createLgTemplate, 500)).current; const copyLgTemplate = useRef(debounce(actions.copyLgTemplate, 500)).current; const removeLgTemplate = useRef(debounce(actions.removeLgTemplate, 500)).current; - const setMessage = useRef(debounce(actions.setMessage, 500)).current; const [templates, setTemplates] = useState([]); const listRef = useRef(null); @@ -44,15 +42,6 @@ const TableView: React.FC = props => { const [focusedIndex, setFocusedIndex] = useState(0); - const _async = new Async(); - - const announce = (message: string) => { - setMessage(message); - _async.setTimeout(() => { - setMessage(undefined); - }, 2000); - }; - useEffect(() => { if (!file || isEmpty(file)) return; @@ -123,7 +112,7 @@ const TableView: React.FC = props => { key: 'delete', name: formatMessage('Delete'), onClick: () => { - announce('item deleted'); + actions.setMessage('item deleted'); onRemoveTemplate(index); }, }, @@ -131,7 +120,7 @@ const TableView: React.FC = props => { key: 'copy', name: formatMessage('Make a copy'), onClick: () => { - announce('item copied'); + actions.setMessage('item copied'); onCopyTemplate(index); }, }, @@ -257,7 +246,7 @@ const TableView: React.FC = props => { iconProps={{ iconName: 'CirclePlus' }} onClick={() => { onCreateNewTemplate(); - announce('item added'); + actions.setMessage('item added'); }} > {formatMessage('New template')} diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 50e367ec5e..548a1a19ca 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -174,6 +174,7 @@ export function useShell(source: EventSource): { api: ShellApi; data: ShellData redo: actions.redo, addCoachMarkRef: actions.onboardingAddCoachMarkRef, updateUserSettings: actions.updateUserSettings, + announce: actions.setMessage, }; const currentDialog = useMemo(() => dialogs.find(d => d.id === dialogId), [dialogs, dialogId]); diff --git a/Composer/packages/client/src/store/action/error.ts b/Composer/packages/client/src/store/action/error.ts index 330e05208a..8c1d96a613 100644 --- a/Composer/packages/client/src/store/action/error.ts +++ b/Composer/packages/client/src/store/action/error.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import debounce from 'lodash/debounce'; + import { ActionCreator } from '../types'; import { ActionTypes } from './../../constants'; @@ -12,9 +14,22 @@ export const setError: ActionCreator = ({ dispatch }, error) => { }); }; -export const setMessage: ActionCreator = ({ dispatch }, message) => { +const _setMessage = debounce((dispatch, message: string) => { dispatch({ type: ActionTypes.SET_MESSAGE, payload: message, }); + + setTimeout( + () => + dispatch({ + type: ActionTypes.SET_MESSAGE, + payload: undefined, + }), + 2000 + ); +}, 500); + +export const setMessage: ActionCreator = ({ dispatch }, message) => { + _setMessage(dispatch, message); }; diff --git a/Composer/packages/extensions/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/extensions/adaptive-form/src/components/fields/ObjectArrayField.tsx index 50fd6b416c..ee1c0c39de 100644 --- a/Composer/packages/extensions/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/extensions/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -4,7 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React, { useState, useMemo, useRef } from 'react'; -import { FieldProps } from '@bfc/extension'; +import { FieldProps, useShellApi } from '@bfc/extension'; import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { JSONSchema7 } from 'json-schema'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; @@ -39,6 +39,13 @@ const ObjectArrayField: React.FC> = props => { const [newObject, setNewObject] = useState({}); const { arrayItems, handleChange, addItem } = useArrayItems(value, onChange); const firstNewFieldRef: React.RefObject = useRef(null); + const { announce } = useShellApi().shellApi; + + const END_OF_ROW_LABEL = formatMessage('press Enter to add this item or Tab to move to the next interactive element'); + + const INSIDE_ROW_LABEL = formatMessage( + 'press Enter to add this name and advance to the next row, or press Tab to advance to the value field' + ); const handleNewObjectChange = (property: string) => (_e: React.FormEvent, newValue?: string) => { setNewObject({ ...newObject, [property]: newValue }); @@ -54,6 +61,7 @@ const ObjectArrayField: React.FC> = props => { return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; }, {}); + announce(INSIDE_ROW_LABEL); addItem(formattedData); setNewObject({}); firstNewFieldRef.current?.focus(); @@ -153,15 +161,7 @@ const ObjectArrayField: React.FC> = props => { value={newObject[property] || ''} onChange={handleNewObjectChange(property)} onKeyDown={handleKeyDown} - ariaLabel={ - lastField - ? formatMessage( - 'press Enter to add this item or Tab to move to the next interactive element' - ) - : formatMessage( - 'press Enter to add this name and advance to the next row, or press Tab to advance to the value field' - ) - } + ariaLabel={lastField ? END_OF_ROW_LABEL : INSIDE_ROW_LABEL} componentRef={index === 0 ? firstNewFieldRef : undefined} /> diff --git a/Composer/packages/lib/shared/src/types/shell.ts b/Composer/packages/lib/shared/src/types/shell.ts index 320fd4e669..ba63f58173 100644 --- a/Composer/packages/lib/shared/src/types/shell.ts +++ b/Composer/packages/lib/shared/src/types/shell.ts @@ -70,4 +70,5 @@ export interface ShellApi { redo: () => void; updateUserSettings: (settings: AllPartial) => void; addSkillDialog: () => Promise<{ manifestUrl: string } | null>; + announce: (message: string) => void; }