diff --git a/package.json b/package.json index aeb61a9a8..dfb215659 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@types/mocha": "^5.2.5", "@types/prop-types": "^15.5.6", "@types/puppeteer": "^1.11.1", - "@types/react": "^16.7.7", + "@types/react": "^16.4.4", "@types/react-dom": "^16.0.11", "@types/react-router-dom": "^4.3.1", "@types/uuid": "^3.4.4", diff --git a/packages/list/ListDivider.js b/packages/list/ListDivider.tsx similarity index 72% rename from packages/list/ListDivider.js rename to packages/list/ListDivider.tsx index 64ac43d2b..e85355108 100644 --- a/packages/list/ListDivider.js +++ b/packages/list/ListDivider.tsx @@ -20,35 +20,27 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import * as React from 'react'; +import * as classnames from 'classnames'; -const ListDivider = (props) => { - const { - tag: Tag, - className, - ...otherProps - } = props; +export interface ListDividerProps extends React.HTMLProps{ + className?: string, + tag?: string, + role?: string +}; +const ListDivider: React.FunctionComponent = ({ + tag: Tag = 'li', className = '', role = 'separator', ...otherProps // eslint-disable-line react/prop-types +}) => { return ( + // https://github.com/Microsoft/TypeScript/issues/28892 + // @ts-ignore ); }; -ListDivider.propTypes = { - className: PropTypes.string, - tag: PropTypes.string, - role: PropTypes.string, -}; - -ListDivider.defaultProps = { - className: '', - tag: 'li', - role: 'separator', -}; - export default ListDivider; diff --git a/packages/list/ListGroup.js b/packages/list/ListGroup.tsx similarity index 75% rename from packages/list/ListGroup.js rename to packages/list/ListGroup.tsx index beb19c80d..f157df6bc 100644 --- a/packages/list/ListGroup.js +++ b/packages/list/ListGroup.tsx @@ -20,35 +20,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import * as React from 'react'; +import * as classnames from 'classnames'; -const ListGroup = (props) => { - const { - tag: Tag, - className, - children, - ...otherProps - } = props; +export interface ListGroupProps extends React.HTMLProps{ + className?: string, + tag?: string +}; +const ListGroup:React.FunctionComponent = ({ + tag: Tag = 'div', className = '', children, ...otherProps // eslint-disable-line react/prop-types +}) => { return ( + // https://github.com/Microsoft/TypeScript/issues/28892 + // @ts-ignore {children} ); }; -ListGroup.propTypes = { - className: PropTypes.string, - children: PropTypes.node, - tag: PropTypes.string, -}; - -ListGroup.defaultProps = { - className: '', - children: null, - tag: 'div', -}; - export default ListGroup; diff --git a/packages/list/ListGroupSubheader.js b/packages/list/ListGroupSubheader.tsx similarity index 67% rename from packages/list/ListGroupSubheader.js rename to packages/list/ListGroupSubheader.tsx index 3360c141e..ba997c09c 100644 --- a/packages/list/ListGroupSubheader.js +++ b/packages/list/ListGroupSubheader.tsx @@ -20,38 +20,26 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -const ListGroupSubheader = (props) => { - const { - tag: Tag, - className, - children, - ...otherProps - } = props; +import * as React from 'react'; +import * as classnames from 'classnames'; +export interface ListGroupSubheaderProps extends React.HTMLProps { + className?: string, + tag?: string +}; +const ListGroupSubheader:React.FunctionComponent = ({ + tag: Tag = 'h3', className = '', children, ...otherProps // eslint-disable-line react/prop-types +}) => { return ( - + // https://github.com/Microsoft/TypeScript/issues/28892 + // @ts-ignore + {children} ); }; -ListGroupSubheader.propTypes = { - className: PropTypes.string, - children: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]), - tag: PropTypes.string, -}; - -ListGroupSubheader.defaultProps = { - className: '', - children: '', - tag: 'h3', -}; - export default ListGroupSubheader; diff --git a/packages/list/ListItem.js b/packages/list/ListItem.tsx similarity index 53% rename from packages/list/ListItem.js rename to packages/list/ListItem.tsx index de94f6fed..2c8750982 100644 --- a/packages/list/ListItem.js +++ b/packages/list/ListItem.tsx @@ -20,26 +20,65 @@ // 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 * as React from 'react'; +import * as classnames from 'classnames'; -export default class ListItem extends Component { - listItemElement_ = React.createRef(); +export interface ListItemProps extends React.HTMLProps { + className: string; + classNamesFromList: string[]; + attributesFromList: object; + childrenTabIndex: number; + tabIndex: number; + shouldFocus: boolean; + shouldFollowHref: boolean; + shouldToggleCheckbox: boolean; + onKeyDown: React.KeyboardEventHandler; + onClick: React.MouseEventHandler; + onFocus: React.FocusEventHandler; + onBlur: React.FocusEventHandler; + tag: string; + children: React.ReactNode; +}; - componentDidUpdate(prevProps) { - const { - shouldFocus, - shouldFollowHref, - shouldToggleCheckbox, - } = this.props; - if (shouldFocus !== prevProps.shouldFocus && shouldFocus) { +function isAnchorElement(element: any): element is HTMLAnchorElement { + return !!element.href; +} + +function isFocusableElement(element: any): element is HTMLElement { + return typeof element.focus === 'function'; +} + +export default class ListItem extends React.Component< + ListItemProps, + {} + > { + listItemElement_: React.RefObject = React.createRef(); + + static defaultProps: Partial> = { + className: '', + classNamesFromList: [], + attributesFromList: {}, + childrenTabIndex: -1, + tabIndex: -1, + shouldFocus: false, + shouldFollowHref: false, + shouldToggleCheckbox: false, + onKeyDown: () => {}, + onClick: () => {}, + onFocus: () => {}, + onBlur: () => {}, + tag: 'li', + }; + + componentDidUpdate(prevProps: ListItemProps) { + const {shouldFocus, shouldFollowHref, shouldToggleCheckbox} = this.props; + if (shouldFocus && !prevProps.shouldFocus) { this.focus(); } - if (shouldFollowHref !== prevProps.shouldFollowHref && shouldFollowHref) { + if (shouldFollowHref && !prevProps.shouldFollowHref) { this.followHref(); } - if (shouldToggleCheckbox !== prevProps.shouldToggleCheckbox && shouldToggleCheckbox) { + if (shouldToggleCheckbox && !prevProps.shouldToggleCheckbox) { this.toggleCheckbox(); } } @@ -51,14 +90,14 @@ export default class ListItem extends Component { focus() { const element = this.listItemElement_.current; - if (element) { + if (isFocusableElement(element)) { element.focus(); } } followHref() { const element = this.listItemElement_.current; - if (element && element.href) { + if (isAnchorElement(element)) { element.click(); } } @@ -83,8 +122,9 @@ export default class ListItem extends Component { tag: Tag, ...otherProps } = this.props; - return ( + // https://github.com/Microsoft/TypeScript/issues/28892 + // @ts-ignore { - const props = Object.assign({}, - child.props, - {tabIndex: this.props.childrenTabIndex} - ); + renderChild = (child: React.ReactChild) => { + if (typeof child === 'string' || typeof child === 'number') { + return child; + } + + const tabIndex = this.props.childrenTabIndex; + const props = {...child.props, tabIndex}; return React.cloneElement(child, props); - } + }; } - -ListItem.propTypes = { - children: PropTypes.node, - className: PropTypes.string, - classNamesFromList: PropTypes.array, - attributesFromList: PropTypes.object, - childrenTabIndex: PropTypes.number, - tabIndex: PropTypes.number, - shouldFocus: PropTypes.bool, - shouldFollowHref: PropTypes.bool, - shouldToggleCheckbox: PropTypes.bool, - onKeyDown: PropTypes.func, - onClick: PropTypes.func, - onFocus: PropTypes.func, - onBlur: PropTypes.func, - tag: PropTypes.string, -}; - -ListItem.defaultProps = { - className: '', - classNamesFromList: [], - attributesFromList: {}, - childrenTabIndex: -1, - tabIndex: -1, - shouldFocus: false, - shouldFollowHref: false, - shouldToggleCheckbox: false, - onKeyDown: () => {}, - onClick: () => {}, - onFocus: () => {}, - onBlur: () => {}, - tag: 'li', -}; diff --git a/packages/list/ListItemGraphic.js b/packages/list/ListItemGraphic.tsx similarity index 67% rename from packages/list/ListItemGraphic.js rename to packages/list/ListItemGraphic.tsx index 96cbbf08d..72f1c88b9 100644 --- a/packages/list/ListItemGraphic.js +++ b/packages/list/ListItemGraphic.tsx @@ -20,40 +20,30 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import * as React from 'react'; +import * as classnames from 'classnames'; -const ListItemGraphic = (props) => { - const { - tabIndex, // eslint-disable-line no-unused-vars - graphic, - tabbableOnListItemFocus, - className, - ...otherProps - } = props; +export interface ListItemGraphicProps { + tabbableOnListItemFocus?: boolean; + className?: string; + tabIndex?: number; + graphic: React.ReactElement; + childrenTabIndex?: number; +}; +const ListItemGraphic:React.FunctionComponent = ({ + tabIndex = -1, // eslint-disable-line no-unused-vars + graphic, + tabbableOnListItemFocus = false, + className = '', + ...otherProps +}) => { const graphicProps = { className: classnames('mdc-list-item__graphic', className), - tabIndex: tabbableOnListItemFocus ? props.tabIndex : -1, + tabIndex: tabbableOnListItemFocus ? tabIndex : -1, ...otherProps, }; - return React.cloneElement(graphic, graphicProps); }; -ListItemGraphic.propTypes = { - tabbableOnListItemFocus: PropTypes.bool, - className: PropTypes.string, - tabIndex: PropTypes.number, - graphic: PropTypes.element, -}; - -ListItemGraphic.defaultProps = { - tabbableOnListItemFocus: false, - className: '', - tabIndex: -1, - graphic: {}, -}; - export default ListItemGraphic; diff --git a/packages/list/ListItemMeta.js b/packages/list/ListItemMeta.tsx similarity index 67% rename from packages/list/ListItemMeta.js rename to packages/list/ListItemMeta.tsx index 41db3b599..337333e8f 100644 --- a/packages/list/ListItemMeta.js +++ b/packages/list/ListItemMeta.tsx @@ -20,50 +20,36 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; - -const ListItemMeta = (props) => { - const { - tabIndex, // eslint-disable-line no-unused-vars - meta, - className, - tabbableOnListItemFocus, - ...otherProps - } = props; +import * as React from 'react'; +import * as classnames from 'classnames'; + +export interface ListItemMetaProps { + tabbableOnListItemFocus?: boolean; + className?: string; + tabIndex?: number; + meta: string | React.ReactElement; + childrenTabIndex?: number; +}; - let metaElement = null; +const ListItemMeta:React.FunctionComponent = ({ + tabIndex = -1, // eslint-disable-line no-unused-vars + meta, + className = '', + tabbableOnListItemFocus = false, + ...otherProps +}) => { + let metaElement: React.ReactElement; if (typeof meta === 'string') { metaElement = {meta}; } else { metaElement = meta; } - const metaProps = { - className: classnames('mdc-list-item__meta', className, meta.className), - tabIndex: tabbableOnListItemFocus ? props.tabIndex : -1, + className: classnames('mdc-list-item__meta', className, metaElement.props.className), + tabIndex: tabbableOnListItemFocus ? tabIndex : -1, ...otherProps, }; - return React.cloneElement(metaElement, metaProps); }; -ListItemMeta.propTypes = { - tabbableOnListItemFocus: PropTypes.bool, - className: PropTypes.string, - tabIndex: PropTypes.number, - meta: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]), -}; - -ListItemMeta.defaultProps = { - tabbableOnListItemFocus: false, - className: '', - tabIndex: -1, - meta: null, -}; - export default ListItemMeta; diff --git a/packages/list/ListItemText.js b/packages/list/ListItemText.tsx similarity index 60% rename from packages/list/ListItemText.js rename to packages/list/ListItemText.tsx index 39627effe..20bce8071 100644 --- a/packages/list/ListItemText.js +++ b/packages/list/ListItemText.tsx @@ -20,22 +20,36 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; +import * as React from 'react'; +import * as classnames from 'classnames'; -const ListItemText = (props) => { - const { - primaryText, - secondaryText, - tabbableOnListItemFocus, - tabIndex, - className, - ...otherProps - } = props; +export interface ListItemTextProps { + tabbableOnListItemFocus?: boolean; + tabIndex?: number; + className?: string; + primaryText?: React.ReactNode; + secondaryText?: React.ReactNode; + childrenTabIndex?: number; +}; + +// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/14064 +function isReactElement(element: any): element is React.ReactElement { + return element !== null && element.props !== undefined; +} - const renderText = (text, className) => { - if (typeof text === 'string') { +const ListItemText: React.FunctionComponent = ({ + /* eslint-disable react/prop-types */ + primaryText = '', + secondaryText = '', + tabbableOnListItemFocus = false, + tabIndex = -1, + className = '', + /* eslint-enable react/prop-types */ + ...otherProps +}) => { + const renderText = (text: React.ReactNode, className: string) => { + if (text === undefined) return null; + if (typeof text === 'string' || typeof text === 'number') { return ( { ); } + if (!isReactElement(text)) return null; + const {className: textClassName, ...otherProps} = text.props; - const props = Object.assign({ - className: classnames(className, textClassName), - }, ...otherProps); + className = classnames(className, textClassName); + const props = {...otherProps, className}; + return React.cloneElement(text, props); }; if (!secondaryText) { - return renderText(primaryText, classnames('mdc-list-item__text', className)); + return renderText( + primaryText, + classnames('mdc-list-item__text', className) + ); } return ( @@ -68,26 +87,4 @@ const ListItemText = (props) => { ); }; -ListItemText.propTypes = { - tabbableOnListItemFocus: PropTypes.bool, - tabIndex: PropTypes.number, - className: PropTypes.string, - primaryText: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]), - secondaryText: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]), -}; - -ListItemText.defaultProps = { - tabbableOnListItemFocus: false, - tabIndex: -1, - className: '', - primaryText: '', - secondaryText: '', -}; - export default ListItemText; diff --git a/packages/list/index.js b/packages/list/index.tsx similarity index 58% rename from packages/list/index.js rename to packages/list/index.tsx index a0b7ec1b1..199b732be 100644 --- a/packages/list/index.js +++ b/packages/list/index.tsx @@ -20,30 +20,65 @@ // 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 * as React from 'react'; +import * as classnames from 'classnames'; +// no .d.ts file +// @ts-ignore import {MDCListFoundation} from '@material/list/dist/mdc.list'; - -import ListItem from './ListItem'; +import ListItem, {ListItemProps} from './ListItem'; // eslint-disable-line no-unused-vars import ListItemGraphic from './ListItemGraphic'; import ListItemText from './ListItemText'; import ListItemMeta from './ListItemMeta'; import ListDivider from './ListDivider'; import ListGroup from './ListGroup'; import ListGroupSubheader from './ListGroupSubheader'; - const ARIA_ORIENTATION = 'aria-orientation'; const VERTICAL = 'vertical'; +const CHECKBOX_TYPE = 'checkbox'; + +export interface ListProps extends React.HTMLProps { + className: string; + nonInteractive: boolean; + dense: boolean; + avatarList: boolean; + twoLine: boolean; + singleSelection: boolean; + selectedIndex: number; + handleSelect: (selectedIndex: number) => void; + wrapFocus: boolean; + tag: string; + children: ListItem | ListItem[] | React.ReactNode; +}; + +interface ListState { + focusListItemAtIndex: number; + followHrefAtIndex: number; + toggleCheckboxAtIndex: number; + listItemAttributes: {[N: number]: any}; + listItemClassNames: {[N: number]: string[]}; + listItemChildrenTabIndex: {[N: number]: number}; +}; + +function isCheckbox(element: any): element is HTMLInputElement { + return element.type === CHECKBOX_TYPE; +} + +function isReactText(element: any): element is React.ReactText { + return typeof element === 'string' || typeof element === 'number'; +} -export default class List extends Component { +function isListItem(element: any): element is ListItem { + return element.type === ListItem; +} + +export default class List extends React.Component, ListState> { listItemCount = 0; - state = { + foundation = MDCListFoundation; + + state: ListState = { focusListItemAtIndex: -1, followHrefAtIndex: -1, toggleCheckboxAtIndex: -1, - // listItemAttributes: {index: {attr: value}} listItemAttributes: { 0: { @@ -56,40 +91,72 @@ export default class List extends Component { listItemChildrenTabIndex: {}, }; + static defaultProps: Partial> = { + 'className': '', + 'nonInteractive': false, + 'dense': false, + 'avatarList': false, + 'twoLine': false, + 'singleSelection': false, + 'selectedIndex': -1, + 'handleSelect': () => {}, + 'wrapFocus': true, + 'aria-orientation': VERTICAL, + 'tag': 'ul', + }; + componentDidMount() { const {singleSelection, wrapFocus, selectedIndex} = this.props; - this.foundation_ = new MDCListFoundation(this.adapter); - this.foundation_.init(); - this.foundation_.setSingleSelection(singleSelection); - if (singleSelection && typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { - this.foundation_.setSelectedIndex(selectedIndex); + this.foundation = new MDCListFoundation(this.adapter); + this.foundation.init(); + this.foundation.setSingleSelection(singleSelection); + if ( + singleSelection && + typeof selectedIndex === 'number' && + !isNaN(selectedIndex) + ) { + this.foundation.setSelectedIndex(selectedIndex); } - this.foundation_.setWrapFocus(wrapFocus); - this.foundation_.setVerticalOrientation(this.props[ARIA_ORIENTATION] === VERTICAL); + this.foundation.setWrapFocus(wrapFocus); + this.foundation.setVerticalOrientation( + this.props[ARIA_ORIENTATION] === VERTICAL + ); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: ListProps) { const {singleSelection, wrapFocus, selectedIndex} = this.props; if (singleSelection !== prevProps.singleSelection) { - this.foundation_.setSingleSelection(singleSelection); + this.foundation.setSingleSelection(singleSelection); } - if (selectedIndex !== prevProps.selectedIndex && typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { - this.foundation_.setSelectedIndex(selectedIndex); + if ( + selectedIndex !== prevProps.selectedIndex && + typeof selectedIndex === 'number' && + !isNaN(selectedIndex) + ) { + this.foundation.setSelectedIndex(selectedIndex); } if (wrapFocus !== prevProps.wrapFocus) { - this.foundation_.setWrapFocus(wrapFocus); + this.foundation.setWrapFocus(wrapFocus); } if (this.props[ARIA_ORIENTATION] !== prevProps[ARIA_ORIENTATION]) { - this.foundation_.setVerticalOrientation(this.props[ARIA_ORIENTATION] === VERTICAL); + this.foundation.setVerticalOrientation( + this.props[ARIA_ORIENTATION] === VERTICAL + ); } } componentWillUnmount() { - this.foundation_.destroy(); + this.foundation.destroy(); } get classes() { - const {className, nonInteractive, dense, avatarList, twoLine} = this.props; + const { + className, + nonInteractive, + dense, + avatarList, + twoLine, + } = this.props; return classnames('mdc-list', className, { 'mdc-list--non-interactive': nonInteractive, 'mdc-list--dense': dense, @@ -104,7 +171,7 @@ export default class List extends Component { // Remove when MDC Web issue resolves: // https://github.com/material-components/material-components-web/issues/4058 getFocusedElementIndex: () => -1, - setAttributeForElementIndex: (index, attr, value) => { + setAttributeForElementIndex: (index: number, attr: string, value: string) => { const {listItemAttributes} = this.state; attr = attr === 'tabindex' ? 'tabIndex' : attr; if (!listItemAttributes[index]) { @@ -113,7 +180,7 @@ export default class List extends Component { listItemAttributes[index][attr] = value; this.setState({listItemAttributes}); }, - removeAttributeForElementIndex: (index, attr) => { + removeAttributeForElementIndex: (index: number, attr: string) => { const {listItemAttributes} = this.state; attr = attr === 'tabindex' ? 'tabIndex' : attr; if (!listItemAttributes[index]) { @@ -122,7 +189,7 @@ export default class List extends Component { delete listItemAttributes[index][attr]; this.setState({listItemAttributes}); }, - addClassForElementIndex: (index, className) => { + addClassForElementIndex: (index: number, className: string) => { const {listItemClassNames} = this.state; if (!listItemClassNames[index]) { listItemClassNames[index] = []; @@ -130,7 +197,7 @@ export default class List extends Component { listItemClassNames[index].push(className); this.setState({listItemClassNames}); }, - removeClassForElementIndex: (index, className) => { + removeClassForElementIndex: (index: number, className: string) => { const {listItemClassNames} = this.state; if (!listItemClassNames[index]) { return; @@ -141,23 +208,66 @@ export default class List extends Component { this.setState({listItemClassNames}); } }, - setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { + setTabIndexForListItemChildren: (listItemIndex: number, tabIndexValue: number) => { const {listItemChildrenTabIndex} = this.state; listItemChildrenTabIndex[listItemIndex] = tabIndexValue; this.setState({listItemChildrenTabIndex}); }, - focusItemAtIndex: (index) => { + focusItemAtIndex: (index: number) => { this.setState({focusListItemAtIndex: index}); }, - followHref: (index) => { + followHref: (index: number) => { this.setState({followHrefAtIndex: index}); }, - toggleCheckbox: (index) => { + toggleCheckbox: (index: number) => { this.setState({toggleCheckboxAtIndex: index}); }, }; } + handleKeyDown = (e: React.KeyboardEvent, index: number) => { + e.persist(); // Persist the synthetic event to access its `key` + this.foundation.handleKeydown( + e, + true /* isRootListItem is true if index >= 0 */, + index + ); + // Work around until MDC Web issue is resolved: + // https://github.com/material-components/material-components-web/issues/4053 + if ( + index >= 0 && + (e.key === 'Enter' || + e.keyCode === 13 || + e.key === 'Space' || + e.keyCode === 32) + ) { + this.props.handleSelect(index); + } + }; + + handleClick = (e: React.MouseEvent, index: number) => { + // Toggle the checkbox only if it's not the target of the event, or the checkbox will have 2 change events. + const toggleCheckbox = isCheckbox(e.target); + this.foundation.handleClick(index, toggleCheckbox); + // Work around until MDC Web issue is resolved: + // https://github.com/material-components/material-components-web/issues/4053 + if (index >= 0) { + this.props.handleSelect(index); + } + }; + + // Use onFocus as workaround because onFocusIn is not yet supported in React + // https://github.com/facebook/react/issues/6410 + handleFocus = (e: React.FocusEvent, index: number) => { + this.foundation.handleFocusIn(e, index); + }; + + // Use onBlur as workaround because onFocusOut is not yet supported in React + // https://github.com/facebook/react/issues/6410 + handleBlur = (e: React.FocusEvent, index: number) => { + this.foundation.handleFocusOut(e, index); + }; + render() { const { /* eslint-disable no-unused-vars */ @@ -175,60 +285,24 @@ export default class List extends Component { tag: Tag, ...otherProps } = this.props; - this.listItemCount = 0; return ( - + // https://github.com/Microsoft/TypeScript/issues/28892 + // @ts-ignore + {React.Children.map(children, this.renderChild)} ); } - renderChild = (child) => { - if (child.type === ListItem) { + renderChild = (child: React.ReactElement> | React.ReactChild) => { + if (!isReactText(child) && isListItem(child)) { return this.renderListItem(child, this.listItemCount++); - } else { - return child; - } - } - - handleKeyDown = (e, index) => { - e.persist(); // Persist the synthetic event to access its `key` - this.foundation_.handleKeydown(e, true /* isRootListItem is true if index >= 0 */, index); - // Work around until MDC Web issue is resolved: - // https://github.com/material-components/material-components-web/issues/4053 - if (index >= 0 && (e.key === 'Enter' || e.keyCode === 13 || e.key === 'Space' || e.keyCode === 32)) { - this.props.handleSelect(index); } - } - - handleClick = (e, index) => { - // Toggle the checkbox only if it's not the target of the event, or the checkbox will have 2 change events. - const toggleCheckbox = e.target.type === 'checkbox'; - this.foundation_.handleClick(index, toggleCheckbox); - // Work around until MDC Web issue is resolved: - // https://github.com/material-components/material-components-web/issues/4053 - if (index >= 0) { - this.props.handleSelect(index); - } - } - - // Use onFocus as workaround because onFocusIn is not yet supported in React - // https://github.com/facebook/react/issues/6410 - handleFocus = (e, index) => { - this.foundation_.handleFocusIn(e, index); - } - - // Use onBlur as workaround because onFocusOut is not yet supported in React - // https://github.com/facebook/react/issues/6410 - handleBlur = (e, index) => { - this.foundation_.handleFocusOut(e, index); - } + return child; + }; - renderListItem = (listItem, index) => { + renderListItem = (listItem: React.ReactElement>, index: number) => { const { onKeyDown, onClick, @@ -236,7 +310,6 @@ export default class List extends Component { onBlur, ...otherProps } = listItem.props; - const { focusListItemAtIndex, followHrefAtIndex, @@ -245,22 +318,21 @@ export default class List extends Component { listItemClassNames, listItemChildrenTabIndex, } = this.state; - const props = { ...otherProps, - onKeyDown: (e) => { + onKeyDown: (e: React.KeyboardEvent) => { onKeyDown(e); this.handleKeyDown(e, index); }, - onClick: (e) => { + onClick: (e: React.MouseEvent) => { onClick(e); this.handleClick(e, index); }, - onFocus: (e) => { + onFocus: (e: React.FocusEvent) => { onFocus(e); this.handleFocus(e, index); }, - onBlur: (e) => { + onBlur: (e: React.FocusEvent) => { onBlur(e); this.handleBlur(e, index); }, @@ -271,42 +343,18 @@ export default class List extends Component { classNamesFromList: listItemClassNames[index], childrenTabIndex: listItemChildrenTabIndex[index], }; - return React.cloneElement(listItem, props); - } + }; } -/* eslint-disable quote-props */ - -List.propTypes = { - className: PropTypes.string, - children: PropTypes.node, - nonInteractive: PropTypes.bool, - dense: PropTypes.bool, - avatarList: PropTypes.bool, - twoLine: PropTypes.bool, - singleSelection: PropTypes.bool, - selectedIndex: PropTypes.number, - handleSelect: PropTypes.func, - wrapFocus: PropTypes.bool, - 'aria-orientation': PropTypes.string, - tag: PropTypes.string, -}; - -List.defaultProps = { - className: '', - nonInteractive: false, - dense: false, - avatarList: false, - twoLine: false, - singleSelection: false, - selectedIndex: -1, - handleSelect: () => {}, - wrapFocus: true, - 'aria-orientation': VERTICAL, - tag: 'ul', -}; - /* eslint-enable quote-props */ - -export {ListItem, ListItemGraphic, ListItemText, ListItemMeta, ListDivider, ListGroup, ListGroupSubheader}; +export { + ListItem, + ListItemGraphic, + ListItemText, + ListItemMeta, + ListDivider, + ListGroup, + ListGroupSubheader, + ListItemProps, +}; diff --git a/test/screenshot/list/index.js b/test/screenshot/list/index.tsx similarity index 50% rename from test/screenshot/list/index.js rename to test/screenshot/list/index.tsx index 64f75c59e..db96a2f3e 100644 --- a/test/screenshot/list/index.js +++ b/test/screenshot/list/index.tsx @@ -1,7 +1,6 @@ -import React from 'react'; +import * as React from 'react'; import './index.scss'; import '../../../packages/list/index.scss'; - import MaterialIcon from '../../../packages/material-icon'; import List, { ListItem, @@ -12,19 +11,33 @@ import List, { ListGroup, ListGroupSubheader, } from '../../../packages/list/index'; -import uuidv1 from 'uuid/v1'; +import {ListItemTextProps} from '../../../packages/list/ListItemText'; // eslint-disable-line no-unused-vars + +// no .d.ts file +// @ts-ignore +import * as uuidv4 from 'uuid/v4'; + +interface SelectionListTestState { + selectedIndex: number; + listItems: string[]; +}; -const renderListItem = (primaryText, secondaryText) => { +function renderListItem(options: ListItemTextProps) { + const {primaryText, secondaryText} = options; + const key = uuidv4(); return ( - - } /> - - } /> + + } /> + + } + /> ); }; -class SelectionListTest extends React.Component { +class SelectionListTest extends React.Component<{}, SelectionListTestState> { state = { selectedIndex: 1, listItems: ['List item 1', 'List item 2', 'List item 3'], @@ -34,19 +47,18 @@ class SelectionListTest extends React.Component { const {listItems, selectedIndex} = this.state; listItems.splice(0, 0, 'New list item'); this.setState({listItems, selectedIndex: selectedIndex + 1}); - } + }; render() { return ( this.setState({selectedIndex})} > - {this.state.listItems.map((text) => renderListItem(text))} + {this.state.listItems.map((text) => renderListItem({primaryText: text}))} ); @@ -61,30 +73,29 @@ const ListScreenshotTest = () => {

Two-line List

- {renderListItem('List item', 'Secondary text')} - {renderListItem('List item', 'Secondary text')} - {renderListItem('List item', 'Secondary text')} + {renderListItem({primaryText: 'List item', secondaryText: 'Secondary text'})} + {renderListItem({primaryText: 'List item', secondaryText: 'Secondary text'})} + {renderListItem({primaryText: 'List item', secondaryText: 'Secondary text'})} - {renderListItem('List item', 'Secondary text')} - {renderListItem('List item', 'Secondary text')} + {renderListItem({primaryText: 'List item', secondaryText: 'Secondary text'})} + {renderListItem({primaryText: 'List item', secondaryText: 'Secondary text'})}

List group

Folders - {renderListItem('Photos')} - {renderListItem('Recipes')} - {renderListItem('Work')} + {renderListItem({primaryText: 'Photos'})} + {renderListItem({primaryText: 'Recipes'})} + {renderListItem({primaryText: 'Work'})} Recent Files - {renderListItem('Vacation itinerary')} - {renderListItem('Kitchen remodel')} + {renderListItem({primaryText: 'Vacation itinerary'})} + {renderListItem({primaryText: 'Kitchen remodel'})} ); }; - export default ListScreenshotTest; diff --git a/test/unit/chips/ChipSet.test.tsx b/test/unit/chips/ChipSet.test.tsx index 91dede53b..5201e0b16 100644 --- a/test/unit/chips/ChipSet.test.tsx +++ b/test/unit/chips/ChipSet.test.tsx @@ -171,7 +171,6 @@ test('#handleRemove calls foundation.handleChipRemoval with chipId', () => { test('#removeChip does not call #props.updateChips if there are no chips', () => { const updateChips = coerceForTesting<(chips: Partial[]) => void>(td.func()); - // @ts-ignore const wrapper = shallow(); wrapper.instance().removeChip(td.matchers.isA(Number)); td.verify(updateChips(td.matchers.anything()), {times: 0}); diff --git a/test/unit/list/ListDivider.test.js b/test/unit/list/ListDivider.test.tsx similarity index 88% rename from test/unit/list/ListDivider.test.js rename to test/unit/list/ListDivider.test.tsx index 68bc5bc11..e7e2b3143 100644 --- a/test/unit/list/ListDivider.test.js +++ b/test/unit/list/ListDivider.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListDivider} from '../../../packages/list'; +import {ListDivider} from '../../../packages/list/index'; suite('ListDivider'); diff --git a/test/unit/list/ListGroup.test.js b/test/unit/list/ListGroup.test.tsx similarity index 76% rename from test/unit/list/ListGroup.test.js rename to test/unit/list/ListGroup.test.tsx index cbb466c6e..e19f33ffb 100644 --- a/test/unit/list/ListGroup.test.js +++ b/test/unit/list/ListGroup.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListGroup} from '../../../packages/list'; +import {ListGroup} from '../../../packages/list/index'; suite('ListGroup'); @@ -16,7 +16,11 @@ test('has mdc-list-group class', () => { }); test('renders children', () => { - const wrapper = shallow(
); + const wrapper = shallow( + +
+ + ); assert.exists(wrapper.find('.child-list')); }); diff --git a/test/unit/list/ListGroupSubheader.test.js b/test/unit/list/ListGroupSubheader.test.tsx similarity index 75% rename from test/unit/list/ListGroupSubheader.test.js rename to test/unit/list/ListGroupSubheader.test.tsx index 46b59b9f0..b87d2db9d 100644 --- a/test/unit/list/ListGroupSubheader.test.js +++ b/test/unit/list/ListGroupSubheader.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListGroupSubheader} from '../../../packages/list'; +import {ListGroupSubheader} from '../../../packages/list/index'; suite('ListGroupSubheader'); @@ -16,7 +16,11 @@ test('has mdc-list-group__subheader class', () => { }); test('renders children', () => { - const wrapper = shallow(
); + const wrapper = shallow( + +
+ + ); assert.exists(wrapper.find('.child-list')); }); diff --git a/test/unit/list/ListItem.test.js b/test/unit/list/ListItem.test.tsx similarity index 51% rename from test/unit/list/ListItem.test.js rename to test/unit/list/ListItem.test.tsx index c549fdca9..1ed952d1f 100644 --- a/test/unit/list/ListItem.test.js +++ b/test/unit/list/ListItem.test.tsx @@ -1,60 +1,63 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {mount, shallow} from 'enzyme'; -import td from 'testdouble'; -import {ListItem} from '../../../packages/list'; +import * as td from 'testdouble'; +import {ListItem} from '../../../packages/list/index'; +import {coerceForTesting} from '../helpers/types'; suite('ListItem'); test('classNames adds classes', () => { - const wrapper = shallow(); + const wrapper = shallow(
meow
); assert.isTrue(wrapper.hasClass('test-class-name')); }); test('classNamesFromList adds classes', () => { - const wrapper = shallow(); + const wrapper = shallow( +
meow
+ ); assert.isTrue(wrapper.hasClass('test-class-name')); }); test('attributesFromList adds props', () => { - const wrapper = shallow(); + const wrapper = shallow(
meow
); assert.equal(wrapper.props().tabIndex, 0); }); test('calls focus when props.shouldFocus changes from false to true', () => { - const wrapper = mount(); - wrapper.instance().focus = td.func(); + const wrapper = mount>(
meow
); + wrapper.instance().focus = coerceForTesting<() => void>(td.func()); wrapper.setProps({shouldFocus: true}); td.verify(wrapper.instance().focus(), {times: 1}); }); test('calls followHref when props.shouldFollowHref changes from false to true', () => { - const wrapper = mount(); - wrapper.instance().followHref = td.func(); + const wrapper = mount>(
meow
); + wrapper.instance().followHref = coerceForTesting<() => void>(td.func()); wrapper.setProps({shouldFollowHref: true}); td.verify(wrapper.instance().followHref(), {times: 1}); }); test('calls toggleCheckbox when props.shouldToggleCheckbox changes from false to true', () => { - const wrapper = mount(); - wrapper.instance().toggleCheckbox = td.func(); + const wrapper = mount>(
meow
); + wrapper.instance().toggleCheckbox = coerceForTesting<() => void>(td.func()); wrapper.setProps({shouldToggleCheckbox: true}); td.verify(wrapper.instance().toggleCheckbox(), {times: 1}); }); test('#focus focuses the listItemElement_', () => { - const wrapper = mount(); - wrapper.instance().listItemElement_.current.focus = td.func(); + const wrapper = mount>(
meow
); + wrapper.instance().listItemElement_.current!.focus = coerceForTesting<() => void>(td.func()); wrapper.instance().focus(); - td.verify(wrapper.instance().listItemElement_.current.focus(), {times: 1}); + td.verify(wrapper.instance().listItemElement_.current!.focus(), {times: 1}); }); test('#followHref simulates a click on the listItemElement_ if it has href', () => { - const wrapper = mount(); - wrapper.instance().listItemElement_.current.href = true; - wrapper.instance().listItemElement_.current.click = td.func(); + const wrapper = mount>(
meow
); + wrapper.instance().listItemElement_.current!.href = 'https://google.com'; + wrapper.instance().listItemElement_.current!.click = coerceForTesting<() => void>(td.func()); wrapper.instance().followHref(); - td.verify(wrapper.instance().listItemElement_.current.click(), {times: 1}); + td.verify(wrapper.instance().listItemElement_.current!.click(), {times: 1}); }); test('passes props.childrenTabIndex to children as props.tabIndex', () => { @@ -67,11 +70,11 @@ test('passes props.childrenTabIndex to children as props.tabIndex', () => { }); test('renders a list item with default tag', () => { - const wrapper = shallow(); + const wrapper = shallow(
Test
); assert.equal(wrapper.type(), 'li'); }); test('renders a list item with an anchor tag', () => { - const wrapper = shallow(); + const wrapper = shallow(
Test
); assert.equal(wrapper.type(), 'a'); }); diff --git a/test/unit/list/ListItemGraphic.test.js b/test/unit/list/ListItemGraphic.test.tsx similarity index 63% rename from test/unit/list/ListItemGraphic.test.js rename to test/unit/list/ListItemGraphic.test.tsx index 64d88d944..a31c3052e 100644 --- a/test/unit/list/ListItemGraphic.test.js +++ b/test/unit/list/ListItemGraphic.test.tsx @@ -1,12 +1,14 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListItemGraphic} from '../../../packages/list'; +import {ListItemGraphic} from '../../../packages/list/index'; suite('ListItemGraphic'); test('className adds classes', () => { - const wrapper = shallow(} className='test-class-name' />); + const wrapper = shallow( + } className='test-class-name' /> + ); assert.isTrue(wrapper.hasClass('test-class-name')); }); @@ -16,11 +18,15 @@ test('has mdc-list-item__graphic class', () => { }); test('has tabIndex of props.tabIndex if specified and tabbableOnListItemFocus is true', () => { - const wrapper = shallow(} tabIndex={3} tabbableOnListItemFocus/>); + const wrapper = shallow( + } tabIndex={3} tabbableOnListItemFocus /> + ); assert.equal(wrapper.find('.mdc-list-item__graphic').props().tabIndex, 3); }); test('has tabIndex of -1 if tabbableOnListItemFocus is false', () => { - const wrapper = shallow(} childrenTabIndex={3}/>); + const wrapper = shallow( + } childrenTabIndex={3} /> + ); assert.equal(wrapper.find('.mdc-list-item__graphic').props().tabIndex, -1); }); diff --git a/test/unit/list/ListItemMeta.test.js b/test/unit/list/ListItemMeta.test.tsx similarity index 74% rename from test/unit/list/ListItemMeta.test.js rename to test/unit/list/ListItemMeta.test.tsx index 4063b916d..376fde1e0 100644 --- a/test/unit/list/ListItemMeta.test.js +++ b/test/unit/list/ListItemMeta.test.tsx @@ -1,17 +1,21 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListItemMeta} from '../../../packages/list'; +import {ListItemMeta} from '../../../packages/list/index'; suite('ListItemMeta'); test('className adds classes if meta is a string', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); assert.isTrue(wrapper.hasClass('test-class-name')); }); test('className adds classes if meta is an element', () => { - const wrapper = shallow(} className='test-class-name' />); + const wrapper = shallow( + } className='test-class-name' /> + ); assert.isTrue(wrapper.hasClass('test-class-name')); }); @@ -36,11 +40,15 @@ test('renders element if meta is an element', () => { }); test('has tabIndex of props.tabIndex if specified and tabbableOnListItemFocus is true', () => { - const wrapper = shallow(} tabIndex={3} tabbableOnListItemFocus/>); + const wrapper = shallow( + } tabIndex={3} tabbableOnListItemFocus /> + ); assert.equal(wrapper.find('.mdc-list-item__meta').props().tabIndex, 3); }); test('has tabIndex of -1 if tabbableOnListItemFocus is false', () => { - const wrapper = shallow(} childrenTabIndex={3}/>); + const wrapper = shallow( + } childrenTabIndex={3} /> + ); assert.equal(wrapper.find('.mdc-list-item__meta').props().tabIndex, -1); }); diff --git a/test/unit/list/ListItemText.test.js b/test/unit/list/ListItemText.test.tsx similarity index 73% rename from test/unit/list/ListItemText.test.js rename to test/unit/list/ListItemText.test.tsx index 7c054668c..db3eb3472 100644 --- a/test/unit/list/ListItemText.test.js +++ b/test/unit/list/ListItemText.test.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; import {shallow} from 'enzyme'; -import {ListItemText} from '../../../packages/list'; +import {ListItemText} from '../../../packages/list/index'; suite('ListItemText'); @@ -21,25 +21,36 @@ test('renders primary text if text is element', () => { }); test('renders primary and secondary text if secondary text provided', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); assert.isTrue(wrapper.hasClass('mdc-list-item__text')); assert.exists(wrapper.find('.mdc-list-item__primary-text')); assert.exists(wrapper.find('.mdc-list-item__secondary-text')); }); test('renders primary and secondary text if text are elements', () => { - const wrapper = shallow(Hello} secondaryText={World} />); + const wrapper = shallow( + Hello} + secondaryText={World} + /> + ); assert.isTrue(wrapper.hasClass('mdc-list-item__text')); assert.exists(wrapper.find('.mdc-list-item__primary-text')); assert.exists(wrapper.find('.mdc-list-item__secondary-text')); }); test('has tabIndex of props.tabIndex if specified and tabbableOnListItemFocus is true', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); assert.equal(wrapper.find('.mdc-list-item__text').props().tabIndex, 3); }); test('has tabIndex of -1 if tabbableOnListItemFocus is false', () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); assert.equal(wrapper.find('.mdc-list-item__text').props().tabIndex, -1); }); diff --git a/test/unit/list/index.test.js b/test/unit/list/index.test.tsx similarity index 57% rename from test/unit/list/index.test.js rename to test/unit/list/index.test.tsx index 8b4bdd110..8edffed78 100644 --- a/test/unit/list/index.test.js +++ b/test/unit/list/index.test.tsx @@ -1,149 +1,166 @@ -import React from 'react'; +import * as React from 'react'; import {assert} from 'chai'; -import td from 'testdouble'; +import * as td from 'testdouble'; import {shallow, mount} from 'enzyme'; -import List from '../../../packages/list'; -import {ListItem} from '../../../packages/list'; +import List, { + ListItem, ListItemProps, // eslint-disable-line no-unused-vars +} from '../../../packages/list/index'; +import {coerceForTesting} from '../helpers/types'; suite('List'); +const children: (key?: number) => React.ReactElement> + = (key: number = 0) => (
meow
); + +const threeChildren: () => React.ReactElement>[] + = () => [0, 1, 2].map((key) => children(key)); + test('creates foundation', () => { - const wrapper = shallow(); - assert.exists(wrapper.instance().foundation_); + const wrapper = shallow( + {children()} + ); + assert.exists(wrapper.instance().foundation); }); test('#componentWillUnmount destroys foundation', () => { - const wrapper = shallow(); - const foundation = wrapper.instance().foundation_; + const wrapper = shallow({children()}); + const foundation = wrapper.instance().foundation; foundation.destroy = td.func(); wrapper.unmount(); td.verify(foundation.destroy()); }); test('calls foundation.setSingleSelection when props.singleSelection changes from false to true', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setSingleSelection = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setSingleSelection = td.func(); wrapper.setProps({singleSelection: true}); - td.verify(wrapper.instance().foundation_.setSingleSelection(true), {times: 1}); + td.verify(wrapper.instance().foundation.setSingleSelection(true), { + times: 1, + }); }); test('calls foundation.setSingleSelection when props.singleSelection changes from true to false', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setSingleSelection = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setSingleSelection = td.func(); wrapper.setProps({singleSelection: false}); - td.verify(wrapper.instance().foundation_.setSingleSelection(false), {times: 1}); + td.verify(wrapper.instance().foundation.setSingleSelection(false), { + times: 1, + }); }); test('calls foundation.setWrapFocus when props.wrapFocus changes from false to true', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setWrapFocus = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setWrapFocus = td.func(); wrapper.setProps({wrapFocus: true}); - td.verify(wrapper.instance().foundation_.setWrapFocus(true), {times: 1}); + td.verify(wrapper.instance().foundation.setWrapFocus(true), {times: 1}); }); test('calls foundation.setWrapFocus when props.wrapFocus changes from true to false', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setWrapFocus = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setWrapFocus = td.func(); wrapper.setProps({wrapFocus: false}); - td.verify(wrapper.instance().foundation_.setWrapFocus(false), {times: 1}); + td.verify(wrapper.instance().foundation.setWrapFocus(false), {times: 1}); }); test('calls foundation.setSelectedIndex when props.selectedIndex changes', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setSelectedIndex = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setSelectedIndex = td.func(); wrapper.setProps({selectedIndex: 1}); - td.verify(wrapper.instance().foundation_.setSelectedIndex(1), {times: 1}); + td.verify(wrapper.instance().foundation.setSelectedIndex(1), {times: 1}); }); test('calls foundation.setVerticalOrientation when \'aria-orientation\' changes from vertical to horizontal', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setVerticalOrientation = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setVerticalOrientation = td.func(); wrapper.setProps({'aria-orientation': 'horizontal'}); - td.verify(wrapper.instance().foundation_.setVerticalOrientation(false), {times: 1}); + td.verify(wrapper.instance().foundation.setVerticalOrientation(false), { + times: 1, + }); }); test('calls foundation.setVerticalOrientation when \'aria-orientation\' changes from horizontal to vertical', () => { - const wrapper = mount(); - wrapper.instance().foundation_.setVerticalOrientation = td.func(); + const wrapper = mount({children()}); + wrapper.instance().foundation.setVerticalOrientation = td.func(); wrapper.setProps({'aria-orientation': 'vertical'}); - td.verify(wrapper.instance().foundation_.setVerticalOrientation(true), {times: 1}); + td.verify(wrapper.instance().foundation.setVerticalOrientation(true), { + times: 1, + }); }); test('classNames adds classes', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.isTrue(wrapper.hasClass('test-class-name')); }); test('has mdc-list--non-interactive class if props.nonInteractive is true', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.isTrue(wrapper.hasClass('mdc-list--non-interactive')); }); test('has mdc-list--dense class if props.dense is true', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.isTrue(wrapper.hasClass('mdc-list--dense')); }); test('has mdc-list--avatar-list class if props.avatarList is true', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.isTrue(wrapper.hasClass('mdc-list--avatar-list')); }); test('has mdc-list--two-line class if props.twoLine is true', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.isTrue(wrapper.hasClass('mdc-list--two-line')); }); test('#adapter.getListItemCount returns number of list items', () => { - const wrapper = mount( + const wrapper = mount( - - - + {threeChildren()} ); assert.equal(wrapper.instance().adapter.getListItemCount(), 3); }); test('#adapter.getListItemCount returns correct number of list items if List has a non-item child', () => { - const wrapper = mount( + const wrapper = mount( - - -
- +
meow
+
meow
+
meow
); assert.equal(wrapper.instance().adapter.getListItemCount(), 3); }); test('#adapter.setAttributeForElementIndex updates state.listItemAttributes', () => { - const wrapper = mount(); - wrapper.instance().adapter.setAttributeForElementIndex(1, 'tabindex', 0); + const wrapper = mount({children()}); + wrapper.instance().adapter.setAttributeForElementIndex(1, 'tabindex', '0'); assert.equal(wrapper.state().listItemAttributes[1]['tabIndex'], 0); }); test('#adapter.removeAttributeForElementIndex removes attribute from state.listItemAttributes if it exists', () => { - const wrapper = mount(); - wrapper.setState({listItemAttributes: {1: {'tabIndex': 0}}}); + const wrapper = mount({children()}); + wrapper.setState({listItemAttributes: {1: {tabIndex: 0}}}); wrapper.instance().adapter.removeAttributeForElementIndex(1, 'tabindex'); - assert.isFalse(wrapper.state().listItemAttributes[1].hasOwnProperty('tabIndex')); + assert.isFalse( + wrapper.state().listItemAttributes[1].hasOwnProperty('tabIndex') + ); }); test('#adapter.removeAttributeForElementIndex does nothing if attribute is not in state.listItemAttributes', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.instance().adapter.removeAttributeForElementIndex(1, 'tabindex'); assert.isFalse(wrapper.state().listItemAttributes.hasOwnProperty(1)); }); test('#adapter.addClassForElementIndex updates state.listItemClassNames if no classes have been added', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.instance().adapter.addClassForElementIndex(1, 'class1'); assert.isTrue(wrapper.state().listItemClassNames[1].indexOf('class1') >= 0); }); test('#adapter.addClassForElementIndex updates state.listItemClassNames if other classes have been added', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.state().listItemClassNames[1] = ['test']; wrapper.instance().adapter.addClassForElementIndex(1, 'class1'); assert.isTrue(wrapper.state().listItemClassNames[1].indexOf('class1') >= 0); @@ -151,102 +168,108 @@ test('#adapter.addClassForElementIndex updates state.listItemClassNames if other }); test('#adapter.removeClassForElementIndex removes class from state.listItemClassNames if it exists', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.state().listItemClassNames[1] = ['class1']; wrapper.instance().adapter.removeClassForElementIndex(1, 'class1'); assert.isFalse(wrapper.state().listItemClassNames[1].indexOf('class1') >= 0); }); test('#adapter.removeClassForElementIndex does nothing if class is not in state.listItemClassNames', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.state().listItemClassNames[1] = []; wrapper.instance().adapter.removeClassForElementIndex(1, 'class1'); assert.isFalse(wrapper.state().listItemClassNames[1].indexOf('class1') >= 0); }); test('#adapter.removeClassForElementIndex does nothing if index is not in state.listItemClassNames', () => { - const wrapper = mount(); + const wrapper = mount({children()}); wrapper.instance().adapter.removeClassForElementIndex(1, 'class1'); assert.isFalse(wrapper.state().listItemClassNames.hasOwnProperty(1)); }); test('#adapter.setTabIndexForListItemChildren updates state.listItemChildrenTabIndex', () => { - const wrapper = mount(); - wrapper.state().listItemClassNames[1] = -1; + const wrapper = mount({children()}); + // wrapper.state().listItemClassNames[1] = -1; wrapper.instance().adapter.setTabIndexForListItemChildren(1, 0); assert.equal(wrapper.state().listItemChildrenTabIndex[1], 0); }); test('#adapter.focusItemAtIndex sets state.focusListItemAtIndex', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); wrapper.instance().adapter.focusItemAtIndex(1); assert.equal(wrapper.state().focusListItemAtIndex, 1); }); test('#adapter.followHref sets state.followHrefAtIndex', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); wrapper.instance().adapter.followHref(1); assert.equal(wrapper.state().followHrefAtIndex, 1); }); test('#adapter.toggleCheckbox sets state.toggleCheckboxAtIndex', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); wrapper.instance().adapter.toggleCheckbox(1); assert.equal(wrapper.state().toggleCheckboxAtIndex, 1); }); test('#handleKeyDown calls #foudation.handleKeydown', () => { - const wrapper = shallow(); - wrapper.instance().foundation_.handleKeydown = td.func(); - const evt = {persist: () => {}}; + const wrapper = shallow({children()}); + wrapper.instance().foundation.handleKeydown = td.func(); + const evt = coerceForTesting({persist: () => {}}); wrapper.instance().handleKeyDown(evt, 1); - td.verify(wrapper.instance().foundation_.handleKeydown(evt, true, 1), {times: 1}); + td.verify(wrapper.instance().foundation.handleKeydown(evt, true, 1), { + times: 1, + }); }); test('#handleKeyDown calls #props.handleSelect if key is enter', () => { - const handleSelect = td.func(); - const wrapper = shallow(); - const evt = {persist: () => {}, key: 'Enter'}; + const handleSelect = coerceForTesting<(selectedIndex: number) => void>(td.func()); + const wrapper = shallow({children()}); + const evt = coerceForTesting({persist: () => {}, key: 'Enter'}); wrapper.instance().handleKeyDown(evt, 1); td.verify(handleSelect(1), {times: 1}); }); test('#handleClick calls #foudation.handleClick', () => { - const wrapper = shallow(); - wrapper.instance().foundation_.handleClick = td.func(); - wrapper.instance().handleClick({target: {type: 'span'}}, 1); - td.verify(wrapper.instance().foundation_.handleClick(1, false), {times: 1}); + const wrapper = shallow({children()}); + const target = {type: 'span'}; + const evt = coerceForTesting>({target}); + wrapper.instance().foundation.handleClick = td.func(); + wrapper.instance().handleClick(evt, 1); + td.verify(wrapper.instance().foundation.handleClick(1, false), {times: 1}); }); test('#handleClick calls #props.handleSelect', () => { - const handleSelect = td.func(); - const wrapper = shallow(); - wrapper.instance().handleClick({target: {type: 'span'}}, 1); + const handleSelect = coerceForTesting<(selectedIndex: number) => void>(td.func()); + const target = {type: 'span'}; + const evt = coerceForTesting>({target}); + const wrapper = shallow({children()}); + wrapper.instance().handleClick(evt, 1); td.verify(handleSelect(1), {times: 1}); }); test('#handleFocus calls #foudation.handleFocusIn', () => { - const wrapper = shallow(); - wrapper.instance().foundation_.handleFocusIn = td.func(); - const evt = {}; + const wrapper = shallow({children()}); + wrapper.instance().foundation.handleFocusIn = td.func(); + const evt = coerceForTesting({}); wrapper.instance().handleFocus(evt, 1); - td.verify(wrapper.instance().foundation_.handleFocusIn(evt, 1), {times: 1}); + td.verify(wrapper.instance().foundation.handleFocusIn(evt, 1), {times: 1}); }); test('#handleBlur calls #foudation.handleFocusOut', () => { - const wrapper = shallow(); - wrapper.instance().foundation_.handleFocusOut = td.func(); - const evt = {}; + const wrapper = shallow({children()}); + wrapper.instance().foundation.handleFocusOut = td.func(); + const evt = coerceForTesting({}); wrapper.instance().handleBlur(evt, 1); - td.verify(wrapper.instance().foundation_.handleFocusOut(evt, 1), {times: 1}); + td.verify(wrapper.instance().foundation.handleFocusOut(evt, 1), { + times: 1, + }); }); test('#renderListItem renders default list item at index 0', () => { const wrapper = mount( - - - + {threeChildren()} ); const listItemProps = wrapper.children().props().children[0].props; @@ -261,13 +284,10 @@ test('#renderListItem renders default list item at index 0', () => { test('#renderListItem renders default list item at index not 0', () => { const wrapper = mount( - - - + {threeChildren()} ); const listItemProps = wrapper.children().props().children[1].props; - assert.isFalse(listItemProps.shouldFocus); assert.isFalse(listItemProps.shouldFollowHref); assert.isFalse(listItemProps.shouldToggleCheckbox); @@ -279,63 +299,58 @@ test('#renderListItem renders default list item at index not 0', () => { test('#renderListItem renders list item with prop.shouldFocus true if its index is state.focusListItemAtIndex', () => { const wrapper = mount( - - - + {threeChildren()} ); wrapper.setState({focusListItemAtIndex: 1}); - const children = wrapper.children().props().children; assert.isFalse(children[0].props.shouldFocus); assert.isTrue(children[1].props.shouldFocus); assert.isFalse(children[2].props.shouldFocus); }); -test('#renderListItem renders list item with prop.shouldFollowHref true ' + -'if its index is state.followHrefAtIndex', () => { - const wrapper = mount( - - - - - - ); - wrapper.setState({followHrefAtIndex: 1}); - const children = wrapper.children().props().children; - assert.isFalse(children[0].props.shouldFollowHref); - assert.isTrue(children[1].props.shouldFollowHref); - assert.isFalse(children[2].props.shouldFollowHref); -}); - -test('#renderListItem renders list item with prop.shouldToggleCheckbox true ' + -'if its index is state.toggleCheckboxAtIndex', () => { - const wrapper = mount( - - - - - - ); - wrapper.setState({toggleCheckboxAtIndex: 1}); - - const children = wrapper.children().props().children; - assert.isFalse(children[0].props.shouldToggleCheckbox); - assert.isTrue(children[1].props.shouldToggleCheckbox); - assert.isFalse(children[2].props.shouldToggleCheckbox); -}); +test( + '#renderListItem renders list item with prop.shouldFollowHref true ' + + 'if its index is state.followHrefAtIndex', + () => { + const wrapper = mount( + + {threeChildren()} + + ); + wrapper.setState({followHrefAtIndex: 1}); + const children = wrapper.children().props().children; + assert.isFalse(children[0].props.shouldFollowHref); + assert.isTrue(children[1].props.shouldFollowHref); + assert.isFalse(children[2].props.shouldFollowHref); + } +); + +test( + '#renderListItem renders list item with prop.shouldToggleCheckbox true ' + + 'if its index is state.toggleCheckboxAtIndex', + () => { + const wrapper = mount( + + {threeChildren()} + + ); + wrapper.setState({toggleCheckboxAtIndex: 1}); + const children = wrapper.children().props().children; + assert.isFalse(children[0].props.shouldToggleCheckbox); + assert.isTrue(children[1].props.shouldToggleCheckbox); + assert.isFalse(children[2].props.shouldToggleCheckbox); + } +); test('#renderListItem renders list item with state.listItemAttributes at index as prop.attributesFromList', () => { const wrapper = mount( - - - + {threeChildren()} ); const attributes = {tabIndex: 0}; wrapper.setState({listItemAttributes: {1: attributes}}); - const children = wrapper.children().props().children; assert.isEmpty(children[0].props.attributesFromList); assert.equal(children[1].props.attributesFromList, attributes); @@ -345,14 +360,11 @@ test('#renderListItem renders list item with state.listItemAttributes at index a test('#renderListItem renders list item with state.listItemClassNames at index as prop.classNamesFromList', () => { const wrapper = mount( - - - + {threeChildren()} ); const classes = ['test-class']; wrapper.setState({listItemClassNames: {1: classes}}); - const children = wrapper.children().props().children; assert.isEmpty(children[0].props.classNamesFromList); assert.equal(children[1].props.classNamesFromList, classes); @@ -362,13 +374,10 @@ test('#renderListItem renders list item with state.listItemClassNames at index a test('#renderListItem renders list item with state.listItemChildrenTabIndex at index as prop.childrenTabIndex', () => { const wrapper = mount( - - - + {threeChildren()} ); wrapper.setState({listItemChildrenTabIndex: {1: 0}}); - const children = wrapper.children().props().children; assert.equal(children[0].props.childrenTabIndex, -1); assert.equal(children[1].props.childrenTabIndex, 0); @@ -376,22 +385,20 @@ test('#renderListItem renders list item with state.listItemChildrenTabIndex at i }); test('first item is selected if props.selectedIndex is 0', () => { - const wrapper = mount( + const wrapper = mount( - - - + {threeChildren()} ); assert.isTrue(wrapper.state().listItemAttributes[0]['aria-selected']); }); test('renders a list with default tag', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.equal(wrapper.type(), 'ul'); }); test('renders a list with a nav tag', () => { - const wrapper = shallow(); + const wrapper = shallow({children()}); assert.equal(wrapper.type(), 'nav'); }); diff --git a/test/unit/top-app-bar/index.test.tsx b/test/unit/top-app-bar/index.test.tsx index 4725a1e35..ea8d94c30 100644 --- a/test/unit/top-app-bar/index.test.tsx +++ b/test/unit/top-app-bar/index.test.tsx @@ -16,8 +16,6 @@ interface RippleProps extends InjectedProps { type DivRippleProps = RippleProps & React.HTMLProps; type ActionItemRippleProps = RippleProps & React.HTMLProps; -// TODO: Replace with real tsx ripple props. Fix with #528 -// @ts-ignore const NavigationIcon: React.FunctionComponent = ({ /* eslint-disable react/prop-types */ initRipple,