diff --git a/common/changes/office-ui-fabric-react/layer-scss2ms_2018-02-14-22-55.json b/common/changes/office-ui-fabric-react/layer-scss2ms_2018-02-14-22-55.json new file mode 100644 index 00000000000000..60e9c25fa5bf09 --- /dev/null +++ b/common/changes/office-ui-fabric-react/layer-scss2ms_2018-02-14-22-55.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Convert Layer component to mergeStyles", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "v-brgarl@microsoft.com" +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx index ea64faa7cf7f84..57d37f382db255 100644 --- a/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx +++ b/packages/office-ui-fabric-react/src/components/ContextualMenu/ContextualMenu.test.tsx @@ -10,7 +10,7 @@ import { FocusZoneDirection } from '../../FocusZone'; import { ContextualMenu, canAnyMenuItemsCheck } from './ContextualMenu'; import { IContextualMenuItem, ContextualMenuItemType } from './ContextualMenu.types'; -import { Layer } from '../Layer/Layer'; +import { LayerBase as Layer } from '../Layer/Layer.base'; import { mount } from 'enzyme'; describe('ContextualMenu', () => { diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.base.tsx b/packages/office-ui-fabric-react/src/components/Layer/Layer.base.tsx new file mode 100644 index 00000000000000..c37fc26bde0523 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.base.tsx @@ -0,0 +1,178 @@ +/* tslint:disable:no-unused-variable */ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +/* tslint:enable:no-unused-variable */ + +import { Fabric } from '../../Fabric'; +import { + ILayerProps, + ILayerStyleProps, + ILayerStyles, +} from './Layer.types'; +import { + css, + BaseComponent, + classNamesFunction, + customizable, + getDocument, + setVirtualParent +} from '../../Utilities'; + +const _layersByHostId: { [hostId: string]: LayerBase[] } = {}; +let _defaultHostSelector: string | undefined; + +const getClassNames = classNamesFunction(); + +// @customizable('Layer', ['theme']) +export class LayerBase extends BaseComponent { + + public static defaultProps: ILayerProps = { + onLayerDidMount: () => undefined, + onLayerWillUnmount: () => undefined + }; + + private _rootElement: HTMLElement; + private _host: Node; + private _layerElement: HTMLElement | undefined; + private _hasMounted: boolean; + /** + * Used for notifying applicable Layers that a host is available/unavailable and to re-evaluate Layers that + * care about the specific host. + */ + public static notifyHostChanged(id: string) { + if (_layersByHostId[id]) { + _layersByHostId[id].forEach(layer => layer.forceUpdate()); + } + } + + /** + * Sets the default target selector to use when determining the host in which + * Layered content will be injected into. If not provided, an element will be + * created at the end of the document body. + * + * Passing in a falsey value will clear the default target and reset back to + * using a created element at the end of document body. + */ + public static setDefaultTarget(selector?: string) { + _defaultHostSelector = selector; + } + + constructor(props: ILayerProps) { + super(props); + + this._warnDeprecations({ + onLayerMounted: 'onLayerDidMount' + }); + + if (this.props.hostId) { + if (!_layersByHostId[this.props.hostId]) { + _layersByHostId[this.props.hostId] = []; + } + + _layersByHostId[this.props.hostId].push(this); + } + } + + public componentDidMount() { + this.componentDidUpdate(); + } + + public componentWillUnmount() { + this._removeLayerElement(); + + if (this.props.hostId) { + _layersByHostId[this.props.hostId] = _layersByHostId[this.props.hostId].filter(layer => layer !== this); + if (!_layersByHostId[this.props.hostId].length) { + delete _layersByHostId[this.props.hostId]; + } + } + } + + public componentDidUpdate() { + const host = this._getHost(); + + const { className, getStyles, theme } = this.props; + const classNames = getClassNames(getStyles!, + { + theme: theme!, + className, + isNotHost: !this.props.hostId + } + ); + + if (host !== this._host) { + this._removeLayerElement(); + } + + if (host) { + this._host = host; + + if (!this._layerElement) { + const doc = getDocument(this._rootElement) as Document; + + this._layerElement = doc.createElement('div'); + this._layerElement.className = classNames.root; + + host.appendChild(this._layerElement); + setVirtualParent(this._layerElement, this._rootElement); + } + + // Using this 'unstable' method allows us to retain the React context across the layer projection. + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + ( + + { this.props.children } + + ), + this._layerElement, + () => { + if (!this._hasMounted) { + this._hasMounted = true; + + // TODO: @deprecated cleanup required. + if (this.props.onLayerMounted) { + this.props.onLayerMounted(); + } + + this.props.onLayerDidMount!(); + } + }); + } + } + + public render() { + return ( + + ); + } + + private _removeLayerElement() { + if (this._layerElement) { + this.props.onLayerWillUnmount!(); + + ReactDOM.unmountComponentAtNode(this._layerElement); + const parentNode = this._layerElement.parentNode; + if (parentNode) { + parentNode.removeChild(this._layerElement); + } + this._layerElement = undefined; + this._hasMounted = false; + } + } + + private _getHost(): Node { + const { hostId } = this.props; + const doc = getDocument(this._rootElement) as Document; + + if (hostId) { + return doc.getElementById(hostId) as Node; + } else { + return _defaultHostSelector ? doc.querySelector(_defaultHostSelector) as Node : doc.body; + } + } + +} diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.scss b/packages/office-ui-fabric-react/src/components/Layer/Layer.scss deleted file mode 100644 index f09d8c521d96e5..00000000000000 --- a/packages/office-ui-fabric-react/src/components/Layer/Layer.scss +++ /dev/null @@ -1,13 +0,0 @@ -.rootIsFixed { - position: fixed; - z-index: 1000000; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - visibility: hidden; -} - -.content { - visibility: visible; -} diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.styles.ts b/packages/office-ui-fabric-react/src/components/Layer/Layer.styles.ts new file mode 100644 index 00000000000000..47ba53723ec2be --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.styles.ts @@ -0,0 +1,42 @@ +import { ILayerStyleProps, ILayerStyles } from './Layer.types'; +import { + IStyle, + ITheme, +} from '../../Styling'; + +export const getStyles = ( + props: ILayerStyleProps +): ILayerStyles => { + const { + className, + theme, + isNotHost + } = props; + + // const { palette, semanticColors } = theme; + + return ({ + root: [ + 'ms-Layer', + isNotHost && [ + 'ms-Layer--fixed', + { + position: 'fixed', + zIndex: 1000000, + top: 0, + left: 0, + width: '100vw', + height: '100vh', + visibility: 'hidden' + } + ], + className + ], + content: [ + 'ms-Layer-content', + { + visibility: 'visible' + } + ] + }); +}; diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx b/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx index 4fc3473b10318f..0cdd7535ca9dcd 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx @@ -1,159 +1,13 @@ -/* tslint:disable:no-unused-variable */ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -/* tslint:enable:no-unused-variable */ - -import { Fabric } from '../../Fabric'; -import { ILayerProps } from './Layer.types'; -import { css, BaseComponent, getDocument, setVirtualParent } from '../../Utilities'; -import * as stylesImport from './Layer.scss'; -const styles: any = stylesImport; - -const _layersByHostId: { [hostId: string]: Layer[] } = {}; -let _defaultHostSelector: string | undefined; - -export class Layer extends BaseComponent { - - public static defaultProps: ILayerProps = { - onLayerDidMount: () => undefined, - onLayerWillUnmount: () => undefined - }; - - private _rootElement: HTMLElement; - private _host: Node; - private _layerElement: HTMLElement | undefined; - private _hasMounted: boolean; - /** - * Used for notifying applicable Layers that a host is available/unavailable and to re-evaluate Layers that - * care about the specific host. - */ - public static notifyHostChanged(id: string) { - if (_layersByHostId[id]) { - _layersByHostId[id].forEach(layer => layer.forceUpdate()); - } - } - - /** - * Sets the default target selector to use when determining the host in which - * Layered content will be injected into. If not provided, an element will be - * created at the end of the document body. - * - * Passing in a falsey value will clear the default target and reset back to - * using a created element at the end of document body. - */ - public static setDefaultTarget(selector?: string) { - _defaultHostSelector = selector; - } - - constructor(props: ILayerProps) { - super(props); - - this._warnDeprecations({ - onLayerMounted: 'onLayerDidMount' - }); - - if (this.props.hostId) { - if (!_layersByHostId[this.props.hostId]) { - _layersByHostId[this.props.hostId] = []; - } - - _layersByHostId[this.props.hostId].push(this); - } - } - - public componentDidMount() { - this.componentDidUpdate(); - } - - public componentWillUnmount() { - this._removeLayerElement(); - - if (this.props.hostId) { - _layersByHostId[this.props.hostId] = _layersByHostId[this.props.hostId].filter(layer => layer !== this); - if (!_layersByHostId[this.props.hostId].length) { - delete _layersByHostId[this.props.hostId]; - } - } - } - - public componentDidUpdate() { - const host = this._getHost(); - - if (host !== this._host) { - this._removeLayerElement(); - } - - if (host) { - this._host = host; - - if (!this._layerElement) { - const doc = getDocument(this._rootElement) as Document; - - this._layerElement = doc.createElement('div'); - this._layerElement.className = css('ms-Layer', { - ['ms-Layer--fixed ' + styles.rootIsFixed]: !this.props.hostId - }); - - host.appendChild(this._layerElement); - setVirtualParent(this._layerElement, this._rootElement); - } - - // Using this 'unstable' method allows us to retain the React context across the layer projection. - ReactDOM.unstable_renderSubtreeIntoContainer( - this, - ( - - { this.props.children } - - ), - this._layerElement, - () => { - if (!this._hasMounted) { - this._hasMounted = true; - - // TODO: @deprecated cleanup required. - if (this.props.onLayerMounted) { - this.props.onLayerMounted(); - } - - this.props.onLayerDidMount!(); - } - }); - } - } - - public render() { - return ( - - ); - } - - private _removeLayerElement() { - if (this._layerElement) { - this.props.onLayerWillUnmount!(); - - ReactDOM.unmountComponentAtNode(this._layerElement); - const parentNode = this._layerElement.parentNode; - if (parentNode) { - parentNode.removeChild(this._layerElement); - } - this._layerElement = undefined; - this._hasMounted = false; - } - } - - private _getHost(): Node { - const { hostId } = this.props; - const doc = getDocument(this._rootElement) as Document; - - if (hostId) { - return doc.getElementById(hostId) as Node; - } else { - return _defaultHostSelector ? doc.querySelector(_defaultHostSelector) as Node : doc.body; - } - } - -} +import { styled } from '../../Utilities'; +import { + ILayerProps, + ILayerStyleProps, + ILayerStyles +} from './Layer.types'; +import { LayerBase } from './Layer.base'; +import { getStyles } from './Layer.styles'; + +export const Layer = styled( + LayerBase, + getStyles +); diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.types.ts b/packages/office-ui-fabric-react/src/components/Layer/Layer.types.ts index fb6f36842cef02..7b4151727ecdfe 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/Layer.types.ts +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.types.ts @@ -1,17 +1,35 @@ import * as React from 'react'; -import { Layer } from './Layer'; +import { LayerBase } from './Layer.base'; +import { IStyle, ITheme } from '../../Styling'; +import { IStyleFunction } from '../../Utilities'; export interface ILayer { } -export interface ILayerProps extends React.HTMLAttributes { +export interface ILayerProps extends React.HTMLAttributes { /** * Optional callback to access the ILayer interface. Use this instead of ref for accessing * the public methods and properties of the component. */ componentRef?: (component: ILayer) => void; + /** + * Call to provide customized styling that will layer on top of the variant rules + */ + getStyles?: IStyleFunction; + + /** + * Theme provided by HOC. + */ + theme?: ITheme; + + /** + * Additional css class to apply to the Layer + * @defaultvalue undefined + */ + className?: string; + /** Callback for when the layer is mounted. */ onLayerMounted?: () => void; @@ -33,3 +51,31 @@ export interface ILayerProps extends React.HTMLAttributes { @@ -15,11 +13,11 @@ export class LayerHost extends BaseComponent { } public componentDidMount() { - Layer.notifyHostChanged(this.props.id!); + LayerBase.notifyHostChanged(this.props.id!); } public componentWillUnmount() { - Layer.notifyHostChanged(this.props.id!); + LayerBase.notifyHostChanged(this.props.id!); } public render() { diff --git a/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Basic.Example.tsx b/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Basic.Example.tsx index a8cc8e746fa6be..3a0c18741663c4 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Basic.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Basic.Example.tsx @@ -2,10 +2,9 @@ import * as React from 'react'; // tslint:disable-line:no-unused-variable import * as PropTypes from 'prop-types'; import './Layer.Example.scss'; import '../../../common/_exampleStyles.scss'; -import { BaseComponent } from 'office-ui-fabric-react/lib/Utilities'; -import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; -import { Layer } from 'office-ui-fabric-react/lib/Layer'; -import { autobind } from 'office-ui-fabric-react/lib/Utilities'; +import { autobind, BaseComponent } from '../../../Utilities'; +import { Checkbox } from '../../../Checkbox'; +import { Layer } from '../Layer'; import { AnimationClassNames } from '../../../Styling'; import * as exampleStylesImport from '../../../common/_exampleStyles.scss'; const exampleStyles: any = exampleStylesImport; diff --git a/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Hosted.Example.tsx b/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Hosted.Example.tsx index 27ac832ece392c..e166d97068141d 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Hosted.Example.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/examples/Layer.Hosted.Example.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { autobind } from 'office-ui-fabric-react/lib/Utilities'; -import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; -import { Layer, LayerHost } from 'office-ui-fabric-react/lib/Layer'; -import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; +import { autobind } from '../../../Utilities'; +import { Checkbox } from '../../../Checkbox'; +import { Layer } from '../Layer'; +import { LayerHost } from '../LayerHost'; +import { Toggle } from '../../../Toggle'; import { AnimationClassNames } from '../../../Styling'; import './Layer.Example.scss'; import * as exampleStylesImport from '../../../common/_exampleStyles.scss'; @@ -10,6 +11,7 @@ const exampleStyles: any = exampleStylesImport; export class LayerHostedExample extends React.Component<{}, { showLayer: boolean; + showLayerNoId: boolean; showHost: boolean; }> { @@ -17,12 +19,13 @@ export class LayerHostedExample extends React.Component<{}, { super(props); this.state = { showLayer: false, + showLayerNoId: false, showHost: true }; } public render() { - const { showLayer, showHost } = this.state; + const { showLayer, showLayerNoId, showHost } = this.state; const content = (
This is example layer content. @@ -57,6 +60,7 @@ export class LayerHostedExample extends React.Component<{}, { hostId='layerhost1' onLayerDidMount={ this._log('didmount') } onLayerWillUnmount={ this._log('willunmount') } + className={ 'exampleLayerClassName' } > { content } @@ -64,6 +68,26 @@ export class LayerHostedExample extends React.Component<{}, {
I am normally below the content.
+

+ If you do not specify a hostId then the hosted layer will default to being fixed to the page by default. +

+ + + + { showLayerNoId ? ( + + { content } + + ) : content } +
); } @@ -79,6 +103,11 @@ export class LayerHostedExample extends React.Component<{}, { this.setState({ showLayer: checked }); } + @autobind + private _onChangeCheckboxNoId(ev: React.FormEvent, checked: boolean): void { + this.setState({ showLayerNoId: checked }); + } + @autobind private _onChangeToggle(checked: boolean): void { this.setState({ showHost: checked });