diff --git a/index.d.ts b/index.d.ts index 9d5addd4f0..c0b0aafa71 100644 --- a/index.d.ts +++ b/index.d.ts @@ -504,6 +504,11 @@ export { ModalDescriptionProps, StrictModalDescriptionProps, } from './dist/commonjs/modules/Modal/ModalDescription' +export { + default as ModalDimmer, + ModalDimmerProps, + StrictModalDimmerProps, +} from './dist/commonjs/modules/Modal/ModalDimmer' export { default as ModalHeader, ModalHeaderProps, diff --git a/src/index.js b/src/index.js index 8e3864e0b5..dc8144ac41 100644 --- a/src/index.js +++ b/src/index.js @@ -142,6 +142,7 @@ export Modal from './modules/Modal' export ModalActions from './modules/Modal/ModalActions' export ModalContent from './modules/Modal/ModalContent' export ModalDescription from './modules/Modal/ModalDescription' +export ModalDimmer from './modules/Modal/ModalDimmer' export ModalHeader from './modules/Modal/ModalHeader' export Popup from './modules/Popup' diff --git a/src/modules/Modal/Modal.d.ts b/src/modules/Modal/Modal.d.ts index 5872583089..136b9fe18c 100644 --- a/src/modules/Modal/Modal.d.ts +++ b/src/modules/Modal/Modal.d.ts @@ -5,6 +5,7 @@ import { StrictPortalProps } from '../../addons/Portal' import ModalActions, { ModalActionsProps } from './ModalActions' import ModalContent, { ModalContentProps } from './ModalContent' import ModalDescription from './ModalDescription' +import ModalDimmer, { ModalDimmerProps } from './ModalDimmer' import ModalHeader, { ModalHeaderProps } from './ModalHeader' export interface ModalProps extends StrictModalProps { @@ -21,7 +22,7 @@ export interface StrictModalProps extends StrictPortalProps { /** A Modal can reduce its complexity */ basic?: boolean - /** A modal can be vertically centered in the viewport */ + /** A modal can be vertically centered in the viewport. */ centered?: boolean /** Primary content. */ @@ -46,7 +47,7 @@ export interface StrictModalProps extends StrictPortalProps { defaultOpen?: boolean /** A modal can appear in a dimmer. */ - dimmer?: true | 'blurring' | 'inverted' + dimmer?: true | 'blurring' | 'inverted' | SemanticShorthandItem /** Event pool namespace that is used to handle component events */ eventPool?: string @@ -114,6 +115,7 @@ interface ModalComponent extends React.ComponentClass { Actions: typeof ModalActions Content: typeof ModalContent Description: typeof ModalDescription + Dimmer: typeof ModalDimmer Header: typeof ModalHeader } diff --git a/src/modules/Modal/Modal.js b/src/modules/Modal/Modal.js index 90c746b805..81155d03fa 100644 --- a/src/modules/Modal/Modal.js +++ b/src/modules/Modal/Modal.js @@ -18,12 +18,12 @@ import { useKeyOnly, } from '../../lib' import Icon from '../../elements/Icon' -import MountNode from '../../addons/MountNode' import Portal from '../../addons/Portal' -import ModalHeader from './ModalHeader' -import ModalContent from './ModalContent' import ModalActions from './ModalActions' +import ModalContent from './ModalContent' import ModalDescription from './ModalDescription' +import ModalDimmer from './ModalDimmer' +import ModalHeader from './ModalHeader' import { canFit, getLegacyStyles, isLegacy } from './utils' const debug = makeDebugger('modal') @@ -132,17 +132,8 @@ class Modal extends Component { _.invoke(this.props, 'onUnmount', e, this.props) } - setDimmerNodeStyle = () => { - debug('setDimmerNodeStyle()') - const { current } = this.dimmerRef - - if (current && current.style && current.style.display !== 'flex') { - current.style.setProperty('display', 'flex', 'important') - } - } - setPositionAndClassNames = () => { - const { centered, dimmer } = this.props + const { centered } = this.props let scrolling const newState = {} @@ -164,18 +155,8 @@ class Modal extends Component { } } - const classes = cx( - useKeyOnly(dimmer, 'dimmable dimmed'), - useKeyOnly(dimmer === 'blurring', ' blurring'), - useKeyOnly(scrolling, ' scrolling'), - ) - - if (this.state.mountClasses !== classes) newState.mountClasses = classes if (!_.isEmpty(newState)) this.setState(newState) - this.animationRequestId = requestAnimationFrame(this.setPositionAndClassNames) - - this.setDimmerNodeStyle() } renderContent = (rest) => { @@ -187,11 +168,10 @@ class Modal extends Component { closeIcon, content, header, - mountNode, size, style, } = this.props - const { legacyStyles, mountClasses, scrolling } = this.state + const { legacyStyles, scrolling } = this.state const classes = cx( 'ui', @@ -210,8 +190,6 @@ class Modal extends Component { return ( - - {closeIconJSX} {childrenUtils.isNil(children) ? ( <> @@ -228,8 +206,8 @@ class Modal extends Component { } render() { - const { open } = this.state const { centered, closeOnDocumentClick, dimmer, eventPool, trigger } = this.props + const { open, scrolling } = this.state const mountNode = this.getMountNode() // Short circuit when server side rendering @@ -251,14 +229,6 @@ class Modal extends Component { ) const portalProps = _.pick(unhandled, portalPropNames) - // wrap dimmer modals - const dimmerClasses = cx( - 'ui', - dimmer === 'inverted' && 'inverted', - !centered && 'top aligned', - 'page modals dimmer transition visible active', - ) - // Heads up! // // The SUI CSS selector to prevent the modal itself from blurring requires an immediate .dimmer child: @@ -283,9 +253,21 @@ class Modal extends Component { onOpen={this.handleOpen} onUnmount={this.handlePortalUnmount} > -
- {this.renderContent(rest)} -
+ + {ModalDimmer.create(_.isPlainObject(dimmer) ? dimmer : {}, { + autoGenerateKey: false, + defaultProps: { + blurring: dimmer === 'blurring', + inverted: dimmer === 'inverted', + }, + overrideProps: { + children: this.renderContent(rest), + centered, + mountNode, + scrolling, + }, + })} + ) } @@ -326,7 +308,12 @@ Modal.propTypes = { defaultOpen: PropTypes.bool, /** A Modal can appear in a dimmer. */ - dimmer: PropTypes.oneOf([true, 'inverted', 'blurring']), + dimmer: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.func, + PropTypes.object, + PropTypes.oneOf(['inverted', 'blurring']), + ]), /** Event pool namespace that is used to handle component events */ eventPool: PropTypes.string, @@ -405,9 +392,10 @@ Modal.defaultProps = { Modal.autoControlledProps = ['open'] -Modal.Header = ModalHeader +Modal.Actions = ModalActions Modal.Content = ModalContent Modal.Description = ModalDescription -Modal.Actions = ModalActions +Modal.Dimmer = ModalDimmer +Modal.Header = ModalHeader export default Modal diff --git a/src/modules/Modal/ModalDimmer.d.ts b/src/modules/Modal/ModalDimmer.d.ts new file mode 100644 index 0000000000..94f857b3a1 --- /dev/null +++ b/src/modules/Modal/ModalDimmer.d.ts @@ -0,0 +1,39 @@ +import * as React from 'react' +import { SemanticShorthandContent } from '../../generic' + +export interface ModalDimmerProps extends StrictModalDimmerProps { + [key: string]: any +} + +export interface StrictModalDimmerProps { + /** An element type to render as (string or function). */ + as?: any + + /** A dimmer can be blurred. */ + blurring?: boolean + + /** Primary content. */ + children?: React.ReactNode + + /** Additional classes. */ + className?: string + + /** A dimmer can center its contents in the viewport. */ + centered?: boolean + + /** Shorthand for primary content. */ + content?: SemanticShorthandContent + + /** A dimmer can be inverted. */ + inverted?: boolean + + /** The node where the modal should mount. Defaults to document.body. */ + mountNode?: any + + /** A dimmer can make body scrollable. */ + scrolling?: boolean +} + +declare const ModalDimmer: React.StatelessComponent + +export default ModalDimmer diff --git a/src/modules/Modal/ModalDimmer.js b/src/modules/Modal/ModalDimmer.js new file mode 100644 index 0000000000..884bc83115 --- /dev/null +++ b/src/modules/Modal/ModalDimmer.js @@ -0,0 +1,87 @@ +import { Ref } from '@stardust-ui/react-component-ref' +import cx from 'clsx' +import PropTypes from 'prop-types' +import React from 'react' + +import MountNode from '../../addons/MountNode' +import { + childrenUtils, + createShorthandFactory, + customPropTypes, + getElementType, + getUnhandledProps, + useKeyOnly, +} from '../../lib' + +/** + * A modal has a dimmer. + */ +function ModalDimmer(props) { + const { blurring, children, className, centered, content, inverted, mountNode, scrolling } = props + const ref = React.useRef() + + const classes = cx( + 'ui', + useKeyOnly(inverted, 'inverted'), + useKeyOnly(!centered, 'top aligned'), + 'page modals dimmer transition visible active', + className, + ) + const bodyClasses = cx( + 'dimmable dimmed', + useKeyOnly(blurring, 'blurring'), + useKeyOnly(scrolling, 'scrolling'), + ) + + const rest = getUnhandledProps(ModalDimmer, props) + const ElementType = getElementType(ModalDimmer, props) + + React.useEffect(() => { + if (ref.current && ref.current.style) { + ref.current.style.setProperty('display', 'flex', 'important') + } + }, []) + + return ( + + + {childrenUtils.isNil(children) ? content : children} + + + + + ) +} + +ModalDimmer.propTypes = { + /** An element type to render as (string or function). */ + as: PropTypes.elementType, + + /** A dimmer can be blurred. */ + blurring: PropTypes.bool, + + /** Primary content. */ + children: PropTypes.node, + + /** Additional classes. */ + className: PropTypes.string, + + /** A dimmer can center its contents in the viewport. */ + centered: PropTypes.bool, + + /** Shorthand for primary content. */ + content: customPropTypes.contentShorthand, + + /** A dimmer can be inverted. */ + inverted: PropTypes.bool, + + /** The node where the modal should mount. Defaults to document.body. */ + mountNode: PropTypes.any, + + /** A dimmer can make body scrollable. */ + scrolling: PropTypes.bool, +} + +ModalDimmer.create = createShorthandFactory(ModalDimmer, (content) => ({ content })) + +export default ModalDimmer diff --git a/src/modules/Modal/ModalHeader.js b/src/modules/Modal/ModalHeader.js index 1d23b84895..7b50537c6e 100644 --- a/src/modules/Modal/ModalHeader.js +++ b/src/modules/Modal/ModalHeader.js @@ -15,7 +15,7 @@ import { */ function ModalHeader(props) { const { children, className, content } = props - const classes = cx(className, 'header') + const classes = cx('header', className) const rest = getUnhandledProps(ModalHeader, props) const ElementType = getElementType(ModalHeader, props) diff --git a/test/specs/modules/Modal/Modal-test.js b/test/specs/modules/Modal/Modal-test.js index c3138b3e19..5d85f99220 100644 --- a/test/specs/modules/Modal/Modal-test.js +++ b/test/specs/modules/Modal/Modal-test.js @@ -6,6 +6,7 @@ import ModalHeader from 'src/modules/Modal/ModalHeader' import ModalContent from 'src/modules/Modal/ModalContent' import ModalActions from 'src/modules/Modal/ModalActions' import ModalDescription from 'src/modules/Modal/ModalDescription' +import ModalDimmer from 'src/modules/Modal/ModalDimmer' import Portal from 'src/addons/Portal/Portal' import { @@ -46,7 +47,13 @@ describe('Modal', () => { }) common.isConformant(Modal, { rendersPortal: true }) - common.hasSubcomponents(Modal, [ModalHeader, ModalContent, ModalActions, ModalDescription]) + common.hasSubcomponents(Modal, [ + ModalHeader, + ModalContent, + ModalActions, + ModalDescription, + ModalDimmer, + ]) common.hasValidTypings(Modal) common.implementsShorthandProp(Modal, { @@ -228,32 +235,21 @@ describe('Modal', () => { }) describe('dimmer', () => { - describe('defaults', () => { - it('is set to true by default', () => { - Modal.defaultProps.dimmer.should.equal(true) - }) - - it('is present by default', () => { - wrapperMount() - assertBodyContains('.ui.dimmer') - }) + it('adds a "dimmer" className to the body', () => { + wrapperMount() + assertBodyContains('.ui.page.modals.dimmer.transition.visible.active') }) - describe('true', () => { + describe('can be "true"', () => { it('adds/removes body classes "dimmable dimmed" on mount/unmount', () => { assertBodyClasses('dimmable dimmed', false) wrapperMount() assertBodyClasses('dimmable dimmed') - wrapper.unmount() + wrapper.setProps({ open: false }) assertBodyClasses('dimmable dimmed', false) }) - - it('adds a dimmer to the body', () => { - wrapperMount() - assertBodyContains('.ui.page.modals.dimmer.transition.visible.active') - }) }) describe('blurring', () => { @@ -263,7 +259,7 @@ describe('Modal', () => { wrapperMount() assertBodyClasses('dimmable dimmed blurring') - wrapper.unmount() + wrapper.setProps({ open: false }) assertBodyClasses('dimmable dimmed blurring', false) }) @@ -280,7 +276,7 @@ describe('Modal', () => { wrapperMount() assertBodyClasses('dimmable dimmed') - wrapper.unmount() + wrapper.setProps({ open: false }) assertBodyClasses('dimmable dimmed', false) }) @@ -289,6 +285,16 @@ describe('Modal', () => { assertBodyContains('.ui.inverted.page.modals.dimmer.transition.visible.active') }) }) + + describe('object', () => { + it('passes props to a dimmer element', () => { + wrapperMount() + + wrapper.find('ModalDimmer').should.have.prop('inverted', true) + wrapper.find('.dimmer').should.have.className('bar') + wrapper.find('.dimmer').should.have.prop('id', 'dimmer') + }) + }) }) describe('onOpen', () => { diff --git a/test/specs/modules/Modal/ModalDimmer-test.js b/test/specs/modules/Modal/ModalDimmer-test.js new file mode 100644 index 0000000000..eb4b157abb --- /dev/null +++ b/test/specs/modules/Modal/ModalDimmer-test.js @@ -0,0 +1,83 @@ +import React from 'react' + +import ModalDimmer from 'src/modules/Modal/ModalDimmer' +import * as common from 'test/specs/commonTests' + +describe('ModalDimmer', () => { + common.isConformant(ModalDimmer) + common.hasUIClassName(ModalDimmer) + common.rendersChildren(ModalDimmer) + + common.propKeyOnlyToClassName(ModalDimmer, 'inverted') + + describe('children', () => { + it('adds classes to "MountNode"', () => { + const wrapper = shallow() + + wrapper.find('MountNode').should.have.className('dimmable') + wrapper.find('MountNode').should.have.className('dimmed') + }) + }) + + describe('blurring', () => { + it('adds nothing "MountNode" by default', () => { + shallow() + .find('MountNode') + .should.have.not.className('blurring') + }) + + it('adds a class to "MountNode" when is "true"', () => { + shallow() + .find('MountNode') + .should.have.className('blurring') + }) + }) + + describe('centered', () => { + it('adds "top aligned" to "className" by default', () => { + shallow() + .find('.dimmer') + .should.have.className('top aligned') + }) + + it('adds nothing to "className" when is "true"', () => { + shallow() + .find('.dimmer') + .should.have.not.className('top aligned') + }) + }) + + describe('mountNode', () => { + it('is passed to "MountNode" as "node"', () => { + const mountNode = document.createElement('div') + + shallow() + .find('MountNode') + .should.have.prop('node', mountNode) + }) + }) + + describe('scrolling', () => { + it('adds nothing "MountNode" by default', () => { + shallow() + .find('MountNode') + .should.have.not.className('scrolling') + }) + + it('adds "className" to "MountNode"', () => { + shallow() + .find('MountNode') + .should.have.className('scrolling') + }) + }) + + describe('style', () => { + it('adds "display: flex" with "important"', () => { + const wrapper = mount() + const style = wrapper.getDOMNode().style + + style.getPropertyValue('display').should.equal('flex') + style.getPropertyPriority('display').should.equal('important') + }) + }) +})