diff --git a/index.d.ts b/index.d.ts index f7f4662450..8378a1678b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,7 @@ export { export { default as Confirm, ConfirmProps } from './dist/commonjs/addons/Confirm'; export { default as Portal, PortalProps } from './dist/commonjs/addons/Portal'; export { default as Radio, RadioProps } from './dist/commonjs/addons/Radio'; +export { default as Ref, RefProps } from './dist/commonjs/addons/Ref'; export { default as Select, SelectProps } from './dist/commonjs/addons/Select'; export { default as TextArea, TextAreaProps } from './dist/commonjs/addons/TextArea'; diff --git a/src/addons/Ref/Ref.d.ts b/src/addons/Ref/Ref.d.ts new file mode 100644 index 0000000000..a16fbfb75c --- /dev/null +++ b/src/addons/Ref/Ref.d.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; + +export interface RefProps { + [key: string]: any; + + /** Primary content. */ + children?: React.ReactNode; + + /** + * Called when componentDidMount. + * + * @param {HTMLElement} node - Referred node. + */ + innerRef?: (node: HTMLElement) => void; +} + +declare class Ref extends React.Component { +} + +export default Ref; diff --git a/src/addons/Ref/Ref.js b/src/addons/Ref/Ref.js new file mode 100644 index 0000000000..ee8683f177 --- /dev/null +++ b/src/addons/Ref/Ref.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types' +import { Children, Component } from 'react' +import { findDOMNode } from 'react-dom' + +import { TYPES } from '../../lib/META' + +/** + * This component exposes a callback prop that always returns the DOM node of both functional and class component + * children. + */ +export default class Ref extends Component { + static propTypes = { + /** Primary content. */ + children: PropTypes.element, + + /** + * Called when componentDidMount. + * + * @param {HTMLElement} node - Referred node. + */ + innerRef: PropTypes.func, + } + + static _meta = { + name: 'Ref', + type: TYPES.ADDON, + } + + componentDidMount() { + const { innerRef } = this.props + + // Heads up! Don't move this condition, it's a short circle that avoids run of `findDOMNode` + // if `innerRef` isn't passed + // eslint-disable-next-line react/no-find-dom-node + if (innerRef) innerRef(findDOMNode(this)) + } + + render() { + const { children } = this.props + + return Children.only(children) + } +} diff --git a/src/addons/Ref/index.d.ts b/src/addons/Ref/index.d.ts new file mode 100644 index 0000000000..105f0f9bd3 --- /dev/null +++ b/src/addons/Ref/index.d.ts @@ -0,0 +1 @@ +export { default, RefProps } from './Ref'; diff --git a/src/addons/Ref/index.js b/src/addons/Ref/index.js new file mode 100644 index 0000000000..1aa10ed77d --- /dev/null +++ b/src/addons/Ref/index.js @@ -0,0 +1 @@ +export default from './Ref' diff --git a/src/index.js b/src/index.js index 5575d834eb..51faf9ddc6 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ export { default as Responsive } from './addons/Responsive' export { default as Confirm } from './addons/Confirm' export { default as Portal } from './addons/Portal' export { default as Radio } from './addons/Radio' +export { default as Ref } from './addons/Ref' export { default as Select } from './addons/Select' export { default as TextArea } from './addons/TextArea' diff --git a/test/specs/addons/Ref/Ref-test.js b/test/specs/addons/Ref/Ref-test.js new file mode 100644 index 0000000000..729879825b --- /dev/null +++ b/test/specs/addons/Ref/Ref-test.js @@ -0,0 +1,64 @@ +import faker from 'faker' +import React from 'react' + +import Ref from 'src/addons/Ref/Ref' +import * as common from 'test/specs/commonTests' +import { sandbox } from 'test/utils' +import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures' + +const mountNode = (Component, innerRef) => ( + mount( + + + , + ) + .find('#node') + .getDOMNode() +) + +describe('Ref', () => { + common.hasValidTypings(Ref) + + describe('children', () => { + it('renders single child', () => { + const child =
+ + shallow({child}) + .should.contain(child) + }) + }) + + describe('innerRef', () => { + it('returns node from a functional component with DOM node', () => { + const innerRef = sandbox.spy() + const node = mountNode(DOMFunction, innerRef) + + innerRef.should.have.been.calledOnce() + innerRef.should.have.been.calledWithMatch(node) + }) + + it('returns node from a functional component', () => { + const innerRef = sandbox.spy() + const node = mountNode(CompositeFunction, innerRef) + + innerRef.should.have.been.calledOnce() + innerRef.should.have.been.calledWithMatch(node) + }) + + it('returns node from a class component with DOM node', () => { + const innerRef = sandbox.spy() + const node = mountNode(DOMClass, innerRef) + + innerRef.should.have.been.calledOnce() + innerRef.should.have.been.calledWithMatch(node) + }) + + it('returns node from a class component', () => { + const innerRef = sandbox.spy() + const node = mountNode(CompositeClass, innerRef) + + innerRef.should.have.been.calledOnce() + innerRef.should.have.been.calledWithMatch(node) + }) + }) +}) diff --git a/test/specs/addons/Ref/fixtures.js b/test/specs/addons/Ref/fixtures.js new file mode 100644 index 0000000000..e06251d20f --- /dev/null +++ b/test/specs/addons/Ref/fixtures.js @@ -0,0 +1,19 @@ +/* eslint-disable react/no-multi-comp */ +/* eslint-disable react/prefer-stateless-function */ +import React, { Component } from 'react' + +export const DOMFunction = props =>
+ +export const CompositeFunction = props => + +export class DOMClass extends Component { + render() { + return
+ } +} + +export class CompositeClass extends Component { + render() { + return + } +}