Skip to content

Commit cb9fbb3

Browse files
authored
chore(MountNode): deprecate component (#4027)
* chore(MountNode): deprecate component * fix descriptions * Update src/addons/MountNode/MountNode.js * add deprecation notice * Update docs/src/examples/addons/MountNode/index.js * add description to a hook
1 parent ffaf2c1 commit cb9fbb3

21 files changed

+376
-310
lines changed

docs/src/examples/addons/MountNode/index.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
import React from 'react'
2+
import { Icon, Message } from 'semantic-ui-react'
3+
24
import Types from './Types'
35

46
const MountNodeExamples = () => (
57
<div>
8+
<Message icon warning>
9+
<Icon name='warning sign' />
10+
11+
<Message.Content>
12+
<Message.Header>Deprecation notice</Message.Header>
13+
<p>
14+
<code>MountNode</code> component is deprecated and will be removed in
15+
the next major release. Please follow our{' '}
16+
<a href='https://github.com/Semantic-Org/Semantic-UI-React/pull/4027'>
17+
upgrade guide
18+
</a>
19+
.
20+
</p>
21+
</Message.Content>
22+
</Message>
23+
624
<Types />
725
</div>
826
)

src/addons/MountNode/MountNode.js

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,23 @@
11
import PropTypes from 'prop-types'
2-
import { Component } from 'react'
2+
import React from 'react'
33

4-
import { customPropTypes } from '../../lib'
5-
import getNodeRefFromProps from './lib/getNodeRefFromProps'
6-
import handleClassNamesChange from './lib/handleClassNamesChange'
7-
import NodeRegistry from './lib/NodeRegistry'
8-
9-
const nodeRegistry = new NodeRegistry()
4+
import { customPropTypes, useClassNamesOnNode } from '../../lib'
105

116
/**
127
* A component that allows to manage classNames on a DOM node in declarative manner.
8+
*
9+
* @deprecated This component is deprecated and will be removed in next major release.
1310
*/
14-
export default class MountNode extends Component {
15-
shouldComponentUpdate({ className: nextClassName }) {
16-
const { className: currentClassName } = this.props
17-
18-
return nextClassName !== currentClassName
19-
}
20-
21-
componentDidMount() {
22-
const nodeRef = getNodeRefFromProps(this.props)
23-
24-
nodeRegistry.add(nodeRef, this)
25-
nodeRegistry.emit(nodeRef, handleClassNamesChange)
26-
}
11+
function MountNode(props) {
12+
useClassNamesOnNode(props.node, props.className)
2713

28-
componentDidUpdate() {
29-
nodeRegistry.emit(getNodeRefFromProps(this.props), handleClassNamesChange)
14+
// A workaround for `react-docgen`: https://github.com/reactjs/react-docgen/issues/336
15+
if (process.env.NODE_ENV === 'test') {
16+
return <div />
3017
}
3118

32-
componentWillUnmount() {
33-
const nodeRef = getNodeRefFromProps(this.props)
34-
35-
nodeRegistry.del(nodeRef, this)
36-
nodeRegistry.emit(nodeRef, handleClassNamesChange)
37-
}
38-
39-
render() {
40-
return null
41-
}
19+
/* istanbul ignore next */
20+
return null
4221
}
4322

4423
MountNode.propTypes = {
@@ -48,3 +27,5 @@ MountNode.propTypes = {
4827
/** The DOM node where we will apply class names. Defaults to document.body. */
4928
node: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.refObject]),
5029
}
30+
31+
export default MountNode

src/addons/MountNode/lib/NodeRegistry.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/addons/MountNode/lib/computeClassNames.js

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/addons/MountNode/lib/computeClassNamesDifference.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/addons/MountNode/lib/getNodeRefFromProps.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/addons/MountNode/lib/handleClassNamesChange.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/lib/hooks/useClassNamesOnNode.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React from 'react'
2+
import { isRefObject } from '@stardust-ui/react-component-ref'
3+
4+
import useIsomorphicLayoutEffect from './useIsomorphicLayoutEffect'
5+
6+
const CLASS_NAME_DELITIMITER = /\s+/
7+
8+
/**
9+
* Accepts a set of ref objects that contain classnames as a string and returns an array of unique
10+
* classNames.
11+
*
12+
* @param {Set<React.RefObject>|undefined} classNameRefs
13+
* @returns String[]
14+
*/
15+
export function computeClassNames(classNameRefs) {
16+
const classNames = []
17+
18+
if (classNameRefs) {
19+
classNameRefs.forEach((classNameRef) => {
20+
if (typeof classNameRef.current === 'string') {
21+
const classNamesForRef = classNameRef.current.split(CLASS_NAME_DELITIMITER)
22+
23+
classNamesForRef.forEach((className) => {
24+
classNames.push(className)
25+
})
26+
}
27+
})
28+
29+
return classNames.filter(
30+
(className, i, array) => className.length > 0 && array.indexOf(className) === i,
31+
)
32+
}
33+
34+
return []
35+
}
36+
37+
/**
38+
* Computes classnames that should be removed and added to a node based on input differences.
39+
*
40+
* @param {String[]} prevClassNames
41+
* @param {String[]} currentClassNames
42+
*/
43+
export function computeClassNamesDifference(prevClassNames, currentClassNames) {
44+
return [
45+
currentClassNames.filter((className) => prevClassNames.indexOf(className) === -1),
46+
prevClassNames.filter((className) => currentClassNames.indexOf(className) === -1),
47+
]
48+
}
49+
50+
const prevClassNames = new Map()
51+
52+
/**
53+
* @param {HTMLElement} node
54+
* @param {Set<React.RefObject>|undefined} classNameRefs
55+
*/
56+
export const handleClassNamesChange = (node, classNameRefs) => {
57+
const currentClassNames = computeClassNames(classNameRefs)
58+
const [forAdd, forRemoval] = computeClassNamesDifference(
59+
prevClassNames.get(node) || [],
60+
currentClassNames,
61+
)
62+
63+
if (node) {
64+
forAdd.forEach((className) => node.classList.add(className))
65+
forRemoval.forEach((className) => node.classList.remove(className))
66+
}
67+
68+
prevClassNames.set(node, currentClassNames)
69+
}
70+
71+
export class NodeRegistry {
72+
constructor() {
73+
this.nodes = new Map()
74+
}
75+
76+
add = (node, classNameRef) => {
77+
if (this.nodes.has(node)) {
78+
const set = this.nodes.get(node)
79+
80+
set.add(classNameRef)
81+
return
82+
}
83+
84+
// IE11 does not support constructor params
85+
const set = new Set()
86+
set.add(classNameRef)
87+
88+
this.nodes.set(node, set)
89+
}
90+
91+
del = (node, classNameRef) => {
92+
if (!this.nodes.has(node)) {
93+
return
94+
}
95+
96+
const set = this.nodes.get(node)
97+
98+
if (set.size === 1) {
99+
this.nodes.delete(node)
100+
return
101+
}
102+
103+
set.delete(classNameRef)
104+
}
105+
106+
emit = (node, callback) => {
107+
callback(node, this.nodes.get(node))
108+
}
109+
}
110+
111+
const nodeRegistry = new NodeRegistry()
112+
113+
/**
114+
* A React hooks that allows to manage classNames on a DOM node in declarative manner. Accepts
115+
* a HTML element or React ref objects with it.
116+
*
117+
* @param {HTMLElement|React.RefObject} node
118+
* @param {String} className
119+
*/
120+
export default function useClassNamesOnNode(node, className) {
121+
const classNameRef = React.useRef()
122+
const isMounted = React.useRef(false)
123+
124+
useIsomorphicLayoutEffect(() => {
125+
classNameRef.current = className
126+
127+
if (isMounted.current) {
128+
const element = isRefObject(node) ? node.current : node
129+
nodeRegistry.emit(element, handleClassNamesChange)
130+
}
131+
132+
isMounted.current = true
133+
}, [className])
134+
useIsomorphicLayoutEffect(() => {
135+
const element = isRefObject(node) ? node.current : node
136+
137+
nodeRegistry.add(element, classNameRef)
138+
nodeRegistry.emit(element, handleClassNamesChange)
139+
140+
return () => {
141+
nodeRegistry.del(element, classNameRef)
142+
nodeRegistry.emit(element, handleClassNamesChange)
143+
}
144+
}, [node])
145+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import isBrowser from '../isBrowser'
3+
4+
// useLayoutEffect() produces a warning with SSR rendering
5+
// https://medium.com/@alexandereardon/uselayouteffect-and-ssr-192986cdcf7a
6+
const useIsomorphicLayoutEffect =
7+
isBrowser() && process.env.NODE_ENV !== 'test' ? React.useLayoutEffect : React.useEffect
8+
9+
export default useIsomorphicLayoutEffect

src/lib/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ export objectDiff from './objectDiff'
4141

4242
// Heads up! We import/export for this module to safely remove it with "babel-plugin-filter-imports"
4343
export { makeDebugger }
44+
45+
//
46+
// Hooks
47+
//
48+
49+
export useClassNamesOnNode from './hooks/useClassNamesOnNode'

src/modules/Modal/ModalDimmer.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import cx from 'clsx'
33
import PropTypes from 'prop-types'
44
import React from 'react'
55

6-
import MountNode from '../../addons/MountNode'
76
import {
87
childrenUtils,
98
createShorthandFactory,
109
customPropTypes,
1110
getElementType,
1211
getUnhandledProps,
12+
useClassNamesOnNode,
1313
useKeyOnly,
1414
} from '../../lib'
1515

@@ -36,6 +36,7 @@ function ModalDimmer(props) {
3636
const rest = getUnhandledProps(ModalDimmer, props)
3737
const ElementType = getElementType(ModalDimmer, props)
3838

39+
useClassNamesOnNode(mountNode, bodyClasses)
3940
React.useEffect(() => {
4041
if (ref.current && ref.current.style) {
4142
ref.current.style.setProperty('display', 'flex', 'important')
@@ -46,8 +47,6 @@ function ModalDimmer(props) {
4647
<Ref innerRef={ref}>
4748
<ElementType {...rest} className={classes}>
4849
{childrenUtils.isNil(children) ? content : children}
49-
50-
<MountNode className={bodyClasses} node={mountNode} />
5150
</ElementType>
5251
</Ref>
5352
)

0 commit comments

Comments
 (0)