From e915a1926c9b821f9f0d75e88445d1c431a284fc Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 12 Oct 2020 11:51:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(UI=20builder):=20Add=20ability=20to=20comp?= =?UTF-8?q?ute=20screen=20reader=20narration=20of=20focused=20element=20in?= =?UTF-8?q?=20=E2=80=A6=20=E2=80=A6use=20mode=20(#15464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Integrated the message computation script by Adam into UI Builder * Rename and reformat changes * Fix type errors * Implement suggestions from PR for better readability of the code * Refactorring of the code. Mainly rewritten the large computeMessage() function into smaller ones. * Fix narration computation for elements with checked DOM property as state. Fix textarea not to narrate value. Refactoring and comment edit and addition. * Rename to screen reader narration computation * Refactor getDefinitionName() method in NarrationComputer.tsx for conciseness Co-authored-by: Charles Assunção * Replace default exports with named exports. * Remove private modifier * Change to a more readable type * Rewrite the code not to use label for the for-loop and refactoring * Update packages/fluentui/react-builder/src/components/NarrationComputer.tsx Co-authored-by: Charles Assunção * Reverting the last commit, * Comment change * Replace for loop with Array.forEach(). * Resolve lint errors * Remove empty file northstar. Have no idea how it was created- * Remove showNarration condition * Replace condition with ternary operator. * Add missing quotes in a CSS definition. Co-authored-by: Charles Assunção * Replace condition with ternary operator * Rename parameter and add type" * Integrate descendant element screen reader narration computation into the UI Builder * Refactoring and handle the case where there are no elements for the selected component. * Refactoring, narration now computes for all focusable elements, and path shows computed name of element. * Fix for duplicate narration of the text 'Narration: ' * Add a small comment * Add a comment and parenthesis around a arrow function parameter * Edit comment and try to add parenthesis again * Revert the change to config.ts and refactoring of a if statement * Refactor for loop into forEach loop * Screen reader now computes the narration immediately on every change of component props * Add ability to compute screen reader narration of focused element in use mode. Fix bug with narration path dropdown always selecting the first option. Co-authored-by: Charles Assunção --- .../react-builder/src/components/Canvas.tsx | 11 ++- .../src/components/ReaderNarration.tsx | 95 ++++++++++++------- .../DescendantsNarrationsComputer.tsx | 4 +- 3 files changed, 71 insertions(+), 39 deletions(-) diff --git a/packages/fluentui/react-builder/src/components/Canvas.tsx b/packages/fluentui/react-builder/src/components/Canvas.tsx index 118de2c3afe10..e60f53b8fe905 100644 --- a/packages/fluentui/react-builder/src/components/Canvas.tsx +++ b/packages/fluentui/react-builder/src/components/Canvas.tsx @@ -332,11 +332,12 @@ export const Canvas: React.FunctionComponent = ({ )} {inUseMode && } {renderJSONTreeToJSXElement(jsonTree, renderJSONTreeElement)} - {selectedComponent && ( -
- -
- )} +
+ +
)} diff --git a/packages/fluentui/react-builder/src/components/ReaderNarration.tsx b/packages/fluentui/react-builder/src/components/ReaderNarration.tsx index e7f9e12fd8183..9b5e714034cf8 100644 --- a/packages/fluentui/react-builder/src/components/ReaderNarration.tsx +++ b/packages/fluentui/react-builder/src/components/ReaderNarration.tsx @@ -3,73 +3,104 @@ import { Alert, Ref, Dropdown, DropdownProps } from '@fluentui/react-northstar'; import { IAriaElement } from './../narration/NarrationComputer'; import { DescendantsNarrationsComputer } from './../narration/DescendantsNarrationsComputer'; -const computer: DescendantsNarrationsComputer = new DescendantsNarrationsComputer(); -let narrationTexts: Record = {}; +const computer = new DescendantsNarrationsComputer(); +let narrationPath = null; +let narrationPaths = []; +let narrationTexts = {}; const aomMissing = !(window as any).getComputedAccessibleNode; export type ReaderNarrationProps = { selector: string; + inUseMode: boolean; }; -export const ReaderNarration: React.FunctionComponent = ({ selector }) => { +export const ReaderNarration: React.FunctionComponent = ({ selector, inUseMode }) => { const ref = React.useRef(); + const [selectedNarrationPath, setSelectedNarrationPath] = React.useState(null); const [narrationText, setNarrationText] = React.useState(''); - const [narrationPath, setNarrationPath] = React.useState(''); - const [narrationPaths, setNarrationPaths] = React.useState([]); - React.useEffect(() => { - if (!ref.current) { - return; - } - - if (aomMissing) { - return; - } - - const element = ref.current.ownerDocument.querySelector(selector) as IAriaElement; - - // Compute and store the narrations for the element and its focusable descendants + // Computes and saves the narration paths and texts for the given parent element and its focusable descendants + const computeAndSave = React.useCallback(element => { computer.compute(element, 'Win/JAWS').then(narrations => { - const paths: string[] = []; + narrationPaths = []; narrationTexts = {}; narrations.forEach(narration => { // Begin forEach 1 const path = narration.path.join(' > '); - paths.push(path); + narrationPaths.push(path); narrationTexts[path] = narration.text; }); // End forEach 1 - // Update the narration paths dropdown values - setNarrationPaths(paths); + // If narration path has not been selected by user, preselect the first path and its associated narration as defaults + const text = narrationPath == null ? narrations[0]?.text || null : narrationTexts[narrationPath]; + if (narrationPath == null && narrationPaths.length > 1) { + // Begin if 1 + setSelectedNarrationPath(narrationPaths[0]); + } // End if 1 - // If some narration has been retrieved, choose the first one as the narration to be displayed - const text = narrations[0]?.text || null; setCompleteText(text); }); // End compute - }); + }, []); // End computeAndSave - // Composes and sets the complete screen reader narration text to be displayed + // Sets the complete screen reader narration text to be displayed. const setCompleteText = text => { - setNarrationText(`Narration: ${text}`); + setNarrationText(text !== null ? `Narration: ${text}` : null); }; // End setCompleteText + // Handles the "focusin" event by computing and saving the narration paths and texts. + const handleFocusIn = React.useCallback( + event => { + computeAndSave(event.target as IAriaElement); + }, + [computeAndSave], + ); // End handleFocusIn + + // Handles the narration path dropdown change event by saving the narration path. const handleNarrationPathChange = (event: any, props: DropdownProps) => { - setNarrationPath(props.value as string); - const text = narrationTexts[props.value as string]; + narrationPath = props.value as string; + setSelectedNarrationPath(narrationPath); + const text = narrationTexts[narrationPath]; setCompleteText(text); }; // End handleNarrationPathChange - if (!selector) { + // Recomputes the narration paths and texts upon every render. + React.useEffect(() => { + if (inUseMode || !ref.current || aomMissing) { + return; + } + + // Compute and save the narration paths and texts for the selected component's parent element and its focusable descendants + const element = ref.current.ownerDocument.querySelector(selector) as IAriaElement; + computeAndSave(element); + }); // End useEffect + + // Resets the narration path to its defaults if selector changes. + React.useEffect(() => { + narrationPath = null; + }, [selector]); // End useEffect + + // If in the use mode, sets up the "focusin" event listener. + React.useEffect(() => { + const alert = ref.current; + if (!inUseMode || !alert) { + return null; + } + alert.ownerDocument.addEventListener('focusin', handleFocusIn); + return () => { + alert.ownerDocument.removeEventListener('focusin', handleFocusIn); + }; // End return + }, [inUseMode, handleFocusIn]); // End useEffect + + if (selector == null && !inUseMode) { return null; } - return ( <> {narrationPaths.length > 1 && ( `${item} has been selected.`, diff --git a/packages/fluentui/react-builder/src/narration/DescendantsNarrationsComputer.tsx b/packages/fluentui/react-builder/src/narration/DescendantsNarrationsComputer.tsx index 3a1f74aaa1ac8..85c4632685c62 100644 --- a/packages/fluentui/react-builder/src/narration/DescendantsNarrationsComputer.tsx +++ b/packages/fluentui/react-builder/src/narration/DescendantsNarrationsComputer.tsx @@ -28,9 +28,9 @@ export class DescendantsNarrationsComputer { // Begin if 1 parents.push(element); } // End if 1 - Array.from(element.children).forEach((child: IAriaElement) => { + Array.from(element.children).forEach(child => { // Begin foreach 1 - this.findActiveDescendantsParents(child, parents); + this.findActiveDescendantsParents(child as IAriaElement, parents); }); // End foreach 1 } // End findActiveDescendantsParents