Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2b7eabf
Add aria alert text
edwlmsft Jun 7, 2018
eea962f
Add keypress shortcut to open coachmark
edwlmsft Jun 7, 2018
ec0dad4
Have narrator announce TeachingBubbleContent text upon coachmark open…
edwlmsft Jun 7, 2018
f97afeb
Merge branch 'master' of https://github.com/OfficeDev/office-ui-fabri…
Jun 8, 2018
689018c
Add aria-hidden to beak component
Jun 8, 2018
a4f5f14
Add aria-hidden to ariaAlert container once Coachmark is expanded. C…
Jun 8, 2018
6e9019a
Change back to FocusZone. Add ariaLabelled and ariaDescribedBy props…
Jun 8, 2018
1576232
Remove force focus on coachmark after mounting. Remove console logs
Jun 8, 2018
17ac4d7
Only render elements if ariaLabelledBy and ariaDescribedBy props are …
Jun 8, 2018
4fa9c4d
Update snapshot test for TeachingBubble
edwlmsft Jun 13, 2018
1fd35d6
Merge branch 'master' of https://github.com/OfficeDev/office-ui-fabri…
edwlmsft Jun 14, 2018
752ef79
Add change file
edwlmsft Jun 15, 2018
e4c7b20
Add onKeyDown, change from FocusZone to FocusTrapZone. Remove setTim…
edwlmsft Jun 18, 2018
e70b32b
Merge branch 'coachmarkAccessibility' of https://github.com/leddie24/…
edwlmsft Jun 18, 2018
f2173de
Add escape key handler to TeachingBubbleContent
edwlmsft Jun 18, 2018
f1e86c5
Remove onClick, change from h1 to p
edwlmsft Jun 18, 2018
344167f
Add back in setTimeout for appending ariaAlertText.
edwlmsft Jun 19, 2018
93811cf
Change from aria-hidden to role="presentation" for Beak component.
edwlmsft Jun 19, 2018
32029ac
Merge branch 'master' of https://github.com/OfficeDev/office-ui-fabri…
edwlmsft Jun 19, 2018
ef70ee4
Fix inadvertent change to AppState.tsx file (checkout from master)
edwlmsft Jun 19, 2018
101854f
Update Do's. Edit basic example ariaDescribedByText prop. Add chang…
edwlmsft Jun 20, 2018
7774b0e
Change import path of FocusTrapZone to use relative path. Add alertT…
edwlmsft Jun 26, 2018
3b1c254
Merge branch 'master' of https://github.com/OfficeDev/office-ui-fabri…
edwlmsft Jul 6, 2018
94b7034
Fix bad commits
edwlmsft Jul 6, 2018
413e38d
remove unused css import for Beak
edwlmsft Jul 6, 2018
89b06a9
fix bad commit for package.json in dashboard-grid-layout
edwlmsft Jul 6, 2018
e149c87
remove unused change file
edwlmsft Jul 6, 2018
989b8b7
Fix teachingBubbleRef interface
edwlmsft Jul 7, 2018
4884b00
Add line
edwlmsft Jul 7, 2018
b04c5b3
Remove redundant check for this.props.teachingBubbleRef. Move event …
edwlmsft Jul 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -1,5 +1,5 @@
import * as React from 'react';
import { BaseComponent, css, classNamesFunction } from '../../../Utilities';
import { BaseComponent, classNamesFunction } from '../../../Utilities';
import { IBeakProps } from './Beak.types';
import { getStyles, IBeakStyles } from './Beak.styles';
import { IBeakStylesProps } from './Beak.types';
Expand Down Expand Up @@ -73,7 +73,7 @@ export class Beak extends BaseComponent<IBeakProps, {}> {
});

return (
<div className={css('ms-Beak', classNames.root)}>
<div className={classNames.root} role="presentation">
<svg height={svgHeight} width={svgWidth} className={classNames.beak}>
<polygon points={pointOne + ' ' + pointTwo + ' ' + pointThree} />
</svg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export interface ICoachmarkStyles {
* The styles applied when the coachmark has collapsed.
*/
collapsed?: IStyle;

/**
* The styles applied to the ARIA attribute container
*/
ariaContainer?: IStyle;
}

export const translateOne: string = keyframes({
Expand Down Expand Up @@ -353,6 +358,10 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme(
!props.isMeasuring && {
visibility: 'visible'
}
]
],
ariaContainer: {
position: 'fixed',
opacity: 0
}
};
}
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 '../../utilities/positioning';

Expand All @@ -18,7 +18,7 @@ import {
ICoachmarkStyles,
ICoachmarkStyleProps
} from './Coachmark.styles';
import { FocusZone } from '../../FocusZone';
import { FocusTrapZone } from '../FocusTrapZone';

const getClassNames = classNamesFunction<ICoachmarkStyleProps, ICoachmarkStyles>();

Expand Down Expand Up @@ -95,6 +95,11 @@ export interface ICoachmarkState {
* Transform origin of teaching bubble callout
*/
transformOrigin?: string;

/**
* ARIA alert text to read aloud with Narrator once the Coachmark is mounted
*/
alertText?: string;
}

export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> {
Expand All @@ -115,6 +120,7 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> {
*/
private _entityInnerHostElement = createRef<HTMLDivElement>();
private _translateAnimationContainer = createRef<HTMLDivElement>();
private _ariaAlertContainer = createRef<HTMLDivElement>();
private _positioningContainer = createRef<IPositioningContainer>();

/**
Expand Down Expand Up @@ -149,7 +155,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,
Expand All @@ -160,7 +176,8 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> {
isBeaconAnimating,
isMeasuring,
entityInnerHostRect,
transformOrigin
transformOrigin,
alertText
} = this.state;

const classNames = getClassNames(getStyles, {
Expand Down Expand Up @@ -188,6 +205,16 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> {
{...positioningContainerProps}
>
<div className={classNames.root}>
{ariaAlertText && (
<div
className={classNames.ariaContainer}
role="alert"
ref={this._ariaAlertContainer}
aria-hidden={!isCollapsed}
>
{alertText}
</div>
)}
<div className={classNames.pulsingBeacon} />
<div className={classNames.translateAnimationContainer} ref={this._translateAnimationContainer}>
<div className={classNames.scaleAnimationLayer}>
Expand All @@ -202,13 +229,36 @@ 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}>
<div
className={classNames.entityHost}
tabIndex={-1}
data-is-focusable={true}
role="dialog"
aria-labelledby={ariaLabelledBy}
aria-describedby={ariaDescribedBy}
>
{isCollapsed && [
ariaLabelledBy && (
<p id={ariaLabelledBy} className={classNames.ariaContainer}>
{ariaLabelledByText}
</p>
),
ariaDescribedBy && (
<p id={ariaDescribedBy} className={classNames.ariaContainer}>
{ariaDescribedByText}
</p>
)
]}
<div
className={classNames.entityInnerHost}
ref={this._entityInnerHostElement}
aria-hidden={isCollapsed}
>
{children}
</div>
</div>
</FocusZone>
</FocusTrapZone>
</div>
</div>
</div>
Expand Down Expand Up @@ -255,14 +305,43 @@ export class Coachmark extends BaseComponent<ICoachmarkTypes, ICoachmarkState> {
this.forceUpdate();
}

this._events.on(document, 'keydown', this._onKeyDown, true);

// 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!);

// Need to add setTimeout to have narrator read change in alert container
if (this.props.ariaAlertText) {
this._async.setTimeout(() => {
if (this.props.ariaAlertText && this._ariaAlertContainer.current) {
this.setState({
alertText: this.props.ariaAlertText
});
}
}, 0);
}
}
);
}

public componentWillUnmount(): void {
this._events.off(document, 'keydown', this._onKeyDown, true);
}

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();
Expand Down Expand Up @@ -403,6 +482,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.focus();
}
}, 500);

if (this.props.onAnimationOpenEnd) {
this.props.onAnimationOpenEnd();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down Expand Up @@ -32,6 +33,7 @@ export interface ICoachmarkTypes extends React.Props<Coachmark> {

/**
* Whether or not to force the Coachmark/TeachingBubble content to fit within the window bounds.
* @default true
*/
isPositionForced?: boolean;

Expand Down Expand Up @@ -114,4 +116,34 @@ 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;

/**
Copy link
Copy Markdown
Collaborator Author

@leddie24 leddie24 Jul 6, 2018

Choose a reason for hiding this comment

The 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
*/
teachingBubbleRef?: ITeachingBubble;

/**
* 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;
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Only one Coachmark + TeachingBubble combo should be displayed at a time
- Coachmarks can be stand alone or sequential. Sequential Coachmarks should be used sparingly, to walk through complex multi-step interactions. It is recommended that a sequence of Coachmarks does not exceed 3 steps.
- Coachmarks are designed to only hold TeachingBubbles.
- Coachmarks are designed to only hold TeachingBubbles
- Provide descriptive text in the `ariaDescribedByText` prop to let accessibility impaired users know how to open/access the Coachmark with keyboard controls. (See example in documentation)
- The keyboard shortcut for opening the Coachmark is `Alt + C`
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Coachmark } from '../Coachmark';
import { TeachingBubbleContent } from 'office-ui-fabric-react/lib/TeachingBubble';
import { ITeachingBubble, TeachingBubbleContent } from 'office-ui-fabric-react/lib/TeachingBubble';
import { DefaultButton, IButtonProps } from 'office-ui-fabric-react/lib/Button';
import { DirectionalHint } from 'office-ui-fabric-react/lib/common/DirectionalHint';
import { IStyle } from '../../../Styling';
Expand Down Expand Up @@ -31,6 +31,7 @@ export interface ICoachmarkBasicExampleStyles {

export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExampleState> {
private _targetButton = createRef<HTMLDivElement>();
private _teachingBubbleContent: ITeachingBubble;

public constructor(props: {}) {
super(props);
Expand Down Expand Up @@ -104,14 +105,23 @@ export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExam
positioningContainerProps={{
directionalHint: this.state.coachmarkPosition
}}
ariaAlertText="A Coachmark has appeared"
teachingBubbleRef={this._teachingBubbleContent}
ariaDescribedBy={'coachmark-desc1'}
ariaLabelledBy={'coachmark-label1'}
ariaDescribedByText={'Press enter or alt + C to open the Coachmark notification'}
ariaLabelledByText={'Coachmark notification'}
>
<TeachingBubbleContent
componentRef={this._teachingBubbleRef}
headline="Example Title"
hasCloseIcon={true}
closeButtonAriaLabel="Close"
primaryButtonProps={buttonProps}
secondaryButtonProps={buttonProps2}
onDismiss={this._onDismiss}
ariaDescribedBy={'example-description1'}
ariaLabelledBy={'example-label1'}
>
Welcome to the land of Coachmarks!
</TeachingBubbleContent>
Expand All @@ -138,4 +148,8 @@ export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExam
isCoachmarkVisible: !this.state.isCoachmarkVisible
});
};

private _teachingBubbleRef = (component: ITeachingBubble): void => {
this._teachingBubbleContent = component;
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { BaseComponent, classNamesFunction } from '../../Utilities';
import { BaseComponent, classNamesFunction, createRef } from '../../Utilities';
import { TeachingBubbleContent } from './TeachingBubbleContent';
import { ITeachingBubbleProps, ITeachingBubbleStyleProps, ITeachingBubbleStyles } from './TeachingBubble.types';
import { calloutStyles } from './TeachingBubble.styles';
Expand Down Expand Up @@ -28,6 +28,7 @@ export class TeachingBubbleBase extends BaseComponent<ITeachingBubbleProps, ITea
}
};

public rootElement = createRef<HTMLDivElement>();
private _defaultCalloutProps: ICalloutProps;

// Constructor
Expand All @@ -45,6 +46,12 @@ export class TeachingBubbleBase extends BaseComponent<ITeachingBubbleProps, ITea
};
}

public focus(): void {
if (this.rootElement.current) {
this.rootElement.current.focus();
}
}

public render(): JSX.Element {
const { calloutProps: setCalloutProps, targetElement, onDismiss, isWide, styles, theme } = this.props;
const calloutProps = { ...this._defaultCalloutProps, ...setCalloutProps };
Expand All @@ -64,7 +71,9 @@ export class TeachingBubbleBase extends BaseComponent<ITeachingBubbleProps, ITea
className={classNames.root}
styles={calloutStyles(stylesProps)}
>
<TeachingBubbleContent {...this.props} />
<div ref={this.rootElement}>
<TeachingBubbleContent {...this.props} />
</div>
</Callout>
);
}
Expand Down
Loading