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

feat(MountNode): support refs as value for node prop #3449

Merged
merged 1 commit into from
Feb 22, 2019
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
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