-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Coachmark accessibility #5155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coachmark accessibility #5155
Changes from 12 commits
2b7eabf
eea962f
ec0dad4
f97afeb
689018c
a4f5f14
6e9019a
1576232
17ac4d7
4fa9c4d
1fd35d6
752ef79
e4c7b20
e70b32b
f2173de
f1e86c5
344167f
93811cf
32029ac
ef70ee4
101854f
7774b0e
3b1c254
94b7034
413e38d
89b06a9
e149c87
989b8b7
4884b00
b04c5b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "office-ui-fabric-react", | ||
| "comment": "Coachmark: Add accessibility features to component, ARIA props, narrator support, and keyboarding controls", | ||
| "type": "minor" | ||
| } | ||
| ], | ||
| "packageName": "office-ui-fabric-react", | ||
| "email": "edwl@microsoft.com" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -49,4 +49,4 @@ | |
| "git add" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,14 @@ | ||
| // Utilities | ||
| import * as React from 'react'; | ||
| import { BaseComponent, IRectangle, classNamesFunction, createRef, shallowCompare } from '../../Utilities'; | ||
| import { BaseComponent, classNamesFunction, createRef, IRectangle, KeyCodes, shallowCompare } from '../../Utilities'; | ||
| import { DefaultPalette } from '../../Styling'; | ||
| import { IPositionedData, RectangleEdge, getOppositeEdge } from 'office-ui-fabric-react/lib/utilities/positioning'; | ||
|
|
||
| // Component Dependencies | ||
| import { PositioningContainer, IPositioningContainer } from './PositioningContainer/index'; | ||
| import { Beak, BEAK_HEIGHT, BEAK_WIDTH } from './Beak/Beak'; | ||
| import { DirectionalHint } from 'office-ui-fabric-react/lib/common/DirectionalHint'; | ||
| import { FocusZone } from '../../FocusZone'; | ||
|
|
||
| // Coachmark | ||
| import { ICoachmarkTypes } from './Coachmark.types'; | ||
|
|
@@ -18,7 +19,6 @@ import { | |
| ICoachmarkStyles, | ||
| ICoachmarkStyleProps | ||
| } from './Coachmark.styles'; | ||
| import { FocusZone } from '../../FocusZone'; | ||
|
|
||
| const getClassNames = classNamesFunction<ICoachmarkStyleProps, ICoachmarkStyles>(); | ||
|
|
||
|
|
@@ -115,6 +115,7 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| */ | ||
| private _entityInnerHostElement = createRef<HTMLDivElement>(); | ||
| private _translateAnimationContainer = createRef<HTMLDivElement>(); | ||
| private _ariaAlertContainer = createRef<HTMLDivElement>(); | ||
| private _positioningContainer = createRef<IPositioningContainer>(); | ||
|
|
||
| /** | ||
|
|
@@ -149,7 +150,17 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| } | ||
|
|
||
| public render(): JSX.Element { | ||
| const { children, target, color, positioningContainerProps } = this.props; | ||
| const { | ||
| children, | ||
| target, | ||
| color, | ||
| positioningContainerProps, | ||
| ariaDescribedBy, | ||
| ariaDescribedByText, | ||
| ariaLabelledBy, | ||
| ariaLabelledByText, | ||
| ariaAlertText | ||
| } = this.props; | ||
|
|
||
| const { | ||
| beakLeft, | ||
|
|
@@ -188,6 +199,14 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| {...positioningContainerProps} | ||
| > | ||
| <div className={classNames.root}> | ||
| {ariaAlertText && ( | ||
| <div | ||
| className={classNames.ariaContainer} | ||
| aria-live="assertive" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you used role="alert" will it read? We could then remove the timeout, or is the timeout to focus the dialog content?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I reverted the change back to a setTimeout and appending a textNode. It doesn't seem to work unless it's one of these hacky methods: |
||
| ref={this._ariaAlertContainer} | ||
| aria-hidden={!isCollapsed} | ||
| /> | ||
| )} | ||
| <div className={classNames.pulsingBeacon} /> | ||
| <div className={classNames.translateAnimationContainer} ref={this._translateAnimationContainer}> | ||
| <div className={classNames.scaleAnimationLayer}> | ||
|
|
@@ -203,8 +222,32 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| /> | ||
| )} | ||
| <FocusZone> | ||
| <div className={classNames.entityHost} data-is-focusable={true} onFocus={this._onFocusHandler}> | ||
| <div className={classNames.entityInnerHost} ref={this._entityInnerHostElement}> | ||
| <div | ||
| className={classNames.entityHost} | ||
| tabIndex={-1} | ||
| data-is-focusable={true} | ||
| onClick={this._onFocusHandler} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a click?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed onClick |
||
| role="dialog" | ||
| aria-labelledby={ariaLabelledBy} | ||
| aria-describedby={ariaDescribedBy} | ||
| > | ||
| {isCollapsed && | ||
| ariaLabelledBy && ( | ||
| <h1 id={ariaLabelledBy} className={classNames.ariaContainer}> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why H1? I thought we decided to not use a header
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to a |
||
| {ariaLabelledByText} | ||
| </h1> | ||
| )} | ||
| {isCollapsed && | ||
| ariaDescribedBy && ( | ||
| <p id={ariaDescribedBy} className={classNames.ariaContainer}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can move this up to the other isCollapsed section so you don't need to evaluate twice. You might need to make it an array if it complains about needing a single parent but I don't believe you'll need to do that.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in latest commit |
||
| {ariaDescribedByText} | ||
| </p> | ||
| )} | ||
| <div | ||
| className={classNames.entityInnerHost} | ||
| ref={this._entityInnerHostElement} | ||
| aria-hidden={isCollapsed} | ||
| > | ||
| {children} | ||
| </div> | ||
| </div> | ||
|
|
@@ -255,14 +298,37 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| this.forceUpdate(); | ||
| } | ||
|
|
||
| document.addEventListener('keydown', this._onKeyDown, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BaseComponent provides |
||
|
|
||
| // We dont want to the user to immediatley trigger the coachmark when it's opened | ||
| this._async.setTimeout(() => { | ||
| this._addProximityHandler(this.props.mouseProximityOffset); | ||
| }, this.props.delayBeforeMouseOpen!); | ||
|
|
||
| // SetTimeout is hacky solution. ARIA text doesn't read aloud unless it is appended or changed after the component | ||
| // is mounted | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would be the fix for this? Is this an issue with ARIA spec or with Narrator?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like it's behaving correctly now? I removed the hacky solution and just had the text render directly inside of the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. False alarm, I tested this incorrectly. I switched to using setTimeout and appending a textNode. See here: |
||
| if (this.props.ariaAlertText) { | ||
| this._async.setTimeout(() => { | ||
| if (this._ariaAlertContainer.current && this.props.ariaAlertText) { | ||
| this._ariaAlertContainer.current.innerText = this.props.ariaAlertText; | ||
| } | ||
| }, 2000); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| public componentWillUnmount(): void { | ||
| document.removeEventListener('keydown', this._onKeyDown); | ||
| } | ||
|
|
||
| private _onKeyDown = (e: KeyboardEvent): void => { | ||
| // Open coachmark if user presses ALT + C (arbitrary keypress for now, will change later) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This answers a question I had.. how a user selects or activates the coachmark. It appears there is not a clearly defined role for this in ARIA. I'm curious what this could be changed to? Is there a standard of behavior for coachmarks that you know of?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate here? Not entirely sure what your ask is. Are you asking if we can/should customize the shortcut to open Coachmark? @cschlechty , can you chime in here if you have a chance please?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I'm asking how a user using Narrator actually enters and highlights the Coachmark using keyboard navigation. Tabbing seems to go past it. In the ARIA standard, I couldn't find a specified way to highlight a non-modal dialog. Alt+C highlights it, but I'm curious how a user using assistive technology would know this.. is it standardized anywhere?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not a stand control or pattern. We're inventing it, and pushing its usage instructions via embedded usage string. @leddie24 be sure this is included in the examples.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that embedded usage string in this PR? I'm assuming it informs users about hitting ALT+C. I couldn't find it.. thanks
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'll be included in the prop for the control.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So it will only be set from outside the control? The default selection behavior is embedded in the control, so I'm wondering if the default notification on usage should be as well. (Similar to how "A coachmark has appeared" is.)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "A coachmark has appeared" and the usage instructions need to be localize-able, so they'll need to be passed in.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand that, but some are defaulted to English within the component and others are not. I feel like the navigation info is probably the most important one to be present (if any will be.) |
||
| if (e.altKey && e.which === KeyCodes.c) { | ||
| this._onFocusHandler(); | ||
| } | ||
| }; | ||
|
|
||
| private _onFocusHandler = (): void => { | ||
| if (this.state.isCollapsed) { | ||
| this._openCoachmark(); | ||
|
|
@@ -403,6 +469,13 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| this._entityInnerHostElement.current.addEventListener( | ||
| 'transitionend', | ||
| (): void => { | ||
| // Need setTimeout to trigger narrator | ||
| this._async.setTimeout(() => { | ||
| if (this.props.teachingBubbleRef && this.props.teachingBubbleRef.current) { | ||
| this.props.teachingBubbleRef.current.focus(); | ||
| } | ||
| }, 500); | ||
|
|
||
| if (this.props.onAnimationOpenEnd) { | ||
| this.props.onAnimationOpenEnd(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { Coachmark } from './Coachmark'; | |
| import { ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; | ||
| import { IPositioningContainerTypes } from './PositioningContainer/PositioningContainer.types'; | ||
| import { IStyleFunctionOrObject } from '../../Utilities'; | ||
| import { ITeachingBubble } from '../../TeachingBubble'; | ||
|
|
||
| export interface ICoachmark {} | ||
|
|
||
|
|
@@ -114,4 +115,39 @@ export interface ICoachmarkTypes extends React.Props<Coachmark> { | |
| * Beacon color two. | ||
| */ | ||
| beaconColorTwo?: string; | ||
|
|
||
| /** | ||
| * Text to announce to screen reader / narrator when Coachmark is displayed | ||
| */ | ||
| ariaAlertText?: string; | ||
|
|
||
| /** | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JasonGore Any suggestions here? I'm not quite sure how make the interface for teachingBubbleRef look nicer. |
||
| * Ref for TeachingBubble | ||
| * @todo Fix interface here | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What needs to be fixed about this interface? |
||
| */ | ||
| teachingBubbleRef?: { | ||
| (component: ITeachingBubble | null): void; | ||
| current: ITeachingBubble | null; | ||
| value: ITeachingBubble | null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a weird flattened mixing of componentRef and and createRef, did you mean to do this?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, not entirely sure what the right interface is. If I set it to |
||
| }; | ||
|
|
||
| /** | ||
| * Defines the element id referencing the element containing label text for Coachmark. | ||
| */ | ||
| ariaLabelledBy?: string; | ||
|
|
||
| /** | ||
| * Defines the element id referencing the element containing the description for the Coachmark. | ||
| */ | ||
| ariaDescribedBy?: string; | ||
|
|
||
| /** | ||
| * Defines the text content for the ariaLabelledBy element | ||
| */ | ||
| ariaLabelledByText?: string; | ||
|
|
||
| /** | ||
| * Defines the text content for the ariaDescribedBy element | ||
| */ | ||
| ariaDescribedByText?: string; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a strict need for
aria-hidden? I'm curious since this element doesn't have an explicit roleThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure here. Is
aria-hiddenonly required if an element has aroleattribute? I haven't done a lot of accessibility work before, so advice is appreciated here. It looks like narrator focuses on the Beak and reads aloud as "image" if it's in scan mode, which I don't think is desirable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aria-hidden hides all the subelements so it won't try to read the svg. Seems reasonable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try
role='presentation'and see if that solves the issue?aria-hiddenis kinda like a last resort hack.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, updated to
role="presentation"There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You likely need it on the svg itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of the root element, or both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it needed to be on the element and wasn't inherited. Give it a try w/ narrator+edge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can leave it on the root element only. We have a FocusTrapZone on Coachmark anyways, so we can't tab out to the Beak element.