diff --git a/packages/tab-indicator/index.js b/packages/tab-indicator/index.tsx similarity index 65% rename from packages/tab-indicator/index.js rename to packages/tab-indicator/index.tsx index 65fef5137..12d9b0406 100644 --- a/packages/tab-indicator/index.js +++ b/packages/tab-indicator/index.tsx @@ -20,41 +20,56 @@ // 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 * as React from 'react'; import classnames from 'classnames'; - import { MDCFadingTabIndicatorFoundation, MDCSlidingTabIndicatorFoundation, +// No mdc .d.ts files +// @ts-ignore } from '@material/tab-indicator/dist/mdc.tabIndicator'; -export default class TabIndicator extends Component { - tabIndicatorElement_ = React.createRef(); +export interface TabIndicatorProps extends React.HTMLProps { + active?: boolean; + className?: string; + fade?: boolean; + icon?: boolean; + previousIndicatorClientRect?: ClientRect; +} + +export default class TabIndicator extends React.Component { + private tabIndicatorElement: React.RefObject = React.createRef(); + foundation?: MDCFadingTabIndicatorFoundation | MDCSlidingTabIndicatorFoundation; + + static defaultProps: Partial = { + active: false, + className: '', + fade: false, + icon: false, + }; componentDidMount() { if (this.props.fade) { - this.foundation_ = new MDCFadingTabIndicatorFoundation(this.adapter); + this.foundation = new MDCFadingTabIndicatorFoundation(this.adapter); } else { - this.foundation_ = new MDCSlidingTabIndicatorFoundation(this.adapter); + this.foundation = new MDCSlidingTabIndicatorFoundation(this.adapter); } - this.foundation_.init(); - + this.foundation.init(); if (this.props.active) { - this.foundation_.activate(); + this.foundation.activate(); } } componentWillUnmount() { - this.foundation_.destroy(); + this.foundation.destroy(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: TabIndicatorProps) { if (this.props.active !== prevProps.active) { if (this.props.active) { - this.foundation_.activate(this.props.previousIndicatorClientRect); + this.foundation.activate(this.props.previousIndicatorClientRect); } else { - this.foundation_.deactivate(); + this.foundation.deactivate(); } } } @@ -76,46 +91,51 @@ export default class TabIndicator extends Component { get adapter() { return { - addClass: (className) => { - if (!this.tabIndicatorElement_.current) return; + addClass: (className: string) => { + if (!this.tabIndicatorElement.current) return; // since the sliding indicator depends on the FLIP method, // our regular pattern of managing classes does not work here. // setState is async, which does not work well with the FLIP method // without a requestAnimationFrame, which was done in this PR: // https://github.com/material-components // /material-components-web/pull/3337/files#diff-683d792d28dad99754294121e1afbfb5L62 - this.tabIndicatorElement_.current.classList.add(className); + this.tabIndicatorElement.current.classList.add(className); this.forceUpdate(); }, - removeClass: (className) => { - if (!this.tabIndicatorElement_.current) return; - this.tabIndicatorElement_.current.classList.remove(className); + removeClass: (className: string) => { + if (!this.tabIndicatorElement.current) return; + this.tabIndicatorElement.current.classList.remove(className); this.forceUpdate(); }, computeContentClientRect: this.computeContentClientRect, // setContentStyleProperty was using setState, but due to the method's // async nature, its not condusive to the FLIP technique - setContentStyleProperty: (prop, value) => { - const contentElement = this.getNativeContentElement(); - if (!contentElement) return; + setContentStyleProperty: (prop: keyof CSSStyleDeclaration, value: string) => { + const contentElement = this.getNativeContentElement() as HTMLElement; + // length and parentRule are readonly properties of CSSStyleDeclaration that + // cannot be set + if (!contentElement || prop === 'length' || prop === 'parentRule') { + return; + } + // https://github.com/Microsoft/TypeScript/issues/11914 contentElement.style[prop] = value; }, }; } - getNativeContentElement = () => { - if (!this.tabIndicatorElement_.current) return; + private getNativeContentElement = () => { + if (!this.tabIndicatorElement.current) return; // need to use getElementsByClassName since tabIndicator could be // a non-semantic element (span, i, etc.). This is a problem since refs to a non semantic elements // return the instance of the component. - return this.tabIndicatorElement_.current.getElementsByClassName('mdc-tab-indicator__content')[0]; - } + return this.tabIndicatorElement.current.getElementsByClassName('mdc-tab-indicator__content')[0]; + }; computeContentClientRect = () => { const contentElement = this.getNativeContentElement(); if (!(contentElement && contentElement.getBoundingClientRect)) return; return contentElement.getBoundingClientRect(); - } + }; render() { const { @@ -129,11 +149,10 @@ export default class TabIndicator extends Component { /* eslint-enable */ ...otherProps } = this.props; - return ( {this.renderContent()} @@ -152,26 +171,7 @@ export default class TabIndicator extends Component { if (this.props.children) { return this.addContentClassesToChildren(); } - return ( - - ); + return ; } } -TabIndicator.propTypes = { - active: PropTypes.bool, - className: PropTypes.string, - children: PropTypes.element, - fade: PropTypes.bool, - icon: PropTypes.bool, - previousIndicatorClientRect: PropTypes.object, -}; - -TabIndicator.defaultProps = { - active: false, - className: '', - children: null, - fade: false, - icon: false, - previousIndicatorClientRect: {}, -}; diff --git a/test/screenshot/tab-indicator/index.js b/test/screenshot/tab-indicator/index.js deleted file mode 100644 index 5d85b420b..000000000 --- a/test/screenshot/tab-indicator/index.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import TabIndicator from '../../../packages/tab-indicator'; -import MaterialIcon from '../../../packages/material-icon'; - -import '../../../packages/tab-indicator/index'; -import './index.scss'; - -const Tab = ({ - children, index, active, icon, // eslint-disable-line react/prop-types -}) => { - return ( -
- Tab {index} - - {children} - -
- ); -}; - -const Tabs = ({ - children, activeIndex, // eslint-disable-line react/prop-types -}) => { - return ( -
- {[1, 2, 3].map((number, index) => - - {children} - - )} -
- ); -}; - -const TabIndicatorScreenshotTest = () => { - return ( -
- - - - -
- ); -}; - -export default TabIndicatorScreenshotTest; diff --git a/test/screenshot/tab-indicator/index.tsx b/test/screenshot/tab-indicator/index.tsx new file mode 100644 index 000000000..a6ddb3c22 --- /dev/null +++ b/test/screenshot/tab-indicator/index.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import TabIndicator from '../../../packages/tab-indicator/index'; +import MaterialIcon from '../../../packages/material-icon'; +import '../../../packages/tab-indicator/index.scss'; +import './index.scss'; + +const Tab: React.FunctionComponent<{ + children?: React.ReactElement, + index: number, + active: boolean, + icon: boolean +}> = ({children, index, active, icon}) => { // eslint-disable-line react/prop-types + return ( +
+ Tab {index} + + {children} + +
+ ); +}; + +const Tabs: React.FunctionComponent<{ + children?: React.ReactElement, + activeIndex: number +}> = ({children, activeIndex}) => { // eslint-disable-line react/prop-types + return ( +
+ {[1, 2, 3].map((number, index) => ( + + {children} + + ))} +
+ ); +}; + +const TabIndicatorScreenshotTest: React.FunctionComponent = () => { + return ( +
+ + + + +
+ ); +}; +export default TabIndicatorScreenshotTest; diff --git a/test/unit/tab-indicator/index.test.js b/test/unit/tab-indicator/index.test.tsx similarity index 69% rename from test/unit/tab-indicator/index.test.js rename to test/unit/tab-indicator/index.test.tsx index 7919b1b56..2676a5443 100644 --- a/test/unit/tab-indicator/index.test.js +++ b/test/unit/tab-indicator/index.test.tsx @@ -1,14 +1,16 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; -import td from 'testdouble'; +import * as td from 'testdouble'; import {mount, shallow} from 'enzyme'; import TabIndicator from '../../../packages/tab-indicator/index'; +// TODO: fix when #513 +// @ts-ignore import MaterialIcon from '../../../packages/material-icon/index'; suite('TabIndicator'); test('classNames adds classes', () => { - const wrapper = shallow(); + const wrapper = shallow(); assert.isTrue(wrapper.hasClass('test-class-name')); assert.isTrue(wrapper.hasClass('mdc-tab-indicator')); }); @@ -38,92 +40,102 @@ test('adds the underline class to the content element by default', () => { }); test('if props.active changes from true to false, it calls deactivate', () => { - const wrapper = mount(); - wrapper.instance().foundation_.deactivate = td.func(); + const wrapper = mount(); + wrapper.instance().foundation.deactivate = td.func(); wrapper.setProps({active: false}); - td.verify(wrapper.instance().foundation_.deactivate(), {times: 1}); + td.verify(wrapper.instance().foundation.deactivate(), {times: 1}); }); test('if props.active changes from false to true, it calls activate', () => { - const previousIndicatorClientRect = {width: 20}; - const wrapper = shallow(); - wrapper.instance().foundation_.activate = td.func(); + const previousIndicatorClientRect = {width: 20} as ClientRect; + const wrapper = shallow(); + wrapper.instance().foundation.activate = td.func(); wrapper.setProps({active: true}); - td.verify(wrapper.instance().foundation_.activate(previousIndicatorClientRect), {times: 1}); + td.verify(wrapper.instance().foundation.activate(previousIndicatorClientRect), {times: 1}); }); test('#adapter.addClass adds to dom element classList', () => { - const wrapper = mount(); + const wrapper = mount(); wrapper.instance().adapter.addClass('meow-class'); assert.isTrue(wrapper.getDOMNode().classList.contains('meow-class')); }); test('#adapter.removeClass removes from dom element classList', () => { - const wrapper = mount(); + const wrapper = mount(); wrapper.getDOMNode().classList.add('meow-class'); wrapper.instance().adapter.removeClass('meow-class'); assert.isFalse(wrapper.getDOMNode().classList.contains('meow-class')); }); test('#adapter.setContentStyleProperty sets the style property on the contentElement', () => { - const wrapper = mount(); + const wrapper = mount(); const transform = 'translateX(10px)'; wrapper.instance().adapter.setContentStyleProperty('transform', transform); - const contentElement = wrapper.find('.mdc-tab-indicator__content').getDOMNode(); + const contentElement = wrapper.find('.mdc-tab-indicator__content').getDOMNode() as HTMLElement; assert.equal(contentElement.style.transform, transform); }); test('#adapter.computeContentClientRect calls getBoundingClientRect on the contentElement', () => { - const wrapper = mount(); - const contentElement = wrapper.find('.mdc-tab-indicator__content').getDOMNode(); - contentElement.getBoundingClientRect = td.func(); + const wrapper = mount(); + const contentElement = wrapper + .find('.mdc-tab-indicator__content') + .getDOMNode(); + contentElement.getBoundingClientRect = td.func() as () => ClientRect; wrapper.instance().adapter.computeContentClientRect(); td.verify(contentElement.getBoundingClientRect(), {times: 1}); }); test('#computeContentClientRect calls getBoundingClientRect on the contentElement', () => { - const wrapper = mount(); + const wrapper = mount(); const contentElement = wrapper.find('.mdc-tab-indicator__content').getDOMNode(); - contentElement.getBoundingClientRect = td.func(); + contentElement.getBoundingClientRect = td.func() as () => ClientRect; wrapper.instance().computeContentClientRect(); td.verify(contentElement.getBoundingClientRect(), {times: 1}); }); test('child element should be rendered', () => { - const wrapper = shallow( - meow - ); + const wrapper = shallow( + + meow + + ); assert.equal(wrapper.children().first().type(), 'i'); assert.equal(wrapper.children().first().text(), 'meow'); }); test('child element should include props.className and contentClasses', () => { - const wrapper = shallow( - meow - ); + const wrapper = shallow( + + meow + + ); assert.isTrue(wrapper.children().first().hasClass('test-class-name')); assert.isTrue(wrapper.children().first().hasClass('mdc-tab-indicator__content')); assert.isTrue(wrapper.children().first().hasClass('mdc-tab-indicator__content--underline')); }); test('child custom element should render correctly', () => { - const wrapper = shallow( - - ); + const wrapper = shallow( + + + + ); assert.equal(wrapper.children().first().type(), MaterialIcon); }); test('child custom element should have content classes', () => { - const wrapper = shallow( - - ); + const wrapper = shallow( + + + + ); assert.isTrue(wrapper.children().first().hasClass('mdc-tab-indicator__content')); assert.isTrue(wrapper.children().first().hasClass('mdc-tab-indicator__content--icon')); }); test('#componentWillUnmount destroys foundation', () => { - const wrapper = shallow(); - const foundation = wrapper.instance().foundation_; + const wrapper = shallow(); + const foundation = wrapper.instance().foundation; foundation.destroy = td.func(); wrapper.unmount(); td.verify(foundation.destroy(), {times: 1}); diff --git a/test/unit/tab/index.test.js b/test/unit/tab/index.test.js index 687986c03..0d69325fb 100644 --- a/test/unit/tab/index.test.js +++ b/test/unit/tab/index.test.js @@ -3,7 +3,7 @@ import {assert} from 'chai'; import td from 'testdouble'; import {mount, shallow} from 'enzyme'; import Tab from '../../../packages/tab/index'; -import TabIndicator from '../../../packages/tab-indicator/index'; +import TabIndicator from '../../../packages/tab-indicator/index.tsx'; suite('Tab');