diff --git a/package.json b/package.json index bfd530806efe27..71ddf6df2f0959 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "fs-extra": "^3.0.1", "glob": "^7.1.2", "jsdom": "^11.1.0", + "jsdom-global": "^3.0.2", "json-loader": "^0.5.4", "karma": "^1.7.0", "karma-browserstack-launcher": "^1.3.0", diff --git a/src/Dialog/Dialog.js b/src/Dialog/Dialog.js index c50cee594c4634..f9570824afb93b 100644 --- a/src/Dialog/Dialog.js +++ b/src/Dialog/Dialog.js @@ -10,6 +10,7 @@ import Modal from '../internal/Modal'; import Fade from '../transitions/Fade'; import { duration } from '../styles/transitions'; import Paper from '../Paper'; +import type { TransitionCallback } from '../internal/Transition'; export const styleSheet = createStyleSheet('MuiDialog', theme => ({ root: { @@ -91,15 +92,15 @@ type Props = { /** * Callback fired before the dialog enters. */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the dialog is entering. */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the dialog has entered. */ - onEntered?: Function, // eslint-disable-line react/sort-prop-types + onEntered?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fires when the escape key is released and the modal is in focus. */ @@ -107,15 +108,15 @@ type Props = { /** * Callback fired before the dialog exits. */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the dialog is exiting. */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the dialog has exited. */ - onExited?: Function, // eslint-disable-line react/sort-prop-types + onExited?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fired when the dialog requests to be closed. */ diff --git a/src/Menu/Menu.js b/src/Menu/Menu.js index 277c1c10f7d694..af3f101b60b14d 100644 --- a/src/Menu/Menu.js +++ b/src/Menu/Menu.js @@ -9,6 +9,7 @@ import getScrollbarSize from 'dom-helpers/util/scrollbarSize'; import Popover from '../internal/Popover'; import withStyles from '../styles/withStyles'; import MenuList from './MenuList'; +import type { TransitionCallback } from '../internal/Transition'; type DefaultProps = { open: boolean, @@ -39,27 +40,27 @@ type Props = DefaultProps & { /** * Callback fired before the Menu enters. */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the Menu is entering. */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the Menu has entered. */ - onEntered?: Function, // eslint-disable-line react/sort-prop-types + onEntered?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fired before the Menu exits. */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the Menu is exiting. */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the Menu has exited. */ - onExited?: Function, // eslint-disable-line react/sort-prop-types + onExited?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback function fired when the menu is requested to be closed. * @@ -78,7 +79,12 @@ type Props = DefaultProps & { export const styleSheet = createStyleSheet('MuiMenu', { root: { - maxHeight: 250, + /** + * specZ: The maximum height of a simple menu should be one or more rows less than the view + * height. This ensures a tappable area outside of the simple menu with which to dismiss + * the menu. + */ + maxHeight: 'calc(100vh - 96px)', }, }); @@ -90,7 +96,7 @@ class Menu extends Component { menuList = undefined; - handleEnter = element => { + handleEnter = (element: HTMLElement) => { const list = findDOMNode(this.menuList); if (this.menuList && this.menuList.selectedItem) { @@ -115,7 +121,7 @@ class Menu extends Component { } }; - handleListKeyDown = (event, key) => { + handleListKeyDown = (event: SyntheticUIEvent, key: string) => { if (key === 'tab') { event.preventDefault(); const { onRequestClose } = this.props; @@ -161,7 +167,6 @@ class Menu extends Component { getContentAnchorEl={this.getContentAnchorEl} className={classNames(classes.root, className)} open={open} - enteredClassName={classes.entered} onEnter={this.handleEnter} onEntering={onEntering} onEntered={onEntered} diff --git a/src/Menu/Menu.spec.js b/src/Menu/Menu.spec.js index 31e6a8cb135595..88ed13df1c0435 100644 --- a/src/Menu/Menu.spec.js +++ b/src/Menu/Menu.spec.js @@ -43,15 +43,6 @@ describe('', () => { assert.strictEqual(wrapper.hasClass(classes.root), true, 'should be classes.root'); }); - it('should pass `classes.entered` to the Popover for the enteredClassName', () => { - const wrapper = shallow(); - assert.strictEqual( - wrapper.props().enteredClassName, - classes.entered, - 'should be classes.entered', - ); - }); - it('should pass the instance function `getContentAnchorEl` to Popover', () => { const wrapper = shallow(); assert.strictEqual( diff --git a/src/Menu/MenuList.js b/src/Menu/MenuList.js index e0a6b549fbf4ca..93aa65dab37d5b 100644 --- a/src/Menu/MenuList.js +++ b/src/Menu/MenuList.js @@ -25,7 +25,7 @@ type Props = { /** * @ignore */ - onKeyDown?: Function, + onKeyDown?: (event: SyntheticUIEvent, key: string) => void, }; type State = { diff --git a/src/Snackbar/Snackbar.js b/src/Snackbar/Snackbar.js index 05c00673cbea47..a5fa14465c5682 100644 --- a/src/Snackbar/Snackbar.js +++ b/src/Snackbar/Snackbar.js @@ -10,6 +10,7 @@ import ClickAwayListener from '../internal/ClickAwayListener'; import { capitalizeFirstLetter, createChainedFunction } from '../utils/helpers'; import Slide from '../transitions/Slide'; import SnackbarContent from './SnackbarContent'; +import type { TransitionCallback } from '../internal/Transition'; export const styleSheet = createStyleSheet('MuiSnackbar', theme => { const gutter = theme.spacing.unit * 3; @@ -127,27 +128,27 @@ type Props = DefaultProps & { /** * Callback fired before the transition is entering. */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the transition is entering. */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the transition has entered. */ - onEntered?: Function, + onEntered?: TransitionCallback, /** * Callback fired before the transition is exiting. */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the transition is exiting. */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the transition has exited. */ - onExited?: Function, + onExited?: TransitionCallback, /** * @ignore */ diff --git a/src/internal/Modal.js b/src/internal/Modal.js index 28937bb1a6a274..c2962810e878e9 100644 --- a/src/internal/Modal.js +++ b/src/internal/Modal.js @@ -18,6 +18,7 @@ import withStyles from '../styles/withStyles'; import createModalManager from './modalManager'; import Backdrop from './Backdrop'; import Portal from './Portal'; +import type { TransitionCallback } from './Transition'; /** * Modals don't open on the server so this won't break concurrency. @@ -109,15 +110,15 @@ type Props = DefaultProps & { /** * Callback fired before the modal is entering. */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the modal is entering. */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the modal has entered. */ - onEntered?: Function, // eslint-disable-line react/sort-prop-types + onEntered?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fires when the escape key is pressed and the modal is in focus. */ @@ -125,15 +126,15 @@ type Props = DefaultProps & { /** * Callback fired before the modal is exiting. */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the modal is exiting. */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the modal has exited. */ - onExited?: Function, // eslint-disable-line react/sort-prop-types + onExited?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fired when the modal requests to be closed. */ diff --git a/src/internal/Popover.js b/src/internal/Popover.js index e23ceed0ecc8c5..1ed497657498ff 100644 --- a/src/internal/Popover.js +++ b/src/internal/Popover.js @@ -1,4 +1,4 @@ -// @flow weak +// @flow import React, { Component } from 'react'; import type { Element } from 'react'; @@ -10,6 +10,7 @@ import customPropTypes from '../utils/customPropTypes'; import Modal from './Modal'; import Transition from './Transition'; import Paper from '../Paper'; +import type { TransitionCallback } from './Transition'; function getOffsetTop(rect, vertical) { let offset = 0; @@ -127,27 +128,27 @@ type Props = DefaultProps & { /** * Callback fired before the component is entering */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the component is entering */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the component has entered */ - onEntered?: Function, // eslint-disable-line react/sort-prop-types + onEntered?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fired before the component is exiting */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the component is exiting */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the component has exited */ - onExited?: Function, // eslint-disable-line react/sort-prop-types + onExited?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback function fired when the popover is requested to be closed. * @@ -197,8 +198,8 @@ class Popover extends Component { autoTransitionDuration = undefined; - handleEnter = element => { - element.style.opacity = 0; + handleEnter = (element: HTMLElement) => { + element.style.opacity = '0'; element.style.transform = Popover.getScale(0.75); if (this.props.onEnter) { @@ -229,16 +230,16 @@ class Popover extends Component { ].join(','); }; - handleEntering = element => { - element.style.opacity = 1; + handleEntering = (element: HTMLElement) => { + element.style.opacity = '1'; element.style.transform = Popover.getScale(1); if (this.props.onEntering) { - this.props.onEntering(); + this.props.onEntering(element); } }; - handleExit = element => { + handleExit = (element: HTMLElement) => { let { transitionDuration } = this.props; const { transitions } = this.context.styleManager.theme; @@ -257,11 +258,11 @@ class Popover extends Component { }), ].join(','); - element.style.opacity = 0; + element.style.opacity = '0'; element.style.transform = Popover.getScale(0.75); if (this.props.onExit) { - this.props.onExit(); + this.props.onExit(element); } }; diff --git a/src/internal/Popover.spec.js b/src/internal/Popover.spec.js index 65c5e6f599e3e3..bbc85d0927542d 100644 --- a/src/internal/Popover.spec.js +++ b/src/internal/Popover.spec.js @@ -224,7 +224,7 @@ describe('', () => { }); it('should set the inline styles for the enter phase', () => { - assert.strictEqual(element.style.opacity, 0, 'should be transparent'); + assert.strictEqual(element.style.opacity, '0', 'should be transparent'); assert.strictEqual( element.style.transform, Popover.getScale(0.75), @@ -263,7 +263,7 @@ describe('', () => { }); it('should set the inline styles for the entering phase', () => { - assert.strictEqual(element.style.opacity, 1, 'should be visible'); + assert.strictEqual(element.style.opacity, '1', 'should be visible'); assert.strictEqual( element.style.transform, Popover.getScale(1), @@ -287,7 +287,7 @@ describe('', () => { }); it('should set the inline styles for the exit phase', () => { - assert.strictEqual(element.style.opacity, 0, 'should be transparent'); + assert.strictEqual(element.style.opacity, '0', 'should be transparent'); assert.strictEqual( element.style.transform, Popover.getScale(0.75), diff --git a/src/internal/Transition.js b/src/internal/Transition.js index d75deda3b8d045..48aeb6c85be7aa 100644 --- a/src/internal/Transition.js +++ b/src/internal/Transition.js @@ -1,4 +1,4 @@ -// @flow weak +// @flow import React, { Component } from 'react'; import type { Element as ReactElement } from 'react'; // DOM type `Element` used below @@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'; import transitionInfo from 'dom-helpers/transition/properties'; import addEventListener from 'dom-helpers/events/on'; import classNames from 'classnames'; +import type { SyntheticUIEventHandler } from './types'; const transitionEndEvent = transitionInfo.end; @@ -15,13 +16,31 @@ export const ENTERING = 2; export const ENTERED = 3; export const EXITING = 4; +/** + * A helper function that calls back when any pending animations have started This is needed as the + * callback hooks might be setting some style properties that needs a frame to take effect. + */ +function requestAnimationStart(callback) { + // Feature detect rAF, fallback to setTimeout + if (window.requestAnimationFrame) { + // Chrome and Safari have a bug where calling rAF once returns the current + // frame instead of the next frame, so we need to call a double rAF here. + // See https://crbug.com/675795 for more. + window.requestAnimationFrame(() => { + window.requestAnimationFrame(callback); + }); + } else { + setTimeout(callback, 0); + } +} + type State = { status: 0 | 1 | 2 | 3 | 4, }; -type DOMNode = Element | Text | null; // return type of ReactDOM.findDOMNode() +export type TransitionCallback = (element: HTMLElement) => void; -type TransitionCallback = (node: DOMNode) => void; +export type TransitionRequestTimeout = (element: HTMLElement) => number; type DefaultProps = { in: boolean, @@ -36,23 +55,6 @@ type DefaultProps = { onExited: TransitionCallback, }; -// A helper function that calls back when any pending animations have started -// This is needed as the callback hooks might be setting some style properties -// that needs a frame to take effect. -function requestAnimationStart(callback) { - // Feature detect rAF, fallback to setTimeout - if (window.requestAnimationFrame) { - // Chrome and Safari have a bug where calling rAF once returns the current - // frame instead of the next frame, so we need to call a double rAF here. - // See https://crbug.com/675795 for more. - window.requestAnimationFrame(() => { - window.requestAnimationFrame(callback); - }); - } else { - setTimeout(callback, 0); - } -} - type Props = DefaultProps & { /** * The content of the component. @@ -109,7 +111,7 @@ type Props = DefaultProps & { /** * @ignore */ - onRequestTimeout?: TransitionCallback, + onRequestTimeout?: TransitionRequestTimeout, /** * A Timeout for the animation, in milliseconds, to ensure that a node doesn't * transition indefinitely if the browser transitionEnd events are @@ -246,18 +248,19 @@ class Transition extends Component { performEnter(props: Props) { this.cancelNextCallback(); const node = ReactDOM.findDOMNode(this); - - props.onEnter(node); - return this.performEntering(node); + if (node instanceof HTMLElement) { + props.onEnter(node); + this.performEntering(node); + } } - performEntering(node: DOMNode) { + performEntering(element: HTMLElement) { this.safeSetState({ status: ENTERING }, () => { - this.props.onEntering(node); + this.props.onEntering(element); - this.onTransitionEnd(node, () => { + this.onTransitionEnd(element, () => { this.safeSetState({ status: ENTERED }, () => { - this.props.onEntered(node); + this.props.onEntered(element); }); }); }); @@ -266,19 +269,20 @@ class Transition extends Component { performExit(props: Props) { this.cancelNextCallback(); const node = ReactDOM.findDOMNode(this); + if (node instanceof HTMLElement) { + // Not this.props, because we might be about to receive new props. + props.onExit(node); - // Not this.props, because we might be about to receive new props. - props.onExit(node); - - this.safeSetState({ status: EXITING }, () => { - this.props.onExiting(node); + this.safeSetState({ status: EXITING }, () => { + this.props.onExiting(node); - this.onTransitionEnd(node, () => { - this.safeSetState({ status: EXITED }, () => { - this.props.onExited(node); + this.onTransitionEnd(node, () => { + this.safeSetState({ status: EXITED }, () => { + this.props.onExited(node); + }); }); }); - }); + } } cancelNextCallback() { @@ -295,12 +299,12 @@ class Transition extends Component { this.setState(nextState, this.setNextCallback(callback)); } - setNextCallback(callback) { + setNextCallback(callback: SyntheticUIEventHandler) { let active = true; // FIXME: These next two blocks are a real enigma for flow typing outside of weak mode. // FIXME: I suggest we refactor - rosskevin - this.nextCallback = (event?: Event) => { + this.nextCallback = (event?: SyntheticUIEvent) => { requestAnimationStart(() => { if (active) { active = false; @@ -318,26 +322,26 @@ class Transition extends Component { return this.nextCallback; } - onTransitionEnd(node: DOMNode, handler) { + onTransitionEnd(element: HTMLElement, handler: SyntheticUIEventHandler) { this.setNextCallback(handler); - if (node) { - addEventListener(node, transitionEndEvent, event => { - if (event.target === node && this.nextCallback) { + if (element) { + addEventListener(element, transitionEndEvent, event => { + if (event.target === element && this.nextCallback) { this.nextCallback(); } }); - setTimeout(this.nextCallback, this.getTimeout(node)); + setTimeout(this.nextCallback, this.getTimeout(element)); } else { setTimeout(this.nextCallback, 0); } } - getTimeout(node: DOMNode) { + getTimeout(element: HTMLElement) { let timeout; - if (this.props.onRequestTimeout) { - timeout = this.props.onRequestTimeout(node); + if (this.props.onRequestTimeout && element instanceof HTMLElement) { + timeout = this.props.onRequestTimeout(element); } if (typeof timeout !== 'number') { diff --git a/src/internal/types.js b/src/internal/types.js new file mode 100644 index 00000000000000..74ca6bf1885470 --- /dev/null +++ b/src/internal/types.js @@ -0,0 +1,12 @@ +// @flow + +export type SyntheticUIEventHandler = (event?: SyntheticUIEvent) => void; + +/** + * return type of ReactDOM.findDOMNode() + * + * NOTE: `Element` is NOT the same as `type { Element } from 'react'` a.k.a React$Element + * + * To use it as a typical node, check with `if (node instanceof HTMLElement) { ... }` + */ +export type DOMNode = Element | Text | null; diff --git a/src/transitions/Fade.js b/src/transitions/Fade.js index cf75685e4b1438..9a76f4998e0a64 100644 --- a/src/transitions/Fade.js +++ b/src/transitions/Fade.js @@ -5,6 +5,7 @@ import type { Element } from 'react'; import Transition from '../internal/Transition'; import { duration } from '../styles/transitions'; import customPropTypes from '../utils/customPropTypes'; +import type { TransitionCallback } from '../internal/Transition'; type DefaultProps = { in: boolean, @@ -29,27 +30,27 @@ type Props = DefaultProps & { /** * Callback fired before the component enters. */ - onEnter?: Function, + onEnter?: TransitionCallback, /** * Callback fired when the component is entering. */ - onEntering?: Function, + onEntering?: TransitionCallback, /** * Callback fired when the component has entered. */ - onEntered?: Function, // eslint-disable-line react/sort-prop-types + onEntered?: TransitionCallback, // eslint-disable-line react/sort-prop-types /** * Callback fired before the component exits. */ - onExit?: Function, + onExit?: TransitionCallback, /** * Callback fired when the component is exiting. */ - onExiting?: Function, + onExiting?: TransitionCallback, /** * Callback fired when the component has exited. */ - onExited?: Function, // eslint-disable-line react/sort-prop-types + onExited?: TransitionCallback, // eslint-disable-line react/sort-prop-types }; class Fade extends Component { diff --git a/test/mocha.opts b/test/mocha.opts index 07ae0510cf8c51..48055d997a4ddf 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,5 @@ --require babel-register +--require jsdom-global/register --reporter dot --recursive test/utils/setup.js diff --git a/yarn.lock b/yarn.lock index 5a22731033fcea..509b84447fbcde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3601,6 +3601,10 @@ jschardet@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.4.2.tgz#2aa107f142af4121d145659d44f50830961e699a" +jsdom-global@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-3.0.2.tgz#6bd299c13b0c4626b2da2c0393cd4385d606acb9" + jsdom@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.1.0.tgz#6c48d7a48ffc5c300283c312904d15da8360509b" @@ -4988,11 +4992,7 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -q@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" - -q@~1.5.0: +q@^1.0.1, q@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"