diff --git a/CHANGELOG.md b/CHANGELOG.md index 946df075f5..c14f4dd052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `iconPosition` property to `Input` component @mnajdova ([#442](https://github.com/stardust-ui/react/pull/442)) - Add `color`, `inverted` and `renderContent` props and `content` slot to `Segment` component @Bugaa92 ([#389](https://github.com/stardust-ui/react/pull/389)) - Add focus trap behavior to `Popup` @kuzhelov ([#457](https://github.com/stardust-ui/react/pull/457)) +- Export `Ref` component and add `handleRef` util @layershifter ([#459](https://github.com/stardust-ui/react/pull/459)) ### Documentation - Add all missing component descriptions and improve those existing @levithomason ([#400](https://github.com/stardust-ui/react/pull/400)) diff --git a/docs/src/examples/components/Ref/Types/RefExampleRef.tsx b/docs/src/examples/components/Ref/Types/RefExampleRef.tsx new file mode 100644 index 0000000000..af52ca3b33 --- /dev/null +++ b/docs/src/examples/components/Ref/Types/RefExampleRef.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { Button, Grid, Ref, Segment } from '@stardust-ui/react' + +class RefExampleRef extends React.Component { + state = { isMounted: false } + + createdRef = React.createRef() + functionalRef = null + + handleRef = node => (this.functionalRef = node) + + componentDidMount() { + this.setState({ isMounted: true }) + } + + render() { + const { isMounted } = this.state + + return ( + + + + + + + + + + + {isMounted && ( + +
+              {JSON.stringify(
+                {
+                  nodeName: this.functionalRef.nodeName,
+                  nodeType: this.functionalRef.nodeType,
+                  textContent: this.functionalRef.textContent,
+                },
+                null,
+                2,
+              )}
+            
+
+              {JSON.stringify(
+                {
+                  nodeName: this.createdRef.current.nodeName,
+                  nodeType: this.createdRef.current.nodeType,
+                  textContent: this.createdRef.current.textContent,
+                },
+                null,
+                2,
+              )}
+            
+
+ )} +
+ ) + } +} + +export default RefExampleRef diff --git a/docs/src/examples/components/Ref/Types/index.tsx b/docs/src/examples/components/Ref/Types/index.tsx new file mode 100644 index 0000000000..698ebddebd --- /dev/null +++ b/docs/src/examples/components/Ref/Types/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' +import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' + +const RefTypesExamples = () => ( + + + A component exposes the innerRef prop that always returns the DOM node of + both functional and class component children. + + } + examplePath="components/Ref/Types/RefExampleRef" + /> + +) + +export default RefTypesExamples diff --git a/docs/src/examples/components/Ref/index.tsx b/docs/src/examples/components/Ref/index.tsx new file mode 100644 index 0000000000..6225f5d6f4 --- /dev/null +++ b/docs/src/examples/components/Ref/index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import Types from './Types' + +const RefExamples: React.SFC = () => ( +
+ +
+) + +export default RefExamples diff --git a/src/components/Ref/Ref.tsx b/src/components/Ref/Ref.tsx index 384b449d8f..fd2dc23a99 100644 --- a/src/components/Ref/Ref.tsx +++ b/src/components/Ref/Ref.tsx @@ -1,19 +1,20 @@ import * as PropTypes from 'prop-types' -import * as _ from 'lodash' -import { Children, Component } from 'react' +import * as React from 'react' import { findDOMNode } from 'react-dom' -import { ReactChildren } from 'utils' + +import { ReactChildren } from '../../../types/utils' +import { handleRef } from '../../lib' export interface RefProps { children?: ReactChildren - innerRef?: (ref: HTMLElement) => void + 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 Component { +export default class Ref extends React.Component { static propTypes = { /** * Used to set content when using childrenApi - internal only @@ -22,18 +23,22 @@ export default class Ref extends Component { children: PropTypes.element, /** - * Called when componentDidMount. + * Called when a child component will be mounted or updated. * * @param {HTMLElement} node - Referred node. */ - innerRef: PropTypes.func, + innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), } componentDidMount() { - _.invoke(this.props, 'innerRef', findDOMNode(this)) + handleRef(this.props.innerRef, findDOMNode(this)) + } + + componentWillUnmount() { + handleRef(this.props.innerRef, null) } render() { - return this.props.children && Children.only(this.props.children) + return this.props.children && React.Children.only(this.props.children) } } diff --git a/src/index.ts b/src/index.ts index 47e0306698..72ad9df203 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ export { RadioGroupItemProps, } from './components/RadioGroup/RadioGroupItem' +export { default as Ref, RefProps } from './components/Ref/Ref' export { default as Segment, SegmentProps } from './components/Segment/Segment' export { default as Status, StatusPropsWithDefaults, StatusProps } from './components/Status/Status' diff --git a/src/lib/handleRef.ts b/src/lib/handleRef.ts new file mode 100644 index 0000000000..6fcea02285 --- /dev/null +++ b/src/lib/handleRef.ts @@ -0,0 +1,30 @@ +import * as React from 'react' + +/** + * The function that correctly handles passing refs. + * + * @param ref An ref object or function + * @param node A node that should be passed by ref + */ +const handleRef = (ref: React.Ref, node: N) => { + if (process.env.NODE_ENV !== 'production') { + if (typeof ref === 'string') { + throw new Error( + 'We do not support refs as string, this is a legacy API and will be likely to be removed in one of the future releases of React.', + ) + } + } + + if (typeof ref === 'function') { + ref(node) + return + } + + if (typeof ref === 'object') { + // @ts-ignore The `current` property is defined as readonly, however it's a valid way because + // `ref` is a mutable object + ref.current = node + } +} + +export default handleRef diff --git a/src/lib/index.ts b/src/lib/index.ts index a5366da356..4e1b9fddde 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -15,6 +15,8 @@ export { default as getElementType } from './getElementType' export { default as getUnhandledProps } from './getUnhandledProps' export { default as mergeThemes } from './mergeThemes' export { default as renderComponent, RenderResultConfig } from './renderComponent' + +export { default as handleRef } from './handleRef' export { htmlImageProps, htmlInputAttrs, diff --git a/test/specs/components/Ref/Ref-test.tsx b/test/specs/components/Ref/Ref-test.tsx index 1da88ada72..28705965d5 100644 --- a/test/specs/components/Ref/Ref-test.tsx +++ b/test/specs/components/Ref/Ref-test.tsx @@ -1,8 +1,8 @@ -import * as React from 'react' import { shallow, mount } from 'enzyme' +import * as React from 'react' -import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures' import Ref from 'src/components/Ref/Ref' +import { CompositeClass, CompositeFunction, DOMClass, DOMFunction } from './fixtures' const testInnerRef = Component => { const innerRef = jest.fn() @@ -42,5 +42,20 @@ describe('Ref', () => { 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/lib/handleRef-test.ts b/test/specs/lib/handleRef-test.ts new file mode 100644 index 0000000000..fc698353f5 --- /dev/null +++ b/test/specs/lib/handleRef-test.ts @@ -0,0 +1,28 @@ +import * as React from 'react' +import handleRef from 'src/lib/handleRef' + +describe('handleRef', () => { + it('throws an error when "ref" is string', () => { + const node = document.createElement('div') + + expect(() => { + handleRef('ref', node) + }).toThrowError() + }) + + it('calls with node when "ref" is function', () => { + const ref = jest.fn() + const node = document.createElement('div') + + handleRef(ref, node) + expect(ref).toBeCalledWith(node) + }) + + it('assigns to "current" when "ref" is object', () => { + const ref = React.createRef() + const node = document.createElement('div') + + handleRef(ref, node) + expect(ref.current).toBe(node) + }) +})