diff --git a/docs/app/Examples/modules/Popup/Triggers/PopupClickExample.js b/docs/app/Examples/modules/Popup/Triggers/PopupClickExample.js new file mode 100644 index 0000000000..08714fe408 --- /dev/null +++ b/docs/app/Examples/modules/Popup/Triggers/PopupClickExample.js @@ -0,0 +1,13 @@ +import React from 'react' +import { Button, Popup } from 'semantic-ui-react' + +const PopupClickExample = () => ( + } + content={} + flowing + hoverable + > + + +
Basic Plan
+

2 projects, $10 a month

+ +
+ +
Business Plan
+

5 projects, $20 a month

+ +
+ +
Premium Plan
+

8 projects, $25 a month

+ +
+
+
+) + +export default PopupFlowingExample diff --git a/docs/app/Examples/modules/Popup/Variations/PopupHideOnScrollExample.js b/docs/app/Examples/modules/Popup/Variations/PopupHideOnScrollExample.js new file mode 100644 index 0000000000..a16e24e501 --- /dev/null +++ b/docs/app/Examples/modules/Popup/Variations/PopupHideOnScrollExample.js @@ -0,0 +1,20 @@ +import React from 'react' +import { Button, Popup } from 'semantic-ui-react' + +const PopupHideOnScrollExample = () => ( +
+ Click me} + content='Hide the popup on any scroll event' + on='click' + hideOnScroll + /> + Hover me} + content='Hide the popup on any scroll event' + hideOnScroll + /> +
+) + +export default PopupHideOnScrollExample diff --git a/docs/app/Examples/modules/Popup/Variations/PopupInvertedExample.js b/docs/app/Examples/modules/Popup/Variations/PopupInvertedExample.js new file mode 100644 index 0000000000..253efbb79a --- /dev/null +++ b/docs/app/Examples/modules/Popup/Variations/PopupInvertedExample.js @@ -0,0 +1,19 @@ +import React from 'react' +import { Button, Icon, Popup } from 'semantic-ui-react' + +const PopupInvertedExample = () => ( +
+ } + content='Hello. This is an inverted popup' + inverted + /> + } + content='Hello. This is an inverted popup' + inverted + /> +
+) + +export default PopupInvertedExample diff --git a/docs/app/Examples/modules/Popup/Variations/PopupOffsetExample.js b/docs/app/Examples/modules/Popup/Variations/PopupOffsetExample.js new file mode 100644 index 0000000000..2844a33b0e --- /dev/null +++ b/docs/app/Examples/modules/Popup/Variations/PopupOffsetExample.js @@ -0,0 +1,21 @@ +import React from 'react' +import { Icon, Popup } from 'semantic-ui-react' + +const PopupOffsetExample = () => ( +
+ } + 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' + /> +
+) + +export default PopupSizeExample diff --git a/docs/app/Examples/modules/Popup/Variations/PopupStyleExample.js b/docs/app/Examples/modules/Popup/Variations/PopupStyleExample.js new file mode 100644 index 0000000000..7870270315 --- /dev/null +++ b/docs/app/Examples/modules/Popup/Variations/PopupStyleExample.js @@ -0,0 +1,19 @@ +import React from 'react' +import { Button, Popup } from 'semantic-ui-react' + +const style = { + borderRadius: 0, + opacity: 0.7, + padding: '2em', +} + +const PopupStyleExample = () => ( + } + content='Popup with a custom style prop' + style={style} + inverted + /> +) + +export default PopupStyleExample diff --git a/docs/app/Examples/modules/Popup/Variations/PopupWideExample.js b/docs/app/Examples/modules/Popup/Variations/PopupWideExample.js new file mode 100644 index 0000000000..5d638bfe01 --- /dev/null +++ b/docs/app/Examples/modules/Popup/Variations/PopupWideExample.js @@ -0,0 +1,30 @@ +import React from 'react' +import { Icon, Popup } from 'semantic-ui-react' + +const PopupWideExample = () => ( +
+ } > + 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) +})