diff --git a/apps/fabric-website/src/components/App/AppState.tsx b/apps/fabric-website/src/components/App/AppState.tsx index 4037e4e10691f6..bf69b3fb7f6919 100644 --- a/apps/fabric-website/src/components/App/AppState.tsx +++ b/apps/fabric-website/src/components/App/AppState.tsx @@ -134,6 +134,12 @@ export const AppState: IAppState = { component: () => , getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/CheckboxComponentPage').CheckboxComponentPage)) }, + { + title: 'Coachmark', + url: '#/components/coachmark', + component: () => , + getComponent: cb => require.ensure([], (require) => cb(require('../../pages/Components/CoachmarkComponentPage').CoachmarkComponentPage)) + }, { title: 'ChoiceGroup', url: '#/components/choicegroup', diff --git a/apps/fabric-website/src/pages/Components/CoachmarkComponentPage.tsx b/apps/fabric-website/src/pages/Components/CoachmarkComponentPage.tsx new file mode 100644 index 00000000000000..6096e069887363 --- /dev/null +++ b/apps/fabric-website/src/pages/Components/CoachmarkComponentPage.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { CoachmarkPage } from 'office-ui-fabric-react/lib/components/Coachmark/CoachmarkPage'; +import { PageHeader } from '../../components/PageHeader/PageHeader'; +import { ComponentPage } from '../../components/ComponentPage/ComponentPage'; +const pageStyles: any = require('../PageStyles.module.scss'); + +export class CoachmarkComponentPage extends React.Component { + public render() { + return ( + + + + + + + ); + } +} diff --git a/common/changes/@uifabric/experiments/component-beak_2018-02-08-13-45.json b/common/changes/@uifabric/experiments/component-beak_2018-02-08-13-45.json new file mode 100644 index 00000000000000..34aab286454d2b --- /dev/null +++ b/common/changes/@uifabric/experiments/component-beak_2018-02-08-13-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/experiments", + "comment": "Removing Coachmark", + "type": "minor" + } + ], + "packageName": "@uifabric/experiments", + "email": "miljo@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/@uifabric/utilities/component-beak_2018-02-08-13-45.json b/common/changes/@uifabric/utilities/component-beak_2018-02-08-13-45.json new file mode 100644 index 00000000000000..706e47ee22514d --- /dev/null +++ b/common/changes/@uifabric/utilities/component-beak_2018-02-08-13-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/utilities", + "comment": "Added a triangle abstraction class", + "type": "minor" + } + ], + "packageName": "@uifabric/utilities", + "email": "miljo@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/component-beak_2018-02-08-13-45.json b/common/changes/office-ui-fabric-react/component-beak_2018-02-08-13-45.json new file mode 100644 index 00000000000000..776a42207c30be --- /dev/null +++ b/common/changes/office-ui-fabric-react/component-beak_2018-02-08-13-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Migrating Coachmark to main office-ui-fabrc-react package and adding a beak compoentn as well as updating the experimental component PositioningContainer to reflect some of the latest bits in Callout", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "miljo@microsoft.com" +} \ No newline at end of file diff --git a/packages/experiments/src/PositioningContainer.ts b/packages/experiments/src/PositioningContainer.ts deleted file mode 100644 index ddecfbb4488e65..00000000000000 --- a/packages/experiments/src/PositioningContainer.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/PositioningContainer/index'; \ No newline at end of file diff --git a/packages/experiments/src/components/Coachmark/Coachmark.tsx b/packages/experiments/src/components/Coachmark/Coachmark.tsx deleted file mode 100644 index 8e41d4eeb33745..00000000000000 --- a/packages/experiments/src/components/Coachmark/Coachmark.tsx +++ /dev/null @@ -1,230 +0,0 @@ -// Utilities -import * as React from 'react'; -import { BaseComponent, classNamesFunction } from '../../Utilities'; - -// Component Dependencies -import { PositioningContainer } from '../PositioningContainer/PositioningContainer'; - -// Coachmark -import { ICoachmarkProps } from './Coachmark.types'; -import { getStyles, ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; - -const getClassNames = classNamesFunction(); - -/** - * An interface for the cached dimensions of entity inner host. - */ -export interface IEntityRect { - width: number; - height: number; -} - -export interface ICoachmarkState { - /** - * Is the Coachmark currently collapsed into - * a tear drop shape - */ - collapsed: boolean; - - /** - * Enables/Disables the beacon that radiates - * from the center of the coachmark. - */ - isBeaconAnimating: boolean; - - /** - * Is the teaching bubble currently retreiving the - * original dimensions of the hosted entity. - */ - isMeasuring: boolean; - - /** - * Cached width and height of _entityInnerHostElement - */ - entityInnerHostRect: IEntityRect; - - /** - * Is the mouse in proximity of the default target element - */ - isMouseInProximity: boolean; -} - -export class Coachmark extends BaseComponent { - public static defaultProps: Partial = { - collapsed: true, - mouseProximityOffset: 100 - }; - - /** - * The cached HTMLElement reference to the Entity Inner Host - * element. - */ - private _entityInnerHostElement: HTMLElement; - private _translateAnimationContainer: HTMLElement; - - constructor(props: ICoachmarkProps) { - super(props); - - // Set defaults for state - this.state = { - collapsed: props.collapsed!, - isBeaconAnimating: true, - isMeasuring: true, - entityInnerHostRect: { - width: 0, - height: 0 - }, - isMouseInProximity: true - }; - } - - public render(): JSX.Element { - let { - children - } = this.props; - - 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' - }); - - return ( - - - - - - - - - { children } - - - - - - - - ); - } - - public componentWillReceiveProps(newProps: ICoachmarkProps): void { - if (this.props.collapsed && !newProps.collapsed) { - // The coachmark is about to open - this._openCoachmark(); - } - } - - public componentDidMount(): void { - // If we have already accessed the width and height - // We do not wan't to do it again. - if (this.state.isMeasuring) { - this._async.setTimeout((): void => { - if ((this.state.entityInnerHostRect.width + this.state.entityInnerHostRect.width) === 0) { - this.setState({ - isMeasuring: false, - entityInnerHostRect: { - width: this._entityInnerHostElement.offsetWidth, - height: this._entityInnerHostElement.offsetHeight - } - }); - - this.forceUpdate(); - } - - // Initialize element in proximity now that initial - // measurements have been taken - this._isElementInProximity(this._entityInnerHostElement); - }, 10); - } - } - - private _openCoachmark(): void { - this.setState({ - collapsed: false - }); - - this._translateAnimationContainer.addEventListener('animationstart', (): void => { - if (this.props.onAnimationOpenStart) { - this.props.onAnimationOpenStart(); - } - }); - - this._translateAnimationContainer.addEventListener('animationend', (): void => { - if (this.props.onAnimationOpenEnd) { - this.props.onAnimationOpenEnd(); - } - }); - } - - private _isElementInProximity(targetElement: HTMLElement, mouseProximityOffset: number = 0): void { - /** - * An array of cached ids returned when setTimeout runs during - * the window resize event trigger. - */ - let timeoutIds: number[] = []; - - /** - * The target element the mouse would be in - * proximity to - */ - let targetElementRect: ClientRect = targetElement.getBoundingClientRect(); - - // When the window resizes we want to async - // get the bounding client rectangle. - // Every time the event is triggered we wan't to - // setTimeout and then clear any previous instances - // of setTimeout. - this._events.on(window, 'resize', (): void => { - timeoutIds.forEach((value: number): void => { - clearInterval(value); - }); - - timeoutIds.push(this._async.setTimeout((): void => { - targetElementRect = targetElement.getBoundingClientRect(); - }, 100)); - }); - - // Everytime the document's mouse move is triggered - // we want to check if inside of an element and - // set the state with the result. - this._events.on(document, 'mousemove', (e: MouseEvent) => { - let mouseY = e.pageY; - let mouseX = e.pageX; - let isMouseInProximity = this._isInsideElement(targetElementRect, mouseX, mouseY); - - if (isMouseInProximity !== this.state.isMouseInProximity) { - this.setState({ - isMouseInProximity: isMouseInProximity - }); - } - }); - } - - private _isInsideElement(elementRect: ClientRect, mouseX: number, mouseY: number, 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); - } -} \ No newline at end of file diff --git a/packages/experiments/src/components/Coachmark/Coachmark.types.ts b/packages/experiments/src/components/Coachmark/Coachmark.types.ts deleted file mode 100644 index 943e5e4ea562b6..00000000000000 --- a/packages/experiments/src/components/Coachmark/Coachmark.types.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react'; -import { Coachmark } from './Coachmark'; -import { ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; -import { IPositioningContainerTypes } from '../PositioningContainer/PositioningContainer.types'; -import { IPoint, IStyleFunction } from '../../Utilities'; - -export interface ICoachmark { -} - -export interface ICoachmarkProps extends React.Props { - /** - * All props for your component are to be defined here. - */ - componentRef?: (component: ICoachmark) => void; - - /** - * Get styles method - */ - getStyles?: IStyleFunction; - - /** - * The target that the TeachingBubble should try to position itself based on. - * It can be either an HTMLElement a querySelector string of a valid HTMLElement - * or a MouseEvent. If MouseEvent is given then the origin point of the event will be used. - */ - target?: HTMLElement | string | MouseEvent | IPoint | null; - - positioningContainerProps?: IPositioningContainerTypes; - - /** - * Is the Coachmark expanded - * @default true - */ - collapsed?: boolean; - - /** - * The distance in pixels the mouse is located - * before opening up the coachmark. - * @default 100 - */ - mouseProximityOffset?: number; - - /** - * Callback when the opening animation begins - */ - onAnimationOpenStart?: () => void; - - /** - * Callback when the opening animation completes - */ - onAnimationOpenEnd?: () => void; -} diff --git a/packages/experiments/src/demo/AppDefinition.tsx b/packages/experiments/src/demo/AppDefinition.tsx index 705d2e5420d294..b009e29266e797 100644 --- a/packages/experiments/src/demo/AppDefinition.tsx +++ b/packages/experiments/src/demo/AppDefinition.tsx @@ -10,18 +10,6 @@ export const AppDefinition: IAppDefinition = { examplePages: [ { links: [ - { - component: require('../components/PositioningContainer/PositioningContainerPage').PositioningContainerPage, - key: 'PositioningContainer', - name: 'PositioningContainer', - url: '#/examples/PositioningContainer' - }, - { - component: require('../components/Coachmark/CoachmarkPage').CoachmarkPage, - key: 'Coachmark', - name: 'Coachmark', - url: '#/examples/coachmark' - }, { component: require('../components/CommandBar/CommandBarPage').CommandBarPage, key: 'CommandBar', diff --git a/packages/experiments/src/Coachmark.ts b/packages/office-ui-fabric-react/src/Coachmark.ts similarity index 100% rename from packages/experiments/src/Coachmark.ts rename to packages/office-ui-fabric-react/src/Coachmark.ts 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 new file mode 100644 index 00000000000000..84c6dc16263789 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.styles.ts @@ -0,0 +1,35 @@ +import { IStyle, DefaultPalette } from '../../../Styling'; +import { IBeakStylesProps } from './Beak.types'; + +export interface IBeakStyles { + /** + * Style for the root element in the default enabled/unchecked state. + */ + root?: IStyle; + beak?: IStyle; +} + +export function getStyles(props: IBeakStylesProps): IBeakStyles { + return { + root: [ + { + position: 'absolute', + boxShadow: 'inherit', + border: 'none', + boxSizing: 'border-box', + transform: 'translateY(-50%)', + left: '50%', + width: props.width, + height: props.height + }, + (props.left && props.top) && { + left: props.left, + top: props.top + } + ], + beak: { + fill: DefaultPalette.themePrimary, + display: 'block' + } + }; +} \ 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 new file mode 100644 index 00000000000000..110771aa50b5f9 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { + BaseComponent, + css, + classNamesFunction +} from '../../../Utilities'; +import { IBeakProps } from './Beak.types'; +import { getStyles, IBeakStyles } from './Beak.styles'; +import { getComboBoxOptionClassNames } from 'src/components/ComboBox/ComboBox.classNames'; +import { IBeakStylesProps } from './Beak.types'; + +export interface IBeakState { + left: string | null; + top: string | null; +} + +export class Beak extends BaseComponent { + constructor(props: IBeakProps) { + super(props); + } + + public render(): JSX.Element { + const { + height = 18, + width = 18, + left, + top + } = this.props; + + const getClassNames = classNamesFunction(); + const classNames = getClassNames(getStyles, { + left: this.props.left, + top: this.props.top, + height: height + 'px', + width: width + 'px' + }); + + const pointOne = width / 2 + ',' + 0; + const pointTwo = width + ',' + height; + const pointThree = 0 + ',' + height; + + return ( + + + + + + ); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000000..c1e19763100fbb --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Beak/Beak.types.ts @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { Beak } from './Beak'; +import { IPositionInfo } from '../PositioningContainer/PositioningContainer.types'; + +export interface IBeak { } + +export interface IBeakProps extends React.Props { + /** + * All props for your component are to be defined here. + */ + componentRef?: (component: IBeak) => void; + + /** + * Beak width. + * @default 18 + */ + width?: number; + + /** + * Beak height. + * @default 18 + */ + height?: number; + + /** + * Color of the beak + */ + color?: string; + + /** + * Left position of the beak + */ + left?: string | null; + + /** + * Top position of the beak + */ + top?: string | null; +} + +export interface IBeakStylesProps { + left?: string | null; + top?: string | null; + width?: string; + height?: string; +} \ No newline at end of file diff --git a/packages/experiments/src/components/Coachmark/Coachmark.styles.ts b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts similarity index 80% rename from packages/experiments/src/components/Coachmark/Coachmark.styles.ts rename to packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts index e0c72a5dbf3698..58164a257efb8f 100644 --- a/packages/experiments/src/components/Coachmark/Coachmark.styles.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.styles.ts @@ -1,7 +1,5 @@ import { IStyle, IRawStyle, keyframes } from '../../Styling'; -export const themePrimary = '#0078d7'; - export interface ICoachmarkStyleProps { /** * Is the Coachmark collapsed @@ -28,6 +26,31 @@ export interface ICoachmarkStyleProps { * The width measured in pixels */ entityHostWidth?: string; + + /** + * Width of the coachmark + */ + width?: string; + + /** + * Height of the coachmark + */ + height?: string; + + /** + * Color + */ + color?: string; + + /** + * Beacon color one + */ + beaconColorOne?: string; + + /** + * Beacon color two + */ + beaconColorTwo?: string; } export interface ICoachmarkStyles { @@ -75,11 +98,7 @@ export interface ICoachmarkStyles { collapsed?: IStyle; } -export const coachmarkCollapsedSize = '36px'; -export const beaconColorOne = '#00FFEC'; -export const beaconColorTwo = '#005EDD'; - -function continuousPulseStepOne(): IRawStyle { +function continuousPulseStepOne(beaconColorOne: string): IRawStyle { return { borderColor: beaconColorOne, borderWidth: '0px', @@ -101,7 +120,7 @@ function continuousPulseStepThree(): IRawStyle { }; } -function continuousPulseStepFour(): IRawStyle { +function continuousPulseStepFour(beaconColorTwo: string): IRawStyle { return { borderWidth: '0', width: '150px', @@ -111,32 +130,12 @@ function continuousPulseStepFour(): IRawStyle { }; } -function continuousPulseStepFive(): IRawStyle { - return Object.assign(continuousPulseStepOne(), { +function continuousPulseStepFive(beaconColorOne: string): IRawStyle { + return Object.assign(continuousPulseStepOne(beaconColorOne), { opacity: '0' }); } -export const ContinuousPulse: string = keyframes({ - '0%': continuousPulseStepOne(), - '1.42%': continuousPulseStepTwo(), - '3.57%': continuousPulseStepThree(), - '7.14%': continuousPulseStepFour(), - '8%': continuousPulseStepFive(), - '29.99%': continuousPulseStepFive(), - '30%': continuousPulseStepOne(), - '31.42%': continuousPulseStepTwo(), - '33.57%': continuousPulseStepThree(), - '37.14%': continuousPulseStepFour(), - '38%': continuousPulseStepFive(), - '79.42%': continuousPulseStepFive(), - '79.43': continuousPulseStepOne(), - '81.85': continuousPulseStepTwo(), - '83.42': continuousPulseStepThree(), - '87%': continuousPulseStepFour(), - '100%': {} -}); - export const translateOne: string = keyframes({ '0%': { transform: 'translate(0, 0)', // orig 25 @@ -252,6 +251,26 @@ export const rotateOne: string = keyframes({ }); export function getStyles(props: ICoachmarkStyleProps): ICoachmarkStyles { + const ContinuousPulse: string = keyframes({ + '0%': continuousPulseStepOne(props.beaconColorOne!), + '1.42%': continuousPulseStepTwo(), + '3.57%': continuousPulseStepThree(), + '7.14%': continuousPulseStepFour(props.beaconColorTwo!), + '8%': continuousPulseStepFive(props.beaconColorOne!), + '29.99%': continuousPulseStepFive(props.beaconColorOne!), + '30%': continuousPulseStepOne(props.beaconColorOne!), + '31.42%': continuousPulseStepTwo(), + '33.57%': continuousPulseStepThree(), + '37.14%': continuousPulseStepFour(props.beaconColorTwo!), + '38%': continuousPulseStepFive(props.beaconColorOne!), + '79.42%': continuousPulseStepFive(props.beaconColorOne!), + '79.43': continuousPulseStepOne(props.beaconColorOne!), + '81.85': continuousPulseStepTwo(), + '83.42': continuousPulseStepThree(), + '87%': continuousPulseStepFour(props.beaconColorTwo!), + '100%': {} + }); + return { root: [ { @@ -319,7 +338,8 @@ export function getStyles(props: ICoachmarkStyleProps): ICoachmarkStyles { rotateAnimationLayer: [ { width: '100%', - height: '100%' + height: '100%', + opacity: '0.8' }, props.collapsed && { animationDuration: '14s', @@ -329,6 +349,9 @@ export function getStyles(props: ICoachmarkStyleProps): ICoachmarkStyles { animationDelay: '0s', animationFillMode: 'forwards', animationName: rotateOne + }, + !props.collapsed && { + opacity: '1' } ], // Layer Host, defaults to collapsed @@ -337,15 +360,14 @@ export function getStyles(props: ICoachmarkStyleProps): ICoachmarkStyles { position: 'relative', outline: 'none', overflow: 'hidden', - backgroundColor: themePrimary, - borderRadius: coachmarkCollapsedSize, - opacity: '0.8', + backgroundColor: props.color, + borderRadius: props.width, transition: 'border-radius 250ms, width 500ms, height 500ms cubic-bezier(0.5, 0, 0, 1)', visibility: 'hidden' }, - (!props.isMeasuring) && { - width: coachmarkCollapsedSize, - height: coachmarkCollapsedSize, + !props.isMeasuring && { + width: props.width, + height: props.height, visibility: 'visible' }, !props.collapsed && { @@ -362,10 +384,12 @@ export function getStyles(props: ICoachmarkStyleProps): ICoachmarkStyles { transform: 'scale(0)' }, (!props.collapsed) && { + width: props.entityHostWidth, + height: props.entityHostHeight, transform: 'scale(1)' }, (!props.isMeasuring) && { - visibility: 'visible' + visibility: 'visible', } ] }; diff --git a/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx new file mode 100644 index 00000000000000..fa5b71968633db --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.tsx @@ -0,0 +1,297 @@ +// Utilities +import * as React from 'react'; +import { BaseComponent, classNamesFunction, autobind } from '../../Utilities'; +import { DefaultPalette } from '../../Styling'; + +// Component Dependencies +import { PositioningContainer } from './PositioningContainer/PositioningContainer'; +import { Beak } from './Beak/Beak'; + +// Coachmark +import { ICoachmarkTypes } from './Coachmark.types'; +import { getStyles, ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; +import { FocusZone } from '../../FocusZone'; + +const getClassNames = classNamesFunction(); + +/** + * An interface for the cached dimensions of entity inner host. + */ +export interface IEntityRect { + width: number; + height: number; +} + +export interface ICoachmarkState { + /** + * Is the Coachmark currently collapsed into + * a tear drop shape + */ + collapsed: boolean; + + /** + * Enables/Disables the beacon that radiates + * from the center of the coachmark. + */ + isBeaconAnimating: boolean; + + /** + * Is the teaching bubble currently retreiving the + * original dimensions of the hosted entity. + */ + isMeasuring: boolean; + + /** + * Cached width and height of _entityInnerHostElement + */ + entityInnerHostRect: IEntityRect; + + /** + * Is the mouse in proximity of the default target element + */ + isMouseInProximity: boolean; + + /** + * The left position of the beak + */ + beakLeft?: string | null; + + /** + * The right position of the beak + */ + beakTop?: string | null; +} + +export class Coachmark extends BaseComponent { + public static defaultProps: Partial = { + collapsed: true, + mouseProximityOffset: 100, + beakWidth: 26, + beakHeight: 12, + delayBeforeMouseOpen: 3600, // The approximate time the coachmark shows up + width: 36, + height: 36, + beaconColorOne: '#00FFEC', + beaconColorTwo: '#005EDD', + color: DefaultPalette.themePrimary + }; + + /** + * The cached HTMLElement reference to the Entity Inner Host + * element. + */ + private _entityInnerHostElement: HTMLElement; + private _translateAnimationContainer: HTMLElement; + private _entityHost: HTMLElement; + private _positioningContainer: PositioningContainer; + + constructor(props: ICoachmarkTypes) { + super(props); + + // Set defaults for state + this.state = { + collapsed: props.collapsed!, + isBeaconAnimating: true, + isMeasuring: true, + entityInnerHostRect: { + width: 0, + height: 0 + }, + isMouseInProximity: false + }; + } + + public render(): JSX.Element { + let { + children, + beakWidth, + beakHeight, + target, + width, + height, + color, + beaconColorOne, + beaconColorTwo + } = this.props; + + 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, + beaconColorOne: beaconColorOne, + beaconColorTwo: beaconColorTwo + }); + + return ( + + + + + + + { + this._positioningContainer && + } + + + + { children } + + + + + + + + + ); + } + + public componentWillReceiveProps(newProps: ICoachmarkTypes): void { + if (this.props.collapsed && !newProps.collapsed) { + // The coachmark is about to open + this._openCoachmark(); + } + } + + public componentDidMount(): void { + this._async.requestAnimationFrame(((): void => { + if ((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.offsetWidth, + height: this._entityInnerHostElement.offsetHeight + }, + beakLeft: beakLeft + 'px', + beakTop: beakTop + 'px' + }); + + this.forceUpdate(); + } + + // We dont want to the user to immediatley trigger the coachmark when it's opened + this._async.setTimeout(() => { + this._addProximityHandler(100); + }, this.props.delayBeforeMouseOpen!); + })); + } + + @autobind + private _onFocusHandler(): void { + this._openCoachmark(); + } + + @autobind + private _openCoachmark(): void { + this.setState({ + collapsed: false + }); + + this._translateAnimationContainer.addEventListener('animationstart', (): void => { + if (this.props.onAnimationOpenStart) { + this.props.onAnimationOpenStart(); + } + }); + + this._translateAnimationContainer.addEventListener('animationend', (): void => { + if (this.props.onAnimationOpenEnd) { + this.props.onAnimationOpenEnd(); + } + }); + } + + private _addProximityHandler(mouseProximityOffset: number = 0): void { + /** + * An array of cached ids returned when setTimeout runs during + * the window resize event trigger. + */ + let 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 unnessecary render. + this._async.setTimeout(() => { + targetElementRect = this._entityInnerHostElement.getBoundingClientRect(); + + // When the window resizes we want to async + // get the bounding client rectangle. + // Every time the event is triggered we want to + // setTimeout and then clear any previous instances + // of setTimeout. + this._events.on(window, 'resize', (): void => { + timeoutIds.forEach((value: number): void => { + clearInterval(value); + }); + + timeoutIds.push(this._async.setTimeout((): void => { + targetElementRect = this._entityInnerHostElement.getBoundingClientRect(); + }, 100)); + }); + }, 10); + + // Every time the document's mouse move is triggered + // we want to check if inside of an element and + // set the state with the result. + this._events.on(document, 'mousemove', (e: MouseEvent) => { + let mouseY = e.pageY; + let mouseX = e.pageX; + let 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.props.onMouseMove) { + this.props.onMouseMove(e); + } + }); + } + + 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); + } +} \ 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 new file mode 100644 index 00000000000000..08cd9fc1a453ae --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { Coachmark } from './Coachmark'; +import { ICoachmarkStyles, ICoachmarkStyleProps } from './Coachmark.styles'; +import { IPositioningContainerTypes } from './PositioningContainer/PositioningContainer.types'; +import { IPoint, IStyleFunction } from '../../Utilities'; + +export interface ICoachmark { +} + +export interface ICoachmarkTypes extends React.Props { + componentRef?: (component: ICoachmark) => void; + + /** + * Get styles method. + */ + getStyles?: IStyleFunction; + + /** + * The target that the TeachingBubble should try to position itself based on. + */ + target: HTMLElement; + + positioningContainerProps?: IPositioningContainerTypes; + + /** + * The starting collapsed state for the Coachmark? + * @default true + */ + collapsed?: boolean; + + /** + * The distance in pixels the mouse is located + * before opening up the coachmark. + * @default 100 + */ + mouseProximityOffset?: number; + + /** + * Callback when the opening animation begins. + */ + onAnimationOpenStart?: () => void; + + /** + * Callback when the opening animation completes. + */ + onAnimationOpenEnd?: () => void; + + /** + * The width of the beak component. + */ + beakWidth?: number; + + /** + * The height of the beak component + */ + beakHeight?: number; + + /** + * Delay before allowing mouse movements to open + * the Coachmark + */ + delayBeforeMouseOpen?: number; + + /** + * Runs every time the mouse moves + */ + onMouseMove?: (e: MouseEvent) => void; + + /** + * The width of the coachmark + */ + width?: number; + + /** + * The height of the coachmark + */ + height?: number; + + /** + * Color + */ + color?: string; + + /** + * Beacon color one + */ + beaconColorOne?: string; + + /** + * Beacon color two + */ + beaconColorTwo?: string; +} diff --git a/packages/experiments/src/components/Coachmark/CoachmarkPage.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/CoachmarkPage.tsx similarity index 90% rename from packages/experiments/src/components/Coachmark/CoachmarkPage.tsx rename to packages/office-ui-fabric-react/src/components/Coachmark/CoachmarkPage.tsx index b82bd9ac3b46be..458642ae39327c 100644 --- a/packages/experiments/src/components/Coachmark/CoachmarkPage.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/CoachmarkPage.tsx @@ -8,7 +8,7 @@ import { import { CoachmarkBasicExample } from './examples/Coachmark.Basic.Example'; const CoachmarkBasicExampleCode = - require('!raw-loader!experiments/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx') as string; + require('!raw-loader!office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx') as string; export class CoachmarkPage extends React.Component { public render(): JSX.Element { @@ -26,7 +26,7 @@ export class CoachmarkPage extends React.Component propertiesTables={ ('!raw-loader!experiments/src/components/coachmark/Coachmark.types.ts') + require('!raw-loader!office-ui-fabric-react/src/components/Coachmark/Coachmark.types.ts') ] } /> } diff --git a/packages/experiments/src/components/PositioningContainer/PositioningContainer.styles.ts b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.styles.ts similarity index 90% rename from packages/experiments/src/components/PositioningContainer/PositioningContainer.styles.ts rename to packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.styles.ts index 5d0a4e80c5ef64..210f64e66ae09f 100644 --- a/packages/experiments/src/components/PositioningContainer/PositioningContainer.styles.ts +++ b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.styles.ts @@ -1,6 +1,6 @@ -import { memoizeFunction } from '../../Utilities'; -import { mergeStyleSets } from '../../Styling'; -import { IStyle } from '../../Styling'; +import { memoizeFunction } from '../../../Utilities'; +import { mergeStyleSets } from '../../../Styling'; +import { IStyle } from '../../../Styling'; export interface IPositioningContainerStyles { /** diff --git a/packages/experiments/src/components/PositioningContainer/PositioningContainer.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx similarity index 88% rename from packages/experiments/src/components/PositioningContainer/PositioningContainer.tsx rename to packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx index 97e97898ce4f0d..39250cf4088bec 100644 --- a/packages/experiments/src/components/PositioningContainer/PositioningContainer.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainer.tsx @@ -18,36 +18,42 @@ import { focusFirstChild, getWindow, getDocument -} from '../../Utilities'; +} from '../../../Utilities'; import { getRelativePositions, IPositionProps, getMaxHeight, - ICalloutPositon + ICalloutPositon, + positionElement, + IPositionedData, + RectangleEdge } from 'office-ui-fabric-react/lib/utilities/positioning'; -import { AnimationClassNames, mergeStyles } from '../../Styling'; +import { + IPositionInfo +} from './PositioningContainer.types'; -export interface IPositionInfo { - calloutPosition: ICalloutPositon; - beakPosition: { position: ICalloutPositon, display: string }; - directionalClassName: string; - submenuDirection: DirectionalHint; -} +import { AnimationClassNames, mergeStyles } from '../../../Styling'; const OFF_SCREEN_STYLE = { opacity: 0 }; // In order for some of the max height logic to work -// properly we need to set the border needs to be set. +// properly we need to set the border. // The value is abitrary. const BORDER_WIDTH = 1; +const SLIDE_ANIMATIONS: { [key: number]: string; } = { + [RectangleEdge.top]: 'slideUpIn20', + [RectangleEdge.bottom]: 'slideDownIn20', + [RectangleEdge.left]: 'slideLeftIn20', + [RectangleEdge.right]: 'slideRightIn20' +}; export interface IPositioningContainerState { /** * Current set of calcualted positions for the outermost parent container. */ - positions?: IPositionInfo; + positions?: IPositionedData; /** * Tracks the current height offset and updates during @@ -153,12 +159,10 @@ export class PositioningContainer extends BaseComponent { children } - { // @TODO apply to the content container + { // @TODO apply to the content container contentMaxHeight } @@ -262,7 +266,12 @@ export class PositioningContainer extends BaseComponent { /** * All props for your component are to be defined here. diff --git a/packages/experiments/src/components/PositioningContainer/PositioningContainerPage.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainerPage.tsx similarity index 100% rename from packages/experiments/src/components/PositioningContainer/PositioningContainerPage.tsx rename to packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/PositioningContainerPage.tsx diff --git a/packages/experiments/src/components/PositioningContainer/examples/PositioningContainer.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/examples/PositioningContainer.Basic.Example.tsx similarity index 100% rename from packages/experiments/src/components/PositioningContainer/examples/PositioningContainer.Basic.Example.tsx rename to packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/examples/PositioningContainer.Basic.Example.tsx diff --git a/packages/experiments/src/components/PositioningContainer/index.ts b/packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/index.ts similarity index 100% rename from packages/experiments/src/components/PositioningContainer/index.ts rename to packages/office-ui-fabric-react/src/components/Coachmark/PositioningContainer/index.ts diff --git a/packages/experiments/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx similarity index 57% rename from packages/experiments/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx rename to packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx index cd445deb2cffad..a4d6fe302bf106 100644 --- a/packages/experiments/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/Coachmark/examples/Coachmark.Basic.Example.tsx @@ -3,13 +3,34 @@ 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 { IStyle, DefaultPalette } from '../../../Styling'; +import { + BaseComponent, + assign, + autobind, + classNamesFunction +} from 'office-ui-fabric-react/lib/Utilities'; export interface ICoachmarkBasicExampleState { isVisible?: boolean; isCoachmarkCollapsed?: boolean; + targetElement?: HTMLElement; } -export class CoachmarkBasicExample extends React.Component<{}, ICoachmarkBasicExampleState> { +export interface ICoachmarkBasicExampleStyles { + /** + * Style for the root element in the default enabled/unchecked state. + */ + root?: IStyle; + + /** + * The example button container + */ + buttonContainer: IStyle; +} + +export class CoachmarkBasicExample extends BaseComponent<{}, ICoachmarkBasicExampleState> { + private _targetButton: HTMLElement; public constructor(props: {}) { super(props); @@ -30,18 +51,30 @@ export class CoachmarkBasicExample extends React.Component<{}, ICoachmarkBasicEx doNotLayer: true }; + const getClassNames = classNamesFunction<{}, ICoachmarkBasicExampleStyles>(); + const classNames = getClassNames(() => { + return { + root: {}, + buttonContainer: { + display: 'inline-block' + } + }; + }); + return ( - - + + { isVisible && ( ('../components/Coachmark/CoachmarkPage').CoachmarkPage, + key: 'Coachmark', + name: 'Coachmark', + url: '#/examples/coachmark' + }, { component: require('../components/ChoiceGroup/ChoiceGroupPage').ChoiceGroupPage, key: 'ChoiceGroup',