diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f977e678c..45f6084936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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](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) diff --git a/docs/src/examples/components/Ref/Types/RefExampleRef.tsx b/docs/src/examples/components/Ref/Types/RefExample.tsx similarity index 95% rename from docs/src/examples/components/Ref/Types/RefExampleRef.tsx rename to docs/src/examples/components/Ref/Types/RefExample.tsx index af52ca3b33..f36982328a 100644 --- a/docs/src/examples/components/Ref/Types/RefExampleRef.tsx +++ b/docs/src/examples/components/Ref/Types/RefExample.tsx @@ -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() @@ -60,4 +60,4 @@ class RefExampleRef extends React.Component { } } -export default RefExampleRef +export default RefExample diff --git a/docs/src/examples/components/Ref/Types/RefForwardingExample.tsx b/docs/src/examples/components/Ref/Types/RefForwardingExample.tsx new file mode 100644 index 0000000000..e0f9bcbd6a --- /dev/null +++ b/docs/src/examples/components/Ref/Types/RefForwardingExample.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { Grid, Ref, Segment } from '@stardust-ui/react' + +const ExampleButton = React.forwardRef((props, ref) => ( +
+
+)) + +class RefForwardingExample extends React.Component { + forwardedRef = React.createRef() + state = { isMounted: false } + + componentDidMount() { + this.setState({ isMounted: true }) + } + + render() { + const { isMounted } = this.state + const buttonNode = this.forwardedRef.current + + return ( + + +

+ A button below uses forwardRef API. +

+ + + A button + +
+ + {isMounted && ( + +
+              {JSON.stringify(
+                {
+                  nodeName: buttonNode.nodeName,
+                  nodeType: buttonNode.nodeType,
+                  textContent: buttonNode.textContent,
+                },
+                null,
+                2,
+              )}
+            
+
+ )} +
+ ) + } +} + +export default RefForwardingExample diff --git a/docs/src/examples/components/Ref/Types/index.tsx b/docs/src/examples/components/Ref/Types/index.tsx index 698ebddebd..8d61a36204 100644 --- a/docs/src/examples/components/Ref/Types/index.tsx +++ b/docs/src/examples/components/Ref/Types/index.tsx @@ -13,7 +13,16 @@ const RefTypesExamples = () => ( both functional and class component children. } - examplePath="components/Ref/Types/RefExampleRef" + examplePath="components/Ref/Types/RefExample" + /> + + Works with forwardRef API. + + } + examplePath="components/Ref/Types/RefForwardingExample" /> ) diff --git a/package.json b/package.json index e4e65ce4fb..1960ea1159 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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", diff --git a/src/components/Ref/Ref.tsx b/src/components/Ref/Ref.tsx index 5d427dfa86..036d093a00 100644 --- a/src/components/Ref/Ref.tsx +++ b/src/components/Ref/Ref.tsx @@ -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 { +export interface RefProps extends ChildrenComponentProps> { /** * Called when a child component will be mounted or updated. * @@ -13,32 +15,18 @@ export interface RefProps extends ChildrenComponentProps { innerRef?: React.Ref } -/** - * 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 { - 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 = 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 {child} +} - 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 diff --git a/src/components/Ref/RefFindNode.tsx b/src/components/Ref/RefFindNode.tsx new file mode 100644 index 0000000000..931f6ea8f0 --- /dev/null +++ b/src/components/Ref/RefFindNode.tsx @@ -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> { + /** + * Called when a child component will be mounted or updated. + * + * @param {HTMLElement} node - Referred node. + */ + innerRef?: React.Ref +} + +export default class RefFindNode extends React.Component { + 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 + } +} diff --git a/src/components/Ref/RefForward.tsx b/src/components/Ref/RefForward.tsx new file mode 100644 index 0000000000..a096b4ff7b --- /dev/null +++ b/src/components/Ref/RefForward.tsx @@ -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 & { ref: React.Ref }> { + /** + * Called when a child component will be mounted or updated. + * + * @param {HTMLElement} node - Referred node. + */ + innerRef?: React.Ref +} + +export default class RefForward extends React.Component { + 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, + }) + } +} diff --git a/test/specs/components/Ref/Ref-test.tsx b/test/specs/components/Ref/Ref-test.tsx index 28705965d5..b77d1debdc 100644 --- a/test/specs/components/Ref/Ref-test.tsx +++ b/test/specs/components/Ref/Ref-test.tsx @@ -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( - - - , - ).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', () => { @@ -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( + + + , + ) - 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( - + , ) - innerRef.mockClear() - wrapper.unmount() - - expect(innerRef).toHaveBeenCalledTimes(1) - expect(innerRef).toHaveBeenCalledWith(null) + expect(wrapper.is(RefForward)).toBe(true) }) }) }) diff --git a/test/specs/components/Ref/RefFindNode-test.tsx b/test/specs/components/Ref/RefFindNode-test.tsx new file mode 100644 index 0000000000..0458b14ce1 --- /dev/null +++ b/test/specs/components/Ref/RefFindNode-test.tsx @@ -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( + + + , + ).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( + + + , + ) + + innerRef.mockClear() + wrapper.unmount() + + expect(innerRef).toHaveBeenCalledTimes(1) + expect(innerRef).toHaveBeenCalledWith(null) + }) + }) +}) diff --git a/test/specs/components/Ref/RefForward-test.tsx b/test/specs/components/Ref/RefForward-test.tsx new file mode 100644 index 0000000000..af217465ff --- /dev/null +++ b/test/specs/components/Ref/RefForward-test.tsx @@ -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() + const innerRef = React.createRef() + + mount( + { as any}, + ) + + expect(forwardedRef.current).toBeInstanceOf(Element) + expect(innerRef.current).toBeInstanceOf(Element) + }) + }) +}) diff --git a/test/specs/components/Ref/fixtures.tsx b/test/specs/components/Ref/fixtures.tsx index 364c65722e..44dbd11c28 100644 --- a/test/specs/components/Ref/fixtures.tsx +++ b/test/specs/components/Ref/fixtures.tsx @@ -16,3 +16,9 @@ export class CompositeClass extends Component { return } } + +export const ForwardedRef = React.forwardRef((props, ref) => ( +
+
+)) diff --git a/yarn.lock b/yarn.lock index d56addd0d3..c5d310be18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -202,6 +202,13 @@ "@types/node" "*" "@types/react" "*" +"@types/react-is@^16.5.0": + version "16.5.0" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-16.5.0.tgz#6b0dd43e60fa7c82b48faf7b487543079a61015a" + integrity sha512-yUYPioB2Sh5d4csgpW/vJwxWM0RG1/QbGiwYap2m/bEAQKRwbagYRc5C7oK2AM9QC2vr2ZViCgpm0DpDpFQ6XA== + dependencies: + "@types/react" "*" + "@types/react-router@^4.0.27": version "4.0.27" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.27.tgz#553f54df7c4b09d6046b0201ce9b91c46b2940e3"