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

Commit

Permalink
feat(Ref): support of forwardRef() API (#491)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
layershifter committed Dec 5, 2018
1 parent b355abd commit 5dc926b
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 69 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Features
- `Ref` components uses `forwardRef` API by default @layershifter ([#491](https://github.com/stardust-ui/react/pull/491))

<!--------------------------------[ v0.14.0 ]------------------------------- -->
## [v0.14.0](https://github.com/stardust-ui/react/tree/v0.14.0) (2018-12-05)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.13.3...v0.14.0)
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 @@ -69,6 +69,7 @@
"lodash": "^4.17.10",
"prop-types": "^15.6.1",
"react-fela": "^7.2.0",
"react-is": "^16.6.3",
"react-popper": "^1.0.2",
"what-input": "^5.1.2"
},
Expand All @@ -86,6 +87,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

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)
})
})
})

0 comments on commit 5dc926b

Please sign in to comment.