diff --git a/src/addons/Portal/Portal.js b/src/addons/Portal/Portal.js index 41203f8cea..50f3a7b213 100644 --- a/src/addons/Portal/Portal.js +++ b/src/addons/Portal/Portal.js @@ -348,7 +348,7 @@ class Portal extends Component { this.mountPortal() // Server side rendering - if (!isBrowser) return null + if (!isBrowser()) return null this.rootNode.className = className || '' @@ -372,13 +372,13 @@ class Portal extends Component { } mountPortal = () => { - if (!isBrowser || this.rootNode) return + if (!isBrowser() || this.rootNode) return debug('mountPortal()') const { eventPool, - mountNode = isBrowser ? document.body : null, + mountNode = isBrowser() ? document.body : null, prepend, } = this.props @@ -396,7 +396,7 @@ class Portal extends Component { } unmountPortal = () => { - if (!isBrowser || !this.rootNode) return + if (!isBrowser() || !this.rootNode) return debug('unmountPortal()') const { eventPool } = this.props diff --git a/src/addons/Responsive/Responsive.js b/src/addons/Responsive/Responsive.js index 6f1d0add7a..66b4cc4fd5 100644 --- a/src/addons/Responsive/Responsive.js +++ b/src/addons/Responsive/Responsive.js @@ -57,7 +57,7 @@ export default class Responsive extends Component { constructor(...args) { super(...args) - this.state = { width: isBrowser ? window.innerWidth : 0 } + this.state = { width: isBrowser() ? window.innerWidth : 0 } } componentDidMount() { diff --git a/src/behaviors/Visibility/Visibility.js b/src/behaviors/Visibility/Visibility.js index bc8a48a5a2..dc1693f43f 100644 --- a/src/behaviors/Visibility/Visibility.js +++ b/src/behaviors/Visibility/Visibility.js @@ -160,7 +160,7 @@ export default class Visibility extends Component { } static defaultProps = { - context: isBrowser ? window : null, + context: isBrowser() ? window : null, continuous: false, offset: [0, 0], once: true, @@ -196,7 +196,7 @@ export default class Visibility extends Component { } componentDidMount() { - if (!isBrowser) return + if (!isBrowser()) return const { context, fireOnMount } = this.props this.pageYOffset = window.pageYOffset diff --git a/src/lib/debug.js b/src/lib/debug.js index 7bb1db8574..dde45373f6 100644 --- a/src/lib/debug.js +++ b/src/lib/debug.js @@ -1,7 +1,7 @@ import _debug from 'debug' import isBrowser from './isBrowser' -if (isBrowser && process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { +if (isBrowser() && process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { // Heads Up! // /~https://github.com/visionmedia/debug/pull/331 // diff --git a/src/lib/eventStack/eventStack.js b/src/lib/eventStack/eventStack.js index da170b351e..420ddfb2e0 100644 --- a/src/lib/eventStack/eventStack.js +++ b/src/lib/eventStack/eventStack.js @@ -34,7 +34,7 @@ class EventStack { // ------------------------------------ sub = (name, handlers, options = {}) => { - if (!isBrowser) return + if (!isBrowser()) return const { target = document, pool = 'default' } = options const eventTarget = this._find(target) @@ -43,7 +43,7 @@ class EventStack { } unsub = (name, handlers, options = {}) => { - if (!isBrowser) return + if (!isBrowser()) return const { target = document, pool = 'default' } = options const eventTarget = this._find(target, false) diff --git a/src/lib/isBrowser.js b/src/lib/isBrowser.js index 5cc59a5d52..bd432d2014 100644 --- a/src/lib/isBrowser.js +++ b/src/lib/isBrowser.js @@ -1,4 +1,11 @@ +import _ from 'lodash' + const hasDocument = typeof document === 'object' && document !== null const hasWindow = typeof window === 'object' && window !== null && window.self === window -export default hasDocument && hasWindow +// eslint-disable-next-line no-confusing-arrow +const isBrowser = () => !_.isNil(isBrowser.override) + ? isBrowser.override + : hasDocument && hasWindow + +export default isBrowser diff --git a/src/modules/Dimmer/Dimmer.js b/src/modules/Dimmer/Dimmer.js index 8aecf265d6..61ebca6060 100644 --- a/src/modules/Dimmer/Dimmer.js +++ b/src/modules/Dimmer/Dimmer.js @@ -72,7 +72,7 @@ export default class Dimmer extends Component { static Dimmable = DimmerDimmable handlePortalMount = () => { - if (!isBrowser) return + if (!isBrowser()) return // Heads up, IE doesn't support second argument in add() document.body.classList.add('dimmed') @@ -80,7 +80,7 @@ export default class Dimmer extends Component { } handlePortalUnmount = () => { - if (!isBrowser) return + if (!isBrowser()) return // Heads up, IE doesn't support second argument in add() document.body.classList.remove('dimmed') diff --git a/src/modules/Modal/Modal.d.ts b/src/modules/Modal/Modal.d.ts index 7109f4823d..deb50ed0c5 100644 --- a/src/modules/Modal/Modal.d.ts +++ b/src/modules/Modal/Modal.d.ts @@ -100,6 +100,9 @@ export interface ModalProps extends PortalProps { /** Custom styles. */ style?: React.CSSProperties; + + /** Element to be rendered in-place where the portal is defined. */ + trigger?: React.ReactNode; } interface ModalComponent extends React.ComponentClass { diff --git a/src/modules/Modal/Modal.js b/src/modules/Modal/Modal.js index 008460e15b..52b5ad203f 100644 --- a/src/modules/Modal/Modal.js +++ b/src/modules/Modal/Modal.js @@ -1,7 +1,7 @@ import cx from 'classnames' import _ from 'lodash' import PropTypes from 'prop-types' -import React from 'react' +import React, { isValidElement } from 'react' import { AutoControlledComponent as Component, @@ -128,6 +128,9 @@ class Modal extends Component { /** Custom styles. */ style: PropTypes.object, + /** Element to be rendered in-place where the portal is defined. */ + trigger: PropTypes.node, + /** * NOTE: Any unhandled props that are defined in Portal are passed-through * to the wrapping Portal. @@ -161,7 +164,7 @@ class Modal extends Component { } // Do not access document when server side rendering - getMountNode = () => (isBrowser ? this.props.mountNode || document.body : null) + getMountNode = () => (isBrowser() ? this.props.mountNode || document.body : null) handleActionsOverrides = predefinedProps => ({ onActionClick: (e, actionProps) => { @@ -317,11 +320,13 @@ class Modal extends Component { render() { const { open } = this.state - const { closeOnDimmerClick, closeOnDocumentClick, dimmer, eventPool } = this.props + const { closeOnDimmerClick, closeOnDocumentClick, dimmer, eventPool, trigger } = this.props const mountNode = this.getMountNode() // Short circuit when server side rendering - if (!isBrowser) return null + if (!isBrowser()) { + return isValidElement(trigger) ? trigger : null + } const unhandled = getUnhandledProps(Modal, this.props) const portalPropNames = Portal.handledProps @@ -358,6 +363,7 @@ class Modal extends Component { closeOnDocumentClick={closeOnDocumentClick} closeOnRootNodeClick={closeOnDimmerClick} {...portalProps} + trigger={trigger} className={dimmerClasses} eventPool={eventPool} mountNode={mountNode} diff --git a/src/modules/Popup/Popup.js b/src/modules/Popup/Popup.js index 0e401b8602..c62cd14acf 100644 --- a/src/modules/Popup/Popup.js +++ b/src/modules/Popup/Popup.js @@ -151,7 +151,7 @@ export default class Popup extends Component { const style = { position: 'absolute' } // Do not access window/document when server side rendering - if (!isBrowser) return style + if (!isBrowser()) return style const { offset } = this.props const { pageYOffset, pageXOffset } = window diff --git a/src/modules/Search/Search.js b/src/modules/Search/Search.js index 5ada428c82..5c2f3de66e 100644 --- a/src/modules/Search/Search.js +++ b/src/modules/Search/Search.js @@ -472,7 +472,7 @@ export default class Search extends Component { scrollSelectedItemIntoView = () => { debug('scrollSelectedItemIntoView()') // Do not access document when server side rendering - if (!isBrowser) return + if (!isBrowser()) return const menu = document.querySelector('.ui.search.active.visible .results.visible') const item = menu.querySelector('.result.active') if (!item) return diff --git a/src/modules/Sticky/Sticky.js b/src/modules/Sticky/Sticky.js index ecd45d42b3..f413ab0d7a 100644 --- a/src/modules/Sticky/Sticky.js +++ b/src/modules/Sticky/Sticky.js @@ -80,7 +80,7 @@ export default class Sticky extends Component { active: true, bottomOffset: 0, offset: 0, - scrollContext: isBrowser ? window : null, + scrollContext: isBrowser() ? window : null, } static _meta = { @@ -93,7 +93,7 @@ export default class Sticky extends Component { } componentDidMount() { - if (!isBrowser) return + if (!isBrowser()) return const { active } = this.props if (active) { @@ -117,7 +117,7 @@ export default class Sticky extends Component { } componentWillUnmount() { - if (!isBrowser) return + if (!isBrowser()) return const { active } = this.props if (active) this.removeListeners() diff --git a/test/specs/lib/isBrowser-test.js b/test/specs/lib/isBrowser-test.js index 7be371857d..47360848d0 100644 --- a/test/specs/lib/isBrowser-test.js +++ b/test/specs/lib/isBrowser-test.js @@ -1,18 +1,35 @@ import isBrowser from 'src/lib/isBrowser' describe('isBrowser', () => { - it('should return true in a browser', () => { - // tests are run in a browser, this should be true - isBrowser.should.be.true() - }) + describe('browser', () => { + it('should return true in a browser', () => { + // tests are run in a browser, this should be true + isBrowser().should.be.true() + }) + + it('should return false when there is no document', () => { + require('imports-loader?document=>undefined!src/lib/isBrowser').default().should.be.false() + require('imports-loader?document=>null!src/lib/isBrowser').default().should.be.false() + }) - it('should return false when there is no document', () => { - require('imports-loader?document=>undefined!src/lib/isBrowser').default.should.be.false() - require('imports-loader?document=>null!src/lib/isBrowser').default.should.be.false() + it('should return false when there is no window', () => { + require('imports-loader?window=>undefined!src/lib/isBrowser').default().should.be.false() + require('imports-loader?window=>null!src/lib/isBrowser').default().should.be.false() + }) }) - it('should return false when there is no window', () => { - require('imports-loader?window=>undefined!src/lib/isBrowser').default.should.be.false() - require('imports-loader?window=>null!src/lib/isBrowser').default.should.be.false() + describe('server-side', () => { + before(() => { + isBrowser.override = false + }) + + after(() => { + isBrowser.override = null + }) + + it('should return override value', () => { + // tests are run in a browser, this should be true + isBrowser().should.be.false() + }) }) }) diff --git a/test/specs/modules/Modal/Modal-test.js b/test/specs/modules/Modal/Modal-test.js index 3fcc2e983a..c99246a4da 100644 --- a/test/specs/modules/Modal/Modal-test.js +++ b/test/specs/modules/Modal/Modal-test.js @@ -1,4 +1,5 @@ import React from 'react' +import ReactDOMServer from 'react-dom/server' import Modal from 'src/modules/Modal/Modal' import ModalHeader from 'src/modules/Modal/ModalHeader' @@ -9,6 +10,7 @@ import Portal from 'src/addons/Portal/Portal' import { assertNodeContains, assertBodyClasses, assertBodyContains, domEvent, sandbox } from 'test/utils' import * as common from 'test/specs/commonTests' +import isBrowser from 'src/lib/isBrowser' // ---------------------------------------- // Wrapper @@ -538,4 +540,24 @@ describe('Modal', () => { }) }) }) + + describe('server-side', () => { + before(() => { + isBrowser.override = false + }) + + after(() => { + isBrowser.override = null + }) + + it('renders empty content when trigger is not a valid component', () => { + const markup = ReactDOMServer.renderToStaticMarkup() + markup.should.equal('') + }) + + it('renders a valid trigger component', () => { + const markup = ReactDOMServer.renderToStaticMarkup(} />) + markup.should.equal('
') + }) + }) })