diff --git a/common/changes/@uifabric/example-app-base/users-cschleid-portalLayers_2018-05-07-14-38.json b/common/changes/@uifabric/example-app-base/users-cschleid-portalLayers_2018-05-07-14-38.json new file mode 100644 index 00000000000000..bab659a6ebc9b1 --- /dev/null +++ b/common/changes/@uifabric/example-app-base/users-cschleid-portalLayers_2018-05-07-14-38.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@uifabric/example-app-base", + "comment": "Pin markdown-to-jsx dependency", + "type": "patch" + } + ], + "packageName": "@uifabric/example-app-base", + "email": "cschleid@microsoft.com" +} \ No newline at end of file diff --git a/common/changes/office-ui-fabric-react/users-cschleid-portalLayers_2018-05-03-15-52.json b/common/changes/office-ui-fabric-react/users-cschleid-portalLayers_2018-05-03-15-52.json new file mode 100644 index 00000000000000..3147447dc7fc9b --- /dev/null +++ b/common/changes/office-ui-fabric-react/users-cschleid-portalLayers_2018-05-03-15-52.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "office-ui-fabric-react", + "comment": "Layer: Use React Portals if available", + "type": "minor" + } + ], + "packageName": "office-ui-fabric-react", + "email": "cschleid@microsoft.com" +} \ No newline at end of file diff --git a/common/config/rush/npm-shrinkwrap.json b/common/config/rush/npm-shrinkwrap.json index 2843776dbc5f15..e5a05de349e669 100644 --- a/common/config/rush/npm-shrinkwrap.json +++ b/common/config/rush/npm-shrinkwrap.json @@ -97,7 +97,7 @@ }, "@rush-temp/build": { "version": "file:projects/build.tgz", - "integrity": "sha1-z6NWyAlBPNkYocdtqHqkUBbXJPk=", + "integrity": "sha1-BDbpYNW8ohV7nGFduSpFbtmZomk=", "requires": { "@microsoft/api-extractor": "4.3.7", "@microsoft/load-themed-styles": "1.7.56", @@ -711,7 +711,7 @@ }, "@rush-temp/example-app-base": { "version": "file:projects/example-app-base.tgz", - "integrity": "sha1-UC2f/hGOHWAirNSGMwtwQZ6C5cw=", + "integrity": "sha1-1DCUT0PQ5j5WiX+3gMcFktzQ9vs=", "requires": { "@types/es6-promise": "0.0.32", "@types/highlight.js": "9.12.2", @@ -732,7 +732,7 @@ }, "@rush-temp/experiments": { "version": "file:projects/experiments.tgz", - "integrity": "sha1-fNE2diLON8XhXd1EaDMp6u8V/Pk=", + "integrity": "sha1-AMtNg9E2EvYKDJHFNTfBihiqAEU=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/enzyme": "3.1.5", @@ -761,7 +761,7 @@ }, "@rush-temp/fabric-website": { "version": "file:projects/fabric-website.tgz", - "integrity": "sha1-C6PtySrdbA4OpRK0CEjeX0kdaZA=", + "integrity": "sha1-iGmrC9WlX/oSfXjX50N80F6LthI=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/es6-promise": "0.0.32", @@ -786,7 +786,7 @@ }, "@rush-temp/file-type-icons": { "version": "file:projects/file-type-icons.tgz", - "integrity": "sha1-dx2eFYGZxJzol7Prc26tuffpwDg=", + "integrity": "sha1-1kyEZs+qsYi3p8e7pN1mTkTRJig=", "requires": { "@types/react": "16.3.13", "@types/react-dom": "16.0.5", @@ -797,21 +797,21 @@ }, "@rush-temp/icons": { "version": "file:projects/icons.tgz", - "integrity": "sha1-+l7i9AZW0UOaNQgs9G4Xwpe/wYQ=", + "integrity": "sha1-EGY+Ne+jFfGWBPfHjMKfOr33opY=", "requires": { "tslib": "1.9.0" } }, "@rush-temp/jest-serializer-merge-styles": { "version": "file:projects/jest-serializer-merge-styles.tgz", - "integrity": "sha1-hgPep1RdSUOFgzIckQGjUKbrwYo=", + "integrity": "sha1-vQZqLnQ1Yzhd4Qb36zjldSSZVZs=", "requires": { "@types/jest": "21.1.8" } }, "@rush-temp/merge-styles": { "version": "file:projects/merge-styles.tgz", - "integrity": "sha1-OeM26zANwCc0VbioUMyCZD+z74I=", + "integrity": "sha1-yOpVuY27oppJpXlMUPVskAmbyhk=", "requires": { "@types/jest": "21.1.8", "tslib": "1.9.0" @@ -819,7 +819,7 @@ }, "@rush-temp/office-ui-fabric-react": { "version": "file:projects/office-ui-fabric-react.tgz", - "integrity": "sha1-TKTm6VE/kEYgZTQ+DaxlDE9QbSE=", + "integrity": "sha1-4QqXzFTXZ5zcGM1W08wDDAWjnRs=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/enzyme": "3.1.5", @@ -835,6 +835,7 @@ "@types/webpack-env": "1.13.0", "enzyme": "3.3.0", "enzyme-adapter-react-16": "1.1.1", + "enzyme-to-json": "3.3.3", "es6-map": "0.1.5", "es6-promise": "4.2.4", "es6-weak-map": "2.0.2", @@ -852,14 +853,14 @@ }, "@rush-temp/office-ui-fabric-react-tslint": { "version": "file:projects/office-ui-fabric-react-tslint.tgz", - "integrity": "sha1-lvCfcd+hZzfIhUpydZ9YxpcI/qs=", + "integrity": "sha1-Pv4IDaYg9vh7FeQDFQfUYGu3vo4=", "requires": { "tslint-react": "3.5.1" } }, "@rush-temp/ssr-tests": { "version": "file:projects/ssr-tests.tgz", - "integrity": "sha1-DUh8VkIINYXpJALFv/gXhT7yhEw=", + "integrity": "sha1-Hv2j9Yj50vUDKkiMSlgLHpNeXME=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/es6-promise": "0.0.32", @@ -1176,7 +1177,7 @@ }, "@rush-temp/styling": { "version": "file:projects/styling.tgz", - "integrity": "sha1-9j5JYGxoDzfIUae0eGZPhtC/9jY=", + "integrity": "sha1-YzRcHkRswW5EHxGcbL8drFtqlfg=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/jest": "21.1.8", @@ -1191,7 +1192,7 @@ }, "@rush-temp/test-bundle-button": { "version": "file:projects/test-bundle-button.tgz", - "integrity": "sha1-EnK1gElS5OcCNiNt+VuP3QZU3XM=", + "integrity": "sha1-Gge2uY/yy1M9tCa2tgYfXb8eBJQ=", "requires": { "@types/prop-types": "15.5.2", "@types/react": "16.3.13", @@ -1204,7 +1205,7 @@ }, "@rush-temp/todo-app": { "version": "file:projects/todo-app.tgz", - "integrity": "sha1-w/KnNInosENE4lCsw/rF/jZeZ+k=", + "integrity": "sha1-X1kK8Km/toBwdeJgEE1dXeq1z8w=", "requires": { "@microsoft/load-themed-styles": "1.7.56", "@types/es6-promise": "0.0.32", @@ -1221,7 +1222,7 @@ }, "@rush-temp/utilities": { "version": "file:projects/utilities.tgz", - "integrity": "sha1-aaZTW5aT5r5JVXTFwrKVxXY3B5U=", + "integrity": "sha1-9j/0+lle216bTwcVODrdXrUL5po=", "requires": { "@types/enzyme": "3.1.5", "@types/enzyme-adapter-react-16": "1.0.1", @@ -1241,7 +1242,7 @@ }, "@rush-temp/variants": { "version": "file:projects/variants.tgz", - "integrity": "sha1-hZ3ywA2HnPEws9Bg8vCm9oLQxtg=", + "integrity": "sha1-3jStDGhpfFip83t1JlIKqCWw1Qw=", "requires": { "@types/jest": "21.1.8", "tslib": "1.9.0" @@ -1249,7 +1250,7 @@ }, "@rush-temp/vr-tests": { "version": "file:projects/vr-tests.tgz", - "integrity": "sha1-zivgf/DyCH8bqt51oSiq07sH9e0=", + "integrity": "sha1-GasyaOFSdX2VOKkP2u3YbHxCvEI=", "requires": { "@storybook/addon-options": "3.2.3", "@storybook/react": "3.4.3", @@ -2130,7 +2131,7 @@ "integrity": "sha512-Iq8TRIB+/9eQ8rbGhcP7ct5cYb/3qjNYAR2SnzLCEcwF6rvVOax8+9+fccgXk4bEhQGjOZd5TLhsksmAdsbGqQ==", "requires": { "browserslist": "2.11.3", - "caniuse-lite": "1.0.30000833", + "caniuse-lite": "1.0.30000835", "normalize-range": "0.1.2", "num2fraction": "1.2.2", "postcss": "6.0.22", @@ -4123,7 +4124,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", "requires": { - "caniuse-lite": "1.0.30000833", + "caniuse-lite": "1.0.30000835", "electron-to-chromium": "1.3.45" } }, @@ -4358,7 +4359,7 @@ "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", "requires": { "browserslist": "1.7.7", - "caniuse-db": "1.0.30000833", + "caniuse-db": "1.0.30000835", "lodash.memoize": "4.1.2", "lodash.uniq": "4.5.0" }, @@ -4368,21 +4369,21 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", "requires": { - "caniuse-db": "1.0.30000833", + "caniuse-db": "1.0.30000835", "electron-to-chromium": "1.3.45" } } } }, "caniuse-db": { - "version": "1.0.30000833", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000833.tgz", - "integrity": "sha1-K9e+cqQBZY0svLj012AN7r6xxnY=" + "version": "1.0.30000835", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000835.tgz", + "integrity": "sha1-ZVaTHN8DWQPYZV1jA/lQG1kV++k=" }, "caniuse-lite": { - "version": "1.0.30000833", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000833.tgz", - "integrity": "sha512-tKNuKu4WLImh4NxoTgntxFpDrRiA0Q6Q1NycNhuMST0Kx+Pt8YnRDW6V8xsyH6AtO2CpAoibatEk5eaEhP3O1g==" + "version": "1.0.30000835", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000835.tgz", + "integrity": "sha512-88MbwAwuVWfwC4xHwKx9Z/VSvmIfLPwURhmxTPqP5Cx6zHQ0xa2AFKJvTdC2aUn07f2tR9yvYL83CollBFANEA==" }, "case-sensitive-paths-webpack-plugin": { "version": "2.1.2", @@ -5406,7 +5407,7 @@ "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", "requires": { "browserslist": "1.7.7", - "caniuse-db": "1.0.30000833", + "caniuse-db": "1.0.30000835", "normalize-range": "0.1.2", "num2fraction": "1.2.2", "postcss": "5.2.18", @@ -5418,7 +5419,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", "requires": { - "caniuse-db": "1.0.30000833", + "caniuse-db": "1.0.30000835", "electron-to-chromium": "1.3.45" } }, @@ -6016,9 +6017,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "ejs": { - "version": "2.5.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.9.tgz", - "integrity": "sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", + "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==" }, "electron-to-chromium": { "version": "1.3.45", @@ -6059,7 +6060,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", "requires": { - "iconv-lite": "0.4.21" + "iconv-lite": "0.4.22" } }, "end-of-stream": { @@ -6158,6 +6159,21 @@ } } }, + "enzyme-to-json": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.3.3.tgz", + "integrity": "sha1-7eRZOPswnNh+vUOG9gx1RSVRWgc=", + "requires": { + "lodash": "4.17.10" + }, + "dependencies": { + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + } + } + }, "errno": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", @@ -6591,7 +6607,7 @@ "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "requires": { "chardet": "0.4.2", - "iconv-lite": "0.4.21", + "iconv-lite": "0.4.22", "tmp": "0.0.33" } }, @@ -8507,9 +8523,9 @@ "integrity": "sha1-MRYKNpMK2vH8BMYHT360FGXU7Es=" }, "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.22.tgz", + "integrity": "sha512-1AinFBeDTnsvVEP+V1QBlHpM1UZZl7gWB6fcz7B1Ho+LI1dUh2sSrxoCfVt2PinRHzXAziSniEV3P7JbTDHcXA==", "requires": { "safer-buffer": "2.1.2" } @@ -10455,13 +10471,13 @@ } }, "mem-fs-editor": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz", - "integrity": "sha512-54fptqhSZX1sSYsVVInG2qzUWPPrEv/6qYxHAwXJZQfzDcviJcL+7p/wmupg8SdAOi42m/vilMBemx3D6Sz22g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-4.0.2.tgz", + "integrity": "sha512-QHvdXLLNmwJXxKdf7x27aNUren6IoPxwcM8Sfd+S6/ddQQMcYdEtVKsh6ilpqMrU18VQuKZEaH0aCGt3JDbA0g==", "requires": { "commondir": "1.0.1", "deep-extend": "0.5.1", - "ejs": "2.5.9", + "ejs": "2.6.1", "glob": "7.1.2", "globby": "8.0.1", "isbinaryfile": "3.0.2", @@ -12902,7 +12918,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", "requires": { - "caniuse-db": "1.0.30000833", + "caniuse-db": "1.0.30000835", "electron-to-chromium": "1.3.45" } }, @@ -19583,7 +19599,7 @@ "bfj-node4": "5.3.1", "chalk": "2.4.1", "commander": "2.15.1", - "ejs": "2.5.9", + "ejs": "2.6.1", "express": "4.16.3", "filesize": "3.6.1", "gzip-size": "4.1.0", @@ -20761,7 +20777,7 @@ "istextorbinary": "2.2.1", "lodash": "4.17.10", "make-dir": "1.2.0", - "mem-fs-editor": "4.0.1", + "mem-fs-editor": "4.0.2", "minimist": "1.2.0", "pretty-bytes": "4.0.2", "read-chunk": "2.1.0", diff --git a/packages/example-app-base/package.json b/packages/example-app-base/package.json index f844cbcfd60db4..a77256ed5f8bb3 100644 --- a/packages/example-app-base/package.json +++ b/packages/example-app-base/package.json @@ -36,7 +36,7 @@ "@uifabric/styling": ">=5.30.1 <6.0.0", "@uifabric/utilities": ">=5.30.1 <6.0.0", "highlight.js": "^9.12.0", - "markdown-to-jsx": "^6.6.0", + "markdown-to-jsx": "6.6.1", "office-ui-fabric-react": ">=5.1.0 <6.0.0", "react-syntax-highlighter": "^7.0.2", "tslib": "^1.7.1" diff --git a/packages/office-ui-fabric-react/package.json b/packages/office-ui-fabric-react/package.json index cc9ba712619148..a0e9269472a0d1 100644 --- a/packages/office-ui-fabric-react/package.json +++ b/packages/office-ui-fabric-react/package.json @@ -33,8 +33,9 @@ "@uifabric/example-app-base": ">=5.11.2 <6.0.0", "enzyme": "^3.2.0", "enzyme-adapter-react-16": "^1.1.0", - "es6-promise": "^4.1.0", + "enzyme-to-json": "^3.3.3", "es6-map": "^0.1.5", + "es6-promise": "^4.1.0", "es6-weak-map": "^2.0.2", "highlight.js": "^9.12.0", "office-ui-fabric-core": ">=9.0.0 <10.0.0", diff --git a/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx b/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx index 807243c1ca152d..090cea431bac54 100644 --- a/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx +++ b/packages/office-ui-fabric-react/src/components/Callout/CalloutContent.base.tsx @@ -66,7 +66,7 @@ export class CalloutContentBase extends BaseComponent(); private _calloutElement = createRef(); @@ -134,7 +134,6 @@ export class CalloutContentBase extends BaseComponent { @@ -665,59 +664,6 @@ describe('ContextualMenu', () => { }).catch(done()); }); - it('ContextualMenu menuOpened callback is called only when menu is available', () => { - let layerMounted = false; - let menuMounted = false; - let menuMountedFirst = false; - let layerMountedFirst = false; - - // Alter the Layer's prototype so that we can confirm that it mounts before the contextualmenu mounts. - /* tslint:disable:no-function-expression */ - Layer.prototype.componentDidMount = function (componentDidMount): () => void { - return function (): void { - if (menuMounted) { - menuMountedFirst = true; - } - layerMounted = true; - return componentDidMount.call(this); - }; - }(Layer.prototype.componentDidMount); - /* tslint:enable:no-function-expression */ - - const items: IContextualMenuItem[] = [ - { - name: 'TestText 1', - key: 'TestKey1', - className: 'testkey1' - }, - { - name: 'TestText 2', - key: 'TestKey2' - }, - ]; - - const onMenuOpened = (): void => { - if (layerMounted) { - layerMountedFirst = true; - } - menuMounted = true; - }; - - ReactTestUtils.renderIntoDocument( -
- - -
- ); - expect(menuMounted).toEqual(true); - expect(layerMountedFirst).toEqual(true); - expect(menuMountedFirst).toEqual(false); - }); - it('merges callout classNames', () => { ReactTestUtils.renderIntoDocument( (); @@ -35,14 +33,14 @@ export class LayerBase extends BaseComponent { 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. + * @deprecated */ public static notifyHostChanged(id: string) { - if (_layersByHostId[id]) { - _layersByHostId[id].forEach(layer => layer.forceUpdate()); - } + notifyHostChanged(id); } /** @@ -52,9 +50,10 @@ export class LayerBase extends BaseComponent { * * Passing in a falsey value will clear the default target and reset back to * using a created element at the end of document body. + * @deprecated */ public static setDefaultTarget(selector?: string) { - _defaultHostSelector = selector; + setDefaultTarget(selector); } constructor(props: ILayerProps) { @@ -65,11 +64,7 @@ export class LayerBase extends BaseComponent { }); if (this.props.hostId) { - if (!_layersByHostId[this.props.hostId]) { - _layersByHostId[this.props.hostId] = []; - } - - _layersByHostId[this.props.hostId].push(this); + registerLayer(this.props.hostId, this); } } @@ -81,10 +76,7 @@ export class LayerBase extends BaseComponent { 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]; - } + unregisterLayer(this.props.hostId, this); } } @@ -180,8 +172,8 @@ export class LayerBase extends BaseComponent { if (hostId) { return doc.getElementById(hostId) as Node; } else { - return _defaultHostSelector ? doc.querySelector(_defaultHostSelector) as Node : doc.body; + const defaultHostSelector = getDefaultTarget(); + return defaultHostSelector ? doc.querySelector(defaultHostSelector) as Node : doc.body; } } - } diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.notification.ts b/packages/office-ui-fabric-react/src/components/Layer/Layer.notification.ts new file mode 100644 index 00000000000000..f18a548fbb4888 --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.notification.ts @@ -0,0 +1,62 @@ +const _layersByHostId: { [hostId: string]: React.Component[] } = {}; + +let _defaultHostSelector: string | undefined; + +/** + * Register a layer for a given host id + * @param hostId Id of the layer host + * @param layer Layer instance + */ +export function registerLayer(hostId: string, layer: React.Component) { + if (!_layersByHostId[hostId]) { + _layersByHostId[hostId] = []; + } + + _layersByHostId[hostId].push(layer); +} + +/** + * Unregister a layer for a given host id + * @param hostId Id of the layer host + * @param layer Layer instance + */ +export function unregisterLayer(hostId: string, layer: React.Component) { + if (_layersByHostId[hostId]) { + const idx = _layersByHostId[hostId].indexOf(layer); + if (idx >= 0) { + _layersByHostId[hostId].splice(idx, 1); + if (_layersByHostId[hostId].length === 0) { + delete _layersByHostId[hostId]; + } + } + } +} + +/** + * Used for notifying applicable Layers that a host is available/unavailable and to re-evaluate Layers that + * care about the specific host. + */ +export function 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. + */ +export function setDefaultTarget(selector?: string) { + _defaultHostSelector = selector; +} + +/** + * Get the default target selector when determining a host + */ +export function getDefaultTarget(): string | undefined { + return _defaultHostSelector; +} \ No newline at end of file diff --git a/packages/office-ui-fabric-react/src/components/Layer/Layer.test.tsx b/packages/office-ui-fabric-react/src/components/Layer/Layer.test.tsx index 9794524990eec4..0a3b06865dac72 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/Layer.test.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.test.tsx @@ -3,16 +3,16 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as PropTypes from 'prop-types'; /* tslint:enable:no-unused-variable */ -import * as renderer from 'react-test-renderer'; +import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; import { Layer } from './Layer'; import { LayerHost } from './LayerHost'; describe('Layer', () => { it('renders Layer correctly', () => { - const component = renderer.create(Content); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + const component = mount(Content); + expect(toJson(component)).toMatchSnapshot(); }); it('can render in a targeted LayerHost and pass context through', () => { 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 0cdd7535ca9dcd..f2343a7b74ede9 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/Layer.tsx @@ -1,3 +1,4 @@ +import * as ReactDOM from 'react-dom'; import { styled } from '../../Utilities'; import { ILayerProps, @@ -5,9 +6,12 @@ import { ILayerStyles } from './Layer.types'; import { LayerBase } from './Layer.base'; +import { PortalLayerBase } from './PortalLayer.base'; import { getStyles } from './Layer.styles'; +const portalSupport: boolean = !!ReactDOM.createPortal; + export const Layer = styled( - LayerBase, + portalSupport ? PortalLayerBase : LayerBase, getStyles ); diff --git a/packages/office-ui-fabric-react/src/components/Layer/LayerHost.tsx b/packages/office-ui-fabric-react/src/components/Layer/LayerHost.tsx index 7a8f81aa60af24..e7c1e3cd8f555d 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/LayerHost.tsx +++ b/packages/office-ui-fabric-react/src/components/Layer/LayerHost.tsx @@ -3,21 +3,20 @@ import { BaseComponent, css } from '../../Utilities'; -import { LayerBase } from './Layer.base'; import { ILayerHostProps } from './LayerHost.types'; +import { notifyHostChanged } from './Layer.notification'; -export class LayerHost extends BaseComponent { - +export class LayerHost extends BaseComponent { public shouldComponentUpdate() { return false; } public componentDidMount(): void { - LayerBase.notifyHostChanged(this.props.id!); + notifyHostChanged(this.props.id!); } public componentWillUnmount(): void { - LayerBase.notifyHostChanged(this.props.id!); + notifyHostChanged(this.props.id!); } public render(): JSX.Element { diff --git a/packages/office-ui-fabric-react/src/components/Layer/PortalLayer.base.tsx b/packages/office-ui-fabric-react/src/components/Layer/PortalLayer.base.tsx new file mode 100644 index 00000000000000..d4fe22259613eb --- /dev/null +++ b/packages/office-ui-fabric-react/src/components/Layer/PortalLayer.base.tsx @@ -0,0 +1,181 @@ +/* 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 { + BaseComponent, + classNamesFunction, + customizable, + getDocument, + createRef, + setVirtualParent +} from '../../Utilities'; +import { registerLayer, getDefaultTarget, unregisterLayer } from './Layer.notification'; + +const getClassNames = classNamesFunction(); + +@customizable('Layer', ['theme', 'hostId']) +export class PortalLayerBase extends BaseComponent { + + public static defaultProps: ILayerProps = { + onLayerDidMount: () => undefined, + onLayerWillUnmount: () => undefined + }; + + private _host: Node; + private _layerElement: HTMLElement | undefined; + private _rootElement = createRef(); + + constructor(props: ILayerProps) { + super(props); + + this._warnDeprecations({ + onLayerMounted: 'onLayerDidMount' + }); + + if (this.props.hostId) { + registerLayer(this.props.hostId, this); + } + } + + public componentWillMount(): void { + this._layerElement = this._getLayerElement(); + } + + public componentWillUpdate(): void { + if (!this._layerElement) { + this._layerElement = this._getLayerElement(); + } + } + + public componentDidMount(): void { + this._setVirtualParent(); + + const { onLayerDidMount, onLayerMounted } = this.props; + if (onLayerMounted) { + onLayerMounted(); + } + + if (onLayerDidMount) { + onLayerDidMount(); + } + } + + public componentWillUnmount(): void { + this._removeLayerElement(); + + const { onLayerWillUnmount, hostId } = this.props; + if (onLayerWillUnmount) { + onLayerWillUnmount(); + } + + if (hostId) { + unregisterLayer(hostId, this); + } + } + + public componentDidUpdate(): void { + this._setVirtualParent(); + } + + public render(): React.ReactNode { + const classNames = this._getClassNames(); + + return ( + + { + this._layerElement && ReactDOM.createPortal( + ( + + { this.props.children } + + ), + this._layerElement + ) + } + + ); + } + + private _getClassNames() { + const { className, getStyles, theme } = this.props; + const classNames = getClassNames(getStyles!, + { + theme: theme!, + className, + isNotHost: !this.props.hostId + } + ); + + return classNames; + } + + private _setVirtualParent() { + if (this._rootElement && this._rootElement.current && this._layerElement) { + setVirtualParent(this._layerElement, this._rootElement.current); + } + } + + private _getLayerElement(): HTMLElement | undefined { + const host = this._getHost(); + + const classNames = this._getClassNames(); + + if (host !== this._host) { + this._removeLayerElement(); + } + + if (host) { + this._host = host; + + if (!this._layerElement) { + const doc = getDocument(); + if (!doc) { + return; + } + + this._layerElement = doc.createElement('div'); + this._layerElement.className = classNames.root!; + + host.appendChild(this._layerElement); + } + } + + return this._layerElement; + } + + private _removeLayerElement(): void { + if (this._layerElement) { + this.props.onLayerWillUnmount!(); + + const parentNode = this._layerElement.parentNode; + if (parentNode) { + parentNode.removeChild(this._layerElement); + } + this._layerElement = undefined; + } + } + + private _getHost(): Node | undefined { + const { hostId } = this.props; + + const doc = getDocument(); + if (!doc) { + return undefined; + } + + if (hostId) { + return doc.getElementById(hostId) as Node; + } else { + const defaultHostSelector = getDefaultTarget(); + return defaultHostSelector ? doc.querySelector(defaultHostSelector) as Node : doc.body; + } + } +} diff --git a/packages/office-ui-fabric-react/src/components/Layer/__snapshots__/Layer.test.tsx.snap b/packages/office-ui-fabric-react/src/components/Layer/__snapshots__/Layer.test.tsx.snap index 2ea317e206ef8f..8754edd7649434 100644 --- a/packages/office-ui-fabric-react/src/components/Layer/__snapshots__/Layer.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/Layer/__snapshots__/Layer.test.tsx.snap @@ -1,7 +1,438 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Layer renders Layer correctly 1`] = ` - + + + + + + +
+ Content +
+
+
+
+
+
+
`; 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 8e2fffbd32cc0d..b288c7072fe569 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 @@ -87,7 +87,7 @@ export class LayerBasicExample extends BaseComponent<{}, { ); } - private _onChange(ev: React.FormEvent, checked: boolean): void { + private _onChange = (ev: React.FormEvent, checked: boolean): void => { this.setState({ showLayer: checked }); } } diff --git a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.test.tsx b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.test.tsx index a2974da0e16e48..ae32f5072ab222 100644 --- a/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.test.tsx +++ b/packages/office-ui-fabric-react/src/components/TeachingBubble/TeachingBubble.test.tsx @@ -5,13 +5,13 @@ import * as ReactTestUtils from 'react-dom/test-utils'; import * as renderer from 'react-test-renderer'; import { TeachingBubble } from './TeachingBubble'; import { TeachingBubbleContent } from './TeachingBubbleContent'; +import { mount } from 'enzyme'; +import EnzymeToJson from 'enzyme-to-json'; describe('TeachingBubble', () => { - it('renders TeachingBubble correctly', () => { - const component = renderer.create(Content); - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); + const component = mount(Content); + expect(EnzymeToJson(component)).toMatchSnapshot(); const componentContent = renderer.create(Content); const treeContent = componentContent.toJSON(); 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..2417d89cea919f 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 @@ -1,9 +1,792 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TeachingBubble renders TeachingBubble correctly 1`] = ` - + + + + + + + + +
+ + + +
+
+ +
+ +
+
+
+

+ Content +

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; exports[`TeachingBubble renders TeachingBubble correctly 2`] = ` diff --git a/packages/office-ui-fabric-react/src/components/Tooltip/Tooltip.test.tsx b/packages/office-ui-fabric-react/src/components/Tooltip/Tooltip.test.tsx index 9be45ad1cbaebe..16e9b85e7bad2e 100644 --- a/packages/office-ui-fabric-react/src/components/Tooltip/Tooltip.test.tsx +++ b/packages/office-ui-fabric-react/src/components/Tooltip/Tooltip.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import * as ReactTestUtils from 'react-dom/test-utils'; -import * as renderer from 'react-test-renderer'; import { mount } from 'enzyme'; +import toJson from 'enzyme-to-json'; import { DirectionalHint } from '../../common/DirectionalHint'; import { TooltipBase } from './Tooltip.base'; @@ -21,8 +21,8 @@ const defaultCalloutProps: ICalloutProps = { describe('Tooltip', () => { it('renders default Tooltip correctly', () => { - const component = renderer.create(); - const tree = component.toJSON(); + const component = mount(); + const tree = toJson(component); expect(tree).toMatchSnapshot(); }); diff --git a/packages/office-ui-fabric-react/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/packages/office-ui-fabric-react/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap index 6ad5c66cbf82a1..16cf815bddfd93 100644 --- a/packages/office-ui-fabric-react/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap +++ b/packages/office-ui-fabric-react/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap @@ -1,7 +1,959 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Tooltip renders default Tooltip correctly 1`] = ` - + + + + + + + + + +
+ + + +
+
+ +
+
+

+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;