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

Commit 5dc926b

Browse files
authored
feat(Ref): support of forwardRef() API (#491)
* feat(Ref): support of `forwardRef()` API * fix styling * update yarn.lock * add entry to changelog * rename examples * fix changelog * clean up test * regenerate lock * fix review comments * fix tests * fix types * add entry to changelog * update changelog
1 parent b355abd commit 5dc926b

File tree

13 files changed

+261
-69
lines changed

13 files changed

+261
-69
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717

1818
## [Unreleased]
1919

20+
### Features
21+
- `Ref` components uses `forwardRef` API by default @layershifter ([#491](https://github.com/stardust-ui/react/pull/491))
22+
2023
<!--------------------------------[ v0.14.0 ]------------------------------- -->
2124
## [v0.14.0](https://github.com/stardust-ui/react/tree/v0.14.0) (2018-12-05)
2225
[Compare changes](https://github.com/stardust-ui/react/compare/v0.13.3...v0.14.0)

docs/src/examples/components/Ref/Types/RefExampleRef.tsx renamed to docs/src/examples/components/Ref/Types/RefExample.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { Button, Grid, Ref, Segment } from '@stardust-ui/react'
33

4-
class RefExampleRef extends React.Component {
4+
class RefExample extends React.Component {
55
state = { isMounted: false }
66

77
createdRef = React.createRef<HTMLButtonElement>()
@@ -60,4 +60,4 @@ class RefExampleRef extends React.Component {
6060
}
6161
}
6262

63-
export default RefExampleRef
63+
export default RefExample
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react'
2+
import { Grid, Ref, Segment } from '@stardust-ui/react'
3+
4+
const ExampleButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
5+
<div>
6+
<button {...props} ref={ref} />
7+
</div>
8+
))
9+
10+
class RefForwardingExample extends React.Component {
11+
forwardedRef = React.createRef<HTMLButtonElement>()
12+
state = { isMounted: false }
13+
14+
componentDidMount() {
15+
this.setState({ isMounted: true })
16+
}
17+
18+
render() {
19+
const { isMounted } = this.state
20+
const buttonNode = this.forwardedRef.current
21+
22+
return (
23+
<Grid columns={2}>
24+
<Segment>
25+
<p>
26+
A button below uses <code>forwardRef</code> API.
27+
</p>
28+
29+
<Ref innerRef={this.forwardedRef}>
30+
<ExampleButton>A button</ExampleButton>
31+
</Ref>
32+
</Segment>
33+
34+
{isMounted && (
35+
<code style={{ margin: 10 }}>
36+
<pre>
37+
{JSON.stringify(
38+
{
39+
nodeName: buttonNode.nodeName,
40+
nodeType: buttonNode.nodeType,
41+
textContent: buttonNode.textContent,
42+
},
43+
null,
44+
2,
45+
)}
46+
</pre>
47+
</code>
48+
)}
49+
</Grid>
50+
)
51+
}
52+
}
53+
54+
export default RefForwardingExample

docs/src/examples/components/Ref/Types/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ const RefTypesExamples = () => (
1313
both functional and class component children.
1414
</span>
1515
}
16-
examplePath="components/Ref/Types/RefExampleRef"
16+
examplePath="components/Ref/Types/RefExample"
17+
/>
18+
<ComponentExample
19+
title="Forward Ref"
20+
description={
21+
<span>
22+
Works with <code>forwardRef</code> API.
23+
</span>
24+
}
25+
examplePath="components/Ref/Types/RefForwardingExample"
1726
/>
1827
</ExampleSection>
1928
)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"lodash": "^4.17.10",
7070
"prop-types": "^15.6.1",
7171
"react-fela": "^7.2.0",
72+
"react-is": "^16.6.3",
7273
"react-popper": "^1.0.2",
7374
"what-input": "^5.1.2"
7475
},
@@ -86,6 +87,7 @@
8687
"@types/react": "^16.3.17",
8788
"@types/react-custom-scrollbars": "^4.0.5",
8889
"@types/react-dom": "^16.0.6",
90+
"@types/react-is": "^16.5.0",
8991
"@types/react-router": "^4.0.27",
9092
"awesome-typescript-loader": "^5.2.1",
9193
"connect-history-api-fallback": "^1.3.0",

src/components/Ref/Ref.tsx

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import * as PropTypes from 'prop-types'
22
import * as React from 'react'
3-
import { findDOMNode } from 'react-dom'
3+
import { isForwardRef } from 'react-is'
44

5-
import { handleRef, ChildrenComponentProps, commonPropTypes } from '../../lib'
5+
import { ChildrenComponentProps } from '../../lib'
6+
import RefFindNode from './RefFindNode'
7+
import RefForward from './RefForward'
68

7-
export interface RefProps extends ChildrenComponentProps<React.ReactChild> {
9+
export interface RefProps extends ChildrenComponentProps<React.ReactElement<any>> {
810
/**
911
* Called when a child component will be mounted or updated.
1012
*
@@ -13,32 +15,18 @@ export interface RefProps extends ChildrenComponentProps<React.ReactChild> {
1315
innerRef?: React.Ref<any>
1416
}
1517

16-
/**
17-
* This component exposes a callback prop that always returns the DOM node of both functional and class component
18-
* children.
19-
*/
20-
export default class Ref extends React.Component<RefProps> {
21-
static propTypes = {
22-
...commonPropTypes.createCommon({
23-
animated: false,
24-
as: false,
25-
className: false,
26-
styled: false,
27-
children: 'element',
28-
content: false,
29-
}),
30-
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
31-
}
18+
const Ref: React.SFC<RefProps> = props => {
19+
const { children, innerRef } = props
3220

33-
componentDidMount() {
34-
handleRef(this.props.innerRef, findDOMNode(this))
35-
}
21+
const child = React.Children.only(children)
22+
const ElementType = isForwardRef(child) ? RefForward : RefFindNode
3623

37-
componentWillUnmount() {
38-
handleRef(this.props.innerRef, null)
39-
}
24+
return <ElementType innerRef={innerRef}>{child}</ElementType>
25+
}
4026

41-
render() {
42-
return this.props.children && React.Children.only(this.props.children)
43-
}
27+
Ref.propTypes = {
28+
children: PropTypes.element.isRequired,
29+
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
4430
}
31+
32+
export default Ref

src/components/Ref/RefFindNode.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as PropTypes from 'prop-types'
2+
import * as React from 'react'
3+
import { findDOMNode } from 'react-dom'
4+
5+
import { ChildrenComponentProps, handleRef } from '../../lib'
6+
7+
export interface RefFindNodeProps extends ChildrenComponentProps<React.ReactElement<any>> {
8+
/**
9+
* Called when a child component will be mounted or updated.
10+
*
11+
* @param {HTMLElement} node - Referred node.
12+
*/
13+
innerRef?: React.Ref<any>
14+
}
15+
16+
export default class RefFindNode extends React.Component<RefFindNodeProps> {
17+
static propTypes = {
18+
children: PropTypes.element.isRequired,
19+
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
20+
}
21+
22+
componentDidMount() {
23+
handleRef(this.props.innerRef, findDOMNode(this))
24+
}
25+
26+
componentWillUnmount() {
27+
handleRef(this.props.innerRef, null)
28+
}
29+
30+
render() {
31+
const { children } = this.props
32+
33+
return children
34+
}
35+
}

src/components/Ref/RefForward.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as PropTypes from 'prop-types'
2+
import * as React from 'react'
3+
4+
import { ChildrenComponentProps, handleRef } from '../../lib'
5+
6+
export interface RefForwardProps
7+
extends ChildrenComponentProps<React.ReactElement<any> & { ref: React.Ref<any> }> {
8+
/**
9+
* Called when a child component will be mounted or updated.
10+
*
11+
* @param {HTMLElement} node - Referred node.
12+
*/
13+
innerRef?: React.Ref<any>
14+
}
15+
16+
export default class RefForward extends React.Component<RefForwardProps> {
17+
static propTypes = {
18+
children: PropTypes.element.isRequired,
19+
innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
20+
}
21+
22+
private handleRefOverride = (node: HTMLElement) => {
23+
const { children, innerRef } = this.props
24+
25+
handleRef(children.ref, node)
26+
handleRef(innerRef, node)
27+
}
28+
29+
render() {
30+
const { children } = this.props
31+
32+
return React.cloneElement(children, {
33+
ref: this.handleRefOverride,
34+
})
35+
}
36+
}
Lines changed: 17 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,10 @@
1-
import { shallow, mount } from 'enzyme'
1+
import { shallow } from 'enzyme'
22
import * as React from 'react'
33

44
import Ref from 'src/components/Ref/Ref'
5-
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'
6-
7-
const testInnerRef = Component => {
8-
const innerRef = jest.fn()
9-
const node = mount(
10-
<Ref innerRef={innerRef}>
11-
<Component />
12-
</Ref>,
13-
).getDOMNode()
14-
15-
expect(innerRef).toHaveBeenCalledTimes(1)
16-
expect(innerRef).toHaveBeenCalledWith(node)
17-
}
5+
import RefFindNode from 'src/components/Ref/RefFindNode'
6+
import RefForward from 'src/components/Ref/RefForward'
7+
import { CompositeClass, ForwardedRef } from './fixtures'
188

199
describe('Ref', () => {
2010
describe('children', () => {
@@ -24,38 +14,27 @@ describe('Ref', () => {
2414

2515
expect(component.contains(child)).toBeTruthy()
2616
})
27-
})
28-
29-
describe('innerRef', () => {
30-
it('returns node from a functional component with DOM node', () => {
31-
testInnerRef(DOMFunction)
32-
})
33-
34-
it('returns node from a functional component', () => {
35-
testInnerRef(CompositeFunction)
36-
})
3717

38-
it('returns node from a class component with DOM node', () => {
39-
testInnerRef(DOMClass)
40-
})
18+
it('renders RefFindNode when a component is passed', () => {
19+
const innerRef = React.createRef()
20+
const wrapper = shallow(
21+
<Ref innerRef={innerRef}>
22+
<CompositeClass />
23+
</Ref>,
24+
)
4125

42-
it('returns node from a class component', () => {
43-
testInnerRef(CompositeClass)
26+
expect(wrapper.is(RefFindNode)).toBe(true)
4427
})
4528

46-
it('returns "null" after unmount', () => {
47-
const innerRef = jest.fn()
48-
const wrapper = mount(
29+
it('renders RefForward when a component wrapper with forwardRef() is passed', () => {
30+
const innerRef = React.createRef()
31+
const wrapper = shallow(
4932
<Ref innerRef={innerRef}>
50-
<CompositeClass />
33+
<ForwardedRef />
5134
</Ref>,
5235
)
5336

54-
innerRef.mockClear()
55-
wrapper.unmount()
56-
57-
expect(innerRef).toHaveBeenCalledTimes(1)
58-
expect(innerRef).toHaveBeenCalledWith(null)
37+
expect(wrapper.is(RefForward)).toBe(true)
5938
})
6039
})
6140
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { mount } from 'enzyme'
2+
import * as React from 'react'
3+
4+
import Ref from 'src/components/Ref/Ref'
5+
import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures'
6+
7+
const testInnerRef = Component => {
8+
const innerRef = jest.fn()
9+
const node = mount(
10+
<Ref innerRef={innerRef}>
11+
<Component />
12+
</Ref>,
13+
).getDOMNode()
14+
15+
expect(innerRef).toHaveBeenCalledTimes(1)
16+
expect(innerRef).toHaveBeenCalledWith(node)
17+
}
18+
19+
describe('Ref', () => {
20+
describe('innerRef', () => {
21+
it('returns node from a functional component with DOM node', () => {
22+
testInnerRef(DOMFunction)
23+
})
24+
25+
it('returns node from a functional component', () => {
26+
testInnerRef(CompositeFunction)
27+
})
28+
29+
it('returns node from a class component with DOM node', () => {
30+
testInnerRef(DOMClass)
31+
})
32+
33+
it('returns node from a class component', () => {
34+
testInnerRef(CompositeClass)
35+
})
36+
37+
it('returns "null" after unmount', () => {
38+
const innerRef = jest.fn()
39+
const wrapper = mount(
40+
<Ref innerRef={innerRef}>
41+
<CompositeClass />
42+
</Ref>,
43+
)
44+
45+
innerRef.mockClear()
46+
wrapper.unmount()
47+
48+
expect(innerRef).toHaveBeenCalledTimes(1)
49+
expect(innerRef).toHaveBeenCalledWith(null)
50+
})
51+
})
52+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { mount } from 'enzyme'
2+
import * as React from 'react'
3+
4+
import RefForward from 'src/components/Ref/RefForward'
5+
import { ForwardedRef } from './fixtures'
6+
7+
describe('RefForward', () => {
8+
describe('innerRef', () => {
9+
it('works with "forwardRef" API', () => {
10+
const forwardedRef = React.createRef<HTMLButtonElement>()
11+
const innerRef = React.createRef()
12+
13+
mount(
14+
<RefForward innerRef={innerRef}>{<ForwardedRef ref={forwardedRef} /> as any}</RefForward>,
15+
)
16+
17+
expect(forwardedRef.current).toBeInstanceOf(Element)
18+
expect(innerRef.current).toBeInstanceOf(Element)
19+
})
20+
})
21+
})

0 commit comments

Comments
 (0)