-
Notifications
You must be signed in to change notification settings - Fork 55
feat(Ref): extract to a separate package, add utils #1281
Changes from all commits
a4df1d5
bd21d5c
45fe17a
1a4ddc2
f20ae30
374cdbb
a345e74
627e8fe
00a7407
8ad9110
dff2746
0f8cf8c
477e532
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ | |
}, | ||
"files": [ | ||
"babel", | ||
"eslint", | ||
"jest" | ||
], | ||
"publishConfig": { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "@stardust-ui/internal-tooling/babel" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module.exports = { | ||
...require('@stardust-ui/internal-tooling/jest'), | ||
name: 'react-component-ref', | ||
moduleNameMapper: require('lerna-alias').jest(), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "@stardust-ui/react-component-ref", | ||
"description": "A set of components and utils to deal with React refs.", | ||
"version": "0.29.1", | ||
"author": "Oleksandr Fediashov <olfedias@microsoft.com>", | ||
"bugs": "https://github.com/stardust-ui/react/issues", | ||
"dependencies": { | ||
"@stardust-ui/react-proptypes": "^0.29.1", | ||
"prop-types": "^15.6.1", | ||
"react-is": "^16.6.3" | ||
}, | ||
"devDependencies": { | ||
"@stardust-ui/internal-tooling": "^0.29.1", | ||
"lerna-alias": "^3.0.3-0" | ||
}, | ||
"files": [ | ||
"dist" | ||
], | ||
"homepage": "https://github.com/stardust-ui/react/tree/master/packages/react-component-ref", | ||
"jsnext:main": "dist/es/index.js", | ||
"license": "MIT", | ||
"main": "dist/commonjs/index.js", | ||
"module": "dist/es/index.js", | ||
"peerDependencies": { | ||
"react": "^16.8.0", | ||
"react-dom": "^16.8.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"repository": "stardust-ui/react.git", | ||
"scripts": { | ||
"build": "cross-env TS_NODE_PROJECT=../../tsconfig.json gulp bundle:package:no-umd --package react-component-ref" | ||
}, | ||
"sideEffects": false, | ||
"types": "dist/es/index.d.ts" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,14 @@ | ||
import * as customPropTypes from '@stardust-ui/react-proptypes' | ||
import * as PropTypes from 'prop-types' | ||
import * as React from 'react' | ||
import { isForwardRef } from 'react-is' | ||
import * as ReactIs from 'react-is' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: To be common with other imports |
||
|
||
import { ChildrenComponentProps } from '../../lib' | ||
import { ReactProps } from '../../types' | ||
import RefFindNode from './RefFindNode' | ||
import RefForward from './RefForward' | ||
|
||
export interface RefProps extends ChildrenComponentProps<React.ReactElement<any>> { | ||
export interface RefProps { | ||
children: React.ReactElement<any> | ||
|
||
/** | ||
* Called when a child component will be mounted or updated. | ||
* | ||
|
@@ -17,11 +17,11 @@ export interface RefProps extends ChildrenComponentProps<React.ReactElement<any> | |
innerRef: React.Ref<any> | ||
} | ||
|
||
const Ref: React.FunctionComponent<ReactProps<RefProps>> = props => { | ||
const Ref: React.FunctionComponent<RefProps> = props => { | ||
const { children, innerRef } = props | ||
|
||
const child = React.Children.only(children) | ||
const ElementType = isForwardRef(child) ? RefForward : RefFindNode | ||
const ElementType = ReactIs.isForwardRef(child) ? RefForward : RefFindNode | ||
|
||
return <ElementType innerRef={innerRef}>{child}</ElementType> | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,10 +3,11 @@ import * as PropTypes from 'prop-types' | |
import * as React from 'react' | ||
import * as ReactDOM from 'react-dom' | ||
|
||
import { ChildrenComponentProps } from '../../lib/commonPropInterfaces' | ||
import handleRef from '../../lib/handleRef' | ||
import handleRef from './handleRef' | ||
|
||
export interface RefFindNodeProps { | ||
children: React.ReactElement<any> | ||
|
||
export interface RefFindNodeProps extends ChildrenComponentProps<React.ReactElement<any>> { | ||
/** | ||
* Called when a child component will be mounted or updated. | ||
* | ||
|
@@ -20,10 +21,10 @@ export default class RefFindNode extends React.Component<RefFindNodeProps> { | |
|
||
static propTypes = { | ||
children: PropTypes.element.isRequired, | ||
innerRef: customPropTypes.ref, | ||
innerRef: customPropTypes.ref.isRequired as PropTypes.Validator<React.Ref<any>>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} | ||
|
||
prevNode: Node = null | ||
prevNode: Node | null = null | ||
|
||
componentDidMount() { | ||
this.prevNode = ReactDOM.findDOMNode(this) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 = <N>(ref: React.Ref<N> | undefined, 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 (ref !== null && typeof ref === 'object') { | ||
// The `current` property is defined as readonly, however it's a valid way because | ||
// `ref` is a mutable object | ||
;(ref as React.MutableRefObject<N>).current = node | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only this line was changed: we don't need |
||
} | ||
} | ||
|
||
export default handleRef |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export { default as handleRef } from './handleRef' | ||
export { default as isRefObject } from './isRefObject' | ||
export { default as toRefObject } from './toRefObject' | ||
|
||
export { default as Ref, RefProps } from './Ref' | ||
export { default as RefFindNode } from './RefFindNode' | ||
export { default as RefForward } from './RefForward' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import * as React from 'react' | ||
|
||
/** | ||
* Check that the passed object is a valid React ref object. | ||
*/ | ||
const isRefObject = (ref: any): ref is React.RefObject<any> => | ||
// https://github.com/facebook/react/blob/v16.8.2/packages/react-reconciler/src/ReactFiberCommitWork.js#L665 | ||
ref !== null && typeof ref === 'object' && ref.hasOwnProperty('current') | ||
|
||
export default isRefObject |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import * as React from 'react' | ||
|
||
const nullRefObject: React.RefObject<null> = { current: null } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if the user uses reference like this in the following way: ref.current.focus()? How is it safe to have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be interface RefObject<T> {
readonly current: T | null;
}
// Mutable it used in hooks an initial value is obviously defined
// React.useRef(null)
interface MutableRefObject<T> {
current: T;
} |
||
|
||
// A map of created ref objects to provide memoization. | ||
const refObjects = new WeakMap<Node, React.RefObject<Node>>() | ||
|
||
/** | ||
* Creates a React ref object from existing DOM node. | ||
*/ | ||
const toRefObject = <T extends Node>(node: T): React.RefObject<T> => { | ||
// A "null" is not valid key for a WeakMap | ||
if (node === null) { | ||
return nullRefObject | ||
} | ||
|
||
if (refObjects.has(node)) { | ||
return refObjects.get(node) as React.RefObject<T> | ||
} | ||
|
||
const refObject: React.RefObject<T> = { current: node } | ||
refObjects.set(node, refObject) | ||
|
||
return refObject | ||
} | ||
|
||
export default toRefObject |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { RefForward } from '@stardust-ui/react-component-ref' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like that we have inconsistency between the imports in these tests vs the imports in the components' tests. Can we change this import or is there a reason why it is like this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to test public APIs and get rid of /cc @kuzhelov |
||
import { mount } from 'enzyme' | ||
import * as React from 'react' | ||
|
||
import RefForward from 'src/components/Ref/RefForward' | ||
import { ForwardedRef } from './fixtures' | ||
|
||
describe('RefForward', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { handleRef } from '@stardust-ui/react-component-ref' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the prev comment for the import |
||
import * as React from 'react' | ||
import handleRef from 'src/lib/handleRef' | ||
|
||
describe('handleRef', () => { | ||
it('throws an error when "ref" is string', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { isRefObject } from '@stardust-ui/react-component-ref' | ||
|
||
describe('isRefObject', () => { | ||
it('checks for a valid param', () => { | ||
expect(isRefObject(document.createElement('div'))).toBe(false) | ||
expect(isRefObject(null)).toBe(false) | ||
|
||
expect(isRefObject({ current: document.createElement('div') })).toBe(true) | ||
expect(isRefObject({ current: null })).toBe(true) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { toRefObject } from '@stardust-ui/react-component-ref' | ||
|
||
describe('toRefObject', () => { | ||
it('creates an ref object from an input', () => { | ||
const node = document.createElement('div') | ||
expect(toRefObject(node)).toHaveProperty('current', node) | ||
}) | ||
|
||
it('handles "null" as input', () => { | ||
expect(toRefObject(null)).toHaveProperty('current', null) | ||
}) | ||
|
||
it('returned object is memoized', () => { | ||
const node = document.createElement('div') | ||
const refObject = toRefObject(node) | ||
|
||
expect(toRefObject(node)).toBe(refObject) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"extends": "../../build/tsconfig.common", | ||
"compilerOptions": { | ||
"noImplicitReturns": true, | ||
"noImplicitThis": true, | ||
"noImplicitAny": true, | ||
"noUnusedParameters": true, | ||
"strictNullChecks": true | ||
}, | ||
"include": ["src", "test"] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI @miroslavstastny
publishConfig
is defined