diff --git a/docs/src/pages/guides/migration-v3/migration-v3.md b/docs/src/pages/guides/migration-v3/migration-v3.md index ee374359dd3949..2ae27d1a636a96 100644 --- a/docs/src/pages/guides/migration-v3/migration-v3.md +++ b/docs/src/pages/guides/migration-v3/migration-v3.md @@ -341,10 +341,26 @@ You should be able to move the custom styles to the root class key. Only special HTML elements have these default behaviors. People should use `event.stopPropagation()` if they don't want to trigger a `onClose` event on the modal. +### Slide + +- [Slide] The child needs to be able to hold a ref. + + ```diff + class Component extends React.Component { + render() { + return
+ } + } + -const MyComponent = props =>
+ +const MyComponent = React.forwardRef((props, ref) =>
); + + +
+ ### Tooltip - [Tooltip] The child needs to be able to hold a ref. - + ```diff class Component extends React.Component { render() { diff --git a/packages/material-ui/src/Slide/Slide.js b/packages/material-ui/src/Slide/Slide.js index 281a455c5e9dac..22ad9a0df80da4 100644 --- a/packages/material-ui/src/Slide/Slide.js +++ b/packages/material-ui/src/Slide/Slide.js @@ -3,10 +3,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; -import EventListener from 'react-event-listener'; import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb. import { Transition } from 'react-transition-group'; -import { setRef } from '../utils/reactHelpers'; +import { useForkRef } from '../utils/reactHelpers'; import withTheme from '../styles/withTheme'; import { duration } from '../styles/transitions'; import { reflow, getTransitionProps } from '../transitions/utils'; @@ -16,8 +15,7 @@ const GUTTER = 24; // Translate the node so he can't be seen on the screen. // Later, we gonna translate back the node to his original location // with `translate3d(0, 0, 0)`.` -function getTranslateValue(props, node) { - const { direction } = props; +function getTranslateValue(direction, node) { const rect = node.getBoundingClientRect(); let transform; @@ -59,8 +57,8 @@ function getTranslateValue(props, node) { return `translateY(-${rect.top + rect.height + GUTTER - offsetY}px)`; } -export function setTranslateValue(props, node) { - const transform = getTranslateValue(props, node); +export function setTranslateValue(direction, node) { + const transform = getTranslateValue(direction, node); if (transform) { node.style.webkitTransform = transform; @@ -72,65 +70,49 @@ export function setTranslateValue(props, node) { * The Slide transition is used by the [Drawer](/demos/drawers/) component. * It uses [react-transition-group](https://github.com/reactjs/react-transition-group) internally. */ -class Slide extends React.Component { - mounted = false; - - constructor() { - super(); - - if (typeof window !== 'undefined') { - this.handleResize = debounce(() => { - // Skip configuration where the position is screen size invariant. - if (this.props.in || this.props.direction === 'down' || this.props.direction === 'right') { - return; - } - - if (this.childDOMNode) { - setTranslateValue(this.props, this.childDOMNode); - } - }, 166); // Corresponds to 10 frames at 60 Hz. - } - } - - componentDidMount() { - this.mounted = true; - - // state.mounted handle SSR, once the component is mounted, we need - // to properly hide it. - if (!this.props.in) { - // We need to set initial translate values of transition element - // otherwise component will be shown when in=false. - this.updatePosition(); - } - } - - componentDidUpdate(prevProps) { - if (prevProps.direction !== this.props.direction && !this.props.in) { - // We need to update the position of the drawer when the direction change and - // when it's hidden. - this.updatePosition(); - } - } - - componentWillUnmount() { - this.handleResize.clear(); - } +function Slide(props) { + const { + children, + direction, + in: inProp, + onEnter, + onEntering, + onExit, + onExited, + style, + theme, + timeout, + ...other + } = props; + + const childrenRef = React.useRef(); + /** + * used in cloneElement(children, { ref: handleRef }) + */ + const handleOwnRef = React.useCallback(ref => { + // #StrictMode ready + childrenRef.current = ReactDOM.findDOMNode(ref); + }, []); + const handleRef = useForkRef(children.ref, handleOwnRef); - handleEnter = node => { - setTranslateValue(this.props, node); + const handleEnter = () => { + const node = childrenRef.current; + setTranslateValue(direction, node); reflow(node); - if (this.props.onEnter) { - this.props.onEnter(node); + if (onEnter) { + onEnter(node); } }; - handleEntering = node => { - const { theme } = this.props; - - const transitionProps = getTransitionProps(this.props, { - mode: 'enter', - }); + const handleEntering = () => { + const node = childrenRef.current; + const transitionProps = getTransitionProps( + { timeout, style }, + { + mode: 'enter', + }, + ); node.style.webkitTransition = theme.transitions.create('-webkit-transform', { ...transitionProps, easing: theme.transitions.easing.easeOut, @@ -141,17 +123,19 @@ class Slide extends React.Component { }); node.style.webkitTransform = 'translate(0, 0)'; node.style.transform = 'translate(0, 0)'; - if (this.props.onEntering) { - this.props.onEntering(node); + if (onEntering) { + onEntering(node); } }; - handleExit = node => { - const { theme } = this.props; - - const transitionProps = getTransitionProps(this.props, { - mode: 'exit', - }); + const handleExit = () => { + const node = childrenRef.current; + const transitionProps = getTransitionProps( + { timeout, style }, + { + mode: 'exit', + }, + ); node.style.webkitTransition = theme.transitions.create('-webkit-transform', { ...transitionProps, easing: theme.transitions.easing.sharp, @@ -160,78 +144,82 @@ class Slide extends React.Component { ...transitionProps, easing: theme.transitions.easing.sharp, }); - setTranslateValue(this.props, node); + setTranslateValue(direction, node); - if (this.props.onExit) { - this.props.onExit(node); + if (onExit) { + onExit(node); } }; - handleExited = node => { + const handleExited = () => { + const node = childrenRef.current; // No need for transitions when the component is hidden node.style.webkitTransition = ''; node.style.transition = ''; - if (this.props.onExited) { - this.props.onExited(node); + if (onExited) { + onExited(node); } }; - /** - * used in cloneElement(children, { ref: handleRef }) - */ - handleRef = ref => { - // #StrictMode ready - this.childDOMNode = ReactDOM.findDOMNode(ref); - setRef(this.props.children.ref, ref); - }; + const updatePosition = React.useCallback(() => { + if (childrenRef.current) { + setTranslateValue(direction, childrenRef.current); + } + }, [direction]); + + React.useEffect(() => { + // Skip configuration where the position is screen size invariant. + if (!inProp && direction !== 'down' && direction !== 'right') { + const handleResize = debounce(() => { + if (childrenRef.current) { + setTranslateValue(direction, childrenRef.current); + } + }, 166); // Corresponds to 10 frames at 60 Hz. - updatePosition() { - if (this.childDOMNode) { - setTranslateValue(this.props, this.childDOMNode); + window.addEventListener('resize', handleResize); + + return () => { + handleResize.clear(); + window.removeEventListener('resize', handleResize); + }; } - } - render() { - const { - children, - direction, - in: inProp, - onEnter, - onEntering, - onExit, - onExited, - style, - theme, - ...other - } = this.props; - - return ( - - - {(state, childProps) => { - return React.cloneElement(children, { - ref: this.handleRef, - style: { - visibility: state === 'exited' && !inProp ? 'hidden' : undefined, - ...style, - ...children.props.style, - }, - ...childProps, - }); - }} - - - ); - } + return () => {}; + }, [direction, inProp]); + + React.useEffect(() => { + if (!inProp) { + // We need to update the position of the drawer when the direction change and + // when it's hidden. + updatePosition(); + } + }, [inProp, updatePosition]); + + return ( + + {(state, childProps) => { + return React.cloneElement(children, { + ref: handleRef, + style: { + visibility: state === 'exited' && !inProp ? 'hidden' : undefined, + ...style, + ...children.props.style, + }, + ...childProps, + }); + }} + + ); } Slide.propTypes = { diff --git a/packages/material-ui/src/Slide/Slide.test.js b/packages/material-ui/src/Slide/Slide.test.js index 9a9a8c43c27734..a6915c93fb1e24 100644 --- a/packages/material-ui/src/Slide/Slide.test.js +++ b/packages/material-ui/src/Slide/Slide.test.js @@ -1,22 +1,19 @@ import React from 'react'; import { assert } from 'chai'; -import { spy, useFakeTimers } from 'sinon'; -import { createShallow, createMount, describeConformance } from '@material-ui/core/test-utils'; +import { spy, stub, useFakeTimers } from 'sinon'; +import { createMount, describeConformance } from '@material-ui/core/test-utils'; import Slide, { setTranslateValue } from './Slide'; -import transitions, { easing } from '../styles/transitions'; import createMuiTheme from '../styles/createMuiTheme'; describe('', () => { - let shallow; let mount; const defaultProps = { in: true, - children:
, + children:
, direction: 'down', }; before(() => { - shallow = createShallow({ dive: true }); // StrictModeViolation: uses react-transition-group mount = createMount({ strict: false }); }); @@ -34,7 +31,7 @@ describe('', () => { inheritComponent: 'Transition', mount, refInstanceof: React.Component, - skip: ['componentProp'], + skip: ['componentProp', 'refForwarding'], }), ); @@ -55,190 +52,280 @@ describe('', () => { }); }); - describe('event callbacks', () => { - it('should fire event callbacks', () => { - const events = ['onEnter', 'onEntering', 'onEntered', 'onExit', 'onExiting', 'onExited']; + describe('transition lifecycle', () => { + let wrapper; + let clock; + let child; + + const handleEnter = spy(); + const handleEntering = spy(); + const handleEntered = spy(); + const handleExit = spy(); + const handleExiting = spy(); + const handleExited = spy(); + + before(() => { + wrapper = mount( + +
{ + child = ref; + }} + /> + , + ); + clock = useFakeTimers(); + }); + + after(() => { + clock.restore(); + }); + + describe('in', () => { + before(() => { + wrapper.setProps({ in: true }); + }); - const handlers = events.reduce((result, n) => { - result[n] = spy(); - return result; - }, {}); + describe('handleEnter()', () => { + it('should call handleEnter', () => { + assert.strictEqual(handleEntering.callCount, 1); + assert.strictEqual(handleEntering.args[0][0], child); + }); + }); + + describe('handleEntering()', () => { + it('should reset the translate3d', () => { + assert.match(handleEntering.args[0][0].style.transform, /translate\(0(px)?, 0(px)?\)/); + }); + + it('should call handleEntering', () => { + assert.strictEqual(handleEntering.callCount, 1); + assert.strictEqual(handleEntering.args[0][0], child); + }); + }); + + describe('handleEntered()', () => { + it('should have called onEntered', () => { + clock.tick(1000); + assert.strictEqual(handleEntered.callCount, 1); + }); + }); + }); + + describe('out', () => { + before(() => { + wrapper.setProps({ in: true }); + wrapper.setProps({ in: false }); + }); + + describe('handleExit()', () => { + it('should call handleExit', () => { + assert.strictEqual(handleExiting.callCount, 1); + assert.strictEqual(handleExiting.args[0][0], child); + }); + }); - const wrapper = shallow().childAt(0); + describe('handleExiting()', () => { + it('should call onExiting', () => { + assert.strictEqual(handleExiting.callCount, 1); + assert.strictEqual(handleExiting.args[0][0], child); + }); + }); - events.forEach(n => { - const event = n.charAt(2).toLowerCase() + n.slice(3); - wrapper.simulate(event, { - fakeTransform: 'none', - style: {}, - getBoundingClientRect: () => ({}), + describe('handleExited()', () => { + it('should call onExited', () => { + clock.tick(1000); + assert.strictEqual(handleExited.callCount, 1); + assert.strictEqual(handleExited.args[0][0], child); }); - assert.strictEqual(handlers[n].callCount, 1, `should have called the ${n} handler`); }); }); }); describe('prop: timeout', () => { let wrapper; - let instance; - let element; const enterDuration = 556; const leaveDuration = 446; + const handleEntering = spy(); + const handleExit = spy(); beforeEach(() => { - wrapper = shallow( + wrapper = mount( , ); - instance = wrapper.instance(); - element = { fakeTransform: 'none', getBoundingClientRect: () => ({}), style: {} }; }); it('should create proper easeOut animation onEntering', () => { - instance.handleEntering(element); - const animation = transitions.create('transform', { - duration: enterDuration, - easing: easing.easeOut, - }); - assert.strictEqual(element.style.transition, animation); + assert.match( + handleEntering.args[0][0].style.transition, + /transform 556ms cubic-bezier\(0(.0)?, 0, 0.2, 1\)( 0ms)?/, + ); }); it('should create proper sharp animation onExit', () => { - instance.handleExit(element); - const animation = transitions.create('transform', { - duration: leaveDuration, - easing: easing.sharp, - }); - assert.strictEqual(element.style.transition, animation); + wrapper.setProps({ in: false }); + assert.match( + handleExit.args[0][0].style.transition, + /transform 446ms cubic-bezier\(0.4, 0, 0.6, 1\)( 0ms)?/, + ); }); }); describe('prop: direction', () => { it('should update the position', () => { const wrapper = mount(); - const transition = wrapper.find('Slide').instance().childDOMNode; + const child = wrapper.find('#testChild').instance(); - const transition1 = transition.style.transform; + const transition1 = child.style.transform; wrapper.setProps({ direction: 'right', }); - const transition2 = transition.style.transform; + const transition2 = child.style.transform; assert.notStrictEqual(transition1, transition2); }); }); - describe('transition lifecycle', () => { + describe('transform styling', () => { let wrapper; - let instance; + let child; + const handleEnter = spy(); + let nodeEnterTransformStyle; + const handleEnterWrapper = (...args) => { + handleEnter(...args); + nodeEnterTransformStyle = args[0].style.transform; + }; + const handleExiting = spy(); + let nodeExitingTransformStyle; + const handleExitingWrapper = (...args) => { + handleExiting(...args); + nodeExitingTransformStyle = args[0].style.transform; + }; before(() => { - wrapper = shallow(); - instance = wrapper.instance(); + wrapper = mount( + +
{ + child = ref; + }} + /> + , + ); + + child.fakeTransform = 'none'; + stub(child, 'getBoundingClientRect').callsFake(() => ({ + width: 500, + height: 300, + left: 300, + right: 800, + top: 200, + bottom: 500, + })); }); describe('handleEnter()', () => { - let element; - - beforeEach(() => { - element = { - fakeTransform: 'none', - getBoundingClientRect: () => ({ - width: 500, - height: 300, - left: 300, - right: 800, - top: 200, - bottom: 500, - }), - style: {}, - }; + afterEach(() => { + wrapper.setProps({ + in: false, + }); }); - it('should set element transform and transition according to the direction', () => { + it('should set element transform and transition in the `left` direction', () => { wrapper.setProps({ direction: 'left' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateX(100vw) translateX(-300px)'); + wrapper.setProps({ in: true }); + assert.strictEqual(nodeEnterTransformStyle, 'translateX(100vw) translateX(-300px)'); + }); + + it('should set element transform and transition in the `right` direction', () => { wrapper.setProps({ direction: 'right' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateX(-824px)'); + wrapper.setProps({ in: true }); + assert.strictEqual(nodeEnterTransformStyle, 'translateX(-824px)'); + }); + + it('should set element transform and transition in the `up` direction', () => { wrapper.setProps({ direction: 'up' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateY(100vh) translateY(-200px)'); + wrapper.setProps({ in: true }); + assert.strictEqual(nodeEnterTransformStyle, 'translateY(100vh) translateY(-200px)'); + }); + + it('should set element transform and transition in the `down` direction', () => { wrapper.setProps({ direction: 'down' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateY(-524px)'); + wrapper.setProps({ in: true }); + assert.strictEqual(nodeEnterTransformStyle, 'translateY(-524px)'); }); it('should reset the previous transition if needed', () => { - element.style.transform = 'translateX(-824px)'; + child.style.transform = 'translateX(-824px)'; wrapper.setProps({ direction: 'right' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateX(-824px)'); + wrapper.setProps({ in: true }); + assert.strictEqual(nodeEnterTransformStyle, 'translateX(-824px)'); }); }); - describe('handleEntering()', () => { - let element; - + describe('handleExiting()', () => { before(() => { - element = { style: {} }; - instance.handleEntering(element); + wrapper.setProps({ + in: true, + }); }); - it('should reset the translate3d', () => { - assert.strictEqual(element.style.transform, 'translate(0, 0)'); + afterEach(() => { + wrapper.setProps({ + in: true, + }); }); - }); - describe('handleExiting()', () => { - let element; - - before(() => { - element = { - fakeTransform: 'none', - getBoundingClientRect: () => ({ - width: 500, - height: 300, - left: 300, - right: 800, - top: 200, - bottom: 500, - }), - style: {}, - }; + it('should set element transform and transition in the `left` direction', () => { + wrapper.setProps({ direction: 'left' }); + wrapper.setProps({ in: false }); + assert.strictEqual(nodeExitingTransformStyle, 'translateX(100vw) translateX(-300px)'); }); - it('should set element transform and transition according to the direction', () => { - wrapper.setProps({ direction: 'left' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateX(100vw) translateX(-300px)'); + it('should set element transform and transition in the `right` direction', () => { wrapper.setProps({ direction: 'right' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateX(-824px)'); + wrapper.setProps({ in: false }); + assert.strictEqual(nodeExitingTransformStyle, 'translateX(-824px)'); + }); + + it('should set element transform and transition in the `up` direction', () => { wrapper.setProps({ direction: 'up' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateY(100vh) translateY(-200px)'); + wrapper.setProps({ in: false }); + assert.strictEqual(nodeExitingTransformStyle, 'translateY(100vh) translateY(-200px)'); + }); + + it('should set element transform and transition in the `down` direction', () => { wrapper.setProps({ direction: 'down' }); - instance.handleEnter(element); - assert.strictEqual(element.style.transform, 'translateY(-524px)'); + wrapper.setProps({ in: false }); + assert.strictEqual(nodeExitingTransformStyle, 'translateY(-524px)'); }); }); }); describe('mount', () => { it('should work when initially hidden', () => { - const wrapper = mount( + const childRef = React.createRef(); + mount( -
Foo
+
Foo
, ); - const transition = wrapper.find('Slide').instance().childDOMNode; + const transition = childRef.current; assert.strictEqual(transition.style.visibility, 'hidden'); assert.notStrictEqual(transition.style.transform, undefined); @@ -259,15 +346,15 @@ describe('', () => { it('should recompute the correct position', () => { const wrapper = mount( -
Foo
+
Foo
, ); - const instance = wrapper.find('Slide').instance(); - instance.handleResize(); + + window.dispatchEvent(new window.Event('resize', {})); clock.tick(166); - const transition = instance.childDOMNode; + const child = wrapper.find('#testChild').instance(); - assert.notStrictEqual(transition.style.transform, undefined); + assert.notStrictEqual(child.style.transform, undefined); }); it('should take existing transform into account', () => { @@ -283,19 +370,13 @@ describe('', () => { }), style: {}, }; - setTranslateValue( - { - direction: 'up', - }, - element, - ); + setTranslateValue('up', element); assert.strictEqual(element.style.transform, 'translateY(100vh) translateY(-780px)'); }); it('should do nothing when visible', () => { - const wrapper = shallow(); - const instance = wrapper.instance(); - instance.handleResize(); + mount(); + window.dispatchEvent(new window.Event('resize', {})); clock.tick(166); }); }); diff --git a/packages/material-ui/src/Table/Table.js b/packages/material-ui/src/Table/Table.js index 81d260355393c8..eefa6bd1f748e3 100644 --- a/packages/material-ui/src/Table/Table.js +++ b/packages/material-ui/src/Table/Table.js @@ -44,11 +44,6 @@ Table.propTypes = { * Either a string to use a DOM element or a component. */ component: PropTypes.elementType, - /** - * @ignore - * from `withForwardRef` - */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** * Allows TableCells to inherit padding of the Table. */