diff --git a/common/changes/office-ui-fabric-react/coachmarkAccessibility_2018-06-15-16-49.json b/common/changes/office-ui-fabric-react/coachmarkAccessibility_2018-06-15-16-49.json new file mode 100644 index 00000000000000..a5fc0c5124f515 --- /dev/null +++ b/common/changes/office-ui-fabric-react/coachmarkAccessibility_2018-06-15-16-49.json @@ -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" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.tsx index 8d0ba0a103c360..64a16c52cc3893 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.tsx @@ -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'; @@ -73,7 +73,7 @@ export class Beak extends BaseComponent { }); return ( -
+
diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts index 8079e9469e7a7b..19eabe09620df3 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts @@ -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({ @@ -353,6 +358,10 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( !props.isMeasuring && { visibility: 'visible' } - ] + ], + ariaContainer: { + position: 'fixed', + opacity: 0 + } }; } diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx index 59589db10ef614..b757084da51fee 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx @@ -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'; @@ -18,7 +18,7 @@ import { ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; -import { FocusZone } from '../../FocusZone'; +import { FocusTrapZone } from '../FocusTrapZone'; const getClassNames = classNamesFunction(); @@ -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 { @@ -115,6 +120,7 @@ export class Coachmark extends BaseComponent { */ private _entityInnerHostElement = createRef(); private _translateAnimationContainer = createRef(); + private _ariaAlertContainer = createRef(); private _positioningContainer = createRef(); /** @@ -149,7 +155,17 @@ export class Coachmark extends BaseComponent { } 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, @@ -160,7 +176,8 @@ export class Coachmark extends BaseComponent { isBeaconAnimating, isMeasuring, entityInnerHostRect, - transformOrigin + transformOrigin, + alertText } = this.state; const classNames = getClassNames(getStyles, { @@ -188,6 +205,16 @@ export class Coachmark extends BaseComponent { {...positioningContainerProps} >
+ {ariaAlertText && ( +
+ {alertText} +
+ )}
@@ -202,13 +229,36 @@ export class Coachmark extends BaseComponent { color={color} /> )} - -
-
+ +
+ {isCollapsed && [ + ariaLabelledBy && ( +

+ {ariaLabelledByText} +

+ ), + ariaDescribedBy && ( +

+ {ariaDescribedByText} +

+ ) + ]} +
{children}
- +
@@ -255,14 +305,43 @@ export class Coachmark extends BaseComponent { 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(); @@ -403,6 +482,13 @@ export class Coachmark extends BaseComponent { 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(); } diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts index bec1690ffd27ec..b7afd92feeb55a 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts @@ -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 {} @@ -32,6 +33,7 @@ export interface ICoachmarkTypes extends React.Props { /** * Whether or not to force the Coachmark/TeachingBubble content to fit within the window bounds. + * @default true */ isPositionForced?: boolean; @@ -114,4 +116,34 @@ export interface ICoachmarkTypes extends React.Props { * Beacon color two. */ beaconColorTwo?: string; + + /** + * Text to announce to screen reader / narrator when Coachmark is displayed + */ + ariaAlertText?: string; + + /** + * 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; } diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkDos.md b/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkDos.md index 0c2e9e5fdece62..41c78655cc1806 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkDos.md +++ b/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkDos.md @@ -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. \ No newline at end of file +- 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` diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx index e413c03984e1e3..e123732700766b 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx @@ -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'; @@ -31,6 +31,7 @@ export interface ICoachmarkBasicExampleStyles { export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExampleState> { private _targetButton = createRef(); + private _teachingBubbleContent: ITeachingBubble; public constructor(props: {}) { super(props); @@ -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'} > Welcome to the land of Coachmarks! @@ -138,4 +148,8 @@ export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExam isCoachmarkVisible: !this.state.isCoachmarkVisible }); }; + + private _teachingBubbleRef = (component: ITeachingBubble): void => { + this._teachingBubbleContent = component; + }; } diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.base.tsx b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.base.tsx index 29dcae60107e3d..8bbc82a6316829 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.base.tsx +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.base.tsx @@ -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'; @@ -28,6 +28,7 @@ export class TeachingBubbleBase extends BaseComponent(); private _defaultCalloutProps: ICalloutProps; // Constructor @@ -45,6 +46,12 @@ export class TeachingBubbleBase extends BaseComponent - +
+ +
); } diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.types.ts b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.types.ts index d4ebbab286afa0..ddb3f12e34253a 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.types.ts +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.types.ts @@ -1,4 +1,5 @@ import * as React from 'react'; + import { TeachingBubbleBase } from './TeachingBubble.base'; import { TeachingBubbleContentBase } from './TeachingBubbleContent.base'; import { IImageProps } from '../../Image'; @@ -8,7 +9,10 @@ import { ICalloutProps } from '../../Callout'; import { IStyle, ITheme } from '../../Styling'; import { IStyleFunctionOrObject } from '../../Utilities'; -export interface ITeachingBubble {} +export interface ITeachingBubble { + /** Sets focus to the TeachingBubble root element */ + focus(): void; +} /** * TeachingBubble component props. @@ -87,6 +91,16 @@ export interface ITeachingBubbleProps * A variation with smaller bold headline and margins to the body (hasCondensedHeadline takes precedence if it is also set to true). */ hasSmallHeadline?: boolean; + + /** + * Defines the element id referencing the element containing label text for TeachingBubble. + */ + ariaLabelledBy?: string; + + /** + * Defines the element id referencing the element containing the description for the TeachingBubble. + */ + ariaDescribedBy?: string; } export type ITeachingBubbleStyleProps = Required> & diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubbleContent.base.tsx b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubbleContent.base.tsx index af950b2a4488fe..0478f1a10652f4 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubbleContent.base.tsx +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubbleContent.base.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { BaseComponent, classNamesFunction } from '../../Utilities'; +import { BaseComponent, classNamesFunction, createRef, KeyCodes } from '../../Utilities'; import { ITeachingBubbleProps, ITeachingBubbleStyleProps, ITeachingBubbleStyles } from './TeachingBubble.types'; import { ITeachingBubbleState } from './TeachingBubble.base'; import { PrimaryButton, DefaultButton, IconButton } from '../../Button'; @@ -18,12 +18,32 @@ export class TeachingBubbleContentBase extends BaseComponent(); + constructor(props: ITeachingBubbleProps) { super(props); this.state = {}; } + public componentDidMount(): void { + if (this.props.onDismiss) { + document.addEventListener('keydown', this._onKeyDown, false); + } + } + + public componentWillUnmount(): void { + if (this.props.onDismiss) { + document.removeEventListener('keydown', this._onKeyDown); + } + } + + public focus(): void { + if (this.rootElement.current) { + this.rootElement.current.focus(); + } + } + public render(): JSX.Element { const { children, @@ -38,7 +58,9 @@ export class TeachingBubbleContentBase extends BaseComponent -

{headline}

+

+ {headline} +

); } @@ -75,7 +99,9 @@ export class TeachingBubbleContentBase extends BaseComponent -

{children}

+

+ {children} +

); } @@ -102,7 +128,15 @@ export class TeachingBubbleContentBase extends BaseComponent +
{imageContent}
{headerContent} @@ -113,4 +147,12 @@ export class TeachingBubbleContentBase extends BaseComponent ); } + + private _onKeyDown = (e: any): void => { + if (this.props.onDismiss) { + if (e.which === KeyCodes.escape) { + this.props.onDismiss(); + } + } + }; } diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/__snapshots__/TeachingBubble.test.tsx.snap b/packages/office-ui-fabric-react/src/components/TeachingBubble/__snapshots__/TeachingBubble.test.tsx.snap index eabbe13c9a270d..cf8215e873233b 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/__snapshots__/TeachingBubble.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/__snapshots__/TeachingBubble.test.tsx.snap @@ -70,54 +70,62 @@ exports[`TeachingBubble renders TeachingBubble correctly 1`] = ` } } > -
+
-

- Test Content -

+

+ Test Content +

+
@@ -128,6 +136,8 @@ exports[`TeachingBubble renders TeachingBubble correctly 1`] = ` exports[`TeachingBubble renders TeachingBubbleContent correctly 1`] = `
Test Title

@@ -198,6 +212,7 @@ exports[`TeachingBubble renders TeachingBubbleContent correctly 1`] = ` margin-right: 0px; margin-top: 0px; } + id={undefined} > Content

@@ -208,6 +223,8 @@ exports[`TeachingBubble renders TeachingBubbleContent correctly 1`] = ` exports[`TeachingBubble renders TeachingBubbleContent with buttons correctly 1`] = `
Test Title

@@ -278,6 +299,7 @@ exports[`TeachingBubble renders TeachingBubbleContent with buttons correctly 1`] margin-right: 0px; margin-top: 0px; } + id={undefined} > Content

@@ -697,6 +719,8 @@ exports[`TeachingBubble renders TeachingBubbleContent with buttons correctly 1`] exports[`TeachingBubble renders TeachingBubbleContent with condensed headline correctly 1`] = `
Test Title

@@ -767,6 +795,7 @@ exports[`TeachingBubble renders TeachingBubbleContent with condensed headline co margin-right: 0px; margin-top: 0px; } + id={undefined} > Content

@@ -777,6 +806,8 @@ exports[`TeachingBubble renders TeachingBubbleContent with condensed headline co exports[`TeachingBubble renders TeachingBubbleContent with image correctly 1`] = `
Test Title

@@ -884,6 +919,7 @@ exports[`TeachingBubble renders TeachingBubbleContent with image correctly 1`] = margin-right: 0px; margin-top: 0px; } + id={undefined} > Content

@@ -894,6 +930,8 @@ exports[`TeachingBubble renders TeachingBubbleContent with image correctly 1`] = exports[`TeachingBubble renders TeachingBubbleContent with small headline correctly 1`] = `
Test Title

@@ -967,6 +1009,7 @@ exports[`TeachingBubble renders TeachingBubbleContent with small headline correc margin-right: 0px; margin-top: 0px; } + id={undefined} > Content