From 4dcf38976d5f97a4058445f74d800cc3596a8557 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Thu, 20 Dec 2018 10:23:10 -1000 Subject: [PATCH] feat(ripple): typescript (#527) BREAKING CHANGE: withRipple is no longer the default export. --- package-lock.json | 6 + package.json | 3 +- packages/button/index.js | 2 +- packages/card/PrimaryContent.js | 2 +- packages/checkbox/index.tsx | 67 ++- packages/chips/Chip.js | 2 +- packages/fab/index.js | 2 +- packages/icon-button/index.js | 2 +- packages/material-icon/index.js | 2 +- packages/radio/index.js | 2 +- packages/ripple/README.md | 49 ++- packages/ripple/index.js | 265 ----------- packages/ripple/index.tsx | 316 +++++++++++++ packages/switch/ThumbUnderlay.js | 2 +- packages/tab/TabRipple.js | 2 +- scripts/karma/config.js | 6 +- test/screenshot/ripple/index.js | 45 -- test/screenshot/ripple/index.tsx | 48 ++ .../standardWithNavigationIconElement.js | 2 +- test/unit/checkbox/index.test.tsx | 53 +-- test/unit/floating-label/index.test.tsx | 11 +- test/unit/helpers/{raf.js => raf.tsx} | 38 +- test/unit/helpers/types.tsx | 3 + test/unit/index.tsx | 1 - test/unit/line-ripple/index.test.tsx | 5 +- test/unit/ripple/index.test.js | 364 --------------- test/unit/ripple/index.test.tsx | 414 ++++++++++++++++++ test/unit/text-field/index.test.tsx | 19 +- test/unit/top-app-bar/index.test.js | 2 +- tsconfig.json | 6 +- 30 files changed, 964 insertions(+), 777 deletions(-) delete mode 100644 packages/ripple/index.js create mode 100644 packages/ripple/index.tsx delete mode 100644 test/screenshot/ripple/index.js create mode 100644 test/screenshot/ripple/index.tsx rename test/unit/helpers/{raf.js => raf.tsx} (77%) create mode 100644 test/unit/helpers/types.tsx delete mode 100644 test/unit/ripple/index.test.js create mode 100644 test/unit/ripple/index.test.tsx diff --git a/package-lock.json b/package-lock.json index 08d93f5dc..012039660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17500,6 +17500,12 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "utility-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-2.1.0.tgz", + "integrity": "sha512-/nP2gqavggo6l38rtQI/CdeV+2fmBGXVvHgj9kV2MAnms3TIi77Mz9BtapPFI0+GZQCqqom0vACQ+VlTTaCovw==", + "dev": true + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index 449b2c596..9f617a337 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "webpack-dev-server --config test/screenshot/webpack.config.js --content-base test/screenshot", "stop": "./test/screenshot/stop.sh", - "build": "npm run clean && mkdirp build && webpack --config packages/webpack.config.js --progress --colors", + "build": "npm run clean && mkdirp build && node --max_old_space_size=8192 node_modules/.bin/webpack --config packages/webpack.config.js --progress --colors", "capture": "MDC_COMMIT_HASH=$(git rev-parse --short HEAD) MDC_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) mocha --require ts-node/register --require babel-core/register --ui tdd --timeout 20000 test/screenshot/capture-suite.tsx", "clean": "rm -rf build/** build packages/**/dist/", "commitmsg": "validate-commit-msg", @@ -144,6 +144,7 @@ "ts-node": "^7.0.1", "typescript": "^3.2.1", "typescript-eslint-parser": "^21.0.1", + "utility-types": "^2.1.0", "uuid": "^3.3.2", "validate-commit-msg": "^2.14.0", "webpack": "^3.11.0", diff --git a/packages/button/index.js b/packages/button/index.js index e1272ad9a..ce008cfdb 100644 --- a/packages/button/index.js +++ b/packages/button/index.js @@ -23,7 +23,7 @@ import React from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export const Button = (props) => { const { diff --git a/packages/card/PrimaryContent.js b/packages/card/PrimaryContent.js index abeb6d36e..a4ac0344c 100644 --- a/packages/card/PrimaryContent.js +++ b/packages/card/PrimaryContent.js @@ -24,7 +24,7 @@ import React from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export const PrimaryContentBase = (props) => { const { diff --git a/packages/checkbox/index.tsx b/packages/checkbox/index.tsx index 5dc2d7953..fde4f5149 100644 --- a/packages/checkbox/index.tsx +++ b/packages/checkbox/index.tsx @@ -21,34 +21,34 @@ // THE SOFTWARE. import * as React from 'react'; import * as classnames from 'classnames'; -// @ts-ignore +// @ts-ignore no mdc .d.ts file import {MDCCheckboxFoundation, MDCCheckboxAdapter} from '@material/checkbox/dist/mdc.checkbox'; -// TODO: fix with #528 -// @ts-ignore -import withRipple from '@material/react-ripple'; +import * as Ripple from '@material/react-ripple'; + import NativeControl from './NativeControl'; -export interface CheckboxProps { - checked: boolean; - className: string; - disabled: boolean; - indeterminate: boolean; +export interface CheckboxProps extends Ripple.InjectedProps { + checked?: boolean; + className?: string; + disabled?: boolean; + indeterminate?: boolean; nativeControlId?: string; - onChange: (evt: React.ChangeEvent) => void; - initRipple: (surface: HTMLDivElement, activator: HTMLInputElement) => void; + onChange?: (evt: React.ChangeEvent) => void; + initRipple: (surface: HTMLDivElement, activator?: HTMLInputElement) => void; + children?: React.ReactNode; unbounded: boolean; }; interface CheckboxState { - checked: boolean; - indeterminate: boolean; + checked?: boolean; + indeterminate?: boolean; classList: Set; 'aria-checked': boolean; }; export class Checkbox extends React.Component { - inputElement_: React.RefObject = React.createRef(); - foundation_ = MDCCheckboxFoundation; + inputElement: React.RefObject = React.createRef(); + foundation = MDCCheckboxFoundation; constructor(props: CheckboxProps) { super(props); @@ -66,18 +66,17 @@ export class Checkbox extends React.Component { disabled: false, indeterminate: false, onChange: () => {}, - initRipple: () => {}, unbounded: true, }; componentDidMount() { - this.foundation_ = new MDCCheckboxFoundation(this.adapter); - this.foundation_.init(); - this.foundation_.setDisabled(this.props.disabled); + this.foundation = new MDCCheckboxFoundation(this.adapter); + this.foundation.init(); + this.foundation.setDisabled(this.props.disabled); // indeterminate property on checkboxes is not supported: // https://github.com/facebook/react/issues/1798#issuecomment-333414857 - if (this.inputElement_.current) { - this.inputElement_.current.indeterminate = this.state.indeterminate; + if (this.inputElement.current) { + this.inputElement.current.indeterminate = this.state.indeterminate!; } } @@ -87,29 +86,29 @@ export class Checkbox extends React.Component { checked !== prevProps.checked || indeterminate !== prevProps.indeterminate ) { - this.handleChange(checked, indeterminate); + this.handleChange(checked!, indeterminate!); } if (disabled !== prevProps.disabled) { - this.foundation_.setDisabled(disabled); + this.foundation.setDisabled(disabled); } } componentWillUnmount() { - if (this.foundation_) { - this.foundation_.destroy(); + if (this.foundation) { + this.foundation.destroy(); } } init = (el: HTMLDivElement) => { - if (!this.inputElement_.current) return; - this.props.initRipple(el, this.inputElement_.current); + if (!this.inputElement.current) return; + this.props.initRipple(el, this.inputElement.current); }; handleChange = (checked: boolean, indeterminate: boolean) => { this.setState({checked, indeterminate}, () => { - this.foundation_.handleChange(); - if (this.inputElement_.current) { - this.inputElement_.current.indeterminate = indeterminate; + this.foundation.handleChange(); + if (this.inputElement.current) { + this.inputElement.current.indeterminate = indeterminate; } }); }; @@ -156,7 +155,7 @@ export class Checkbox extends React.Component { const {onChange} = this.props; const {checked, indeterminate} = evt.target; this.handleChange(checked, indeterminate); - onChange(evt); + onChange!(evt); } render() { @@ -177,7 +176,7 @@ export class Checkbox extends React.Component { return (
this.foundation_.handleAnimationEnd()} + onAnimationEnd={() => this.foundation.handleAnimationEnd()} ref={this.init} {...otherProps} > @@ -187,7 +186,7 @@ export class Checkbox extends React.Component { disabled={disabled} aria-checked={this.state['aria-checked']} onChange={this.onChange} - rippleActivatorRef={this.inputElement_} + rippleActivatorRef={this.inputElement} />
{ } } -export default withRipple(Checkbox); +export default Ripple.withRipple(Checkbox); diff --git a/packages/chips/Chip.js b/packages/chips/Chip.js index c5d6cbc5f..4f02fe1c8 100644 --- a/packages/chips/Chip.js +++ b/packages/chips/Chip.js @@ -23,7 +23,7 @@ import React, {Component} from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; import {MDCChipFoundation} from '@material/chips/dist/mdc.chips'; export class Chip extends Component { diff --git a/packages/fab/index.js b/packages/fab/index.js index 52c776147..97af1c110 100644 --- a/packages/fab/index.js +++ b/packages/fab/index.js @@ -24,7 +24,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export class Fab extends React.Component { get classes() { diff --git a/packages/icon-button/index.js b/packages/icon-button/index.js index 60a4a5658..58c80f90d 100644 --- a/packages/icon-button/index.js +++ b/packages/icon-button/index.js @@ -23,7 +23,7 @@ import React, {Component} from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; import {MDCIconButtonToggleFoundation} from '@material/icon-button/dist/mdc.iconButton'; import IconToggle from './IconToggle'; diff --git a/packages/material-icon/index.js b/packages/material-icon/index.js index 81beb28b3..e22fe8b6f 100644 --- a/packages/material-icon/index.js +++ b/packages/material-icon/index.js @@ -24,7 +24,7 @@ import React from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export default class MaterialIcon extends React.Component { render() { diff --git a/packages/radio/index.js b/packages/radio/index.js index 94198d09a..83031253c 100644 --- a/packages/radio/index.js +++ b/packages/radio/index.js @@ -24,7 +24,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import {MDCRadioFoundation} from '@material/radio/dist/mdc.radio'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; import NativeControl from './NativeControl'; class Radio extends React.Component { diff --git a/packages/ripple/README.md b/packages/ripple/README.md index 2a9fc7f01..c300914e4 100644 --- a/packages/ripple/README.md +++ b/packages/ripple/README.md @@ -40,7 +40,7 @@ import '@material/react-ripple/dist/ripple.css'; To wrap a component with the ripple HOC, please follow this example: ```js -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; const Icon = (props) => { const { @@ -75,6 +75,51 @@ const RippleIcon = withRipple(Icon); Wrap your Icon component with the HOC `withRipple`, which returns a component with a ripple capable surface. +### Typescript + +If you're using TS, you will need to extend from the provided InjectedProps. + +```js + +import {withRipple, InjectedProps} from '@material/react-ripple'; + +interface IconProps extends InjectedProps { + children?: React.ReactNode; + className: string; + initRipple: React.Ref; + unbounded: boolean; +} + +const Icon = (props) => { + const { + children, + className = '', + // You must call `initRipple` from the root element's ref. This attaches the ripple + // to the element. + initRipple, + // include `unbounded` to remove warnings when passing `otherProps` to the + // root element. + unbounded, + ...otherProps + } = props; + + // any classes needed on your component needs to be merged with + // `className` passed from `props`. + const classes = `ripple-icon-component ${className}`; + + return ( +
+ {children} +
+ ); +}; + +const RippleIcon = withRipple(Icon); +``` + ## Advanced Usage ### Ripple surface and ripple activator @@ -84,7 +129,7 @@ You may want to apply the visual treatment (CSS classes and styles) for a ripple The `initRipple` callback prop can take in an extra `activator` argument for the case where the ripple activator differs from the ripple surface. If the `activator` argument is not provided, the ripple surface will also serve as the ripple activator. ```js -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; const MyInput = (props) => { const { diff --git a/packages/ripple/index.js b/packages/ripple/index.js deleted file mode 100644 index a06243f16..000000000 --- a/packages/ripple/index.js +++ /dev/null @@ -1,265 +0,0 @@ -// The MIT License -// -// Copyright (c) 2018 Google, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -import {MDCRippleFoundation, util} from '@material/ripple/dist/mdc.ripple'; -/* TODO: remove when converting from JSX to TSX */ -/* eslint-disable */ - -const withRipple = (WrappedComponent) => { - class RippledComponent extends Component { - foundation_ = null; - - isMounted_ = true; - - state = { - classList: new Set(), - style: {}, - }; - - componentDidMount() { - if (!this.foundation_) { - throw new Error('You must call initRipple from the element\'s ' + - 'ref prop to initialize the adapter for withRipple'); - } - } - - componentWillUnmount() { - if (this.foundation_) { - this.isMounted_ = false; - this.foundation_.destroy(); - } - } - - // surface: This element receives the visual treatment (classes and style) of the ripple. - // activator: This element is used to detect whether to activate the ripple. If this is not - // provided, the ripple surface will be used to detect activation. - initializeFoundation_ = (surface, activator) => { - const adapter = this.createAdapter_(surface, activator); - this.foundation_ = new MDCRippleFoundation(adapter); - this.foundation_.init(); - } - - createAdapter_ = (surface, activator) => { - const MATCHES = util.getMatchesProperty(HTMLElement.prototype); - - return { - browserSupportsCssVars: () => util.supportsCssVariables(window), - isUnbounded: () => this.props.unbounded, - isSurfaceActive: () => activator ? activator[MATCHES](':active') : surface[MATCHES](':active'), - isSurfaceDisabled: () => this.props.disabled, - addClass: (className) => { - if (!this.isMounted_) { - return; - } - this.setState({classList: this.state.classList.add(className)}); - }, - removeClass: (className) => { - if (!this.isMounted_) { - return; - } - - const {classList} = this.state; - classList.delete(className); - this.setState({classList}); - }, - registerDocumentInteractionHandler: (evtType, handler) => - document.documentElement.addEventListener(evtType, handler, util.applyPassive()), - deregisterDocumentInteractionHandler: (evtType, handler) => - document.documentElement.removeEventListener(evtType, handler, util.applyPassive()), - registerResizeHandler: (handler) => window.addEventListener('resize', handler), - deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), - updateCssVariable: this.updateCssVariable, - computeBoundingRect: () => { - if (!this.isMounted_) { - // need to return object since foundation expects it - return {}; - } - if (this.props.computeBoundingRect) { - return this.props.computeBoundingRect(surface); - } - return surface.getBoundingClientRect(); - }, - getWindowPageOffset: () => ({x: window.pageXOffset, y: window.pageYOffset}), - }; - } - - handleFocus = (e) => { - this.props.onFocus(e); - this.foundation_.handleFocus(); - } - - handleBlur = (e) => { - this.props.onBlur(e); - this.foundation_.handleBlur(); - } - - handleMouseDown = (e) => { - this.props.onMouseDown(e); - this.activateRipple(e); - } - - handleMouseUp = (e) => { - this.props.onMouseUp(e); - this.deactivateRipple(e); - } - - handleTouchStart = (e) => { - this.props.onTouchStart(e); - this.activateRipple(e); - } - - handleTouchEnd = (e) => { - this.props.onTouchEnd(e); - this.deactivateRipple(e); - } - - handleKeyDown = (e) => { - this.props.onKeyDown(e); - this.activateRipple(e); - } - - handleKeyUp = (e) => { - this.props.onKeyUp(e); - this.deactivateRipple(e); - } - - activateRipple = (e) => { - // https://reactjs.org/docs/events.html#event-pooling - e.persist(); - requestAnimationFrame(() => { - this.foundation_.activate(e); - }); - } - - deactivateRipple = (e) => { - this.foundation_.deactivate(e); - } - - updateCssVariable = (varName, value) => { - if (!this.isMounted_) { - return; - } - - const updatedStyle = Object.assign({}, this.state.style); - updatedStyle[varName] = value; - this.setState({style: updatedStyle}); - } - - get classes() { - const {className: wrappedComponentClasses} = this.props; - const {classList} = this.state; - return classnames(Array.from(classList), wrappedComponentClasses); - } - - get style() { - const {style: wrappedStyle} = this.props; - const {style} = this.state; - return Object.assign({}, style, wrappedStyle); - } - - render() { - const { - /* start black list of otherprops */ - /* eslint-disable */ - unbounded, - style, - className, - onMouseDown, - onMouseUp, - onTouchStart, - onTouchEnd, - onKeyDown, - onKeyUp, - onFocus, - onBlur, - /* eslint-enable */ - /* end black list of otherprops */ - ...otherProps - } = this.props; - - const updatedProps = Object.assign(otherProps, { - onMouseDown: this.handleMouseDown, - onMouseUp: this.handleMouseUp, - onTouchStart: this.handleTouchStart, - onTouchEnd: this.handleTouchEnd, - onKeyDown: this.handleKeyDown, - onKeyUp: this.handleKeyUp, - onFocus: this.handleFocus, - onBlur: this.handleBlur, - // call initRipple on ref on root element that needs ripple - initRipple: this.initializeFoundation_, - className: this.classes, - style: this.style, - }); - - return ; - } - } - - WrappedComponent.propTypes = Object.assign({ - unbounded: PropTypes.bool, - disabled: PropTypes.bool, - style: PropTypes.object, - className: PropTypes.string, - onMouseDown: PropTypes.func, - onMouseUp: PropTypes.func, - onTouchStart: PropTypes.func, - onTouchEnd: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyUp: PropTypes.func, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - }, WrappedComponent.propTypes); - - WrappedComponent.defaultProps = Object.assign({ - unbounded: false, - disabled: false, - style: {}, - className: '', - onMouseDown: () => {}, - onMouseUp: () => {}, - onTouchStart: () => {}, - onTouchEnd: () => {}, - onKeyDown: () => {}, - onKeyUp: () => {}, - onFocus: () => {}, - onBlur: () => {}, - }, WrappedComponent.defaultProps); - - RippledComponent.propTypes = WrappedComponent.propTypes; - RippledComponent.defaultProps = WrappedComponent.defaultProps; - RippledComponent.displayName = `WithRipple(${getDisplayName(WrappedComponent)})`; - - return RippledComponent; -}; - -/* eslint-enable */ - -function getDisplayName(WrappedComponent) { - return WrappedComponent.displayName || WrappedComponent.name || 'Component'; -} - -export default withRipple; diff --git a/packages/ripple/index.tsx b/packages/ripple/index.tsx new file mode 100644 index 000000000..8836cc521 --- /dev/null +++ b/packages/ripple/index.tsx @@ -0,0 +1,316 @@ +// The MIT License +// +// Copyright (c) 2018 Google, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +import * as React from 'react'; +import * as classnames from 'classnames'; +import {Subtract} from 'utility-types'; // eslint-disable-line no-unused-vars +// no mdc .d.ts file +// @ts-ignore +import {MDCRippleFoundation, MDCRippleAdapter, util} from '@material/ripple/dist/mdc.ripple'; + +const MATCHES = util.getMatchesProperty(HTMLElement.prototype); + +export interface RippledComponentProps { + unbounded?: boolean; + disabled?: boolean; + style?: React.CSSProperties; + className?: string; + onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; + onTouchStart?: React.TouchEventHandler; + onTouchEnd?: React.TouchEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyUp?: React.KeyboardEventHandler; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + computeBoundingRect?: (surface: T) => ClientRect; +} + +export interface RippledComponentState { + classList: Set; + style: React.CSSProperties; +} + +// props to be injected by this HOC. +export interface InjectedProps extends RippledComponentProps { + initRipple: React.Ref | ((surface: S | null, activator?: A | null) => void); +} + +function isElement(element: any): element is Element { + return element[MATCHES as 'matches'] !== undefined; +} + +type ActivateEventTypes + = React.MouseEvent | React.TouchEvent | React.KeyboardEvent | React.FocusEvent; + +// This is an HOC that adds Ripple to the component passed as an argument +export const withRipple = < + P extends InjectedProps, + Surface extends Element = Element, + Activator extends Element = Element +>(WrappedComponent: React.ComponentType

) => class RippledComponent extends React.Component< + // Subtract removes any props "InjectedProps" if they are on "P" + // This allows the developer to override any props + // https://medium.com/@jrwebdev/react-higher-order-component-patterns-in-typescript-42278f7590fb + Subtract> & RippledComponentProps, + RippledComponentState + > { + foundation?: MDCRippleFoundation; + isComponentMounted: boolean = true; + + displayName = `WithRipple(${getDisplayName

(WrappedComponent)})`; + + state: RippledComponentState = { + classList: new Set(), + style: {}, + }; + + static defaultProps: Partial> = { + unbounded: false, + disabled: false, + style: {}, + className: '', + onMouseDown: () => {}, + onMouseUp: () => {}, + onTouchStart: () => {}, + onTouchEnd: () => {}, + onKeyDown: () => {}, + onKeyUp: () => {}, + onFocus: () => {}, + onBlur: () => {}, + ...WrappedComponent.defaultProps, + }; + + componentDidMount() { + if (!this.foundation) { + throw new Error( + 'You must call initRipple from the element\'s ' + + 'ref prop to initialize the adapter for withRipple' + ); + } + } + + componentWillUnmount() { + if (this.foundation) { + this.isComponentMounted = false; + this.foundation.destroy(); + } + } + + // surface: This element receives the visual treatment (classes and style) of the ripple. + // activator: This element is used to detect whether to activate the ripple. If this is not + // provided, the ripple surface will be used to detect activation. + initializeFoundation = (surface: Surface, activator?: Activator) => { + const adapter = this.createAdapter_(surface, activator); + this.foundation = new MDCRippleFoundation(adapter); + this.foundation.init(); + }; + + createAdapter_: MDCRippleAdapter = (surface: Surface, activator?: Activator) => { + return { + browserSupportsCssVars: () => util.supportsCssVariables(window), + isUnbounded: () => this.props.unbounded, + isSurfaceActive: () => { + if (activator) { + if (isElement(activator)) { + return activator[MATCHES as 'matches'](':active'); + } + return false; + } + + if (isElement(surface)) { + return surface[MATCHES as 'matches'](':active'); + } + return false; + }, + isSurfaceDisabled: () => this.props.disabled, + addClass: (className: string) => { + if (!this.isComponentMounted) { + return; + } + this.setState({classList: this.state.classList.add(className)}); + }, + removeClass: (className: string) => { + if (!this.isComponentMounted) { + return; + } + const {classList} = this.state; + classList.delete(className); + this.setState({classList}); + }, + registerDocumentInteractionHandler: (evtType: string, handler: EventListener) => + document.documentElement.addEventListener( + evtType, + handler, + util.applyPassive() + ), + deregisterDocumentInteractionHandler: (evtType: string, handler: EventListener) => + document.documentElement.removeEventListener( + evtType, + handler, + util.applyPassive() + ), + registerResizeHandler: (handler: EventListener) => + window.addEventListener('resize', handler), + deregisterResizeHandler: (handler: EventListener) => + window.removeEventListener('resize', handler), + updateCssVariable: this.updateCssVariable, + computeBoundingRect: () => { + if (!this.isComponentMounted) { + // need to return object since foundation expects it + return {}; + } + if (this.props.computeBoundingRect) { + return this.props.computeBoundingRect(surface); + } + return surface.getBoundingClientRect(); + }, + getWindowPageOffset: () => ({ + x: window.pageXOffset, + y: window.pageYOffset, + }), + }; + }; + + handleFocus = (e: React.FocusEvent) => { + this.props.onFocus && this.props.onFocus(e); + this.foundation.handleFocus(); + }; + + handleBlur = (e: React.FocusEvent) => { + this.props.onBlur && this.props.onBlur(e); + this.foundation.handleBlur(); + }; + + handleMouseDown = (e: React.MouseEvent) => { + this.props.onMouseDown && this.props.onMouseDown(e); + this.activateRipple(e); + }; + + handleMouseUp = (e: React.MouseEvent) => { + this.props.onMouseUp && this.props.onMouseUp(e); + this.deactivateRipple(e); + }; + + handleTouchStart = (e: React.TouchEvent) => { + this.props.onTouchStart && this.props.onTouchStart(e); + this.activateRipple(e); + }; + + handleTouchEnd = (e: React.TouchEvent) => { + this.props.onTouchEnd && this.props.onTouchEnd(e); + this.deactivateRipple(e); + }; + + handleKeyDown = (e: React.KeyboardEvent) => { + this.props.onKeyDown && this.props.onKeyDown(e); + this.activateRipple(e); + }; + + handleKeyUp = (e: React.KeyboardEvent) => { + this.props.onKeyUp && this.props.onKeyUp(e); + this.deactivateRipple(e); + }; + + activateRipple = (e: ActivateEventTypes) => { + // https://reactjs.org/docs/events.html#event-pooling + e.persist(); + requestAnimationFrame(() => { + this.foundation.activate(e); + }); + }; + + deactivateRipple = (e: ActivateEventTypes) => { + this.foundation.deactivate(e); + }; + + updateCssVariable = (varName: keyof React.CSSProperties, value: string | number) => { + if (!this.isComponentMounted) { + return; + } + this.setState((prevState) => { + const updatedStyle = Object.assign({}, this.state.style) as React.CSSProperties; + updatedStyle[varName] = value; + return Object.assign(prevState, { + style: updatedStyle, + }); + }); + }; + + get classes() { + const {className: wrappedComponentClasses} = this.props; + const {classList} = this.state; + return classnames(Array.from(classList), wrappedComponentClasses); + } + + get style() { + const {style: wrappedStyle} = this.props; + const {style} = this.state; + return Object.assign({}, style, wrappedStyle); + } + + render() { + const { + /* eslint-disable */ + unbounded, + style, + className, + onMouseDown, + onMouseUp, + onTouchStart, + onTouchEnd, + onKeyDown, + onKeyUp, + onFocus, + onBlur, + /* eslint-enable */ + ...otherProps + } = this.props as P; + + const updatedProps = { + ...otherProps, + onMouseDown: this.handleMouseDown, + onMouseUp: this.handleMouseUp, + onTouchStart: this.handleTouchStart, + onTouchEnd: this.handleTouchEnd, + onKeyDown: this.handleKeyDown, + onKeyUp: this.handleKeyUp, + onFocus: this.handleFocus, + onBlur: this.handleBlur, + initRipple: this.initializeFoundation, + className: this.classes, + style: this.style, + }; + + return ( + // this issue is only appearing in TS v3.2.x. I am not seeing this issue appear in v2.9.1 + // @ts-ignore + + ); + } + }; + + +function getDisplayName

(WrappedComponent: React.ComponentType

): string { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} diff --git a/packages/switch/ThumbUnderlay.js b/packages/switch/ThumbUnderlay.js index c315feac1..c18d0b1b9 100644 --- a/packages/switch/ThumbUnderlay.js +++ b/packages/switch/ThumbUnderlay.js @@ -23,7 +23,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export class ThumbUnderlay extends React.Component { init = (el) => { diff --git a/packages/tab/TabRipple.js b/packages/tab/TabRipple.js index a9397d503..b344a9885 100644 --- a/packages/tab/TabRipple.js +++ b/packages/tab/TabRipple.js @@ -23,7 +23,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import withRipple from '@material/react-ripple'; +import {withRipple} from '@material/react-ripple'; export class TabRipple extends React.Component { get classes() { diff --git a/scripts/karma/config.js b/scripts/karma/config.js index e71af8b6d..726b9879f 100644 --- a/scripts/karma/config.js +++ b/scripts/karma/config.js @@ -65,9 +65,9 @@ module.exports = { ], }, plugins: [ - new webpack.SourceMapDevToolPlugin({ - test: /\.(tsx|js)($|\?)/i, - }), + // new webpack.SourceMapDevToolPlugin({ + // test: /\.(tsx|js)($|\?)/i, + // }), ], node: { fs: 'empty', diff --git a/test/screenshot/ripple/index.js b/test/screenshot/ripple/index.js deleted file mode 100644 index c529c8231..000000000 --- a/test/screenshot/ripple/index.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; - -import withRipple from '../../../packages/ripple'; - -import './index.scss'; - -/*eslint-disable */ -const Div = ({children, className = '', initRipple, unbounded, ...otherProps}) => { -/* eslint-enable */ - const classes = `ripple-test-component ${className}`; - return ( -

- {children} -
- ); -}; - -const DivRipple = withRipple(Div); - - -const RippleScreenshotTest = () => { - return ( -
- - Woof - - -
- - - Disabled - - -
- - - Unbounded - -
- ); -}; - -export default RippleScreenshotTest; diff --git a/test/screenshot/ripple/index.tsx b/test/screenshot/ripple/index.tsx new file mode 100644 index 000000000..0354b0ca3 --- /dev/null +++ b/test/screenshot/ripple/index.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import {withRipple, InjectedProps} from '../../../packages/ripple'; +import './index.scss'; + +interface DivProps extends InjectedProps { + children: React.ReactNode, + className: string, + initRipple: (surface: HTMLDivElement) => void, + unbounded: boolean, +} + +/*eslint-disable */ +const Div: React.FunctionComponent = ({ + children, + className = "", + initRipple, + unbounded, + ...otherProps +}) => { + /* eslint-enable */ + const classes = `ripple-test-component ${className}`; + return ( +
+ {children} +
+ ); +}; + +const DivRipple = withRipple(Div); + +const RippleScreenshotTest: React.FunctionComponent = () => { + return ( +
+ Woof + +
+ + Disabled + +
+ + + Unbounded + +
+ ); +}; +export default RippleScreenshotTest; diff --git a/test/screenshot/top-app-bar/standardWithNavigationIconElement.js b/test/screenshot/top-app-bar/standardWithNavigationIconElement.js index 65d414086..86d908803 100644 --- a/test/screenshot/top-app-bar/standardWithNavigationIconElement.js +++ b/test/screenshot/top-app-bar/standardWithNavigationIconElement.js @@ -1,7 +1,7 @@ import React from 'react'; import TopAppBar from '../../../packages/top-app-bar'; import MaterialIcon from '../../../packages/material-icon'; -import withRipple from '../../../packages/ripple'; +import {withRipple} from '../../../packages/ripple'; import MainTopAppBarContent from './mainContent'; const NavigationIcon = ({ diff --git a/test/unit/checkbox/index.test.tsx b/test/unit/checkbox/index.test.tsx index cc33555fb..2f122334f 100644 --- a/test/unit/checkbox/index.test.tsx +++ b/test/unit/checkbox/index.test.tsx @@ -3,12 +3,13 @@ import {assert} from 'chai'; import {shallow} from 'enzyme'; import * as td from 'testdouble'; import {Checkbox} from '../../../packages/checkbox/index'; +import {coerceForTesting} from '../helpers/types'; suite('Checkbox'); test('creates foundation', () => { const wrapper = shallow(); - assert.exists(wrapper.instance().foundation_); + assert.exists(wrapper.instance().foundation); }); test('has mdc-checkbox class', () => { @@ -45,30 +46,30 @@ test('native control props.checked is true when props.checked is true', () => { assert.isTrue(nativeControl.props().checked); }); -test('#foundation_.handleChange gets called when prop.checked updates', () => { +test('#foundation.handleChange gets called when prop.checked updates', () => { const wrapper = shallow(); - wrapper.instance().foundation_.handleChange = td.func(); + wrapper.instance().foundation.handleChange = td.func(); wrapper.setProps({checked: true}); - td.verify(wrapper.instance().foundation_.handleChange(), {times: 1}); + td.verify(wrapper.instance().foundation.handleChange(), {times: 1}); }); -test('#foundation_.handleChange gets called when prop.indeterminate updates', () => { +test('#foundation.handleChange gets called when prop.indeterminate updates', () => { const wrapper = shallow(); - wrapper.instance().foundation_.handleChange = td.func(); + wrapper.instance().foundation.handleChange = td.func(); wrapper.setProps({indeterminate: true}); - td.verify(wrapper.instance().foundation_.handleChange(), {times: 1}); + td.verify(wrapper.instance().foundation.handleChange(), {times: 1}); }); -test('#foundation_.setDisabled gets called when prop.disabled updates', () => { +test('#foundation.setDisabled gets called when prop.disabled updates', () => { const wrapper = shallow(); - wrapper.instance().foundation_.setDisabled = td.func(); + wrapper.instance().foundation.setDisabled = td.func(); wrapper.setProps({disabled: true}); - td.verify(wrapper.instance().foundation_.setDisabled(true), {times: 1}); + td.verify(wrapper.instance().foundation.setDisabled(true), {times: 1}); }); test('#componentWillUnmount destroys foundation', () => { const wrapper = shallow(); - const foundation = wrapper.instance().foundation_; + const foundation = wrapper.instance().foundation; foundation.destroy = td.func(); wrapper.unmount(); td.verify(foundation.destroy(), {times: 1}); @@ -76,46 +77,46 @@ test('#componentWillUnmount destroys foundation', () => { test('#adapter.addClass adds class to state.classList', () => { const wrapper = shallow(); - wrapper.instance().foundation_.adapter_.addClass('test-class-name'); + wrapper.instance().foundation.adapter_.addClass('test-class-name'); assert.isTrue(wrapper.state().classList.has('test-class-name')); }); test('#adapter.removeClass removes class from state.classList', () => { const wrapper = shallow(); wrapper.setState({classList: new Set(['test-class-name'])}); - wrapper.instance().foundation_.adapter_.removeClass('test-class-name'); + wrapper.instance().foundation.adapter_.removeClass('test-class-name'); assert.isFalse(wrapper.state().classList.has('test-class-name')); }); test('#adapter.isChecked returns state.checked if true', () => { const wrapper = shallow(); wrapper.setState({checked: true}); - assert.isTrue(wrapper.instance().foundation_.adapter_.isChecked()); + assert.isTrue(wrapper.instance().foundation.adapter_.isChecked()); }); test('#adapter.isChecked returns state.checked if false', () => { const wrapper = shallow(); wrapper.setState({checked: false}); - assert.isFalse(wrapper.instance().foundation_.adapter_.isChecked()); + assert.isFalse(wrapper.instance().foundation.adapter_.isChecked()); }); test('#adapter.isIndeterminate returns state.indeterminate if true', () => { const wrapper = shallow(); wrapper.setState({indeterminate: true}); - assert.isTrue(wrapper.instance().foundation_.adapter_.isIndeterminate()); + assert.isTrue(wrapper.instance().foundation.adapter_.isIndeterminate()); }); test('#adapter.isIndeterminate returns state.indeterminate if false', () => { const wrapper = shallow(); wrapper.setState({indeterminate: false}); - assert.isFalse(wrapper.instance().foundation_.adapter_.isIndeterminate()); + assert.isFalse(wrapper.instance().foundation.adapter_.isIndeterminate()); }); test('#adapter.setNativeControlAttr sets aria-checked state', () => { const wrapper = shallow(); wrapper .instance() - .foundation_.adapter_.setNativeControlAttr('aria-checked', true); + .foundation.adapter_.setNativeControlAttr('aria-checked', true); assert.isTrue(wrapper.state()['aria-checked']); }); @@ -124,7 +125,7 @@ test('#adapter.removeNativeControlAttr sets aria-checked state as false', () => wrapper.setState({'aria-checked': true}); wrapper .instance() - .foundation_.adapter_.removeNativeControlAttr('aria-checked'); + .foundation.adapter_.removeNativeControlAttr('aria-checked'); assert.isFalse(wrapper.state()['aria-checked']); }); @@ -142,21 +143,21 @@ test('calls foundation.handleChange in native control props.onChange', () => { indeterminate: false, }, }; - wrapper.instance().foundation_.handleChange = td.func(); + wrapper.instance().foundation.handleChange = td.func(); nativeControl.simulate('change', mockEvt); - td.verify(wrapper.instance().foundation_.handleChange(), {times: 1}); + td.verify(wrapper.instance().foundation.handleChange(), {times: 1}); }); test('calls props.onChange in native control props.onChange', () => { - const onChange = td.func() as (evt: React.ChangeEvent) => void; + const onChange = coerceForTesting<(evt: React.ChangeEvent) => void>(td.func()); const wrapper = shallow(); const nativeControl = wrapper.childAt(0); - const mockEvt = ({ - target: ({ + const mockEvt = coerceForTesting>({ + target: { checked: true, indeterminate: false, - } as HTMLInputElement), - } as React.ChangeEvent); + }, + }); nativeControl.simulate('change', mockEvt); td.verify(onChange(mockEvt), {times: 1}); }); diff --git a/test/unit/floating-label/index.test.tsx b/test/unit/floating-label/index.test.tsx index 57a6fd44f..b28b17a62 100644 --- a/test/unit/floating-label/index.test.tsx +++ b/test/unit/floating-label/index.test.tsx @@ -4,6 +4,7 @@ import {suite, test} from 'mocha'; import {assert} from 'chai'; import {mount, shallow} from 'enzyme'; import FloatingLabel from '../../../packages/floating-label/index'; +import {coerceForTesting} from '../helpers/types'; suite('Floating Label'); @@ -36,7 +37,7 @@ test('initializing with float to true floats the label', () => { }); test('calls handleWidthChange with the offhandleWidthChange of the labelElement_', () => { - const handleWidthChange = td.func() as (width: number) => void; + const handleWidthChange = coerceForTesting<(width: number) => void>(td.func()); const div = document.createElement('div'); // needs to be attached to real DOM to get width // https://github.com/airbnb/enzyme/issues/1525 @@ -46,12 +47,12 @@ test('calls handleWidthChange with the offhandleWidthChange of the labelElement_ Test, options ); - td.verify(handleWidthChange((wrapper.getDOMNode() as HTMLLabelElement).offsetWidth), {times: 1}); + td.verify(handleWidthChange(coerceForTesting(wrapper.getDOMNode()).offsetWidth), {times: 1}); div.remove(); }); test('#componentDidUpdate updating the children updates width', () => { - const handleWidthChange = td.func() as (width: number) => void; + const handleWidthChange = coerceForTesting<(width: number) => void>(td.func()); const div = document.createElement('div'); // needs to be attached to real DOM to get width // https://github.com/airbnb/enzyme/issues/1525 @@ -62,9 +63,9 @@ test('#componentDidUpdate updating the children updates width', () => { options ); - const firstLength = (wrapper.getDOMNode() as HTMLLabelElement).offsetWidth; + const firstLength = coerceForTesting(wrapper.getDOMNode()).offsetWidth; wrapper.setProps({children: 'Test More Text'}); - const secondLength = (wrapper.getDOMNode() as HTMLLabelElement).offsetWidth; + const secondLength = coerceForTesting(wrapper.getDOMNode()).offsetWidth; td.verify(handleWidthChange(firstLength), {times: 1}); td.verify(handleWidthChange(secondLength), {times: 1}); div.remove(); diff --git a/test/unit/helpers/raf.js b/test/unit/helpers/raf.tsx similarity index 77% rename from test/unit/helpers/raf.js rename to test/unit/helpers/raf.tsx index 94260c508..e627b38ca 100644 --- a/test/unit/helpers/raf.js +++ b/test/unit/helpers/raf.tsx @@ -19,7 +19,6 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. - // Creates an object which stubs out window.{requestAnimationFrame,cancelAnimationFrame}, and returns an object // that gives control over to when animation frames are executed. It works a lot like lolex, but for rAF. // @@ -35,32 +34,55 @@ // raf.flush(); // logs "first frame inner" // raf.restore(); // window.{rAF,cAF} set back to normal. // ``` -export function createMockRaf() { + +interface Frame { + id: number, + fn: FrameRequestCallback, +} + +interface MockRaf { + lastFrameId: number, + pendingFrames: Frame[], + flush: () => void, + restore: () => void, + requestAnimationFrame: (fn: FrameRequestCallback) => number, + cancelAnimationFrame: (id: number) => void, +} + +// creates a mock of a requestAnimationFrame. This allows tests to synchronously run. +export function createMockRaf(): MockRaf { const origRaf = window.requestAnimationFrame; const origCancel = window.cancelAnimationFrame; - const mockRaf = { + + const mockRaf: MockRaf = { lastFrameId: 0, pendingFrames: [], + flush() { const framesToRun = this.pendingFrames.slice(); while (framesToRun.length) { - const {id, fn} = framesToRun.shift(); - fn(); + const frame = framesToRun.shift(); + if (!frame) return; + const {id, fn} = frame; + fn(id); // Short-cut to remove the frame from the actual pendingFrames array. cancelAnimationFrame(id); } }, + restore() { this.lastFrameId = 0; this.pendingFrames = []; window.requestAnimationFrame = origRaf; window.cancelAnimationFrame = origCancel; }, - requestAnimationFrame(fn) { + + requestAnimationFrame(fn: FrameRequestCallback): number { const frameId = ++this.lastFrameId; this.pendingFrames.push({id: frameId, fn}); return frameId; }, + cancelAnimationFrame(id) { for (let i = 0, frame; (frame = this.pendingFrames[i]); i++) { if (frame.id === id) { @@ -71,8 +93,8 @@ export function createMockRaf() { }, }; - window.requestAnimationFrame = (fn) => mockRaf.requestAnimationFrame(fn); - window.cancelAnimationFrame = (id) => mockRaf.cancelAnimationFrame(id); + window.requestAnimationFrame = (fn: FrameRequestCallback) => mockRaf.requestAnimationFrame(fn); + window.cancelAnimationFrame = (id: number) => mockRaf.cancelAnimationFrame(id); return mockRaf; } diff --git a/test/unit/helpers/types.tsx b/test/unit/helpers/types.tsx new file mode 100644 index 000000000..0b5e8c2ef --- /dev/null +++ b/test/unit/helpers/types.tsx @@ -0,0 +1,3 @@ +export function coerceForTesting(value: {}): T { + return value as T; +} diff --git a/test/unit/index.tsx b/test/unit/index.tsx index fbd8435cc..033748dab 100644 --- a/test/unit/index.tsx +++ b/test/unit/index.tsx @@ -1,6 +1,5 @@ import * as Enzyme from 'enzyme'; import * as Adapter from 'enzyme-adapter-react-16'; - Enzyme.configure({adapter: new Adapter()}); const context = require.context('.', true, /\.test\.(j|t)sx?$/); context.keys().forEach(context); diff --git a/test/unit/line-ripple/index.test.tsx b/test/unit/line-ripple/index.test.tsx index 4d0ee4b02..c8bd9dad2 100644 --- a/test/unit/line-ripple/index.test.tsx +++ b/test/unit/line-ripple/index.test.tsx @@ -3,6 +3,7 @@ import * as td from 'testdouble'; import {assert} from 'chai'; import {shallow} from 'enzyme'; import LineRipple from '../../../packages/line-ripple/index'; +import {coerceForTesting} from '../helpers/types'; suite('LineRipple'); @@ -98,7 +99,7 @@ test('#adapter.hasClass returns true if exists in classList', () => { test('#adapter.setStyle updates style', () => { const wrapper = shallow(); wrapper.instance().foundation_.adapter_.setStyle('color', 'blue'); - const style = wrapper.state().style as React.CSSProperties; + const style = coerceForTesting(wrapper.state().style); assert.equal(style.color, 'blue'); }); @@ -123,7 +124,7 @@ test('#componentWillUnmount destroys foundation', () => { test('#adapter.setStyle updates style names to camel case', () => { const wrapper = shallow(); wrapper.instance().foundation_.adapter_.setStyle('transform-origin', 25); - const style = (wrapper.state().style as React.CSSProperties); + const style = coerceForTesting(wrapper.state().style); assert.equal(style.transformOrigin, 25); // @ts-ignore assert.equal(style['transform-origin'], undefined); diff --git a/test/unit/ripple/index.test.js b/test/unit/ripple/index.test.js deleted file mode 100644 index c07bfb9e8..000000000 --- a/test/unit/ripple/index.test.js +++ /dev/null @@ -1,364 +0,0 @@ -import React from 'react'; -import {assert} from 'chai'; -import td from 'testdouble'; -// must use mount for refs to work -import {mount} from 'enzyme'; -import withRipple from '../../../packages/ripple/index'; -import {createMockRaf} from '../helpers/raf'; - -/*eslint-disable */ -const Div = ({children, className = '', initRipple, unbounded, ...otherProps}) => { -/* eslint-enable */ - const classes = `ripple-test-component ${className}`; - return ( -
- {children} -
- ); -}; - -const DivRipple = withRipple(Div); - -suite('Ripple'); - -test('classNames adds classes', () => { - const mockRaf = createMockRaf(); - const wrapper = mount(); - mockRaf.flush(); - - assert.equal(wrapper.find('.ripple-test-component').length, 1); - // need update because setState is async - assert.isTrue( - wrapper.update() - .find('.ripple-test-component') - .hasClass('mdc-ripple-upgraded')); - - mockRaf.restore(); -}); - -test('mouseDown event triggers activateRipple', () => { - const mockRaf = createMockRaf(); - const mouseDownHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.activate = td.func(); - wrapper.simulate('mouseDown'); - mockRaf.flush(); - td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); - td.verify(mouseDownHandler(td.matchers.isA(Object)), {times: 1}); - mockRaf.restore(); -}); - -test('mouseUp event triggers deactivateRipple', () => { - const mouseUpHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('mouseUp'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); - td.verify(mouseUpHandler(td.matchers.isA(Object)), {times: 1}); -}); - -test('mouseUp event triggers deactivateRipple with no onMouseUp handler', () => { - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('mouseUp'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); -}); - -test('touchStart event triggers activateRipple', () => { - const mockRaf = createMockRaf(); - const touchStartHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.activate = td.func(); - wrapper.simulate('touchStart'); - mockRaf.flush(); - td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); - td.verify(touchStartHandler(td.matchers.isA(Object)), {times: 1}); - mockRaf.restore(); -}); - -test('touchStart event triggers activateRipple with no onTouchStart handler', () => { - const mockRaf = createMockRaf(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.activate = td.func(); - wrapper.simulate('touchStart'); - mockRaf.flush(); - td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); - mockRaf.restore(); -}); - -test('touchEnd event triggers deactivateRipple', () => { - const touchEndHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('touchEnd'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); - td.verify(touchEndHandler(td.matchers.isA(Object)), {times: 1}); -}); - -test('touchEnd event triggers deactivateRipple with no onTouchEnd handler', () => { - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('touchEnd'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); -}); - -test('keyDown event triggers activateRipple', () => { - const mockRaf = createMockRaf(); - const keyDownHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.activate = td.func(); - wrapper.simulate('keyDown'); - mockRaf.flush(); - td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); - td.verify(keyDownHandler(td.matchers.isA(Object)), {times: 1}); - mockRaf.restore(); -}); - -test('keyDown event triggers activateRipple with no onKeyDown handler', () => { - const mockRaf = createMockRaf(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.activate = td.func(); - wrapper.simulate('keyDown'); - mockRaf.flush(); - td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); - mockRaf.restore(); -}); - -test('keyUp event triggers deactivateRipple', () => { - const keyUpHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('keyUp'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); - td.verify(keyUpHandler(td.matchers.isA(Object)), {times: 1}); -}); - -test('keyUp event triggers deactivateRipple with no onKeyUp handler', () => { - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.deactivate = td.func(); - wrapper.simulate('keyUp'); - td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); -}); - -test('focus event proxies to foundation focus handler', () => { - const focusHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.handleFocus = td.func(); - wrapper.simulate('focus'); - td.verify(foundation.handleFocus(), {times: 1}); - td.verify(focusHandler(td.matchers.isA(Object)), {times: 1}); -}); - -test('focus event proxies to foundation focus handler with no onFocus handler', () => { - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.handleFocus = td.func(); - wrapper.simulate('focus'); - td.verify(foundation.handleFocus(), {times: 1}); -}); - -test('blur event proxies to foundation blur handler', () => { - const blurHandler = td.func(); - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.handleBlur = td.func(); - wrapper.simulate('blur'); - td.verify(foundation.handleBlur(), {times: 1}); - td.verify(blurHandler(td.matchers.isA(Object)), {times: 1}); -}); - -test('blur event proxies to foundation blur handler with no onBlur handler', () => { - const wrapper = mount(); - const foundation = wrapper.instance().foundation_; - foundation.handleBlur = td.func(); - wrapper.simulate('blur'); - td.verify(foundation.handleBlur(), {times: 1}); -}); - -test('#adapter.isUnbounded returns true is prop is set', () => { - const wrapper = mount(); - assert.isTrue(wrapper.instance().foundation_.adapter_.isUnbounded()); -}); - -test('#adapter.isUnbounded returns false prop is not set', () => { - const wrapper = mount(); - assert.isFalse(wrapper.instance().foundation_.adapter_.isUnbounded()); -}); - -test('#adapter.isSurfaceDisabled returns true is prop is set', () => { - const wrapper = mount(); - assert.isTrue(wrapper.instance().foundation_.adapter_.isSurfaceDisabled()); -}); - -test('#adapter.isSurfaceDisabled returns false prop is not set', () => { - const wrapper = mount(); - assert.isFalse(wrapper.instance().foundation_.adapter_.isSurfaceDisabled()); -}); - -test('#adapter.addClass adds a class to the root element', () => { - const wrapper = mount(); - wrapper.instance().foundation_.adapter_.addClass('test-class'); - assert.isTrue( - wrapper.update() - .find('.ripple-test-component') - .hasClass('test-class')); -}); - -test('#adapter.addClass does not add class if isMounted is false', () => { - const wrapper = mount(); - wrapper.instance().isMounted_ = false; - wrapper.instance().foundation_.adapter_.addClass('test-class'); - assert.isFalse( - wrapper.update() - .find('.ripple-test-component') - .hasClass('test-class')); -}); - -test('#adapter.removeClass removes a class to the root element', () => { - const wrapper = mount(); - wrapper.instance().foundation_.adapter_.addClass('test-class'); - - wrapper.update(); - wrapper.instance().foundation_.adapter_.removeClass('test-class'); - - assert.isFalse( - wrapper.update() - .find('.ripple-test-component') - .hasClass('test-class')); -}); - -test('#adapter.removeClass removes a class to the root element', () => { - const wrapper = mount(); - wrapper.instance().foundation_.adapter_.addClass('test-class'); - - wrapper.instance().isMounted_ = false; - wrapper.update(); - wrapper.instance().foundation_.adapter_.removeClass('test-class'); - - assert.isTrue( - wrapper.update() - .find('.ripple-test-component') - .hasClass('test-class')); -}); - -test('#adapter.updateCssVariable updates style', () => { - const wrapper = mount(); - wrapper.instance().foundation_.adapter_.updateCssVariable('color', 'blue'); - assert.equal(wrapper.state().style.color, 'blue'); -}); - -test('#adapter.updateCssVariable does not update style if isMounted_ is false', () => { - const wrapper = mount(); - wrapper.instance().isMounted_ = false; - wrapper.instance().foundation_.adapter_.updateCssVariable('color', 'blue'); - assert.notEqual(wrapper.state().style.color, 'blue'); -}); - -test('#adapter.registerDocumentInteractionHandler triggers handler on document scroll', () => { - const wrapper = mount(); - const testHandler = td.func(); - wrapper.instance().foundation_.adapter_.registerDocumentInteractionHandler('scroll', testHandler); - const event = new Event('scroll'); - document.documentElement.dispatchEvent(event); - td.verify(testHandler(event), {times: 1}); -}); - -test('#adapter.deregisterDocumentInteractionHandler does not trigger handler on document scroll', () => { - const wrapper = mount(); - const testHandler = td.func(); - wrapper.instance().foundation_.adapter_.registerDocumentInteractionHandler('scroll', testHandler); - const event = new Event('scroll'); - wrapper.instance().foundation_.adapter_.deregisterDocumentInteractionHandler('scroll', testHandler); - document.documentElement.dispatchEvent(event); - td.verify(testHandler(event), {times: 0}); -}); - -test('#adapter.registerResizeHandler triggers handler on window resize', () => { - const wrapper = mount(); - const testHandler = td.func(); - wrapper.instance().foundation_.adapter_.registerResizeHandler(testHandler); - const event = new Event('resize'); - window.dispatchEvent(event); - td.verify(testHandler(event), {times: 1}); -}); - -test('#adapter.deregisterResizeHandler does not trigger handler ' + - 'after registering resize handler', () => { - const wrapper = mount(); - const testHandler = td.func(); - wrapper.instance().foundation_.adapter_.registerResizeHandler(testHandler); - const event = new Event('resize'); - wrapper.instance().foundation_.adapter_.deregisterResizeHandler(testHandler); - window.dispatchEvent(event); - td.verify(testHandler(event), {times: 0}); -}); - -test('#adapter.computeBoundingRect returns height and width', () => { - const wrapper = mount(); - const domRect = { - x: 0, y: 0, width: 0, height: 0, top: 0, - right: 0, bottom: 0, left: 0, - }; - assert.deepInclude(wrapper.update().instance().foundation_.adapter_.computeBoundingRect(), domRect); -}); - -test('#adapter.getWindowPageOffset returns height and width', () => { - const wrapper = mount(); - const offset = {x: 0, y: 0}; - assert.deepEqual(wrapper.update().instance().foundation_.adapter_.getWindowPageOffset(), offset); -}); - -test('#componentWillUnmount destroys foundation', () => { - const mockRaf = createMockRaf(); - const wrapper = mount(); - mockRaf.flush(); - - const foundation = wrapper.instance().foundation_; - foundation.destroy = td.func(); - wrapper.unmount(); - td.verify(foundation.destroy()); - mockRaf.restore(); -}); - -test('throws error if no foundation', () => { - const DivNoRef = () =>
; - const DivNoRefRipple = withRipple(DivNoRef); - assert.throws(DivNoRefRipple.prototype.componentDidMount); -}); - -test('unmounting component does not throw errors', (done) => { - // related to - // https://github.com/material-components/material-components-web-react/issues/199 - class TestComp extends React.Component { - state = {showRippleElement: true}; - - render() { - if (!this.state.showRippleElement) return (hi); - - return ( - this.setState({showRippleElement: false})} /> - ); - } - }; - - const wrapper = mount(); - wrapper.simulate('mouseDown'); - requestAnimationFrame(() => { - assert.equal(wrapper.getDOMNode().innerText, 'hi'); - done(); - }); -}); diff --git a/test/unit/ripple/index.test.tsx b/test/unit/ripple/index.test.tsx new file mode 100644 index 000000000..656a1d8b5 --- /dev/null +++ b/test/unit/ripple/index.test.tsx @@ -0,0 +1,414 @@ +import * as React from 'react'; +import {assert} from 'chai'; +import * as td from 'testdouble'; +// no mdc .d.ts file +// @ts-ignore +import {MDCRippleFoundation} from '@material/ripple'; +import {mount} from 'enzyme'; +import {withRipple, InjectedProps} from '../../../packages/ripple/index'; +import {createMockRaf} from '../helpers/raf'; +import {coerceForTesting} from '../helpers/types'; + +interface DivProps extends InjectedProps { + children?: React.ReactNode; + className: string; + initRipple: React.Ref; + unbounded: boolean; +} + +const Div: React.FunctionComponent = ({ + /* eslint-disable react/prop-types */ + children, + className = '', + initRipple, + unbounded, + /* eslint-enable */ + ...otherProps +}) => { + const classes = `ripple-test-component ${className}`; + return ( +
+ {children} +
+ ); +}; + +const DivRipple = withRipple(Div); + +interface RippledComponent extends React.Component> { + foundation: MDCRippleFoundation + isComponentMounted: boolean; +} + +suite('Ripple'); + +test('classNames adds classes', () => { + const mockRaf = createMockRaf(); + const wrapper = mount(); + mockRaf.flush(); + assert.equal(wrapper.find('.ripple-test-component').length, 1); + // need update because setState is async + assert.isTrue( + wrapper + .update() + .find('.ripple-test-component') + .hasClass('mdc-ripple-upgraded') + ); + mockRaf.restore(); +}); + +test('mouseDown event triggers activateRipple', () => { + const mockRaf = createMockRaf(); + const mouseDownHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.activate = td.func(); + wrapper.simulate('mouseDown'); + mockRaf.flush(); + td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); + td.verify(mouseDownHandler(td.matchers.isA(Object)), {times: 1}); + mockRaf.restore(); +}); + +test('mouseUp event triggers deactivateRipple', () => { + const mouseUpHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('mouseUp'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); + td.verify(mouseUpHandler(td.matchers.isA(Object)), {times: 1}); +}); + +test('mouseUp event triggers deactivateRipple with no onMouseUp handler', () => { + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('mouseUp'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); +}); + +test('touchStart event triggers activateRipple', () => { + const mockRaf = createMockRaf(); + const touchStartHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.activate = td.func(); + wrapper.simulate('touchStart'); + mockRaf.flush(); + td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); + td.verify(touchStartHandler(td.matchers.isA(Object)), {times: 1}); + mockRaf.restore(); +}); + +test('touchStart event triggers activateRipple with no onTouchStart handler', () => { + const mockRaf = createMockRaf(); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.activate = td.func(); + wrapper.simulate('touchStart'); + mockRaf.flush(); + td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); + mockRaf.restore(); +}); + +test('touchEnd event triggers deactivateRipple', () => { + const touchEndHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('touchEnd'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); + td.verify(touchEndHandler(td.matchers.isA(Object)), {times: 1}); +}); + +test('touchEnd event triggers deactivateRipple with no onTouchEnd handler', () => { + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('touchEnd'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); +}); + +test('keyDown event triggers activateRipple', () => { + const mockRaf = createMockRaf(); + const keyDownHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.activate = td.func(); + wrapper.simulate('keyDown'); + mockRaf.flush(); + td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); + td.verify(keyDownHandler(td.matchers.isA(Object)), {times: 1}); + mockRaf.restore(); +}); + +test('keyDown event triggers activateRipple with no onKeyDown handler', () => { + const mockRaf = createMockRaf(); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.activate = td.func(); + wrapper.simulate('keyDown'); + mockRaf.flush(); + td.verify(foundation.activate(td.matchers.isA(Object)), {times: 1}); + mockRaf.restore(); +}); + +test('keyUp event triggers deactivateRipple', () => { + const keyUpHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('keyUp'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); + td.verify(keyUpHandler(td.matchers.isA(Object)), {times: 1}); +}); + +test('keyUp event triggers deactivateRipple with no onKeyUp handler', () => { + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.deactivate = td.func(); + wrapper.simulate('keyUp'); + td.verify(foundation.deactivate(td.matchers.isA(Object)), {times: 1}); +}); + +test('focus event proxies to foundation focus handler', () => { + const focusHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.handleFocus = td.func(); + wrapper.simulate('focus'); + td.verify(foundation.handleFocus(), {times: 1}); + td.verify(focusHandler(td.matchers.isA(Object)), {times: 1}); +}); + +test('focus event proxies to foundation focus handler with no onFocus handler', () => { + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.handleFocus = td.func(); + wrapper.simulate('focus'); + td.verify(foundation.handleFocus(), {times: 1}); +}); + +test('blur event proxies to foundation blur handler', () => { + const blurHandler = coerceForTesting(td.func()); + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.handleBlur = td.func(); + wrapper.simulate('blur'); + td.verify(foundation.handleBlur(), {times: 1}); + td.verify(blurHandler(td.matchers.isA(Object)), {times: 1}); +}); + +test('blur event proxies to foundation blur handler with no onBlur handler', () => { + const wrapper = mount(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.handleBlur = td.func(); + wrapper.simulate('blur'); + td.verify(foundation.handleBlur(), {times: 1}); +}); + +test('#adapter.isUnbounded returns true is prop is set', () => { + const wrapper = mount(); + assert.isTrue(coerceForTesting(wrapper.instance()).foundation.adapter_.isUnbounded()); +}); + +test('#adapter.isUnbounded returns false prop is not set', () => { + const wrapper = mount(); + assert.isFalse(coerceForTesting(wrapper.instance()).foundation.adapter_.isUnbounded()); +}); + +test('#adapter.isSurfaceDisabled returns true is prop is set', () => { + const wrapper = mount(); + assert.isTrue(coerceForTesting(wrapper.instance()).foundation.adapter_.isSurfaceDisabled()); +}); + +test('#adapter.isSurfaceDisabled returns false prop is not set', () => { + const wrapper = mount(); + assert.isFalse(coerceForTesting(wrapper.instance()).foundation.adapter_.isSurfaceDisabled()); +}); + +test('#adapter.addClass adds a class to the root element', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).foundation.adapter_.addClass('test-class'); + assert.isTrue( + wrapper + .update() + .find('.ripple-test-component') + .hasClass('test-class') + ); +}); + +test('#adapter.addClass does not add class if isMounted is false', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).isComponentMounted = false; + coerceForTesting(wrapper.instance()).foundation.adapter_.addClass('test-class'); + assert.isFalse( + wrapper + .update() + .find('.ripple-test-component') + .hasClass('test-class') + ); +}); + +test('#adapter.removeClass removes a class to the root element', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).foundation.adapter_.addClass('test-class'); + wrapper.update(); + coerceForTesting(wrapper.instance()).foundation.adapter_.removeClass('test-class'); + assert.isFalse( + wrapper + .update() + .find('.ripple-test-component') + .hasClass('test-class') + ); +}); + +test('#adapter.removeClass removes a class to the root element', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).foundation.adapter_.addClass('test-class'); + coerceForTesting(wrapper.instance()).isComponentMounted = false; + wrapper.update(); + coerceForTesting(wrapper.instance()).foundation.adapter_.removeClass('test-class'); + assert.isTrue( + wrapper + .update() + .find('.ripple-test-component') + .hasClass('test-class') + ); +}); + +test('#adapter.updateCssVariable updates style', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).foundation.adapter_.updateCssVariable('color', 'blue'); + assert.equal(wrapper.state().style.color, 'blue'); +}); + +test('#adapter.updateCssVariable does not update style if isComponentMounted is false', () => { + const wrapper = mount(); + coerceForTesting(wrapper.instance()).isComponentMounted = false; + coerceForTesting(wrapper.instance()).foundation.adapter_.updateCssVariable('color', 'blue'); + assert.notEqual(wrapper.state().style.color, 'blue'); +}); + +test('#adapter.registerDocumentInteractionHandler triggers handler on document scroll', () => { + const wrapper = mount(); + const testHandler = td.func(); + coerceForTesting(wrapper.instance()) + .foundation.adapter_.registerDocumentInteractionHandler( + 'scroll', + testHandler + ); + const event = new Event('scroll'); + document.documentElement.dispatchEvent(event); + td.verify(testHandler(event), {times: 1}); +}); + +test('#adapter.deregisterDocumentInteractionHandler does not trigger handler on document scroll', () => { + const wrapper = mount(); + const testHandler = td.func(); + coerceForTesting(wrapper.instance()) + .foundation.adapter_.registerDocumentInteractionHandler( + 'scroll', + testHandler + ); + const event = new Event('scroll'); + coerceForTesting(wrapper.instance()) + .foundation.adapter_.deregisterDocumentInteractionHandler( + 'scroll', + testHandler + ); + document.documentElement.dispatchEvent(event); + td.verify(testHandler(event), {times: 0}); +}); + +test('#adapter.registerResizeHandler triggers handler on window resize', () => { + const wrapper = mount(); + const testHandler = td.func(); + coerceForTesting(wrapper.instance()).foundation.adapter_.registerResizeHandler(testHandler); + const event = new Event('resize'); + window.dispatchEvent(event); + td.verify(testHandler(event), {times: 1}); +}); +test( + '#adapter.deregisterResizeHandler does not trigger handler ' + + 'after registering resize handler', + () => { + const wrapper = mount(); + const testHandler = td.func(); + coerceForTesting(wrapper.instance()).foundation.adapter_.registerResizeHandler(testHandler); + const event = new Event('resize'); + coerceForTesting(wrapper.instance()) + .foundation.adapter_.deregisterResizeHandler(testHandler); + window.dispatchEvent(event); + td.verify(testHandler(event), {times: 0}); + } +); + +test('#adapter.computeBoundingRect returns height and width', () => { + const wrapper = mount(); + const domRect = { + x: 0, + y: 0, + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0, + }; + assert.deepInclude( + coerceForTesting(wrapper.update().instance()) + .foundation.adapter_.computeBoundingRect(), + domRect + ); +}); + +test('#adapter.getWindowPageOffset returns height and width', () => { + const wrapper = mount(); + const offset = {x: 0, y: 0}; + assert.deepEqual( + coerceForTesting(wrapper.update().instance()) + .foundation.adapter_.getWindowPageOffset(), + offset + ); +}); + +test('#componentWillUnmount destroys foundation', () => { + const mockRaf = createMockRaf(); + const wrapper = mount(); + mockRaf.flush(); + const foundation = coerceForTesting(wrapper.instance()).foundation; + foundation.destroy = td.func(); + wrapper.unmount(); + td.verify(foundation.destroy()); + mockRaf.restore(); +}); + +test('throws error if no foundation', () => { + const DivNoRef = () =>
; + const DivNoRefRipple = withRipple(DivNoRef); + assert.throws(DivNoRefRipple.prototype.componentDidMount); +}); + +test('unmounting component does not throw errors', (done) => { + // related to + // https://github.com/material-components/material-components-web-react/issues/199 + class TestComp extends React.Component { + state = {showRippleElement: true}; + render() { + if (!this.state.showRippleElement) return hi; + return ( + this.setState({showRippleElement: false})} + /> + ); + } + } + const wrapper = mount(); + wrapper.simulate('mouseDown'); + requestAnimationFrame(() => { + assert.equal(coerceForTesting(wrapper.getDOMNode()).innerText, 'hi'); + done(); + }); +}); diff --git a/test/unit/text-field/index.test.tsx b/test/unit/text-field/index.test.tsx index 3b7523670..d6a0c79a5 100644 --- a/test/unit/text-field/index.test.tsx +++ b/test/unit/text-field/index.test.tsx @@ -6,6 +6,7 @@ import TextField, { HelperText, Input, } from '../../../packages/text-field'; +import {coerceForTesting} from '../helpers/types'; import {InputProps} from '../../../packages/text-field/Input'; // eslint-disable-line no-unused-vars /* eslint-disable */ import FloatingLabel from '../../../packages/floating-label'; @@ -272,11 +273,11 @@ test('#adapter.label.shakeLabel does not call floatingLabelElement shake if fals ); - wrapper.instance().floatingLabelElement = td.object({ + wrapper.instance().floatingLabelElement = coerceForTesting>(td.object({ current: td.object({ shake: td.func(), }), - }) as React.RefObject; + })); wrapper.state().foundation.adapter_.shakeLabel(false); td.verify(wrapper.instance().floatingLabelElement.current!.shake(), { times: 0, @@ -589,7 +590,7 @@ test('#inputProps.handleFocusChange updates state.isFocused', () => { ); wrapper .instance() - .inputProps({} as React.ReactElement>) + .inputProps(coerceForTesting>>({})) .handleFocusChange(true); assert.isTrue(wrapper.state().isFocused); }); @@ -602,8 +603,8 @@ test('#inputProps.handleValueChange updates state.value', () => { ); wrapper .instance() - .inputProps({} as React.ReactElement>) - .handleValueChange('meow', td.func() as () => void); + .inputProps(coerceForTesting>>({})) + .handleValueChange('meow', coerceForTesting<() => void>(td.func())); assert.equal(wrapper.state().value, 'meow'); }); @@ -616,8 +617,8 @@ test('#inputProps.handleValueChange calls cb after state is set', () => { const callback = td.func(); wrapper .instance() - .inputProps({} as React.ReactElement>) - .handleValueChange('meow', callback as () => void); + .inputProps(coerceForTesting>>({})) + .handleValueChange('meow', coerceForTesting<() => void>(callback)); td.verify(callback(), {times: 1}); }); @@ -629,7 +630,7 @@ test('#inputProps.setDisabled updates state.disabled', () => { ); wrapper .instance() - .inputProps({} as React.ReactElement>) + .inputProps(coerceForTesting>>({})) .setDisabled(true); assert.isTrue(wrapper.state().disabled); }); @@ -642,7 +643,7 @@ test('#inputProps.setInputId updates state.disabled', () => { ); wrapper .instance() - .inputProps({} as React.ReactElement>) + .inputProps(coerceForTesting>>({})) .setInputId('my-id'); assert.equal(wrapper.state().inputId, 'my-id'); }); diff --git a/test/unit/top-app-bar/index.test.js b/test/unit/top-app-bar/index.test.js index ee699d54a..37d48d09b 100644 --- a/test/unit/top-app-bar/index.test.js +++ b/test/unit/top-app-bar/index.test.js @@ -3,7 +3,7 @@ import {assert} from 'chai'; import {mount, shallow} from 'enzyme'; import td from 'testdouble'; import TopAppBar from '../../../packages/top-app-bar/index'; -import withRipple from '../../../packages/ripple/index'; +import {withRipple} from '../../../packages/ripple/index'; suite('TopAppBar'); diff --git a/tsconfig.json b/tsconfig.json index adf929b8f..c3255ea3f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,9 @@ "exclude": [ "node_modules", "build" - ] + ], + "baseUrl": ".", + "paths": { + "@material/react-ripple": ["packages/ripple/"] + } }