Skip to content

Commit

Permalink
feat(UI builder): Add ability to compute screen reader narration of f…
Browse files Browse the repository at this point in the history
…ocused element in … …use mode (#15464)

* 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 <junioassuncaocharles@gmail.com>

* 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 <junioassuncaocharles@gmail.com>

* 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 <junioassuncaocharles@gmail.com>

* 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 <junioassuncaocharles@gmail.com>
  • Loading branch information
adamsamec and assuncaocharles authored Oct 12, 2020
1 parent bab2806 commit e915a19
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 39 deletions.
11 changes: 6 additions & 5 deletions packages/fluentui/react-builder/src/components/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,12 @@ export const Canvas: React.FunctionComponent<CanvasProps> = ({
)}
{inUseMode && <EventListener capture type="focus" listener={handleFocus} target={document} />}
{renderJSONTreeToJSXElement(jsonTree, renderJSONTreeElement)}
{selectedComponent && (
<div style={{ bottom: '0', position: 'absolute' }}>
<ReaderNarration selector={`[data-builder-id="${selectedComponent.uuid}"]`} />
</div>
)}
<div style={{ bottom: '0', position: 'absolute' }}>
<ReaderNarration
selector={selectedComponent ? `[data-builder-id="${selectedComponent.uuid}"]` : null}
inUseMode={inUseMode}
/>
</div>
</Provider>
</>
)}
Expand Down
95 changes: 63 additions & 32 deletions packages/fluentui/react-builder/src/components/ReaderNarration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {};
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<ReaderNarrationProps> = ({ selector }) => {
export const ReaderNarration: React.FunctionComponent<ReaderNarrationProps> = ({ selector, inUseMode }) => {
const ref = React.useRef<HTMLElement>();
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 && (
<Dropdown
items={narrationPaths}
defaultValue={narrationPath}
value={narrationPath}
defaultValue={selectedNarrationPath}
value={selectedNarrationPath}
onChange={handleNarrationPathChange}
getA11ySelectionMessage={{
onAdd: item => `${item} has been selected.`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit e915a19

Please sign in to comment.