+ }
+ content='Way off to the left'
+ offset={50}
+ positioning='left center'
+ />
+ }
+ content='As expected this popup is way off to the right'
+ offset={50}
+ positioning='right center'
+ />
+
+)
+
+export default PopupOffsetExample
diff --git a/docs/app/Examples/modules/Popup/Variations/PopupPositionExample.js b/docs/app/Examples/modules/Popup/Variations/PopupPositionExample.js
new file mode 100644
index 0000000000..46c8d80b60
--- /dev/null
+++ b/docs/app/Examples/modules/Popup/Variations/PopupPositionExample.js
@@ -0,0 +1,71 @@
+import React from 'react'
+import { Icon, Popup, Grid } from 'semantic-ui-react'
+
+const PopupPositionExample = () => (
+
+
+
+ }
+ content='I am positioned to the top left'
+ positioning='top left'
+ />
+
+
+ }
+ content='I am positioned to the top center'
+ positioning='top center'
+ />
+
+
+ }
+ content='I am positioned to the top right'
+ positioning='top right'
+ />
+
+
+
+
+ }
+ content='I am positioned to the left center'
+ positioning='left center'
+ />
+
+
+ }
+ content='I am positioned to the right center'
+ positioning='right center'
+ />
+
+
+
+
+ }
+ content='I am positioned to the bottom left'
+ positioning='bottom left'
+ />
+
+
+ }
+ content='I am positioned to the bottom center'
+ positioning='bottom center'
+ />
+
+
+ }
+ content='I am positioned to the bottom right'
+ positioning='bottom right'
+ />
+
+
+
+)
+
+export default PopupPositionExample
diff --git a/docs/app/Examples/modules/Popup/Variations/PopupSizeExample.js b/docs/app/Examples/modules/Popup/Variations/PopupSizeExample.js
new file mode 100644
index 0000000000..49fea45097
--- /dev/null
+++ b/docs/app/Examples/modules/Popup/Variations/PopupSizeExample.js
@@ -0,0 +1,34 @@
+import React from 'react'
+import { Icon, Popup } from 'semantic-ui-react'
+
+const PopupSizeExample = () => (
+
+ }
+ content='Hello. This is a mini popup'
+ size='mini'
+ />
+ }
+ content='Hello. This is a tiny popup'
+ size='tiny'
+ />
+ }
+ content='Hello. This is a small popup'
+ size='small'
+ />
+ }
+ content='Hello. This is a large popup'
+ size='large'
+ />
+ }
+ content='Hello. This is a huge popup'
+ size='huge'
+ />
+
+ } >
+ Hello. This is a regular pop-up which does not allow for lots
+ of content. You cannot fit a lot of words here as the
+ paragraphs will be pretty narrow.
+
+ }
+ wide
+ >
+ Hello. This is a wide pop-up which allows for lots of content
+ with additional space. You can fit a lot of words here and the
+ paragraphs will be pretty wide.
+
+ }
+ wide='very'
+ >
+ Hello. This is a very wide pop-up which allows for lots of
+ content with additional space. You can fit a lot of words
+ here and the paragraphs will be pretty wide.
+
+
+)
+
+export default PopupWideExample
diff --git a/docs/app/Examples/modules/Popup/Variations/index.js b/docs/app/Examples/modules/Popup/Variations/index.js
new file mode 100644
index 0000000000..309e8eda8d
--- /dev/null
+++ b/docs/app/Examples/modules/Popup/Variations/index.js
@@ -0,0 +1,55 @@
+import React from 'react'
+import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
+import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
+
+const PopupVariationsExamples = () => (
+
+
+
+
+
+
+
+
+
+
+
+)
+
+export default PopupVariationsExamples
diff --git a/docs/app/Examples/modules/Popup/index.js b/docs/app/Examples/modules/Popup/index.js
new file mode 100644
index 0000000000..8c80526132
--- /dev/null
+++ b/docs/app/Examples/modules/Popup/index.js
@@ -0,0 +1,15 @@
+import React from 'react'
+
+import Types from './Types'
+import Variations from './Variations'
+import Triggers from './Triggers'
+
+const PopupExamples = () => (
+
+
+
+
+
+)
+
+export default PopupExamples
diff --git a/src/addons/Portal/Portal.js b/src/addons/Portal/Portal.js
index c7299d4c77..869f2e4e55 100644
--- a/src/addons/Portal/Portal.js
+++ b/src/addons/Portal/Portal.js
@@ -205,14 +205,14 @@ class Portal extends Component {
e.stopPropagation()
this.close(e)
} else if (!open && openOnTriggerClick) {
- // Prevents closeOnDocumentClick from closing the portal when
- // openOnTriggerFocus is set. Focus shifts on mousedown so the portal opens
- // before the click finishes so it may actually wind up on the document.
- e.nativeEvent.stopImmediatePropagation()
-
e.stopPropagation()
this.open(e)
}
+
+ // Prevents closeOnDocumentClick from closing the portal when
+ // openOnTriggerFocus is set. Focus shifts on mousedown so the portal opens
+ // before the click finishes so it may actually wind up on the document.
+ e.nativeEvent.stopImmediatePropagation()
}
handleTriggerFocus = (e) => {
diff --git a/src/index.js b/src/index.js
index 3dc0d54994..bb53b67cbe 100644
--- a/src/index.js
+++ b/src/index.js
@@ -112,6 +112,10 @@ export { default as ModalContent } from './modules/Modal/ModalContent'
export { default as ModalDescription } from './modules/Modal/ModalDescription'
export { default as ModalHeader } from './modules/Modal/ModalHeader'
+export { default as Popup } from './modules/Popup'
+export { default as PopupContent } from './modules/Popup/PopupContent'
+export { default as PopupHeader } from './modules/Popup/PopupHeader'
+
export { default as Progress } from './modules/Progress'
export { default as Rating } from './modules/Rating'
diff --git a/src/modules/Popup/Popup.js b/src/modules/Popup/Popup.js
new file mode 100644
index 0000000000..b22b998737
--- /dev/null
+++ b/src/modules/Popup/Popup.js
@@ -0,0 +1,306 @@
+import React, { Component, PropTypes } from 'react'
+import cx from 'classnames'
+import _ from 'lodash'
+import {
+ getElementType,
+ getUnhandledProps,
+ META,
+ SUI,
+ useKeyOnly,
+ useKeyOrValueAndKey,
+} from '../../lib'
+import Portal from '../../addons/Portal'
+import PopupContent from './PopupContent'
+import PopupHeader from './PopupHeader'
+
+const _meta = {
+ name: 'Popup',
+ type: META.TYPES.MODULE,
+ props: {
+ content: [PropTypes.string, PropTypes.node],
+ on: ['hover', 'click', 'focus'],
+ positioning: [
+ 'top left',
+ 'top right',
+ 'bottom right',
+ 'bottom left',
+ 'right center',
+ 'left center',
+ 'top center',
+ 'bottom center',
+ ],
+ size: _.without(SUI.SIZES, 'medium', 'big', 'massive'),
+ wide: [true, false, 'very'],
+ },
+}
+
+/**
+ * A Popup displays additional information on top of a page.
+ */
+export default class Popup extends Component {
+ static propTypes = {
+ /** Display the popup without the pointing arrow */
+ basic: PropTypes.bool,
+
+ /** You may pass a content as children of the Popup */
+ children: PropTypes.node,
+
+ /** Classes to add to the Popup className. */
+ className: PropTypes.string,
+
+ /** Simple text content for the popover */
+ content: PropTypes.oneOfType(_meta.props.content),
+
+ /** A Flowing popup have no maximum width and continue to flow to fit its content */
+ flowing: PropTypes.bool,
+
+ /** Takes up the entire width of its offset container */
+ // TODO: implement the Popup fluid layout
+ // fluid: PropTypes.bool,
+
+ /** Header displayed above the content in bold */
+ header: PropTypes.string,
+
+ /** Whether the popup should not close on hover */
+ hoverable: PropTypes.bool,
+
+ /** Invert the colors of the popup */
+ inverted: PropTypes.bool,
+
+ /** The node where the popup should mount.. */
+ hideOnScroll: PropTypes.bool,
+
+ /** Horizontal offset in pixels to be applied to the popup */
+ offset: PropTypes.number,
+
+ /** Event triggering the popup */
+ on: PropTypes.oneOf(_meta.props.on),
+
+ /** Positioning for the popover */
+ positioning: PropTypes.oneOf(_meta.props.positioning),
+
+ /** Popup size */
+ size: PropTypes.oneOf(_meta.props.size),
+
+ /** custom popup style */
+ style: PropTypes.object,
+
+ /** Element to be rendered in-place where the popup is defined. */
+ trigger: PropTypes.node,
+
+ /** Popup width */
+ wide: PropTypes.oneOf(_meta.props.wide),
+ }
+
+ static defaultProps = {
+ positioning: 'top left',
+ on: 'hover',
+ }
+
+ static _meta = _meta
+ static Content = PopupContent
+ static Header = PopupHeader
+
+ state = {}
+
+ computePopupStyle(positions) {
+ const style = { position: 'absolute' }
+ const { offset } = this.props
+ const { pageYOffset, pageXOffset } = window
+ const { clientWidth, clientHeight } = document.documentElement
+
+ if (_.includes(positions, 'right')) {
+ style.right = Math.round(clientWidth - (this.coords.right + pageXOffset))
+ style.left = 'auto'
+ } else if (_.includes(positions, 'left')) {
+ style.left = Math.round(this.coords.left + pageXOffset)
+ style.right = 'auto'
+ } else { // if not left nor right, we are horizontally centering the element
+ const xOffset = (this.coords.width - this.popupCoords.width) / 2
+ style.left = Math.round(this.coords.left + xOffset + pageXOffset)
+ style.right = 'auto'
+ }
+
+ if (_.includes(positions, 'top')) {
+ style.bottom = Math.round(clientHeight - (this.coords.top + pageYOffset))
+ style.top = 'auto'
+ } else if (_.includes(positions, 'bottom')) {
+ style.top = Math.round(this.coords.bottom + pageYOffset)
+ style.bottom = 'auto'
+ } else { // if not top nor bottom, we are vertically centering the element
+ const yOffset = (this.coords.height + this.popupCoords.height) / 2
+ style.top = Math.round(this.coords.bottom + pageYOffset - yOffset)
+ style.bottom = 'auto'
+
+ const xOffset = this.popupCoords.width + 8
+ if (_.includes(positions, 'right')) {
+ style.right -= xOffset
+ } else {
+ style.left -= xOffset
+ }
+ }
+
+ if (offset) {
+ if (_.isNumber(style.right)) {
+ style.right -= offset
+ } else {
+ style.left -= offset
+ }
+ }
+
+ return style
+ }
+
+ // check if the style would display
+ // the popup outside of the view port
+ isStyleInViewport(style) {
+ const { pageYOffset, pageXOffset } = window
+ const { clientWidth, clientHeight } = document.documentElement
+
+ const element = {
+ top: style.top,
+ left: style.left,
+ width: this.popupCoords.width,
+ height: this.popupCoords.height,
+ }
+ if (_.isNumber(style.right)) {
+ element.left = clientWidth - style.right - element.width
+ }
+ if (_.isNumber(style.bottom)) {
+ element.top = clientHeight - style.bottom - element.height
+ }
+
+ // hidden on top
+ if (element.top < pageYOffset) return false
+ // hidden on the bottom
+ if (element.top + element.height > pageYOffset + clientHeight) return false
+ // hidden the left
+ if (element.left < pageXOffset) return false
+ // hidden on the right
+ if (element.left + element.width > pageXOffset + clientWidth) return false
+
+ return true
+ }
+
+ setPopupStyle() {
+ if (!this.coords || !this.popupCoords) return
+ let positioning = this.props.positioning
+ let style = this.computePopupStyle(positioning)
+
+ // Lets detect if the popup is out of the viewport and adjust
+ // the position accordingly
+ const positions = _.without(_meta.props.positioning, positioning)
+ for (let i = 0; !this.isStyleInViewport(style) && i < positions.length; i++) {
+ style = this.computePopupStyle(positions[i])
+ positioning = positions[i]
+ }
+
+ // Append 'px' to every numerical values in the style
+ style = _.mapValues(style, value => _.isNumber(value) ? value + 'px' : value)
+ this.setState({ style, positioning })
+ }
+
+ getPortalProps() {
+ const portalProps = { onOpen: this.onOpen }
+ const { on, hoverable } = this.props
+
+ switch (on) {
+ case 'click':
+ portalProps.openOnTriggerClick = true
+ portalProps.closeOnTriggerClick = true
+ portalProps.closeOnDocumentClick = true
+ break
+
+ case 'focus':
+ portalProps.openOnTriggerFocus = true
+ portalProps.closeOnTriggerBlur = true
+ break
+
+ default: // default to hover
+ portalProps.openOnTriggerMouseOver = true
+ portalProps.closeOnTriggerMouseLeave = true
+ // Taken from SUI: https://git.io/vPmCm
+ portalProps.mouseLeaveDelay = 70
+ portalProps.mouseOverDelay = 50
+ break
+ }
+
+ if (hoverable) {
+ portalProps.closeOnPortalMouseLeave = true
+ portalProps.mouseLeaveDelay = 300
+ }
+
+ return portalProps
+ }
+
+ hideOnScroll = (event) => {
+ this.setState({ closed: true })
+ window.removeEventListener('scroll', this.hideOnScroll)
+ setTimeout(() => this.setState({ closed: false }), 50)
+ }
+
+ onOpen = (event) => {
+ this.coords = event.currentTarget.getBoundingClientRect()
+ if (this.props.hideOnScroll) {
+ window.addEventListener('scroll', this.hideOnScroll)
+ }
+ }
+
+ popupMounted = (ref) => {
+ this.popupCoords = ref ? ref.getBoundingClientRect() : null
+ this.setPopupStyle()
+ }
+
+ render() {
+ const {
+ basic,
+ children,
+ className,
+ content,
+ flowing,
+ header,
+ inverted,
+ size,
+ trigger,
+ wide,
+ } = this.props
+
+ const { positioning, closed } = this.state
+ const style = _.assign({}, this.state.style, this.props.style)
+ const classes = cx(
+ 'ui',
+ positioning,
+ size,
+ useKeyOrValueAndKey(wide, 'wide'),
+ useKeyOnly(basic, 'basic'),
+ useKeyOnly(flowing, 'flowing'),
+ useKeyOnly(inverted, 'inverted'),
+ 'popup transition visible',
+ className,
+ )
+
+ if (closed) return trigger
+
+ const rest = getUnhandledProps(Popup, this.props)
+ const ElementType = getElementType(Popup, this.props)
+ const portalProps = _.pick(rest, _.keys(Portal.propTypes))
+
+ const popupJSX = (
+
+ {children}
+ {!children && PopupHeader.create(header)}
+ {!children && PopupContent.create(content)}
+
+ )
+
+ return (
+
+ {popupJSX}
+
+ )
+ }
+}
diff --git a/src/modules/Popup/PopupContent.js b/src/modules/Popup/PopupContent.js
new file mode 100644
index 0000000000..940cf7bfcb
--- /dev/null
+++ b/src/modules/Popup/PopupContent.js
@@ -0,0 +1,36 @@
+import React, { PropTypes } from 'react'
+import cx from 'classnames'
+import {
+ createShorthandFactory,
+ getElementType,
+ getUnhandledProps,
+ META,
+} from '../../lib'
+
+/**
+ * A PopupContent displays the content body of a Popover.
+ */
+export default function PopupContent(props) {
+ const { children, className } = props
+ const classes = cx('content', className)
+ const rest = getUnhandledProps(PopupContent, props)
+ const ElementType = getElementType(PopupContent, props)
+
+ return {children}
+}
+
+PopupContent.create = createShorthandFactory(PopupContent, value => ({ children: value }))
+
+PopupContent.propTypes = {
+ /** The content of the Popup */
+ children: PropTypes.node,
+
+ /** Classes to add to the Popup content className. */
+ className: PropTypes.string,
+}
+
+PopupContent._meta = {
+ name: 'PopupContent',
+ type: META.TYPES.MODULE,
+ parent: 'Popup',
+}
diff --git a/src/modules/Popup/PopupHeader.js b/src/modules/Popup/PopupHeader.js
new file mode 100644
index 0000000000..2a5662c477
--- /dev/null
+++ b/src/modules/Popup/PopupHeader.js
@@ -0,0 +1,36 @@
+import React, { PropTypes } from 'react'
+import cx from 'classnames'
+import {
+ createShorthandFactory,
+ getElementType,
+ getUnhandledProps,
+ META,
+} from '../../lib'
+
+/**
+ * A PopupHeader displays a header in a Popover.
+ */
+export default function PopupHeader(props) {
+ const { children, className } = props
+ const classes = cx('header', className)
+ const rest = getUnhandledProps(PopupHeader, props)
+ const ElementType = getElementType(PopupHeader, props)
+
+ return {children}
+}
+
+PopupHeader.create = createShorthandFactory(PopupHeader, value => ({ children: value }))
+
+PopupHeader.propTypes = {
+ /** The header of the Popup */
+ children: PropTypes.node,
+
+ /** Classes to add to the Popup header className. */
+ className: PropTypes.string,
+}
+
+PopupHeader._meta = {
+ name: 'PopupHeader',
+ type: META.TYPES.MODULE,
+ parent: 'Popup',
+}
diff --git a/src/modules/Popup/index.js b/src/modules/Popup/index.js
new file mode 100644
index 0000000000..28969afc9a
--- /dev/null
+++ b/src/modules/Popup/index.js
@@ -0,0 +1 @@
+export default from './Popup'
diff --git a/test/specs/modules/Popup/Popup-test.js b/test/specs/modules/Popup/Popup-test.js
new file mode 100644
index 0000000000..8ad033670d
--- /dev/null
+++ b/test/specs/modules/Popup/Popup-test.js
@@ -0,0 +1,327 @@
+import _ from 'lodash'
+import React from 'react'
+
+import Popup from 'src/modules/Popup/Popup'
+import PopupHeader from 'src/modules/Popup/PopupHeader'
+import PopupContent from 'src/modules/Popup/PopupContent'
+import Portal from 'src/addons/Portal/Portal'
+
+import { keyboardKey } from 'src/lib'
+import { domEvent, sandbox } from 'test/utils'
+import * as common from 'test/specs/commonTests'
+
+// ----------------------------------------
+// Wrapper
+// ----------------------------------------
+let wrapper
+
+// we need to unmount the Popup after every test to remove it from the document
+// wrap the render methods to update a global wrapper that is unmounted after each test
+const wrapperMount = (...args) => (wrapper = mount(...args))
+const wrapperShallow = (...args) => (wrapper = shallow(...args))
+
+const assertIn = (node, selector, isPresent = true) => {
+ const didFind = node.querySelector(selector) !== null
+ didFind.should.equal(isPresent, `${didFind ? 'Found' : 'Did not find'} "${selector}" in the ${node}.`)
+}
+const assertInBody = (...args) => assertIn(document.body, ...args)
+
+const nativeEvent = { nativeEvent: { stopImmediatePropagation: _.noop } }
+
+describe('Popup', () => {
+ beforeEach(() => {
+ wrapper = undefined
+ document.body.innerHTML = ''
+ })
+
+ afterEach(() => {
+ if (wrapper && wrapper.unmount) wrapper.unmount()
+ })
+
+ common.hasSubComponents(Popup, [PopupHeader, PopupContent])
+
+ // Heads up!
+ //
+ // Our commonTests do not currently handle wrapped components.
+ // Nor do they handle components rendered to the body with Portal.
+ // The Popup is wrapped in a Portal, so we manually test a few things here.
+
+ it('renders a Portal', () => {
+ wrapperShallow()
+ .type()
+ .should.equal(Portal)
+ })
+
+ it('renders to the document body', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible')
+ })
+
+ it('renders child text', () => {
+ wrapperMount(child text)
+
+ document.querySelector('.ui.popup.visible')
+ .innerText
+ .should.equal('child text')
+ })
+
+ it('renders child components', () => {
+ const child =
+ wrapperMount({child})
+
+ document
+ .querySelector('.ui.popup.visible')
+ .querySelector('[data-child]')
+ .should.not.equal(null, 'Popup did not render the child component.')
+ })
+
+ it('should add className to the Popup wrapping node', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible.some-class')
+ })
+
+ describe('offest', () => {
+ it('accepts an offest to the left', () => {
+ wrapperMount(
+ foo}
+ />
+ )
+
+ wrapper.find('button').simulate('click', nativeEvent)
+ assertInBody('.ui.popup.visible')
+ })
+ it('accepts an offest to the right', () => {
+ wrapperMount(
+ foo}
+ />
+ )
+
+ wrapper.find('button').simulate('click', nativeEvent)
+ assertInBody('.ui.popup.visible')
+ })
+ })
+
+ describe('positioning', () => {
+ it('is always within the viewport', () => {
+ _.each(Popup._meta.props.positions, position => {
+ wrapperMount(
+ foo}
+ on='click'
+ />
+ )
+ wrapper.find('button').simulate('click', nativeEvent)
+ const {
+ top,
+ right,
+ bottom,
+ left,
+ } = document.querySelector('.popup.ui').getBoundingClientRect()
+
+ expect(top).to.be.at.least(0)
+ expect(left).to.be.at.least(0)
+ expect(bottom).to.be.at.most(document.documentElement.clientHeight)
+ expect(right).to.be.at.most(document.documentElement.clientWidth)
+ })
+ })
+ })
+
+ describe('hoverable', () => {
+ it('can be set to stay visible while hovering the popup', () => {
+ shallow()
+ .find('Portal')
+ .should.have.prop('closeOnPortalMouseLeave', true)
+ })
+ })
+
+ describe('hide on scroll', () => {
+ it('hides on window scroll', () => {
+ const trigger =
+ wrapperMount()
+
+ wrapper.find('button').simulate('click', nativeEvent)
+ assertInBody('.ui.popup.visible')
+
+ document.body.scrollTop = 100
+
+ const evt = document.createEvent('CustomEvent')
+ evt.initCustomEvent('scroll', false, false, null)
+
+ window.dispatchEvent(evt)
+
+ assertInBody('.ui.popup.visible', false)
+ })
+ })
+
+ describe('trigger', () => {
+ it('it appears on click', () => {
+ const trigger =
+ wrapperMount()
+
+ wrapper.find('button').simulate('click', nativeEvent)
+ assertInBody('.ui.popup.visible')
+ })
+
+ it('it appears on hover', (done) => {
+ const trigger =
+ wrapperMount()
+
+ wrapper.find('button').simulate('mouseover', nativeEvent)
+ setTimeout(() => {
+ assertInBody('.ui.popup.visible')
+ done()
+ }, 51)
+ })
+
+ it('it appears on focus', () => {
+ const trigger =
+ wrapperMount()
+
+ wrapper.find('input').simulate('focus', nativeEvent)
+ assertInBody('.ui.popup.visible')
+ })
+ })
+
+ describe('open', () => {
+ it('is not open by default', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible', false)
+ })
+
+ it('is passed to Portal open', () => {
+ shallow()
+ .find('Portal')
+ .should.have.prop('open', true)
+
+ shallow()
+ .find('Portal')
+ .should.have.prop('open', false)
+ })
+
+ it('does not show the popup when false', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible', false)
+ })
+
+ it('shows the popup on changing from false to true', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible', false)
+
+ wrapper.setProps({ open: true })
+
+ assertInBody('.ui.popup.visible')
+ })
+
+ it('hides the popup on changing from true to false', () => {
+ wrapperMount()
+ assertInBody('.ui.popup.visible')
+
+ wrapper.setProps({ open: false })
+
+ assertInBody('.ui.popup.visible', false)
+ })
+ })
+
+ describe('basic', () => {
+ it('adds basic to the popup className', () => {
+ wrapperMount()
+ assertInBody('.ui.basic.popup.visible')
+ })
+ })
+
+ describe('flowing', () => {
+ it('adds flowing to the popup className', () => {
+ wrapperMount()
+ assertInBody('.ui.flowing.popup.visible')
+ })
+ })
+
+ describe('inverted', () => {
+ it('adds inverted to the popup className', () => {
+ wrapperMount()
+ assertInBody('.ui.inverted.popup.visible')
+ })
+ })
+
+ describe('wide', () => {
+ it('adds wide to the popup className', () => {
+ wrapperMount()
+ assertInBody('.ui.wide.popup.visible')
+ })
+ })
+
+ describe('very wide', () => {
+ it('adds very wide to the popup className', () => {
+ wrapperMount()
+ assertInBody('.ui.very.wide.popup.visible')
+ })
+ })
+
+ describe('size', () => {
+ it('defines prop options in _meta', () => {
+ Popup._meta.props.should.have.any.keys('size')
+ Popup._meta.props.size.should.be.an('array')
+ })
+
+ it('adds the size to the popup className', () => {
+ Popup._meta.props.size.forEach(size => {
+ wrapperMount()
+ assertInBody(`.ui.${size}.popup`)
+ })
+ })
+ })
+
+ describe('onClose', () => {
+ let spy
+
+ beforeEach(() => {
+ spy = sandbox.spy()
+ wrapperMount()
+ })
+
+ it('is called on body click', () => {
+ domEvent.click(document.querySelector('.ui.popup').parentNode)
+ spy.should.have.been.calledOnce()
+ })
+
+ it('is not called on click inside of the popup', () => {
+ domEvent.click(document.querySelector('.ui.popup'))
+ spy.should.not.have.been.calledOnce()
+ })
+
+ it('is called on body click', () => {
+ domEvent.click('body')
+ spy.should.have.been.calledOnce()
+ })
+
+ it('is called when pressing escape', () => {
+ domEvent.keyDown(document, { key: 'Escape' })
+ spy.should.have.been.calledOnce()
+ })
+
+ it('is not called when pressing a key other than "Escape"', () => {
+ _.each(keyboardKey, (val, key) => {
+ // skip Escape key
+ if (val === keyboardKey.Escape) return
+
+ domEvent.keyDown(document, { key })
+ spy.should.not.have.been.called(`onClose was called when pressing "${key}"`)
+ })
+ })
+
+ it('is not called when the open prop changes to false', () => {
+ wrapper.setProps({ open: false })
+ spy.should.not.have.been.called()
+ })
+ })
+})
diff --git a/test/specs/modules/Popup/PopupContent-test.js b/test/specs/modules/Popup/PopupContent-test.js
new file mode 100644
index 0000000000..414e3688fe
--- /dev/null
+++ b/test/specs/modules/Popup/PopupContent-test.js
@@ -0,0 +1,7 @@
+import PopupContent from 'src/modules/Popup/PopupContent'
+import * as common from 'test/specs/commonTests'
+
+describe('PopupContent', () => {
+ common.isConformant(PopupContent)
+ common.rendersChildren(PopupContent)
+})
diff --git a/test/specs/modules/Popup/PopupHeader-test.js b/test/specs/modules/Popup/PopupHeader-test.js
new file mode 100644
index 0000000000..d2307011c9
--- /dev/null
+++ b/test/specs/modules/Popup/PopupHeader-test.js
@@ -0,0 +1,7 @@
+import PopupHeader from 'src/modules/Popup/PopupHeader'
+import * as common from 'test/specs/commonTests'
+
+describe('PopupHeader', () => {
+ common.isConformant(PopupHeader)
+ common.rendersChildren(PopupHeader)
+})