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/common/changes/office-ui-fabric-react/coachmarkFixes_2018-05-30-22-04.json b/common/changes/office-ui-fabric-react/coachmarkFixes_2018-05-30-22-04.json new file mode 100644 index 00000000000000..1d0701fa954227 --- /dev/null +++ b/common/changes/office-ui-fabric-react/coachmarkFixes_2018-05-30-22-04.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Coachmark: Fix positioning bugs and add in support for different Coachmark directions.", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "edwl@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/coachmarkTeachingBubbleFix_2018-06-06-20-52.json b/common/changes/office-ui-fabric-react/coachmarkTeachingBubbleFix_2018-06-06-20-52.json new file mode 100644 index 00000000000000..379837a965caca --- /dev/null +++ b/common/changes/office-ui-fabric-react/coachmarkTeachingBubbleFix_2018-06-06-20-52.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "TeachingBubble: Fix content from wrapping to next line unncessarily", + "type": "patch" + } + ], + "packageName": "office-ui-fabric-react", + "email": "edwl@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/fixImportPaths_2018-06-25-22-09.json b/common/changes/office-ui-fabric-react/fixImportPaths_2018-06-25-22-09.json new file mode 100644 index 00000000000000..3dfba2a2562ebe --- /dev/null +++ b/common/changes/office-ui-fabric-react/fixImportPaths_2018-06-25-22-09.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Fix import paths to use relative paths for office-ui-fabric-react", + "type": "patch" + } + ], + "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.styles.ts b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.styles.ts index 84c6dc16263789..675a8a8265b100 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.styles.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.styles.ts @@ -1,4 +1,4 @@ -import { IStyle, DefaultPalette } from '../../../Styling'; +import { IStyle } from '../../../Styling'; import { IBeakStylesProps } from './Beak.types'; export interface IBeakStyles { @@ -17,18 +17,18 @@ export function getStyles(props: IBeakStylesProps): IBeakStyles { boxShadow: 'inherit', border: 'none', boxSizing: 'border-box', - transform: 'translateY(-50%)', - left: '50%', + transform: props.transform, width: props.width, - height: props.height - }, - (props.left && props.top) && { + height: props.height, left: props.left, - top: props.top + top: props.top, + right: props.right, + bottom: props.bottom, + } ], beak: { - fill: DefaultPalette.themePrimary, + fill: props.color, display: 'block' } }; 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 066d7ad9c25d73..c56a8fd39f9f86 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 @@ -7,44 +7,89 @@ import { import { IBeakProps } from './Beak.types'; import { getStyles, IBeakStyles } from './Beak.styles'; import { IBeakStylesProps } from './Beak.types'; +import { RectangleEdge } from '../../../utilities/positioning'; -export interface IBeakState { - left: string | null; - top: string | null; -} +export const BEAK_HEIGHT = 10; +export const BEAK_WIDTH = 18; -export class Beak extends BaseComponent { +export class Beak extends BaseComponent { constructor(props: IBeakProps) { super(props); } public render(): JSX.Element { const { - height = 18, - width = 18, left, - top + top, + bottom, + right, + color, + direction = RectangleEdge.top } = this.props; + let svgHeight: number; + let svgWidth: number; + + if (direction === RectangleEdge.top || direction === RectangleEdge.bottom) { + svgHeight = BEAK_HEIGHT; + svgWidth = BEAK_WIDTH; + } else { + svgHeight = BEAK_WIDTH; + svgWidth = BEAK_HEIGHT; + } + + let pointOne: string; + let pointTwo: string; + let pointThree: string; + let transform: string; + + switch (direction) { + case RectangleEdge.top: + default: + pointOne = `${BEAK_WIDTH / 2}, 0`; + pointTwo = `${BEAK_WIDTH}, ${BEAK_HEIGHT}`; + pointThree = `0, ${BEAK_HEIGHT}`; + transform = 'translateY(-100%)'; + break; + case RectangleEdge.right: + pointOne = `0, 0`; + pointTwo = `${BEAK_HEIGHT}, ${BEAK_HEIGHT}`; + pointThree = `0, ${BEAK_WIDTH}`; + transform = 'translateX(100%)'; + break; + case RectangleEdge.bottom: + pointOne = `0, 0`; + pointTwo = `${BEAK_WIDTH}, 0`; + pointThree = `${BEAK_WIDTH / 2}, ${BEAK_HEIGHT}`; + transform = 'translateY(100%)'; + break; + case RectangleEdge.left: + pointOne = `${BEAK_HEIGHT}, 0`; + pointTwo = `0, ${BEAK_HEIGHT}`; + pointThree = `${BEAK_HEIGHT}, ${BEAK_WIDTH}`; + transform = 'translateX(-100%)'; + break; + } + const getClassNames = classNamesFunction(); const classNames = getClassNames(getStyles, { left, top, - height: height + 'px', - width: width + 'px' + bottom, + right, + height: `${svgHeight}px`, + width: `${svgWidth}px`, + transform: transform, + color }); - const pointOne = width / 2 + ',' + 0; - const pointTwo = width + ',' + height; - const pointThree = 0 + ',' + height; - return (
diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.types.ts b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.types.ts index ae4db0cb36c420..9065fc1bb60e1f 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.types.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.types.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { Beak } from './Beak'; +import { RectangleEdge } from '../../../utilities/positioning'; export interface IBeak { } @@ -12,12 +13,14 @@ export interface IBeakProps extends React.Props { /** * Beak width. * @default 18 + * @deprecated */ width?: number; /** * Beak height. * @default 18 + * @deprecated */ height?: number; @@ -29,17 +32,36 @@ export interface IBeakProps extends React.Props { /** * Left position of the beak */ - left?: string | null; + left?: string; /** * Top position of the beak */ - top?: string | null; + top?: string; + + /** + * Right position of the beak + */ + right?: string; + + /** + * Bottom position of the beak + */ + bottom?: string; + + /** + * Direction of beak + */ + direction?: RectangleEdge; } export interface IBeakStylesProps { - left?: string | null; - top?: string | null; + left?: string | undefined; + top?: string | undefined; + bottom?: string | undefined; + right?: string | undefined; width?: string; height?: string; + transform?: string; + color?: string; } \ No newline at end of file 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 97c163d1a661a8..cc2f7742724e51 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 @@ -6,11 +6,20 @@ import { getTheme } from '../../Styling'; +export const COACHMARK_WIDTH = 32; +export const COACHMARK_HEIGHT = 32; + export interface ICoachmarkStyleProps { + /** + * Is the Coachmark collapsed. Deprecated: use isCollapsed instead. + * @deprecated + */ + collapsed?: boolean; + /** * Is the Coachmark collapsed */ - collapsed: boolean; + isCollapsed: boolean; /** * Is the beacon currently animating. @@ -57,6 +66,11 @@ export interface ICoachmarkStyleProps { * Beacon color two */ beaconColorTwo?: string; + + /** + * Transform origin for teaching bubble content + */ + transformOrigin?: string; } export interface ICoachmarkStyles { @@ -102,6 +116,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({ @@ -252,7 +271,7 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( borderStyle: 'solid', opacity: '0' }, - (props.collapsed && props.isBeaconAnimating) && ContinuousPulseAnimation + (props.isCollapsed && props.isBeaconAnimating) && ContinuousPulseAnimation ], // Translate Animation Layer translateAnimationContainer: [ @@ -260,7 +279,7 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( width: '100%', height: '100%' }, - props.collapsed && { + props.isCollapsed && { animationDuration: '14s', animationTimingFunction: 'linear', animationDirection: 'normal', @@ -270,7 +289,7 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( animationName: translateOne, transition: 'opacity 0.5s ease-in-out' }, - (!props.collapsed) && { + (!props.isCollapsed) && { opacity: '1' } ], @@ -280,7 +299,7 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( width: '100%', height: '100%' }, - props.collapsed && { + props.isCollapsed && { animationDuration: '14s', animationTimingFunction: 'linear', animationDirection: 'normal', @@ -294,10 +313,9 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( rotateAnimationLayer: [ { width: '100%', - height: '100%', - opacity: '0.8' + height: '100%' }, - props.collapsed && { + props.isCollapsed && { animationDuration: '14s', animationTimingFunction: 'linear', animationDirection: 'normal', @@ -306,7 +324,7 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( animationFillMode: 'forwards', animationName: rotateOne }, - !props.collapsed && { + !props.isCollapsed && { opacity: '1' } ], @@ -317,16 +335,16 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( outline: 'none', overflow: 'hidden', backgroundColor: props.color, - borderRadius: props.width, + borderRadius: COACHMARK_WIDTH, transition: 'border-radius 250ms, width 500ms, height 500ms cubic-bezier(0.5, 0, 0, 1)', visibility: 'hidden' }, !props.isMeasuring && { - width: props.width, - height: props.height, + width: COACHMARK_WIDTH, + height: COACHMARK_HEIGHT, visibility: 'visible' }, - !props.collapsed && { + !props.isCollapsed && { borderRadius: '1px', opacity: '1', width: props.entityHostWidth, @@ -336,10 +354,10 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( entityInnerHost: [ { transition: 'transform 500ms cubic-bezier(0.5, 0, 0, 1)', - transformOrigin: 'top left', + transformOrigin: props.transformOrigin, transform: 'scale(0)' }, - (!props.collapsed) && { + (!props.isCollapsed) && { width: props.entityHostWidth, height: props.entityHostHeight, transform: 'scale(1)' @@ -347,6 +365,10 @@ export function getStyles(props: ICoachmarkStyleProps, theme: ITheme = getTheme( (!props.isMeasuring) && { visibility: 'visible', } - ] + ], + ariaContainer: { + position: 'fixed', + opacity: 0 + } }; } \ No newline at end of file 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 795f7e374a41a1..2ddf5fd1c3f62c 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx @@ -1,19 +1,29 @@ // Utilities import * as React from 'react'; -import { BaseComponent, classNamesFunction, createRef } from '../../Utilities'; +import { BaseComponent, classNamesFunction, createRef, IRectangle, KeyCodes, shallowCompare } from '../../Utilities'; import { DefaultPalette } from '../../Styling'; +import { IPositionedData, RectangleEdge, getOppositeEdge } from '../../utilities/positioning'; // Component Dependencies import { PositioningContainer, IPositioningContainer } from './PositioningContainer/index'; -import { Beak } from './Beak/Beak'; +import { Beak, BEAK_HEIGHT, BEAK_WIDTH } from './Beak/Beak'; +import { DirectionalHint } from '../../common/DirectionalHint'; // Coachmark import { ICoachmarkTypes } from './Coachmark.types'; -import { getStyles, ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; -import { FocusZone } from '../../FocusZone'; +import { + COACHMARK_HEIGHT, + COACHMARK_WIDTH, + getStyles, + ICoachmarkStyles, + ICoachmarkStyleProps +} from './Coachmark.styles'; +import { FocusTrapZone } from '../FocusTrapZone'; const getClassNames = classNamesFunction(); +export const COACHMARK_ATTRIBUTE_NAME = 'data-coachmarkid'; + /** * An interface for the cached dimensions of entity inner host. */ @@ -27,7 +37,7 @@ export interface ICoachmarkState { * Is the Coachmark currently collapsed into * a tear drop shape */ - collapsed: boolean; + isCollapsed: boolean; /** * Enables/Disables the beacon that radiates @@ -54,24 +64,54 @@ export interface ICoachmarkState { /** * The left position of the beak */ - beakLeft?: string | null; + beakLeft?: string; + + /** + * The right position of the beak + */ + beakTop?: string; /** * The right position of the beak */ - beakTop?: string | null; + beakRight?: string; + + /** + * The bottom position of the beak + */ + beakBottom?: string; + + /** + * Alignment edge of callout in relation to target + */ + targetAlignment?: RectangleEdge; + + /** + * Position of Coachmark/TeachingBubble in relation to target + */ + targetPosition?: RectangleEdge; + + /** + * 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 { public static defaultProps: Partial = { - collapsed: true, - mouseProximityOffset: 100, - beakWidth: 26, - beakHeight: 12, + isCollapsed: true, + mouseProximityOffset: 10, delayBeforeMouseOpen: 3600, // The approximate time the coachmark shows up - width: 36, - height: 36, - color: DefaultPalette.themePrimary + color: DefaultPalette.themePrimary, + isPositionForced: true, + positioningContainerProps: { + directionalHint: DirectionalHint.bottomAutoEdge, + } }; /** @@ -80,14 +120,21 @@ export class Coachmark extends BaseComponent { */ private _entityInnerHostElement = createRef(); private _translateAnimationContainer = createRef(); + private _ariaAlertContainer = createRef(); private _positioningContainer = createRef(); + /** + * The target element the mouse would be in + * proximity to + */ + private _targetElementRect: ClientRect; + constructor(props: ICoachmarkTypes) { super(props); // Set defaults for state this.state = { - collapsed: props.collapsed!, + isCollapsed: props.isCollapsed!, isBeaconAnimating: true, isMeasuring: true, entityInnerHostRect: { @@ -98,35 +145,76 @@ export class Coachmark extends BaseComponent { }; } + private get _beakDirection(): RectangleEdge { + const { targetPosition } = this.state; + if (targetPosition === undefined) { + return RectangleEdge.bottom; + } + + return getOppositeEdge(targetPosition); + } + public render(): JSX.Element { const { children, - beakWidth, - beakHeight, target, - width, - height, - color + color, + positioningContainerProps, + ariaDescribedBy, + ariaDescribedByText, + ariaLabelledBy, + ariaLabelledByText, + ariaAlertText } = this.props; + const { + beakLeft, + beakTop, + beakRight, + beakBottom, + isCollapsed, + isBeaconAnimating, + isMeasuring, + entityInnerHostRect, + transformOrigin, + alertText + } = this.state; + const classNames = getClassNames(getStyles, { - collapsed: this.state.collapsed, - isBeaconAnimating: this.state.isBeaconAnimating, - isMeasuring: this.state.isMeasuring, - entityHostHeight: this.state.entityInnerHostRect.height + 'px', - entityHostWidth: this.state.entityInnerHostRect.width + 'px', - width: width + 'px', - height: height + 'px', - color: color + isCollapsed: isCollapsed, + isBeaconAnimating: isBeaconAnimating, + isMeasuring: isMeasuring, + entityHostHeight: `${entityInnerHostRect.height}px`, + entityHostWidth: `${entityInnerHostRect.width}px`, + width: `${COACHMARK_WIDTH}px`, + height: `${COACHMARK_HEIGHT}px`, + color: color, + transformOrigin: transformOrigin }); + const finalHeight: number = isCollapsed ? COACHMARK_HEIGHT : entityInnerHostRect.height; + return (
+ { ariaAlertText && ( +
+ { alertText } +
+ ) }
{
{ this._positioningContainer.current && } - +
+ { isCollapsed && [ + ariaLabelledBy && ( +

+ { ariaLabelledByText } +

+ ), + ariaDescribedBy && ( +

+ { ariaDescribedByText } +

+ ) + ] }
{ children }
-
+
@@ -165,60 +271,226 @@ export class Coachmark extends BaseComponent { } public componentWillReceiveProps(newProps: ICoachmarkTypes): void { - if (this.props.collapsed && !newProps.collapsed) { + if (this.props.isCollapsed && !newProps.isCollapsed) { // The coachmark is about to open this._openCoachmark(); } } - public componentDidMount(): void { - this._async.requestAnimationFrame(((): void => { - if (this._entityInnerHostElement.current && (this.state.entityInnerHostRect.width + this.state.entityInnerHostRect.width) === 0) { - - // @TODO Eventually we need to add the various directions - const beakLeft = (this.props.width! / 2) - (this.props.beakWidth! / 2); - const beakTop = 0; - - this.setState({ - isMeasuring: false, - entityInnerHostRect: { - width: this._entityInnerHostElement.current.offsetWidth, - height: this._entityInnerHostElement.current.offsetHeight - }, - beakLeft: beakLeft + 'px', - beakTop: beakTop + 'px' - }); + public shouldComponentUpdate(newProps: ICoachmarkTypes, newState: ICoachmarkState): boolean { + return !shallowCompare(newProps, this.props) || !shallowCompare(newState, this.state); + } - this.forceUpdate(); + public componentDidUpdate(prevProps: ICoachmarkTypes, prevState: ICoachmarkState): void { + if (prevState.targetAlignment !== this.state.targetAlignment || prevState.targetPosition !== this.state.targetPosition) { + this._setBeakPosition(); + } + } + + public componentDidMount(): void { + this._async.requestAnimationFrame( + (): void => { + if ( + this._entityInnerHostElement.current && + this.state.entityInnerHostRect.width + this.state.entityInnerHostRect.width === 0 + ) { + this.setState({ + isMeasuring: false, + entityInnerHostRect: { + width: this._entityInnerHostElement.current.offsetWidth, + height: this._entityInnerHostElement.current.offsetHeight + } + }); + this._setBeakPosition(); + 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); + } } + ); + } - // We dont want to the user to immediatley trigger the coachmark when it's opened - this._async.setTimeout(() => { - this._addProximityHandler(100); - }, this.props.delayBeforeMouseOpen!); - })); + 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 => { - this._openCoachmark(); + if (this.state.isCollapsed) { + this._openCoachmark(); + } } - private _openCoachmark = (): void => { - this.setState({ - collapsed: false - }); + private _onPositioned = (positionData: IPositionedData): void => { + this._async.requestAnimationFrame(((): void => { + this.setState({ + targetAlignment: positionData.alignmentEdge, + targetPosition: positionData.targetEdge + }); + })); + } - this._translateAnimationContainer.current && this._translateAnimationContainer.current.addEventListener('animationstart', (): void => { - if (this.props.onAnimationOpenStart) { - this.props.onAnimationOpenStart(); + private _getBounds(): IRectangle | undefined { + const { isPositionForced, positioningContainerProps } = this.props; + if (isPositionForced) { + // If directionalHint direction is the top or bottom auto edge, then we want to set the left/right bounds + // to the window x-axis to have auto positioning work correctly. + if (positioningContainerProps && ( + positioningContainerProps.directionalHint === DirectionalHint.topAutoEdge || + positioningContainerProps.directionalHint === DirectionalHint.bottomAutoEdge + )) { + return { + left: 0, + top: -Infinity, + bottom: Infinity, + right: window.innerWidth, + width: window.innerWidth, + height: Infinity + }; + } else { + return { + left: -Infinity, + top: -Infinity, + bottom: Infinity, + right: Infinity, + width: Infinity, + height: Infinity + }; } + } else { + return undefined; + } + } + + private _setBeakPosition = (): void => { + let beakLeft; + let beakTop; + let beakRight; + let beakBottom; + let transformOriginX; + let transformOriginY; + + const { targetAlignment } = this.state; + const distanceAdjustment = '3px'; // Adjustment distance for Beak to shift towards Coachmark bubble. + + switch (this._beakDirection) { + // If Beak is pointing Up or Down + case RectangleEdge.top: + case RectangleEdge.bottom: + // If there is no target alignment, then beak is X-axis centered in callout + if (!targetAlignment) { + beakLeft = `calc(50% - ${BEAK_WIDTH / 2}px)`; + transformOriginX = 'center'; + } else { + // Beak is aligned to the left of target + if (targetAlignment === RectangleEdge.left) { + beakLeft = `${COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2}px`; + transformOriginX = 'left'; + } else { + // Beak is aligned to the right of target + beakRight = `${COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2}px`; + transformOriginX = 'right'; + } + } + + if (this._beakDirection === RectangleEdge.top) { + beakTop = distanceAdjustment; + transformOriginY = 'top'; + } else { + beakBottom = distanceAdjustment; + transformOriginY = 'bottom'; + } + break; + // If Beak is pointing Left or Right + case RectangleEdge.left: + case RectangleEdge.right: + // If there is no target alignment, then beak is Y-axis centered in callout + if (!targetAlignment) { + beakTop = `calc(50% - ${BEAK_WIDTH / 2}px)`; + transformOriginY = `center`; + } else { + // Beak is aligned to the top of target + if (targetAlignment === RectangleEdge.top) { + beakTop = `${COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2}px`; + transformOriginY = `top`; + } else { + // Beak is aligned to the bottom of target + beakBottom = `${COACHMARK_WIDTH / 2 - BEAK_WIDTH / 2}px`; + transformOriginY = `bottom`; + } + } + + if (this._beakDirection === RectangleEdge.left) { + beakLeft = distanceAdjustment; + transformOriginX = 'left'; + } else { + beakRight = distanceAdjustment; + transformOriginX = 'right'; + } + break; + } + + this.setState({ + beakLeft: beakLeft, + beakRight: beakRight, + beakBottom: beakBottom, + beakTop: beakTop, + transformOrigin: `${transformOriginX} ${transformOriginY}` }); + } - this._translateAnimationContainer.current && this._translateAnimationContainer.current.addEventListener('animationend', (): void => { - if (this.props.onAnimationOpenEnd) { - this.props.onAnimationOpenEnd(); - } + private _openCoachmark = (): void => { + this.setState({ + isCollapsed: false }); + + if (this.props.onAnimationOpenStart) { + this.props.onAnimationOpenStart(); + } + + this._entityInnerHostElement.current && + 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(); + } + } + ); } private _addProximityHandler(mouseProximityOffset: number = 0): void { @@ -228,18 +500,10 @@ export class Coachmark extends BaseComponent { */ const timeoutIds: number[] = []; - /** - * The target element the mouse would be in - * proximity to - */ - let targetElementRect: ClientRect; - // Take the initial measure out of the initial render to prevent // an unnecessary render. this._async.setTimeout(() => { - if (this._entityInnerHostElement.current) { - targetElementRect = this._entityInnerHostElement.current.getBoundingClientRect(); - } + this._setTargetElementRect(); // When the window resizes we want to async // get the bounding client rectangle. @@ -252,9 +516,7 @@ export class Coachmark extends BaseComponent { }); timeoutIds.push(this._async.setTimeout((): void => { - if (this._entityInnerHostElement.current) { - targetElementRect = this._entityInnerHostElement.current.getBoundingClientRect(); - } + this._setTargetElementRect(); }, 100)); }); }, 10); @@ -263,17 +525,15 @@ export class Coachmark extends BaseComponent { // we want to check if inside of an element and // set the state with the result. this._events.on(document, 'mousemove', (e: MouseEvent) => { - const mouseY = e.pageY; - const mouseX = e.pageX; - const isMouseInProximity = this._isInsideElement(mouseX, mouseY, targetElementRect, mouseProximityOffset); - - if (isMouseInProximity !== this.state.isMouseInProximity) { - // We don't want to update the isMouseInProximtiy state because - // The coachmark only opens and does not collapse. - // Setting isMouseInProximity here will cause the coachmark to open and close - this.setState({ - collapsed: !isMouseInProximity - }); + if (this.state.isCollapsed) { + const mouseY = e.pageY; + const mouseX = e.pageX; + this._setTargetElementRect(); + const isMouseInProximity = this._isInsideElement(mouseX, mouseY, mouseProximityOffset); + + if (isMouseInProximity !== this.state.isMouseInProximity) { + this._openCoachmark(); + } } if (this.props.onMouseMove) { @@ -282,10 +542,16 @@ export class Coachmark extends BaseComponent { }); } - private _isInsideElement(mouseX: number, mouseY: number, elementRect: ClientRect, mouseProximityOffset: number = 0): boolean { - return mouseX > (elementRect.left - mouseProximityOffset) && - mouseX < ((elementRect.left + elementRect.width) + mouseProximityOffset) && - mouseY > (elementRect.top - mouseProximityOffset) && - mouseY < ((elementRect.top + elementRect.height) + mouseProximityOffset); + private _setTargetElementRect(): void { + if (this._translateAnimationContainer && this._translateAnimationContainer.current) { + this._targetElementRect = this._translateAnimationContainer!.current!.getBoundingClientRect(); + } + } + + private _isInsideElement(mouseX: number, mouseY: number, mouseProximityOffset: number = 0): boolean { + return mouseX > (this._targetElementRect.left - mouseProximityOffset) && + mouseX < ((this._targetElementRect.left + this._targetElementRect.width) + mouseProximityOffset) && + mouseY > (this._targetElementRect.top - mouseProximityOffset) && + mouseY < ((this._targetElementRect.top + this._targetElementRect.height) + mouseProximityOffset); } } \ No newline at end of file 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 ebecf59fe72e56..3a3369269176cb 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,35 +3,58 @@ import { Coachmark } from './Coachmark'; import { ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; import { IPositioningContainerTypes } from './PositioningContainer/PositioningContainer.types'; import { IStyleFunction } from '../../Utilities'; +import { ITeachingBubble } from '../../TeachingBubble'; export interface ICoachmark { } export interface ICoachmarkTypes extends React.Props { + /** + * Optional callback to access the ICoachmark interface. Use this instead of ref for accessing + * the public methods and properties of the component. + */ componentRef?: (component: ICoachmark | null) => void; /** - * Get styles method. + * Call to provide customized styling that will layer on top of the variant rules */ getStyles?: IStyleFunction; /** - * The target that the TeachingBubble should try to position itself based on. + * The target that the Coachmark should try to position itself based on. */ - target: HTMLElement | null; + target: HTMLElement | string | null; + /** + * Props to pass to the PositioningContainer component. Specific the `directionalHint` to indicate which edge the + * Coachmark/TeachingBubble should live. + * @default directionalHint: DirectionalHint.bottomAutoEdge + */ positioningContainerProps?: IPositioningContainerTypes; /** - * The starting collapsed state for the Coachmark? + * Whether or not to force the Coachmark/TeachingBubble content to fit within the window bounds. + * @default true + */ + isPositionForced?: boolean; + + /** + * The starting collapsed state for the Coachmark. Use isCollapsed instead. * @default true + * @deprecated */ collapsed?: boolean; + /** + * The starting collapsed state for the Coachmark. + * @default true + */ + isCollapsed?: boolean; + /** * The distance in pixels the mouse is located - * before opening up the coachmark. - * @default 100 + * before opening up the Coachmark. + * @default 10 */ mouseProximityOffset?: number; @@ -46,48 +69,82 @@ export interface ICoachmarkTypes extends React.Props { onAnimationOpenEnd?: () => void; /** - * The width of the beak component. + * The width of the Beak component. + * @deprecated */ beakWidth?: number; /** - * The height of the beak component + * The height of the Beak component. + * @deprecated */ beakHeight?: number; /** - * Delay before allowing mouse movements to open - * the Coachmark + * Delay before allowing mouse movements to open the Coachmark. + * @default 3600 */ delayBeforeMouseOpen?: number; /** - * Runs every time the mouse moves + * Callback to run when the mouse moves. */ onMouseMove?: (e: MouseEvent) => void; /** - * The width of the coachmark + * The width of the Coachmark. + * @deprecated */ width?: number; /** - * The height of the coachmark + * The height of the Coachmark. + * @deprecated */ height?: number; /** - * Color + * Color of the Coachmark/TeachingBubble. */ color?: string; /** - * Beacon color one + * Beacon color one. */ beaconColorOne?: string; /** - * Beacon color two + * 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/PositioningContainer/PositioningContainer.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx index 9123fe2b073981..d8946c7a882c5d 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { IPositioningContainerTypes } from './PositioningContainer.types'; import { getClassNames } from './PositioningContainer.styles'; -import { Layer } from 'office-ui-fabric-react/lib/Layer'; +import { Layer } from '../../Layer'; // Utilites/Helpers -import { DirectionalHint } from 'office-ui-fabric-react/lib/common/DirectionalHint'; +import { DirectionalHint } from '../../../common/DirectionalHint'; import { BaseComponent, IPoint, @@ -19,13 +19,13 @@ import { } from '../../../Utilities'; import { - IPositionProps, getMaxHeight, ICalloutPositon, positionElement, IPositionedData, + IPositionProps, RectangleEdge -} from 'office-ui-fabric-react/lib/utilities/positioning'; +} from '../../../utilities/positioning'; import { AnimationClassNames, mergeStyles } from '../../../Styling'; @@ -197,11 +197,20 @@ export class PositioningContainer ); } + /** + * Deprecated. Use onResize instead. + * @deprecated + */ public dismiss = (ev?: Event | React.MouseEvent | React.KeyboardEvent): void => { - const { onDismiss } = this.props; + this.onResize(ev); + } + public onResize = (ev?: Event | React.MouseEvent | React.KeyboardEvent): void => { + const { onDismiss } = this.props; if (onDismiss) { onDismiss(ev); + } else { + this._updateAsyncPosition(); } } @@ -222,7 +231,7 @@ export class PositioningContainer clickedOutsideCallout && ((this._target as MouseEvent).stopPropagation || (!this._target || (target !== this._target && !elementContains(this._target as HTMLElement, target))))) { - this.dismiss(ev); + this.onResize(ev); } } @@ -239,8 +248,8 @@ export class PositioningContainer // to be required to avoid React firing an async focus event in IE from // the target changing focus quickly prior to rendering the positioningContainer. this._async.setTimeout(() => { - this._events.on(this._targetWindow, 'scroll', this._dismissOnScroll, true); - this._events.on(this._targetWindow, 'resize', this.dismiss, true); + this._events.on(this._targetWindow, 'scroll', this._async.throttle(this._dismissOnScroll, 10), true); + this._events.on(this._targetWindow, 'resize', this._async.throttle(this.onResize, 10), true); this._events.on(this._targetWindow.document.body, 'focus', this._dismissOnLostFocus, true); this._events.on(this._targetWindow.document.body, 'click', this._dismissOnLostFocus, true); }, 0); @@ -272,25 +281,34 @@ export class PositioningContainer currentProps = assign(currentProps, this.props); currentProps!.bounds = this._getBounds(); currentProps!.target = this._target!; - currentProps!.gapSpace = offsetFromTarget; - const newPositions: IPositionedData = positionElement(currentProps!, hostElement, positioningContainerElement); - - // Set the new position only when the positions are not exists or one of the new positioningContainer positions are different. - // The position should not change if the position is within 2 decimal places. - if ((!positions && newPositions) || - (positions && newPositions && !this._arePositionsEqual(positions, newPositions) - && this._positionAttempts < 5)) { - // We should not reposition the positioningContainer more than a few times, if it is then the content is likely resizing - // and we should stop trying to reposition to prevent a stack overflow. - this._positionAttempts++; + if (document.body.contains(currentProps!.target as Node)) { + currentProps!.gapSpace = offsetFromTarget; + const newPositions: IPositionedData = positionElement(currentProps!, hostElement, positioningContainerElement); + // Set the new position only when the positions are not exists or one of the new positioningContainer positions are different. + // The position should not change if the position is within 2 decimal places. + if ((!positions && newPositions) || + (positions && newPositions && !this._arePositionsEqual(positions, newPositions) + && this._positionAttempts < 5)) { + // We should not reposition the positioningContainer more than a few times, if it is then the content is likely resizing + // and we should stop trying to reposition to prevent a stack overflow. + this._positionAttempts++; + this.setState({ + positions: newPositions + }, () => { + if (onPositioned) { + onPositioned(newPositions); + } + }); + } else { + this._positionAttempts = 0; + if (onPositioned) { + onPositioned(newPositions); + } + } + } else if (positions !== undefined) { this.setState({ - positions: newPositions + positions: undefined }); - } else { - this._positionAttempts = 0; - if (onPositioned) { - onPositioned(); - } } } } diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.types.ts b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.types.ts index 693b311e77c52b..abc8388f11fc90 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.types.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.types.ts @@ -1,14 +1,13 @@ import * as React from 'react'; import { PositioningContainer } from './PositioningContainer'; -import { DirectionalHint } from 'office-ui-fabric-react/lib/common/DirectionalHint'; +import { DirectionalHint } from '../../../common/DirectionalHint'; +import { IPoint, IRectangle } from '../../../Utilities'; import { - IPoint, - IRectangle -} from '../../../Utilities'; -import { ICalloutPositon } from 'office-ui-fabric-react/lib/utilities/positioning'; + ICalloutPositon, + IPositionedData +} from '../../../utilities/positioning'; -export interface IPositioningContainer { -} +export interface IPositioningContainer { } export interface IPositionInfo { calloutPosition: ICalloutPositon; @@ -60,7 +59,7 @@ export interface IPositioningContainerTypes extends React.Props void; + onPositioned?: (positions?: IPositionedData) => void; /** * Callback when the positioningContainer tries to close. 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 e633c88b6505b0..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 + callout combo will 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 coachmakrs does not exceed 3 steps. -- Coachmarks are designed to only hold Callouts. \ No newline at end of file +- 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 +- 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/docs/CoachmarkOverview.md b/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkOverview.md index c498d3cc459ceb..2f2cf70c03fe45 100644 --- a/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkOverview.md +++ b/packages/office-ui-fabric-react/src/components/Coachmark/docs/CoachmarkOverview.md @@ -1,4 +1,4 @@ Coachmarks are used to draw a persons attention to a part of the UI, and increase engagement with that element in the product. -They should be contextual whenever possible, or related to something that will make existing user flows more efficient +They should be contextual whenever possible, or related to something that will make existing user flows more efficient. \ No newline at end of file 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 65bbb98b80f669..bbd50505957877 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,9 +1,10 @@ import * as React from 'react'; import { Coachmark } from '../Coachmark'; -import { TeachingBubbleContent } from 'office-ui-fabric-react/lib/TeachingBubble'; -import { ICalloutProps } from 'office-ui-fabric-react/lib/Callout'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +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'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { BaseComponent, classNamesFunction, @@ -11,9 +12,8 @@ import { } from 'office-ui-fabric-react/lib/Utilities'; export interface ICoachmarkBasicExampleState { - isVisible?: boolean; - isCoachmarkCollapsed?: boolean; - targetElement?: HTMLElement; + isCoachmarkVisible?: boolean; + coachmarkPosition: DirectionalHint; } export interface ICoachmarkBasicExampleStyles { @@ -26,60 +26,113 @@ export interface ICoachmarkBasicExampleStyles { * The example button container */ buttonContainer: IStyle; + + /** + * The dropdown component container + */ + dropdownContainer: IStyle; } export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExampleState> { private _targetButton = createRef(); + private _teachingBubbleContent: ITeachingBubble; public constructor(props: {}) { super(props); - this._onShowMenuClicked = this._onShowMenuClicked.bind(this); - this._onCalloutDismiss = this._onCalloutDismiss.bind(this); - this.state = { - isVisible: false, - isCoachmarkCollapsed: true + isCoachmarkVisible: false, + coachmarkPosition: DirectionalHint.bottomAutoEdge }; } public render(): JSX.Element { - const { isVisible } = this.state; - - const calloutProps: ICalloutProps = { - doNotLayer: true - }; + const { isCoachmarkVisible } = this.state; const getClassNames = classNamesFunction<{}, ICoachmarkBasicExampleStyles>(); const classNames = getClassNames(() => { return { - root: {}, + dropdownContainer: { + maxWidth: '400px' + }, buttonContainer: { + marginTop: '30px', display: 'inline-block' } }; }); + const buttonProps: IButtonProps = { + text: 'Try it' + }; + + const buttonProps2: IButtonProps = { + text: 'Try it again' + }; + return (
+
+ +
+
- { isVisible && ( + { isCoachmarkVisible && ( - Welcome to the land of coachmarks + Welcome to the land of Coachmarks! ) } @@ -87,15 +140,25 @@ export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExam ); } - private _onShowMenuClicked(): void { + private _onDismiss = (): void => { + this.setState({ + isCoachmarkVisible: false + }); + } + + private _onDropdownChange = (option: IDropdownOption): void => { this.setState({ - isVisible: !this.state.isVisible + coachmarkPosition: option.data }); } - private _onCalloutDismiss(): void { + private _onShowMenuClicked = (): void => { this.setState({ - isVisible: false + isCoachmarkVisible: !this.state.isCoachmarkVisible }); } -} \ No newline at end of file + + private _teachingBubbleRef = (component: ITeachingBubble): void => { + this._teachingBubbleContent = component; + } +} diff --git a/packages/office-ui-fabric-react/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx b/packages/office-ui-fabric-react/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx index 4fc40d65513ab2..ab58825f8c4493 100644 --- a/packages/office-ui-fabric-react/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx +++ b/packages/office-ui-fabric-react/src/components/SelectedItemsList/SelectedPeopleList/Items/EditingItem.tsx @@ -14,7 +14,7 @@ import { FloatingPeoplePicker, IBaseFloatingPickerProps } from '../../../../Floa import { ISelectedPeopleItemProps } from '../SelectedPeopleList'; import { IExtendedPersonaProps } from '../SelectedPeopleList'; import { IPeoplePickerItemState } from './ExtendedSelectedItem'; -import { IPersonaProps } from 'office-ui-fabric-react/lib/Persona'; +import { IPersonaProps } from '../../../Persona'; import * as stylesImport from './EditingItem.scss'; diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.scss b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.scss index 113d36a6e24d08..428650410a9eea 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.scss +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.scss @@ -60,6 +60,7 @@ max-width: 364px; border: 0; box-shadow: none !important; + width: calc(100% + 1px); animation-name: (bounceAnimation, opacityFadeIn); animation-duration: 2000ms; @@ -118,9 +119,14 @@ top: 0; color: $ms-color-white; font-size: $ms-icon-size-s; + + &:hover { + background: transparent; + } } .footer { + display: flex; :global(.ms-Button):not(:first-child) { @include margin-left(20px); } @@ -155,9 +161,10 @@ } //Primary button style override -.root .primaryButton { +.bodyContent .primaryButton { background-color: $ms-color-white; border-color: $ms-color-white; + white-space: nowrap; :global(.ms-Button-label) { @include ms-font-m; @@ -181,9 +188,10 @@ } //Secondary button style override -.root .secondaryButton { +.bodyContent .secondaryButton { background-color: $ms-color-themePrimary; border-color: $ms-color-white; + white-space: nowrap; :global(.ms-Button-label) { @include ms-font-m; diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.tsx b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.tsx index 73d7599b7d9def..9e71cac6446c63 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.tsx +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { BaseComponent, css } from '../../Utilities'; +import { BaseComponent, css, createRef } from '../../Utilities'; import { TeachingBubbleContent } from './TeachingBubbleContent'; import { ITeachingBubbleProps } from './TeachingBubble.types'; import { Callout, ICalloutProps } from '../../Callout'; @@ -23,10 +23,11 @@ export class TeachingBubble extends BaseComponent(); private _defaultCalloutProps: ICalloutProps; // Constructor @@ -41,12 +42,23 @@ export class TeachingBubble extends BaseComponent - +
+ +
); } -} \ No newline at end of file +} 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 fd1bb9855a0bc1..7312e0e9a84a59 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,20 +1,21 @@ -import * as React from 'react'; -import { TeachingBubble } from './TeachingBubble'; -import { TeachingBubbleContent } from './TeachingBubbleContent'; import { IImageProps } from '../../Image'; import { IButtonProps } from '../../Button'; import { IAccessiblePopupProps } from '../../common/IAccessiblePopupProps'; import { ICalloutProps } from '../../Callout'; +import { RefObject } from '../../Utilities'; export interface ITeachingBubble { + rootElement: RefObject; + /** Sets focus to the TeachingBubble root element */ + focus(): void; } /** * TeachingBubble component props. */ -export interface ITeachingBubbleProps extends React.Props, IAccessiblePopupProps { +export interface ITeachingBubbleProps extends React.Props, IAccessiblePopupProps { /** * Optional callback to access the ISlider interface. Use this instead of ref for accessing * the public methods and properties of the component. @@ -75,4 +76,14 @@ export interface ITeachingBubbleProps extends React.Props { - +export class TeachingBubbleContent extends BaseComponent< + ITeachingBubbleProps, + ITeachingBubbleState + > { // Specify default props values public static defaultProps = { hasCondensedHeadline: false, @@ -19,15 +21,48 @@ export class TeachingBubbleContent extends BaseComponent(); + constructor(props: ITeachingBubbleProps) { super(props); - this.state = { - }; + 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 { illustrationImage, primaryButtonProps, secondaryButtonProps, headline, hasCondensedHeadline, hasCloseIcon, onDismiss, closeButtonAriaLabel, hasSmallHeadline } = this.props; + const { + children, + illustrationImage, + primaryButtonProps, + secondaryButtonProps, + headline, + hasCondensedHeadline, + hasCloseIcon, + onDismiss, + closeButtonAriaLabel, + hasSmallHeadline, + isWide, + ariaDescribedBy, + ariaLabelledBy + } = this.props; let imageContent; let headerContent; @@ -48,25 +83,26 @@ export class TeachingBubbleContent extends BaseComponent -

+

{ headline }

); } - if (this.props.children) { + if (children) { bodyContent = (
-

- { this.props.children } +

+ { children }

); @@ -78,13 +114,21 @@ export class TeachingBubbleContent extends BaseComponent ) } { secondaryButtonProps && ( ) }
@@ -104,15 +148,37 @@ export class TeachingBubbleContent extends BaseComponent +
{ imageContent } - { closeButton } -
+
{ headerContent } { bodyContent } { footerContent }
+ { closeButton }
); } -} \ No newline at end of file + + 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 5cd04d29e00325..612d2dbdb808db 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 @@ -8,7 +8,12 @@ exports[`TeachingBubble renders TeachingBubble correctly 1`] = ` exports[`TeachingBubble renders TeachingBubble correctly 2`] = `

Title

@@ -27,6 +33,7 @@ exports[`TeachingBubble renders TeachingBubble correctly 2`] = ` >

Content

diff --git a/packages/office-ui-fabric-react/src/components/pickers/PeoplePicker/PeoplePickerItems/SuggestionItemDefault.tsx b/packages/office-ui-fabric-react/src/components/pickers/PeoplePicker/PeoplePickerItems/SuggestionItemDefault.tsx index 2ca73c550c4196..2a13e6886f489e 100644 --- a/packages/office-ui-fabric-react/src/components/pickers/PeoplePicker/PeoplePickerItems/SuggestionItemDefault.tsx +++ b/packages/office-ui-fabric-react/src/components/pickers/PeoplePicker/PeoplePickerItems/SuggestionItemDefault.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; /* tslint:enable */ import { css } from '../../../../Utilities'; import { Persona, PersonaSize, IPersonaProps, PersonaPresence } from '../../../../Persona'; -import { IBasePickerSuggestionsProps, ISuggestionItemProps } from 'office-ui-fabric-react/lib/Pickers'; +import { IBasePickerSuggestionsProps, ISuggestionItemProps } from '../../../../Pickers'; import * as stylesImport from '../PeoplePicker.scss'; const styles: any = stylesImport; diff --git a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts index ec5db470ba2da3..190bf3154fb79d 100644 --- a/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts +++ b/packages/office-ui-fabric-react/src/utilities/positioning/positioning.ts @@ -814,4 +814,11 @@ export function getMaxHeight(target: Element | MouseEvent | IPoint, targetEdge: } return _getMaxHeightFromTargetRectangle(targetRect, targetEdge, gapSpace, boundingRectangle, coverTarget); +} + +/** + * Returns the opposite edge of the given RectangleEdge. + */ +export function getOppositeEdge(edge: RectangleEdge): RectangleEdge { + return edge * -1; } \ No newline at end of file