Skip to content

Commit

Permalink
feat(MountNode): support refs as value for node prop (#3449)
Browse files Browse the repository at this point in the history
  • Loading branch information
layershifter authored Feb 22, 2019
1 parent aaee3b1 commit 527863b
Show file tree
Hide file tree
Showing 15 changed files with 131 additions and 113 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, { Component } from 'react'
import React, { Component, createRef } from 'react'
import { Form, Grid, MountNode, Segment } from 'semantic-ui-react'

export default class MountNodeExampleMountNode extends Component {
state = { className: '' }
nodeRef = createRef()

handleChange = (e, { value }) => this.setState({ className: value })

handleRef = node => this.setState({ node })

render() {
const { className, node } = this.state
const { className } = this.state

return (
<Grid columns={2}>
Expand All @@ -24,9 +23,9 @@ export default class MountNodeExampleMountNode extends Component {
</Grid.Column>
<Grid.Column>
<Segment>
{node && <MountNode className={className} node={node} />}
<div ref={this.handleRef}>An example node</div>
<div ref={this.nodeRef}>An example node</div>
</Segment>
<MountNode className={className} node={this.nodeRef} />
</Grid.Column>
</Grid>
)
Expand Down
2 changes: 1 addition & 1 deletion src/addons/MountNode/MountNode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface StrictMountNodeProps {
className?: string

/** The DOM node where we will apply class names. Defaults to document.body. */
node?: HTMLElement
node?: HTMLElement | React.Ref<any>
}

declare class MountNode extends React.Component<MountNodeProps, {}> {}
Expand Down
24 changes: 9 additions & 15 deletions src/addons/MountNode/MountNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'
import { Component } from 'react'

import { customPropTypes } from '../../lib'
import getNodeFromProps from './lib/getNodeFromProps'
import getNodeRefFromProps from './lib/getNodeRefFromProps'
import handleClassNamesChange from './lib/handleClassNamesChange'
import NodeRegistry from './lib/NodeRegistry'

Expand All @@ -17,7 +17,7 @@ export default class MountNode extends Component {
className: PropTypes.string,

/** The DOM node where we will apply class names. Defaults to document.body. */
node: customPropTypes.domNode,
node: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.refObject]),
}

shouldComponentUpdate({ className: nextClassName }) {
Expand All @@ -27,27 +27,21 @@ export default class MountNode extends Component {
}

componentDidMount() {
const node = getNodeFromProps(this.props)
const nodeRef = getNodeRefFromProps(this.props)

if (node) {
nodeRegistry.add(node, this)
nodeRegistry.emit(node, handleClassNamesChange)
}
nodeRegistry.add(nodeRef, this)
nodeRegistry.emit(nodeRef, handleClassNamesChange)
}

componentDidUpdate() {
const node = getNodeFromProps(this.props)

if (node) nodeRegistry.emit(node, handleClassNamesChange)
nodeRegistry.emit(getNodeRefFromProps(this.props), handleClassNamesChange)
}

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

if (node) {
nodeRegistry.del(node, this)
nodeRegistry.emit(node, handleClassNamesChange)
}
nodeRegistry.del(nodeRef, this)
nodeRegistry.emit(nodeRef, handleClassNamesChange)
}

render() {
Expand Down
20 changes: 10 additions & 10 deletions src/addons/MountNode/lib/NodeRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@ export default class NodeRegistry {
this.nodes = new Map()
}

add = (node, component) => {
if (this.nodes.has(node)) {
const set = this.nodes.get(node)
add = (nodeRef, component) => {
if (this.nodes.has(nodeRef)) {
const set = this.nodes.get(nodeRef)

set.add(component)
return
}

this.nodes.set(node, new Set([component]))
this.nodes.set(nodeRef, new Set([component]))
}

del = (node, component) => {
if (!this.nodes.has(node)) return
del = (nodeRef, component) => {
if (!this.nodes.has(nodeRef)) return

const set = this.nodes.get(node)
const set = this.nodes.get(nodeRef)

if (set.size === 1) {
this.nodes.delete(node)
this.nodes.delete(nodeRef)
return
}

set.delete(component)
}

emit = (node, callback) => {
callback(node, this.nodes.get(node))
emit = (nodeRef, callback) => {
callback(nodeRef, this.nodes.get(nodeRef))
}
}
19 changes: 0 additions & 19 deletions src/addons/MountNode/lib/getNodeFromProps.js

This file was deleted.

21 changes: 21 additions & 0 deletions src/addons/MountNode/lib/getNodeRefFromProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import _ from 'lodash'
import { isBrowser, isRefObject } from '../../../lib'

const toRef = _.memoize(node => ({ current: node }))

/**
* 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) ? toRef(document.body) : toRef(node)
}
}

export default getNodeRefFromProps
16 changes: 11 additions & 5 deletions src/addons/MountNode/lib/handleClassNamesChange.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ import computeClassNamesDifference from './computeClassNamesDifference'

const prevClassNames = new Map()

const handleClassNamesChange = (node, components) => {
/**
* @param {React.RefObject} nodeRef
* @param {Object[]} components
*/
const handleClassNamesChange = (nodeRef, components) => {
const currentClassNames = computeClassNames(components)
const [forAdd, forRemoval] = computeClassNamesDifference(
prevClassNames.get(node),
prevClassNames.get(nodeRef),
currentClassNames,
)

_.forEach(forAdd, className => node.classList.add(className))
_.forEach(forRemoval, className => node.classList.remove(className))
if (nodeRef.current) {
_.forEach(forAdd, className => nodeRef.current.classList.add(className))
_.forEach(forRemoval, className => nodeRef.current.classList.remove(className))
}

prevClassNames.set(node, currentClassNames)
prevClassNames.set(nodeRef, currentClassNames)
}

export default handleClassNamesChange
7 changes: 6 additions & 1 deletion src/lib/customPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,5 +396,10 @@ export const deprecate = (help, validator) => (props, propName, componentName, .
return error
}

/** A checker that matches the React.RefObject type. */
export const refObject = PropTypes.shape({
current: PropTypes.object,
})

/** A checker that matches the React.Ref type. */
export const ref = PropTypes.oneOfType([PropTypes.func, PropTypes.object])
export const ref = PropTypes.oneOfType([PropTypes.func, refObject])
2 changes: 1 addition & 1 deletion src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ export { numberToWordMap, numberToWord } from './numberToWord'
export normalizeOffset from './normalizeOffset'
export normalizeTransitionDuration from './normalizeTransitionDuration'
export objectDiff from './objectDiff'
export { handleRef, isRef } from './refUtils'
export { handleRef, isRefObject } from './refUtils'
2 changes: 1 addition & 1 deletion src/lib/refUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const handleRef = (ref, node) => {
}
}

export const isRef = ref =>
export const isRefObject = ref =>
// https://github.com/facebook/react/blob/v16.8.2/packages/react-reconciler/src/ReactFiberCommitWork.js#L665
// eslint-disable-next-line
ref !== null && typeof ref === 'object' && ref.hasOwnProperty('current')
12 changes: 6 additions & 6 deletions src/modules/Sticky/Sticky.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
getElementType,
getUnhandledProps,
isBrowser,
isRef,
isRefObject,
} from '../../lib'

/**
Expand All @@ -33,7 +33,7 @@ export default class Sticky extends Component {
className: PropTypes.string,

/** Context which sticky element should stick to. */
context: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.ref]),
context: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.refObject]),

/** Offset in pixels from the top of the screen when fixing element to viewport. */
offset: PropTypes.number,
Expand Down Expand Up @@ -74,7 +74,7 @@ export default class Sticky extends Component {
pushing: PropTypes.bool,

/** Context which sticky should attach onscroll events. */
scrollContext: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.ref]),
scrollContext: PropTypes.oneOfType([customPropTypes.domNode, customPropTypes.refObject]),

/** Custom style for sticky element. */
styleElement: PropTypes.object,
Expand Down Expand Up @@ -142,7 +142,7 @@ export default class Sticky extends Component {

addListeners = (props) => {
const { scrollContext } = props
const scrollContextNode = isRef(scrollContext) ? scrollContext.current : scrollContext
const scrollContextNode = isRefObject(scrollContext) ? scrollContext.current : scrollContext

if (scrollContextNode) {
eventStack.sub('resize', this.handleUpdate, { target: scrollContextNode })
Expand All @@ -152,7 +152,7 @@ export default class Sticky extends Component {

removeListeners = () => {
const { scrollContext } = this.props
const scrollContextNode = isRef(scrollContext) ? scrollContext.current : scrollContext
const scrollContextNode = isRefObject(scrollContext) ? scrollContext.current : scrollContext

if (scrollContextNode) {
eventStack.unsub('resize', this.handleUpdate, { target: scrollContextNode })
Expand Down Expand Up @@ -202,7 +202,7 @@ export default class Sticky extends Component {

assignRects = () => {
const { context } = this.props
const contextNode = isRef(context) ? context.current : context || document.body
const contextNode = isRefObject(context) ? context.current : context || document.body

this.triggerRect = this.triggerRef.current.getBoundingClientRect()
this.contextRect = contextNode.getBoundingClientRect()
Expand Down
29 changes: 0 additions & 29 deletions test/specs/addons/MountNode/lib/getNodeFromProps-test.js

This file was deleted.

39 changes: 39 additions & 0 deletions test/specs/addons/MountNode/lib/getNodeRefFromProps-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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')
})
})
})
Loading

0 comments on commit 527863b

Please sign in to comment.