Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(MountNode): deprecate component #4027

Merged
merged 7 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/src/examples/addons/MountNode/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import React from 'react'
import { Icon, Message } from 'semantic-ui-react'

import Types from './Types'

const MountNodeExamples = () => (
<div>
<Message icon warning>
<Icon name='warning sign' />

<Message.Content>
<Message.Header>Deprecation notice</Message.Header>
<p>
<code>MountNode</code> component is deprecated and will be removed in
the next major release. Please follow our{' '}
<a href='https://github.com/Semantic-Org/Semantic-UI-React/pull/4027'>
upgrade guide
</a>
.
</p>
</Message.Content>
</Message>

<Types />
</div>
)
Expand Down
45 changes: 13 additions & 32 deletions src/addons/MountNode/MountNode.js
Original file line number Diff line number Diff line change
@@ -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 <div />
}

componentWillUnmount() {
const nodeRef = getNodeRefFromProps(this.props)

nodeRegistry.del(nodeRef, this)
nodeRegistry.emit(nodeRef, handleClassNamesChange)
}

render() {
return null
}
/* istanbul ignore next */
return null
}

MountNode.propTypes = {
Expand All @@ -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
33 changes: 0 additions & 33 deletions src/addons/MountNode/lib/NodeRegistry.js

This file was deleted.

11 changes: 0 additions & 11 deletions src/addons/MountNode/lib/computeClassNames.js

This file was deleted.

8 changes: 0 additions & 8 deletions src/addons/MountNode/lib/computeClassNamesDifference.js

This file was deleted.

21 changes: 0 additions & 21 deletions src/addons/MountNode/lib/getNodeRefFromProps.js

This file was deleted.

27 changes: 0 additions & 27 deletions src/addons/MountNode/lib/handleClassNamesChange.js

This file was deleted.

145 changes: 145 additions & 0 deletions src/lib/hooks/useClassNamesOnNode.js
Original file line number Diff line number Diff line change
@@ -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<React.RefObject>|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<React.RefObject>|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])
}
9 changes: 9 additions & 0 deletions src/lib/hooks/useIsomorphicLayoutEffect.js
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
5 changes: 2 additions & 3 deletions src/modules/Modal/ModalDimmer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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')
Expand All @@ -46,8 +47,6 @@ function ModalDimmer(props) {
<Ref innerRef={ref}>
<ElementType {...rest} className={classes}>
{childrenUtils.isNil(children) ? content : children}

<MountNode className={bodyClasses} node={mountNode} />
</ElementType>
</Ref>
)
Expand Down
Loading