Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(Ref): support of forwardRef() API #491

Merged
merged 25 commits into from
Dec 5, 2018
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
62929d9
feat(Ref): support of `forwardRef()` API
layershifter Nov 19, 2018
9882478
fix styling
layershifter Nov 19, 2018
46c6913
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Nov 19, 2018
17cbc97
update yarn.lock
layershifter Nov 19, 2018
33c95c1
add entry to changelog
layershifter Nov 19, 2018
57faf87
rename examples
layershifter Nov 21, 2018
bb93b77
Merge branch 'master' of https://github.com/stardust-ui/react into fe…
layershifter Nov 21, 2018
306ca3b
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Nov 27, 2018
97594a7
fix changelog
layershifter Nov 27, 2018
c418eec
clean up test
layershifter Nov 27, 2018
747931c
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Nov 29, 2018
d11dcc4
regenerate lock
layershifter Nov 29, 2018
1501b0d
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Nov 29, 2018
0d75b11
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Dec 3, 2018
6f0039f
fix review comments
layershifter Dec 3, 2018
1c77d7b
Merge branch 'master' of https://github.com/stardust-ui/react into fe…
layershifter Dec 3, 2018
31de50a
fix tests
layershifter Dec 3, 2018
5642eab
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Dec 3, 2018
bce1d43
fix types
layershifter Dec 3, 2018
098ee71
Merge branch 'master' of https://github.com/stardust-ui/react into fe…
layershifter Dec 4, 2018
a2f7fd1
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Dec 5, 2018
8e41c02
add entry to changelog
layershifter Dec 5, 2018
b8a3384
Merge branch 'master' of https://github.com/stardust-ui/react into fe…
layershifter Dec 5, 2018
e1d73e1
update changelog
layershifter Dec 5, 2018
921f35a
Merge branches 'feat/ref-forward' and 'master' of https://github.com/…
layershifter Dec 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Features
- Add `render` callback as an option for shorthand value @kuzhelov ([#519](https://github.com/stardust-ui/react/pull/519))
- `Ref` components uses `forwardRef` API by default @layershifter ([#491](https://github.com/stardust-ui/react/pull/491))

<!--------------------------------[ v0.13.1 ]------------------------------- -->
## [v0.13.1](https://github.com/stardust-ui/react/tree/v0.13.1) (2018-12-03)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { Button, Grid, Ref, Segment } from '@stardust-ui/react'

class RefExampleRef extends React.Component {
class RefExample extends React.Component {
state = { isMounted: false }

createdRef = React.createRef<HTMLButtonElement>()
Expand Down Expand Up @@ -60,4 +60,4 @@ class RefExampleRef extends React.Component {
}
}

export default RefExampleRef
export default RefExample
54 changes: 54 additions & 0 deletions docs/src/examples/components/Ref/Types/RefForwardingExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react'
import { Grid, Ref, Segment } from '@stardust-ui/react'

const ExampleButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
<div>
<button {...props} ref={ref} />
</div>
))

class RefForwardingExample extends React.Component {
forwardedRef = React.createRef<HTMLButtonElement>()
state = { isMounted: false }

componentDidMount() {
this.setState({ isMounted: true })
}

render() {
const { isMounted } = this.state
const buttonNode = this.forwardedRef.current

return (
<Grid columns={2}>
<Segment>
<p>
A button below uses <code>forwardRef</code> API.
</p>

<Ref innerRef={this.forwardedRef}>
<ExampleButton>A button</ExampleButton>
</Ref>
</Segment>

{isMounted && (
<code style={{ margin: 10 }}>
<pre>
{JSON.stringify(
{
nodeName: buttonNode.nodeName,
nodeType: buttonNode.nodeType,
textContent: buttonNode.textContent,
},
null,
2,
)}
</pre>
</code>
)}
</Grid>
)
}
}

export default RefForwardingExample
11 changes: 10 additions & 1 deletion docs/src/examples/components/Ref/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ const RefTypesExamples = () => (
both functional and class component children.
</span>
}
examplePath="components/Ref/Types/RefExampleRef"
examplePath="components/Ref/Types/RefExample"
/>
<ComponentExample
title="Forward Ref"
description={
<span>
Works with <code>forwardRef</code> API.
</span>
}
examplePath="components/Ref/Types/RefForwardingExample"
/>
</ExampleSection>
)
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"lodash": "^4.17.10",
"prop-types": "^15.6.1",
"react-fela": "^7.2.0",
"react-is": "^16.6.3",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(!) A new dependency.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"react-popper": "^1.0.2",
"what-input": "^5.1.2"
},
Expand All @@ -84,6 +85,7 @@
"@types/react": "^16.3.17",
"@types/react-custom-scrollbars": "^4.0.5",
"@types/react-dom": "^16.0.6",
"@types/react-is": "^16.5.0",
"@types/react-router": "^4.0.27",
"awesome-typescript-loader": "^5.2.1",
"connect-history-api-fallback": "^1.3.0",
Expand Down
44 changes: 16 additions & 28 deletions src/components/Ref/Ref.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as PropTypes from 'prop-types'
import * as React from 'react'
import { findDOMNode } from 'react-dom'
import { isForwardRef } from 'react-is'

import { handleRef, ChildrenComponentProps, commonPropTypes } from '../../lib'
import { ChildrenComponentProps } from '../../lib'
import RefFindNode from './RefFindNode'
import RefForward from './RefForward'

export interface RefProps extends ChildrenComponentProps<React.ReactChild> {
export interface RefProps extends ChildrenComponentProps<React.ReactElement<any>> {
/**
* Called when a child component will be mounted or updated.
*
Expand All @@ -13,32 +15,18 @@ export interface RefProps extends ChildrenComponentProps<React.ReactChild> {
innerRef?: React.Ref<any>
}

/**
* This component exposes a callback prop that always returns the DOM node of both functional and class component
* children.
*/
export default class Ref extends React.Component<RefProps> {
static propTypes = {
...commonPropTypes.createCommon({
animated: false,
as: false,
className: false,
styled: false,
children: 'element',
content: false,
}),
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}
const Ref: React.SFC<RefProps> = props => {
const { children, innerRef } = props

componentDidMount() {
handleRef(this.props.innerRef, findDOMNode(this))
}
const child = React.Children.only(children)
const ElementType = isForwardRef(child) ? RefForward : RefFindNode

layershifter marked this conversation as resolved.
Show resolved Hide resolved
componentWillUnmount() {
handleRef(this.props.innerRef, null)
}
return <ElementType innerRef={innerRef}>{child}</ElementType>
}

render() {
return this.props.children && React.Children.only(this.props.children)
}
Ref.propTypes = {
children: PropTypes.element.isRequired,
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}

export default Ref
35 changes: 35 additions & 0 deletions src/components/Ref/RefFindNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as PropTypes from 'prop-types'
import * as React from 'react'
import { findDOMNode } from 'react-dom'

import { ChildrenComponentProps, handleRef } from '../../lib'

export interface RefFindNodeProps extends ChildrenComponentProps<React.ReactElement<any>> {
/**
* Called when a child component will be mounted or updated.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef?: React.Ref<any>
}

export default class RefFindNode extends React.Component<RefFindNodeProps> {
static propTypes = {
children: PropTypes.element.isRequired,
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}

componentDidMount() {
handleRef(this.props.innerRef, findDOMNode(this))
}

componentWillUnmount() {
handleRef(this.props.innerRef, null)
}

render() {
const { children } = this.props

return children
}
}
36 changes: 36 additions & 0 deletions src/components/Ref/RefForward.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as PropTypes from 'prop-types'
import * as React from 'react'

import { ChildrenComponentProps, handleRef } from '../../lib'

export interface RefForwardProps
extends ChildrenComponentProps<React.ReactElement<any> & { ref: React.Ref<any> }> {
/**
* Called when a child component will be mounted or updated.
*
* @param {HTMLElement} node - Referred node.
*/
innerRef?: React.Ref<any>
}

export default class RefForward extends React.Component<RefForwardProps> {
static propTypes = {
children: PropTypes.element.isRequired,
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}

private handleRefOverride = (node: HTMLElement) => {
const { children, innerRef } = this.props

handleRef(children.ref, node)
handleRef(innerRef, node)
}

render() {
const { children } = this.props

return React.cloneElement(children, {
ref: this.handleRefOverride,
})
}
}
55 changes: 17 additions & 38 deletions test/specs/components/Ref/Ref-test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import { shallow, mount } from 'enzyme'
import { shallow } from 'enzyme'
import * as React from 'react'

import Ref from 'src/components/Ref/Ref'
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'

const testInnerRef = Component => {
const innerRef = jest.fn()
const node = mount(
<Ref innerRef={innerRef}>
<Component />
</Ref>,
).getDOMNode()

expect(innerRef).toHaveBeenCalledTimes(1)
expect(innerRef).toHaveBeenCalledWith(node)
}
import RefFindNode from 'src/components/Ref/RefFindNode'
import RefForward from 'src/components/Ref/RefForward'
import { CompositeClass, ForwardedRef } from './fixtures'

describe('Ref', () => {
describe('children', () => {
Expand All @@ -24,38 +14,27 @@ describe('Ref', () => {

expect(component.contains(child)).toBeTruthy()
})
})

describe('innerRef', () => {
it('returns node from a functional component with DOM node', () => {
testInnerRef(DOMFunction)
})

it('returns node from a functional component', () => {
testInnerRef(CompositeFunction)
})

it('returns node from a class component with DOM node', () => {
testInnerRef(DOMClass)
})
it('renders RefFindNode when a component is passed', () => {
const innerRef = React.createRef()
const wrapper = shallow(
<Ref innerRef={innerRef}>
<CompositeClass />
</Ref>,
)

it('returns node from a class component', () => {
testInnerRef(CompositeClass)
expect(wrapper.is(RefFindNode)).toBe(true)
})

it('returns "null" after unmount', () => {
const innerRef = jest.fn()
const wrapper = mount(
it('renders RefForward when a component wrapper with forwardRef() is passed', () => {
const innerRef = React.createRef()
const wrapper = shallow(
<Ref innerRef={innerRef}>
<CompositeClass />
<ForwardedRef />
</Ref>,
)

innerRef.mockClear()
wrapper.unmount()

expect(innerRef).toHaveBeenCalledTimes(1)
expect(innerRef).toHaveBeenCalledWith(null)
expect(wrapper.is(RefForward)).toBe(true)
})
})
})
52 changes: 52 additions & 0 deletions test/specs/components/Ref/RefFindNode-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { mount } from 'enzyme'
import * as React from 'react'

import Ref from 'src/components/Ref/Ref'
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'

const testInnerRef = Component => {
const innerRef = jest.fn()
const node = mount(
<Ref innerRef={innerRef}>
<Component />
</Ref>,
).getDOMNode()

expect(innerRef).toHaveBeenCalledTimes(1)
expect(innerRef).toHaveBeenCalledWith(node)
}

describe('Ref', () => {
describe('innerRef', () => {
it('returns node from a functional component with DOM node', () => {
testInnerRef(DOMFunction)
})

it('returns node from a functional component', () => {
testInnerRef(CompositeFunction)
})

it('returns node from a class component with DOM node', () => {
testInnerRef(DOMClass)
})

it('returns node from a class component', () => {
testInnerRef(CompositeClass)
})

it('returns "null" after unmount', () => {
const innerRef = jest.fn()
const wrapper = mount(
<Ref innerRef={innerRef}>
<CompositeClass />
</Ref>,
)

innerRef.mockClear()
wrapper.unmount()

expect(innerRef).toHaveBeenCalledTimes(1)
expect(innerRef).toHaveBeenCalledWith(null)
})
})
})
21 changes: 21 additions & 0 deletions test/specs/components/Ref/RefForward-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mount } from 'enzyme'
import * as React from 'react'

import RefForward from 'src/components/Ref/RefForward'
import { ForwardedRef } from './fixtures'

describe('RefForward', () => {
describe('innerRef', () => {
it('works with "forwardRef" API', () => {
const forwardedRef = React.createRef<HTMLButtonElement>()
const innerRef = React.createRef()

mount(
<RefForward innerRef={innerRef}>{<ForwardedRef ref={forwardedRef} /> as any}</RefForward>,
)

expect(forwardedRef.current).toBeInstanceOf(Element)
expect(innerRef.current).toBeInstanceOf(Element)
})
})
})