diff --git a/docs/src/examples/addons/MountNode/index.js b/docs/src/examples/addons/MountNode/index.js index 4b40401268..009db17183 100644 --- a/docs/src/examples/addons/MountNode/index.js +++ b/docs/src/examples/addons/MountNode/index.js @@ -1,8 +1,26 @@ import React from 'react' +import { Icon, Message } from 'semantic-ui-react' + import Types from './Types' const MountNodeExamples = () => (
+ + + + + Deprecation notice +

+ MountNode component is deprecated and will be removed in + the next major release. Please follow our{' '} + + upgrade guide + + . +

+
+
+
) diff --git a/src/addons/MountNode/MountNode.js b/src/addons/MountNode/MountNode.js index 3ee0c9ba9e..def438faea 100644 --- a/src/addons/MountNode/MountNode.js +++ b/src/addons/MountNode/MountNode.js @@ -1,44 +1,23 @@ import PropTypes from 'prop-types' -import { Component } from 'react' +import React from 'react' -import { customPropTypes } from '../../lib' -import getNodeRefFromProps from './lib/getNodeRefFromProps' -import handleClassNamesChange from './lib/handleClassNamesChange' -import NodeRegistry from './lib/NodeRegistry' - -const nodeRegistry = new NodeRegistry() +import { customPropTypes, useClassNamesOnNode } from '../../lib' /** * A component that allows to manage classNames on a DOM node in declarative manner. + * + * @deprecated This component is deprecated and will be removed in next major release. */ -export default class MountNode extends Component { - shouldComponentUpdate({ className: nextClassName }) { - const { className: currentClassName } = this.props - - return nextClassName !== currentClassName - } - - componentDidMount() { - const nodeRef = getNodeRefFromProps(this.props) - - nodeRegistry.add(nodeRef, this) - nodeRegistry.emit(nodeRef, handleClassNamesChange) - } +function MountNode(props) { + useClassNamesOnNode(props.node, props.className) - componentDidUpdate() { - nodeRegistry.emit(getNodeRefFromProps(this.props), handleClassNamesChange) + // A workaround for `react-docgen`: https://github.com/reactjs/react-docgen/issues/336 + if (process.env.NODE_ENV === 'test') { + return
} - componentWillUnmount() { - const nodeRef = getNodeRefFromProps(this.props) - - nodeRegistry.del(nodeRef, this) - nodeRegistry.emit(nodeRef, handleClassNamesChange) - } - - render() { - return null - } + /* istanbul ignore next */ + return null } MountNode.propTypes = { @@ -48,3 +27,5 @@ MountNode.propTypes = { /** The DOM node where we will apply class names. Defaults to document.body. */ node: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.refObject]), } + +export default MountNode diff --git a/src/addons/MountNode/lib/NodeRegistry.js b/src/addons/MountNode/lib/NodeRegistry.js deleted file mode 100644 index 90c5e42d07..0000000000 --- a/src/addons/MountNode/lib/NodeRegistry.js +++ /dev/null @@ -1,33 +0,0 @@ -export default class NodeRegistry { - constructor() { - this.nodes = new Map() - } - - add = (nodeRef, component) => { - if (this.nodes.has(nodeRef)) { - const set = this.nodes.get(nodeRef) - - set.add(component) - return - } - - this.nodes.set(nodeRef, new Set([component])) - } - - del = (nodeRef, component) => { - if (!this.nodes.has(nodeRef)) return - - const set = this.nodes.get(nodeRef) - - if (set.size === 1) { - this.nodes.delete(nodeRef) - return - } - - set.delete(component) - } - - emit = (nodeRef, callback) => { - callback(nodeRef, this.nodes.get(nodeRef)) - } -} diff --git a/src/addons/MountNode/lib/computeClassNames.js b/src/addons/MountNode/lib/computeClassNames.js deleted file mode 100644 index 080a1ee566..0000000000 --- a/src/addons/MountNode/lib/computeClassNames.js +++ /dev/null @@ -1,11 +0,0 @@ -import _ from 'lodash/fp' - -const computeClassNames = _.flow( - _.toArray, - _.map('props.className'), - _.flatMap(_.split(/\s+/)), - _.filter(_.identity), - _.uniq, -) - -export default computeClassNames diff --git a/src/addons/MountNode/lib/computeClassNamesDifference.js b/src/addons/MountNode/lib/computeClassNamesDifference.js deleted file mode 100644 index b2f2905857..0000000000 --- a/src/addons/MountNode/lib/computeClassNamesDifference.js +++ /dev/null @@ -1,8 +0,0 @@ -import _ from 'lodash' - -const computeClassNamesDifference = (prevClassNames, currentClassNames) => [ - _.difference(currentClassNames, prevClassNames), - _.difference(prevClassNames, currentClassNames), -] - -export default computeClassNamesDifference diff --git a/src/addons/MountNode/lib/getNodeRefFromProps.js b/src/addons/MountNode/lib/getNodeRefFromProps.js deleted file mode 100644 index 4c7644bece..0000000000 --- a/src/addons/MountNode/lib/getNodeRefFromProps.js +++ /dev/null @@ -1,21 +0,0 @@ -import { isRefObject, toRefObject } from '@stardust-ui/react-component-ref' -import _ from 'lodash' - -import { isBrowser } from '../../../lib' - -/** - * Given `this.props`, return a `node` value or undefined. - * - * @param {object|React.RefObject} props Component's props - * @return {React.RefObject|undefined} - */ -const getNodeRefFromProps = (props) => { - const { node } = props - - if (isBrowser()) { - if (isRefObject(node)) return node - return _.isNil(node) ? toRefObject(document.body) : toRefObject(node) - } -} - -export default getNodeRefFromProps diff --git a/src/addons/MountNode/lib/handleClassNamesChange.js b/src/addons/MountNode/lib/handleClassNamesChange.js deleted file mode 100644 index 7a8d811271..0000000000 --- a/src/addons/MountNode/lib/handleClassNamesChange.js +++ /dev/null @@ -1,27 +0,0 @@ -import _ from 'lodash' - -import computeClassNames from './computeClassNames' -import computeClassNamesDifference from './computeClassNamesDifference' - -const prevClassNames = new Map() - -/** - * @param {React.RefObject} nodeRef - * @param {Object[]} components - */ -const handleClassNamesChange = (nodeRef, components) => { - const currentClassNames = computeClassNames(components) - const [forAdd, forRemoval] = computeClassNamesDifference( - prevClassNames.get(nodeRef), - currentClassNames, - ) - - if (nodeRef.current) { - _.forEach(forAdd, (className) => nodeRef.current.classList.add(className)) - _.forEach(forRemoval, (className) => nodeRef.current.classList.remove(className)) - } - - prevClassNames.set(nodeRef, currentClassNames) -} - -export default handleClassNamesChange diff --git a/src/lib/hooks/useClassNamesOnNode.js b/src/lib/hooks/useClassNamesOnNode.js new file mode 100644 index 0000000000..6de281e099 --- /dev/null +++ b/src/lib/hooks/useClassNamesOnNode.js @@ -0,0 +1,145 @@ +import React from 'react' +import { isRefObject } from '@stardust-ui/react-component-ref' + +import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect' + +const CLASS_NAME_DELITIMITER = /\s+/ + +/** + * Accepts a set of ref objects that contain classnames as a string and returns an array of unique + * classNames. + * + * @param {Set|undefined} classNameRefs + * @returns String[] + */ +export function computeClassNames(classNameRefs) { + const classNames = [] + + if (classNameRefs) { + classNameRefs.forEach((classNameRef) => { + if (typeof classNameRef.current === 'string') { + const classNamesForRef = classNameRef.current.split(CLASS_NAME_DELITIMITER) + + classNamesForRef.forEach((className) => { + classNames.push(className) + }) + } + }) + + return classNames.filter( + (className, i, array) => className.length > 0 && array.indexOf(className) === i, + ) + } + + return [] +} + +/** + * Computes classnames that should be removed and added to a node based on input differences. + * + * @param {String[]} prevClassNames + * @param {String[]} currentClassNames + */ +export function computeClassNamesDifference(prevClassNames, currentClassNames) { + return [ + currentClassNames.filter((className) => prevClassNames.indexOf(className) === -1), + prevClassNames.filter((className) => currentClassNames.indexOf(className) === -1), + ] +} + +const prevClassNames = new Map() + +/** + * @param {HTMLElement} node + * @param {Set|undefined} classNameRefs + */ +export const handleClassNamesChange = (node, classNameRefs) => { + const currentClassNames = computeClassNames(classNameRefs) + const [forAdd, forRemoval] = computeClassNamesDifference( + prevClassNames.get(node) || [], + currentClassNames, + ) + + if (node) { + forAdd.forEach((className) => node.classList.add(className)) + forRemoval.forEach((className) => node.classList.remove(className)) + } + + prevClassNames.set(node, currentClassNames) +} + +export class NodeRegistry { + constructor() { + this.nodes = new Map() + } + + add = (node, classNameRef) => { + if (this.nodes.has(node)) { + const set = this.nodes.get(node) + + set.add(classNameRef) + return + } + + // IE11 does not support constructor params + const set = new Set() + set.add(classNameRef) + + this.nodes.set(node, set) + } + + del = (node, classNameRef) => { + if (!this.nodes.has(node)) { + return + } + + const set = this.nodes.get(node) + + if (set.size === 1) { + this.nodes.delete(node) + return + } + + set.delete(classNameRef) + } + + emit = (node, callback) => { + callback(node, this.nodes.get(node)) + } +} + +const nodeRegistry = new NodeRegistry() + +/** + * A React hooks that allows to manage classNames on a DOM node in declarative manner. Accepts + * a HTML element or React ref objects with it. + * + * @param {HTMLElement|React.RefObject} node + * @param {String} className + */ +export default function useClassNamesOnNode(node, className) { + const classNameRef = React.useRef() + const isMounted = React.useRef(false) + + useIsomorphicLayoutEffect(() => { + classNameRef.current = className + + if (isMounted.current) { + const element = isRefObject(node) ? node.current : node + nodeRegistry.emit(element, handleClassNamesChange) + } + + isMounted.current = true + }, [className]) + useIsomorphicLayoutEffect(() => { + const element = isRefObject(node) ? node.current : node + + nodeRegistry.add(element, classNameRef) + nodeRegistry.emit(element, handleClassNamesChange) + + return () => { + nodeRegistry.del(element, classNameRef) + nodeRegistry.emit(element, handleClassNamesChange) + } + }, [node]) +} diff --git a/src/lib/hooks/useIsomorphicLayoutEffect.js b/src/lib/hooks/useIsomorphicLayoutEffect.js new file mode 100644 index 0000000000..4b80dac212 --- /dev/null +++ b/src/lib/hooks/useIsomorphicLayoutEffect.js @@ -0,0 +1,9 @@ +import React from 'react' +import isBrowser from '../isBrowser' + +// useLayoutEffect() produces a warning with SSR rendering +// https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a +const useIsomorphicLayoutEffect = + isBrowser() && process.env.NODE_ENV !== 'test' ? React.useLayoutEffect : React.useEffect + +export default useIsomorphicLayoutEffect diff --git a/src/lib/index.js b/src/lib/index.js index 4c0e8bb44e..808896b7e9 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -41,3 +41,9 @@ export objectDiff from './objectDiff' // Heads up! We import/export for this module to safely remove it with "babel-plugin-filter-imports" export { makeDebugger } + +// +// Hooks +// + +export useClassNamesOnNode from './hooks/useClassNamesOnNode' diff --git a/src/modules/Modal/ModalDimmer.js b/src/modules/Modal/ModalDimmer.js index 884bc83115..07da858247 100644 --- a/src/modules/Modal/ModalDimmer.js +++ b/src/modules/Modal/ModalDimmer.js @@ -3,13 +3,13 @@ import cx from 'clsx' import PropTypes from 'prop-types' import React from 'react' -import MountNode from '../../addons/MountNode' import { childrenUtils, createShorthandFactory, customPropTypes, getElementType, getUnhandledProps, + useClassNamesOnNode, useKeyOnly, } from '../../lib' @@ -36,6 +36,7 @@ function ModalDimmer(props) { const rest = getUnhandledProps(ModalDimmer, props) const ElementType = getElementType(ModalDimmer, props) + useClassNamesOnNode(mountNode, bodyClasses) React.useEffect(() => { if (ref.current && ref.current.style) { ref.current.style.setProperty('display', 'flex', 'important') @@ -46,8 +47,6 @@ function ModalDimmer(props) { {childrenUtils.isNil(children) ? content : children} - - ) diff --git a/test/specs/addons/MountNode/MountNode-test.js b/test/specs/addons/MountNode/MountNode-test.js index 833a862dca..41636ee6d1 100644 --- a/test/specs/addons/MountNode/MountNode-test.js +++ b/test/specs/addons/MountNode/MountNode-test.js @@ -35,20 +35,4 @@ describe('MountNode', () => { node.classList.contains('foo').should.be.equal(false) }) }) - - describe('shouldComponentUpdate', () => { - it('will not rerender when nextClassName is same', () => { - const wrapper = shallow() - const shouldUpdate = wrapper.instance().shouldComponentUpdate({ className: 'foo' }) - - shouldUpdate.should.be.equal(false) - }) - - it('will rerender when nextClassName is another', () => { - const wrapper = shallow() - const shouldUpdate = wrapper.instance().shouldComponentUpdate({ className: 'bar' }) - - shouldUpdate.should.be.equal(true) - }) - }) }) diff --git a/test/specs/addons/MountNode/lib/computeClassNameDifference-test.js b/test/specs/addons/MountNode/lib/computeClassNameDifference-test.js deleted file mode 100644 index 31ba002aba..0000000000 --- a/test/specs/addons/MountNode/lib/computeClassNameDifference-test.js +++ /dev/null @@ -1,30 +0,0 @@ -import _ from 'lodash' -import computeClassNamesDifference from 'src/addons/MountNode/lib/computeClassNamesDifference' - -const fixtures = [ - { - prevClasses: [], - currentClasses: [], - forAdd: [], - forRemoval: [], - }, - { - prevClasses: ['foo', 'bar'], - currentClasses: ['bar', 'baz'], - forAdd: ['baz'], - forRemoval: ['foo'], - }, -] - -describe('computeClassNamesDifference', () => { - it('computes className difference', () => { - _.forEach(fixtures, (fixture) => { - const { prevClasses, currentClasses, forAdd, forRemoval } = fixture - - computeClassNamesDifference(prevClasses, currentClasses).should.have.deep.members([ - forAdd, - forRemoval, - ]) - }) - }) -}) diff --git a/test/specs/addons/MountNode/lib/getNodeRefFromProps-test.js b/test/specs/addons/MountNode/lib/getNodeRefFromProps-test.js deleted file mode 100644 index 873aceaa4a..0000000000 --- a/test/specs/addons/MountNode/lib/getNodeRefFromProps-test.js +++ /dev/null @@ -1,39 +0,0 @@ -import getNodeRefFromProps from 'src/addons/MountNode/lib/getNodeRefFromProps' -import isBrowser from 'src/lib/isBrowser' - -describe('getNodeRefFromProps', () => { - describe('browser', () => { - it('returns a ref to node when it defined', () => { - const node = document.createElement('div') - const nodeRef = getNodeRefFromProps({ node }) - - nodeRef.should.have.property('current', node) - }) - - it('returns node when it defined as React.Ref object', () => { - const inputRef = { current: document.createElement('div') } - const outputRef = getNodeRefFromProps({ node: inputRef }) - - outputRef.should.equal(inputRef) - }) - - it('returns document.body by default', () => { - getNodeRefFromProps({}).should.have.property('current', document.body) - }) - }) - - describe('browser', () => { - before(() => { - isBrowser.override = false - }) - - after(() => { - isBrowser.override = null - }) - - it('always returns null', () => { - expect(getNodeRefFromProps({ node: 'foo' })).to.be.a('undefined') - expect(getNodeRefFromProps({})).to.be.a('undefined') - }) - }) -}) diff --git a/test/specs/addons/MountNode/lib/NodeRegistry-test.js b/test/specs/lib/hooks/NodeRegistry-test.js similarity index 97% rename from test/specs/addons/MountNode/lib/NodeRegistry-test.js rename to test/specs/lib/hooks/NodeRegistry-test.js index 466b1bcdf5..877e0efdf8 100644 --- a/test/specs/addons/MountNode/lib/NodeRegistry-test.js +++ b/test/specs/lib/hooks/NodeRegistry-test.js @@ -1,4 +1,4 @@ -import NodeRegistry from 'src/addons/MountNode/lib/NodeRegistry' +import { NodeRegistry } from 'src/lib/hooks/useClassNamesOnNode' import { sandbox } from 'test/utils' describe('NodeRegistry', () => { diff --git a/test/specs/lib/hooks/computeClassNameDifference-test.js b/test/specs/lib/hooks/computeClassNameDifference-test.js new file mode 100644 index 0000000000..f8d82a0c4f --- /dev/null +++ b/test/specs/lib/hooks/computeClassNameDifference-test.js @@ -0,0 +1,33 @@ +import { computeClassNamesDifference } from 'src/lib/hooks/useClassNamesOnNode' + +const fixtures = [ + { + prevClasses: [], + currentClasses: [], + forAdd: [], + forRemoval: [], + }, + { + prevClasses: ['foo', 'bar'], + currentClasses: ['bar', 'baz'], + forAdd: ['baz'], + forRemoval: ['foo'], + }, + { + prevClasses: ['foo', 'bar'], + currentClasses: ['foo', 'bar'], + forAdd: [], + forRemoval: [], + }, +] + +describe('computeClassNamesDifference', () => { + it('computes className difference', () => { + fixtures.forEach((fixture) => { + computeClassNamesDifference( + fixture.prevClasses, + fixture.currentClasses, + ).should.have.deep.members([fixture.forAdd, fixture.forRemoval]) + }) + }) +}) diff --git a/test/specs/addons/MountNode/lib/computeClassNames-test.js b/test/specs/lib/hooks/computeClassNames-test.js similarity index 50% rename from test/specs/addons/MountNode/lib/computeClassNames-test.js rename to test/specs/lib/hooks/computeClassNames-test.js index f422ecfda1..a427548c8f 100644 --- a/test/specs/addons/MountNode/lib/computeClassNames-test.js +++ b/test/specs/lib/hooks/computeClassNames-test.js @@ -1,4 +1,4 @@ -import computeClassNames from 'src/addons/MountNode/lib/computeClassNames' +import { computeClassNames } from 'src/lib/hooks/useClassNamesOnNode' describe('computeClassNames', () => { it('accepts Set as value', () => { @@ -9,41 +9,38 @@ describe('computeClassNames', () => { }) it('combines classNames', () => { - const map = new Set([{ props: { className: 'foo' } }, { props: { className: 'bar' } }]) + const map = new Set([{ current: 'foo' }, { current: 'bar' }]) computeClassNames(map).should.have.members(['foo', 'bar']) }) it('combines only unique classNames', () => { - const map = new Set([ - { props: { className: 'foo' } }, - { props: { className: 'bar' } }, - { props: { className: 'foo bar baz' } }, - ]) + const map = new Set([{ current: 'foo' }, { current: 'bar' }, { current: 'foo bar baz' }]) computeClassNames(map).should.have.members(['foo', 'bar', 'baz']) }) it('omits false, undefined and null classNames', () => { const map = new Set([ - { props: { className: 'foo' } }, - { props: {} }, - { props: { className: false } }, - { props: { className: null } }, - { props: { className: undefined } }, - { props: { className: '0' } }, - { props: { className: 'false' } }, + { current: 'foo' }, + {}, + { current: false }, + { current: null }, + { current: undefined }, + { current: '0' }, + { current: 'false' }, ]) computeClassNames(map).should.have.members(['foo', '0', 'false']) }) it('trims classNames', () => { - const map = new Set([ - { props: { className: ' foo bar ' } }, - { props: { className: ' baz qux' } }, - ]) + const map = new Set([{ current: ' foo bar ' }, { current: ' baz qux' }]) computeClassNames(map).should.have.members(['foo', 'bar', 'baz', 'qux']) }) + + it('skips "undefined" as input', () => { + computeClassNames([]).should.have.length(0) + }) }) diff --git a/test/specs/addons/MountNode/lib/handleClassNamesChange-test.js b/test/specs/lib/hooks/handleClassNamesChange-test.js similarity index 54% rename from test/specs/addons/MountNode/lib/handleClassNamesChange-test.js rename to test/specs/lib/hooks/handleClassNamesChange-test.js index 0e112493ce..9d6c551f1d 100644 --- a/test/specs/addons/MountNode/lib/handleClassNamesChange-test.js +++ b/test/specs/lib/hooks/handleClassNamesChange-test.js @@ -1,23 +1,21 @@ -import handleClassNamesChange from 'src/addons/MountNode/lib/handleClassNamesChange' +import { handleClassNamesChange } from 'src/lib/hooks/useClassNamesOnNode' import { sandbox } from 'test/utils' -const FooComponent = { props: { className: 'foo' } } -const BarComponent = { props: { className: 'bar' } } +const fooRef = { current: 'foo' } +const barRef = { current: 'bar' } const nodes = new Set() -const createNodeRefMock = (add, remove) => { - const nodeRef = { - current: { - classList: { add, remove }, - }, +const createNodeMock = (add, remove) => { + const node = { + classList: { add, remove }, reset: () => { add.resetHistory() remove.resetHistory() }, } - nodes.add(nodeRef) + nodes.add(node) - return nodeRef + return node } describe('handleClassNamesChange', () => { @@ -28,10 +26,11 @@ describe('handleClassNamesChange', () => { it('adds new classes to node', () => { const add = sandbox.spy() const remove = sandbox.spy() - const components = new Set([FooComponent, BarComponent]) - const nodeRef = createNodeRefMock(add, remove) - handleClassNamesChange(nodeRef, components) + const refs = new Set([fooRef, barRef]) + const node = createNodeMock(add, remove) + + handleClassNamesChange(node, refs) add.should.have.been.calledTwice() add.should.have.been.calledWith('foo') add.should.have.been.calledWith('bar') @@ -41,18 +40,20 @@ describe('handleClassNamesChange', () => { it('removes nonexistent classes', () => { const add = sandbox.spy() const remove = sandbox.spy() - const components = new Set([FooComponent, BarComponent]) - const nodeRef = createNodeRefMock(add, remove) - handleClassNamesChange(nodeRef, components) + const refs = new Set([fooRef, barRef]) + const node = createNodeMock(add, remove) + + handleClassNamesChange(node, refs) add.should.have.been.calledTwice() add.should.have.been.calledWith('foo') add.should.have.been.calledWith('bar') remove.should.have.not.been.called() - nodeRef.reset() - components.delete(BarComponent) - handleClassNamesChange(nodeRef, components) + node.reset() + refs.delete(barRef) + + handleClassNamesChange(node, refs) add.should.have.not.been.called() remove.should.have.been.calledOnce() remove.should.have.been.calledWith('bar') @@ -61,20 +62,20 @@ describe('handleClassNamesChange', () => { it('handles different nodes', () => { const fooAdd = sandbox.spy() const fooRemove = sandbox.spy() - const fooComponents = new Set([FooComponent]) - const fooNodeRef = createNodeRefMock(fooAdd, fooRemove) + const fooNode = createNodeMock(fooAdd, fooRemove) + const fooRefs = new Set([fooRef]) const barAdd = sandbox.spy() const barRemove = sandbox.spy() - const barComponents = new Set([BarComponent]) - const barNodeRef = createNodeRefMock(barAdd, barRemove) + const barNode = createNodeMock(barAdd, barRemove) + const barRefs = new Set([barRef]) - handleClassNamesChange(fooNodeRef, fooComponents) + handleClassNamesChange(fooNode, fooRefs) barAdd.should.have.not.been.called() barRemove.should.have.not.been.called() - fooNodeRef.reset() + fooNode.reset() - handleClassNamesChange(barNodeRef, barComponents) + handleClassNamesChange(barNode, barRefs) fooAdd.should.have.not.been.called() fooRemove.should.have.not.been.called() }) diff --git a/test/specs/lib/hooks/useClassNamesOnNode-test.js b/test/specs/lib/hooks/useClassNamesOnNode-test.js new file mode 100644 index 0000000000..d8f67a895b --- /dev/null +++ b/test/specs/lib/hooks/useClassNamesOnNode-test.js @@ -0,0 +1,60 @@ +import React from 'react' +import useClassNamesOnNode from 'src/lib/hooks/useClassNamesOnNode' + +function TestComponent(props) { + useClassNamesOnNode(props.node, props.className) + return null +} + +describe('useClassNamesOnNode', () => { + describe('node', () => { + it('will add className to specified node', () => { + const node = document.createElement('div') + mount() + + node.classList.contains('foo').should.be.equal(true) + }) + + it('will update className on specified node', () => { + const node = document.createElement('div') + const wrapper = mount() + + wrapper.setProps({ className: 'bar' }) + node.classList.contains('foo').should.be.equal(false) + node.classList.contains('bar').should.be.equal(true) + }) + + it('will add multiple classNames', () => { + const node = document.createElement('div') + + mount( + <> + + + , + ) + + node.classList.contains('bar').should.be.equal(true) + node.classList.contains('bar').should.be.equal(true) + node.classList.contains('baz').should.be.equal(true) + }) + + it('will remove className on specified node', () => { + const node = document.createElement('div') + const wrapper = mount() + + node.classList.contains('foo').should.be.equal(true) + + wrapper.unmount() + node.classList.contains('foo').should.be.equal(false) + }) + + it('supports React ref objects', () => { + const nodeRef = React.createRef() + nodeRef.current = document.createElement('div') + + mount() + nodeRef.current.classList.contains('foo').should.be.equal(true) + }) + }) +}) diff --git a/test/specs/modules/Modal/Modal-test.js b/test/specs/modules/Modal/Modal-test.js index 5d85f99220..4cabad83db 100644 --- a/test/specs/modules/Modal/Modal-test.js +++ b/test/specs/modules/Modal/Modal-test.js @@ -13,6 +13,7 @@ import { assertNodeContains, assertBodyClasses, assertBodyContains, + assertWithTimeout, domEvent, sandbox, } from 'test/utils' @@ -538,15 +539,16 @@ describe('Modal', () => { wrapperMount(foo) window.innerHeight = 10 - requestAnimationFrame(() => { - assertBodyClasses('scrolling') - window.innerHeight = 10000 - - requestAnimationFrame(() => { - assertBodyClasses('scrolling', false) - done() - }) - }) + assertWithTimeout( + () => { + assertBodyClasses('scrolling') + window.innerHeight = 10000 + }, + () => + assertWithTimeout(() => { + assertBodyClasses('scrolling', false) + }, done), + ) }) it('adds the scrolling class to the body after re-open', (done) => { @@ -555,18 +557,23 @@ describe('Modal', () => { window.innerHeight = 10 wrapperMount(foo) - requestAnimationFrame(() => { - assertBodyClasses('scrolling') - domEvent.click('.ui.dimmer') - - assertBodyClasses('scrolling', false) - - wrapper.setProps({ open: true }) - requestAnimationFrame(() => { + assertWithTimeout( + () => { assertBodyClasses('scrolling') - done() - }) - }) + domEvent.click('.ui.dimmer') + }, + () => + assertWithTimeout( + () => { + assertBodyClasses('scrolling', false) + wrapper.setProps({ open: true }) + }, + () => + assertWithTimeout(() => { + assertBodyClasses('scrolling') + }, done), + ), + ) }) it('removes the scrolling class from the body on unmount', (done) => { diff --git a/test/specs/modules/Modal/ModalDimmer-test.js b/test/specs/modules/Modal/ModalDimmer-test.js index eb4b157abb..a304970947 100644 --- a/test/specs/modules/Modal/ModalDimmer-test.js +++ b/test/specs/modules/Modal/ModalDimmer-test.js @@ -12,24 +12,27 @@ describe('ModalDimmer', () => { describe('children', () => { it('adds classes to "MountNode"', () => { - const wrapper = shallow() + const element = document.createElement('div') + mount() - wrapper.find('MountNode').should.have.className('dimmable') - wrapper.find('MountNode').should.have.className('dimmed') + element.className.should.contain('dimmable') + element.className.should.contain('dimmed') }) }) describe('blurring', () => { it('adds nothing "MountNode" by default', () => { - shallow() - .find('MountNode') - .should.have.not.className('blurring') + const element = document.createElement('div') + mount() + + element.className.should.not.contain('blurring') }) it('adds a class to "MountNode" when is "true"', () => { - shallow() - .find('MountNode') - .should.have.className('blurring') + const element = document.createElement('div') + mount() + + element.className.should.contain('blurring') }) }) @@ -47,27 +50,19 @@ describe('ModalDimmer', () => { }) }) - 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') + const element = document.createElement('div') + mount() + + element.className.should.not.contain('scrolling') }) it('adds "className" to "MountNode"', () => { - shallow() - .find('MountNode') - .should.have.className('scrolling') + const element = document.createElement('div') + mount() + + element.className.should.contain('scrolling') }) })