-
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 14 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,6 +1,6 @@ | ||
| // 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'; | ||
|
|
||
|
|
@@ -18,7 +18,7 @@ import { | |
| ICoachmarkStyles, | ||
| ICoachmarkStyleProps | ||
| } from './Coachmark.styles'; | ||
| import { FocusZone } from '../../FocusZone'; | ||
| import { FocusTrapZone } from 'office-ui-fabric-react/lib/components/FocusTrapZone'; | ||
|
|
||
| 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,16 @@ 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} | ||
| > | ||
| {ariaAlertText} | ||
| </div> | ||
| )} | ||
| <div className={classNames.pulsingBeacon} /> | ||
| <div className={classNames.translateAnimationContainer} ref={this._translateAnimationContainer}> | ||
| <div className={classNames.scaleAnimationLayer}> | ||
|
|
@@ -202,13 +223,37 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| color={color} | ||
| /> | ||
| )} | ||
| <FocusZone> | ||
| <div className={classNames.entityHost} data-is-focusable={true} onFocus={this._onFocusHandler}> | ||
| <div className={classNames.entityInnerHost} ref={this._entityInnerHostElement}> | ||
| <FocusTrapZone isClickableOutsideFocusTrap={true} forceFocusInsideTrap={false}> | ||
|
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's the point of having this focustrap zone if you can click out of it and it doesn't force focus within it?
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 the |
||
| <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> | ||
| </FocusZone> | ||
| </FocusTrapZone> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
@@ -255,6 +300,8 @@ 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); | ||
|
|
@@ -263,6 +310,22 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> { | |
| ); | ||
| } | ||
|
|
||
| public componentWillUnmount(): void { | ||
| document.removeEventListener('keydown', this._onKeyDown); | ||
| } | ||
|
|
||
| private _onKeyDown = (e: any): void => { | ||
| // Open coachmark if user presses ALT + C (arbitrary keypress for now) | ||
| if ( | ||
| (e.altKey && e.which === KeyCodes.c) || | ||
| (e.which === KeyCodes.enter && | ||
| this._translateAnimationContainer.current && | ||
| this._translateAnimationContainer.current.contains(e.target)) | ||
| ) { | ||
| this._onFocusHandler(); | ||
| } | ||
| }; | ||
|
|
||
| private _onFocusHandler = (): void => { | ||
| if (this.state.isCollapsed) { | ||
| this._openCoachmark(); | ||
|
|
@@ -403,6 +466,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.