diff --git a/package.json b/package.json index 542143ebb..efccf35d7 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "testdouble": "^3.6.0", "ts-loader": "^3.5.0", "ts-node": "^7.0.1", - "typescript": "^3.2.2", + "typescript": "^3.3.3", "typescript-eslint-parser": "^21.0.1", "utility-types": "^2.1.0", "uuid": "^3.3.2", diff --git a/packages/list/index.tsx b/packages/list/index.tsx index 9f5f1d268..5902ee20c 100644 --- a/packages/list/index.tsx +++ b/packages/list/index.tsx @@ -185,11 +185,6 @@ export default class List extends React.Component { return []; } - // this is a proxy for ListItem - getListElements = () => { - return this.listElements; - } - get classes() { const { className, diff --git a/packages/menu-surface/index.tsx b/packages/menu-surface/index.tsx index 534a2ba54..fea27f00d 100644 --- a/packages/menu-surface/index.tsx +++ b/packages/menu-surface/index.tsx @@ -44,6 +44,7 @@ export interface MenuSurfaceProps extends React.HTMLProps { }; onClose?: () => void; onOpen?: () => void; + onMount?: (isMounted: boolean) => void; quickOpen?: boolean; open?: boolean; fixed?: boolean; @@ -148,6 +149,9 @@ class MenuSurface extends React.Component { if (this.props.quickOpen !== prevProps.quickOpen) { this.foundation.setQuickOpen(this.props.quickOpen!); } + if (this.state.mounted !== prevState.mounted) { + this.props.onMount && this.props.onMount(this.state.mounted); + } } componentWillUnmount() { @@ -344,6 +348,7 @@ class MenuSurface extends React.Component { onKeyDown, styles, quickOpen, + onMount, /* eslint-enable */ children, ...otherProps diff --git a/packages/menu/index.tsx b/packages/menu/index.tsx index dd3224180..096e24b52 100644 --- a/packages/menu/index.tsx +++ b/packages/menu/index.tsx @@ -33,6 +33,7 @@ const {cssClasses} = MDCMenuFoundation; export interface MenuProps extends MenuSurfaceProps { children: React.ReactElement; onSelected?: (index: number, item: Element) => void; + ref?: React.Ref; }; export interface MenuState { @@ -130,12 +131,7 @@ class Menu extends React.Component { handleOpen: MenuSurfaceProps['onOpen'] = () => { const {onOpen} = this.props; - if (onOpen) { - onOpen(); - } - if (this.listElements.length > 0) { - (this.listElements[0] as HTMLElement).focus(); - } + onOpen && onOpen(); } render() { @@ -147,11 +143,9 @@ class Menu extends React.Component { onOpen, children, onSelected, - ref, /* eslint-enable no-unused-vars */ ...otherProps } = this.props; - return ( { ); } - renderChild() { const {children} = this.props; const {foundation} = this.state; @@ -194,8 +187,8 @@ export { ListDivider as MenuListDivider, ListGroup as MenuListGroup, ListGroupSubheader as MenuListGroupSubheader, - ListItemGraphic as MenuListGraphic, - ListItemMeta as MenuListMeta, + ListItemGraphic as MenuListItemGraphic, + ListItemMeta as MenuListItemMeta, ListItemText as MenuListItemText, } from '@material/react-list'; export {MenuListProps} from './MenuList'; diff --git a/packages/select/BaseSelect.tsx b/packages/select/BaseSelect.tsx new file mode 100644 index 000000000..f622deefe --- /dev/null +++ b/packages/select/BaseSelect.tsx @@ -0,0 +1,158 @@ +// The MIT License +// +// Copyright (c) 2019 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 NativeSelect, { + NativeSelectProps, // eslint-disable-line no-unused-vars +} from './NativeSelect'; +import EnhancedSelect, { + EnhancedSelectProps, // eslint-disable-line no-unused-vars +} from './EnhancedSelect'; +import {MDCSelectFoundation} from '@material/select/foundation'; + +export type BaseSelectProps + = (T extends HTMLSelectElement ? NativeSelectProps : EnhancedSelectProps); + +export interface CommonSelectProps { + enhanced: boolean; + className?: string; + disabled?: boolean; + foundation?: MDCSelectFoundation; + value?: string; + selectClassName?: string; +} + +export class BaseSelect + extends React.Component> { + static defaultProps = { + enhanced: false, + selectClassName: '', + }; + + handleFocus = (evt: React.FocusEvent) => { + const {foundation, onFocus} = this.props; + if (foundation) { + foundation.handleFocus(); + } + onFocus && onFocus(evt); + }; + + handleBlur = (evt: React.FocusEvent) => { + const {foundation, onBlur} = this.props; + if (foundation) { + foundation.handleBlur(); + } + onBlur && onBlur(evt); + }; + + handleTouchStart = (evt: React.TouchEvent) => { + const {foundation, onTouchStart} = this.props; + if (foundation) { + foundation.handleClick(this.getNormalizedXCoordinate(evt)); + } + onTouchStart && onTouchStart(evt); + } + + handleMouseDown = (evt: React.MouseEvent) => { + const {foundation, onMouseDown} = this.props; + if (foundation) { + foundation.handleClick(this.getNormalizedXCoordinate(evt)); + } + onMouseDown && onMouseDown(evt); + } + + handleClick = (evt: React.MouseEvent) => { + const {foundation, onClick} = this.props; + if (foundation) { + foundation.handleClick(this.getNormalizedXCoordinate(evt)); + } + onClick && onClick(evt); + } + + handleKeyDown = (evt: React.KeyboardEvent) => { + const {foundation, onKeyDown} = this.props; + if (foundation) { + foundation.handleKeydown(evt.nativeEvent); + } + onKeyDown && onKeyDown(evt); + } + + private isTouchEvent = (evt: MouseEvent | TouchEvent): evt is TouchEvent => { + return Boolean((evt as TouchEvent).touches); + } + + private getNormalizedXCoordinate + = (evt: React.MouseEvent | React.TouchEvent) => { + const targetClientRect = (evt.currentTarget as Element).getBoundingClientRect(); + const xCoordinate + = this.isTouchEvent(evt.nativeEvent) ? evt.nativeEvent.touches[0].clientX : evt.nativeEvent.clientX; + return xCoordinate - targetClientRect.left; + } + + + render() { + const { + /* eslint-disable no-unused-vars */ + onFocus, + onBlur, + onClick, + onMouseDown, + onTouchStart, + ref, + /* eslint-enable no-unused-vars */ + enhanced, + children, + onKeyDown, + selectClassName, + ...otherProps + } = this.props; + + const props = { + onFocus: this.handleFocus, + onBlur: this.handleBlur, + onMouseDown: this.handleMouseDown, + onClick: this.handleClick, + onTouchStart: this.handleTouchStart, + className: selectClassName, + ...otherProps, + }; + + if (enhanced) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); + } +} diff --git a/packages/select/EnhancedSelect.tsx b/packages/select/EnhancedSelect.tsx new file mode 100644 index 000000000..eca0c0794 --- /dev/null +++ b/packages/select/EnhancedSelect.tsx @@ -0,0 +1,208 @@ +// The MIT License +// +// Copyright (c) 2019 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 {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation'; +import Menu, {MenuList} from '@material/react-menu'; +import {OptionProps} from './Option'; // eslint-disable-line no-unused-vars +import {CommonSelectProps} from './BaseSelect'; +import MDCSelectFoundation from '@material/select/foundation'; + +const {Corner} = MDCMenuSurfaceFoundation; +const TRUE = 'true'; +const FALSE = 'false'; + +export type EnhancedChild = React.ReactElement>; + +export interface EnhancedSelectProps extends CommonSelectProps, React.HTMLProps { + closeMenu?: () => void; + onEnhancedChange?: (index: number, item: Element) => void; + anchorElement: HTMLElement | null; + value?: string; + ref?: React.Ref; + isInvalid?: boolean; +} + +interface EnhancedSelectState { + 'aria-expanded'?: boolean | 'false' | 'true'; + selectedItem: Element | null; + selectedValue?: string; +} + +export default class EnhancedSelect extends React.Component< + EnhancedSelectProps, + EnhancedSelectState + > { + nativeControl: React.RefObject = React.createRef(); + private selectedTextEl = React.createRef(); + menuEl = React.createRef(); + + static defaultProps: Partial = { + disabled: false, + closeMenu: () => {}, + onEnhancedChange: () => {}, + value: '', + anchorElement: null, + isInvalid: false, + }; + + state: EnhancedSelectState = { + 'aria-expanded': undefined, + 'selectedItem': null, + 'selectedValue': '', + } + + componentDidUpdate(prevProps: EnhancedSelectProps) { + if (this.props.value !== prevProps.value) { + this.setSelected(); + } + } + + get listElements() { + return this.menuEl.current !== null && this.menuEl.current!.listElements; + } + + setSelected = () => { + const listElements = this.menuEl.current !== null && this.menuEl.current!.listElements; + if (!listElements || !listElements.length) return; + + const index = this.getIndexByValue(listElements); + const selectedItem = listElements[index]; + const selectedValue + = selectedItem && selectedItem.getAttribute(MDCSelectFoundation.strings.ENHANCED_VALUE_ATTR) || ''; + this.setState({selectedItem, selectedValue}); + } + + private getIndexByValue = (listElements: Element[]) => { + const {value} = this.props; + let index = -1; + if (index < 0 && value) { + listElements.some((element: Element, elementIndex: number) => { + if (element.getAttribute(MDCSelectFoundation.strings.ENHANCED_VALUE_ATTR) === value) { + index = elementIndex; + return true; + } + return false; + }); + } + return index; + } + + private handleMenuClose = () => { + const {closeMenu, foundation} = this.props; + closeMenu!(); + this.setState({'aria-expanded': undefined}); + if (foundation && document.activeElement !== this.selectedTextEl.current) { + foundation.handleBlur(); + } + } + + private handleMenuOpen = () => { + this.setState({'aria-expanded': true}); + if (this.listElements && this.listElements.length > 0) { + let index = this.getIndexByValue(this.listElements); + index = index > -1 ? index : 0; + const listItem = this.listElements[index]; + (listItem as HTMLElement).focus(); + } + } + + render() { + const { + children, + required, + open, + disabled, + anchorElement, + onMouseDown, + onTouchStart, + onKeyDown, + onFocus, + onClick, + onBlur, + onEnhancedChange, + isInvalid, + } = this.props; + + const {'aria-expanded': ariaExpanded, selectedValue, selectedItem} = this.state; + + const selectedTextAttrs: {[key: string]: string} = {}; + if (required) { + selectedTextAttrs['aria-required'] = required.toString(); + } + if (ariaExpanded && ariaExpanded !== FALSE) { + selectedTextAttrs['aria-expanded'] = TRUE; + } + if (isInvalid) { + selectedTextAttrs['aria-invalid'] = TRUE; + } + if (disabled) { + selectedTextAttrs['aria-disabled'] = TRUE; + } else { + selectedTextAttrs['aria-disabled'] = FALSE; + } + + return ( + + +
+ {selectedItem ? (selectedItem as Element).textContent!.trim() : ''} +
+ + + {/* TODO: this should use React.createContext instead */} + {React.Children.map(children, (child) => { + const c = child as React.ReactElement; + return React.cloneElement(c, {...c.props, enhanced: true}); + })} + + +
+ ); + } +} diff --git a/packages/select/NativeControl.tsx b/packages/select/NativeControl.tsx deleted file mode 100644 index bb78c40b6..000000000 --- a/packages/select/NativeControl.tsx +++ /dev/null @@ -1,133 +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 from 'react'; -import classnames from 'classnames'; -// @ts-ignore no .d.ts file -import {MDCSelectFoundation} from '@material/select/dist/mdc.select'; - -export interface NativeControlProps extends React.HTMLProps { - className: string; - disabled: boolean; - foundation: MDCSelectFoundation; - setRippleCenter: (lineRippleCenter: number) => void; - handleDisabled: (disabled: boolean) => void; - value: string; -} - -export default class NativeControl extends React.Component< - NativeControlProps, - {} - > { - nativeControl_: React.RefObject = React.createRef(); - - static defaultProps: NativeControlProps = { - className: '', - children: null, - disabled: false, - foundation: { - handleFocus: () => {}, - handleBlur: () => {}, - }, - setRippleCenter: () => {}, - handleDisabled: () => {}, - value: '', - }; - - - componentDidUpdate(prevProps: NativeControlProps) { - if (this.props.disabled !== prevProps.disabled) { - this.props.handleDisabled(this.props.disabled); - } - } - - get classes() { - return classnames('mdc-select__native-control', this.props.className); - } - - handleFocus = (evt: React.FocusEvent) => { - const {foundation, onFocus} = this.props; - foundation.handleFocus(evt); - onFocus && onFocus(evt); - }; - - handleBlur = (evt: React.FocusEvent) => { - const {foundation, onBlur} = this.props; - foundation.handleBlur(evt); - onBlur && onBlur(evt); - }; - - handleMouseDown = (evt: React.MouseEvent) => { - const {onMouseDown} = this.props; - this.setRippleCenter(evt.clientX, evt.target as HTMLSelectElement); - onMouseDown && onMouseDown(evt); - }; - - handleTouchStart = (evt: React.TouchEvent) => { - const {onTouchStart} = this.props; - const clientX = evt.touches[0] && evt.touches[0].clientX; - this.setRippleCenter(clientX, evt.target as HTMLSelectElement); - onTouchStart && onTouchStart(evt); - }; - - setRippleCenter = (xCoordinate: number, target: HTMLSelectElement) => { - if (target !== this.nativeControl_.current) return; - const targetClientRect = target.getBoundingClientRect(); - const normalizedX = xCoordinate - targetClientRect.left; - this.props.setRippleCenter(normalizedX); - }; - - render() { - const { - disabled, - /* eslint-disable no-unused-vars */ - className, - children, - foundation, - value, - handleDisabled, - onFocus, - onBlur, - onTouchStart, - onMouseDown, - setRippleCenter, - /* eslint-enable no-unused-vars */ - ...otherProps - } = this.props; - - return ( - - ); - } -} diff --git a/packages/select/NativeSelect.tsx b/packages/select/NativeSelect.tsx new file mode 100644 index 000000000..cf5ddc9f6 --- /dev/null +++ b/packages/select/NativeSelect.tsx @@ -0,0 +1,92 @@ +// 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 from 'react'; +import classnames from 'classnames'; +import {CommonSelectProps} from './BaseSelect'; + +type RefCallback = (node: T | null) => void; + +export interface NativeSelectProps extends CommonSelectProps, React.HTMLProps { + innerRef?: RefCallback | React.RefObject; + value?: string; + ref?: React.Ref; +} + +export default class NativeSelect extends React.Component< + NativeSelectProps, + {} + > { + NativeSelect: React.RefObject = React.createRef(); + + static defaultProps: Partial = { + className: '', + children: null, + disabled: false, + value: '', + }; + + get classes() { + return classnames('mdc-select__native-control', this.props.className); + } + + attachRef = (node: HTMLSelectElement | null) => { + const {innerRef} = this.props; + + // https://github.com/facebook/react/issues/13029#issuecomment-410002316 + // @ts-ignore incorrectly typed as readonly - see github comment + this.NativeSelect.current = node; + + if (!innerRef) { + return; + } + + if (typeof innerRef !== 'function') { + // @ts-ignore same as above + innerRef.current = node; + } else { + innerRef(node); + } + } + + render() { + const { + /* eslint-disable no-unused-vars */ + className, + children, + foundation, + innerRef, + /* eslint-enable no-unused-vars */ + ...otherProps + } = this.props; + + return ( + + ); + } +} diff --git a/packages/select/README.md b/packages/select/README.md index 6ea1f3d4c..4f18b301c 100644 --- a/packages/select/README.md +++ b/packages/select/README.md @@ -24,11 +24,11 @@ import '@material/react-select/dist/select.css'; ### Javascript Instantiation -React Select requires at least one ` ); }); } renderLabel() { const {id, label, floatingLabelClassName} = this.props; + if (!label) return; + return ( { className={notchedOutlineClassName} notch={outlineIsNotched} notchWidth={labelWidth} - /> + > + {this.renderLabel()} + ); } + + renderHelperText() { + const {helperText} = this.props; + if (!helperText) return; + const props = { + ...helperText.props, + setHelperTextFoundation: this.setHelperTextFoundation, + } as SelectHelperTextProps; + return React.cloneElement(helperText, props); + } + + renderIcon() { + const {leadingIcon} = this.props; + if (!leadingIcon) return; + const props = { + ...leadingIcon.props, + setIconFoundation: this.setIconFoundation, + } as SelectIconProps; + return React.cloneElement(leadingIcon, props); + } } + +export { + SelectHelperText, + SelectHelperTextProps, +} from './helper-text'; +export { + SelectIcon, + SelectIconProps, +} from './icon'; + +export {Option}; +export { + MenuListDivider as OptionDivider, + MenuListGroup as OptionGroup, + MenuListGroupSubheader as OptionGroupSubheader, + MenuListItemGraphic as OptionGraphic, + MenuListItemMeta as OptionMeta, + MenuListItemText as OptionText, +} from '@material/react-menu'; diff --git a/packages/select/package.json b/packages/select/package.json index 5b918c97f..a20935655 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -19,6 +19,7 @@ "@material/react-floating-label": "^0.11.0", "@material/react-line-ripple": "^0.11.0", "@material/react-menu": "^0.0.0", + "@material/react-menu-surface": "^0.11.0", "@material/react-notched-outline": "^0.11.0", "@material/select": "^1.1.1", "classnames": "^2.2.6", diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index e9140580f..bbfcf1bca 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -49,5 +49,7 @@ "drawer/dismissible": "6ea0638441e0e6df3c97028136239498a31bba206253a01d114431eda20d1060", "drawer/modal": "da83487c9349b253f7d4de01f92d442de55aab92a8028b77ff1a48cfaa265b72", "drawer/permanentToModal": "6355905c2241b5e6fdddc2e25119a1cc3b062577375a88b59e6750c4b76e4561", - "typography": "c5e87d672d8c05ca3b61c0df4971eabe3c6a6a1f24a9b98f71f55a23360c498a" + "typography": "c5e87d672d8c05ca3b61c0df4971eabe3c6a6a1f24a9b98f71f55a23360c498a", + "select/nativeSelect": "d3656c37d75f956e227b8ca731cfb7a96e878e1633bfa2d232f37e825f8505fc", + "select/enhanced": "3c9e9f6754faa76852675cf8e22d3d0ba93d0e82d4680e44666210a2244f9ff2" } diff --git a/test/screenshot/index.tsx b/test/screenshot/index.tsx index 20934cf6f..c1a0ecdf5 100644 --- a/test/screenshot/index.tsx +++ b/test/screenshot/index.tsx @@ -4,11 +4,16 @@ import {HashRouter, Route} from 'react-router-dom'; import App from './App'; import topAppBarVariants from './top-app-bar/variants'; import drawerVariants from './drawer/variants'; +import selectVariants from './select/variants'; import textFieldVariants from './text-field/variants'; import dialogVariants from './dialog/variants'; import {COMPONENTS} from './constants'; import './index.scss'; +const variantRoute = (path: string, Component: React.ComponentClass) => ( + +); + ReactDOM.render(
@@ -27,30 +32,27 @@ ReactDOM.render( {dialogVariants.map((variant: string) => { const path = `dialog/${variant}`; const Component = require(`./dialog/${variant}`).default; - return ( - - ); + return variantRoute(path, Component); })} {drawerVariants.map((variant: string) => { const path = `drawer/${variant}`; const Component = require(`./drawer/${variant}`).default; - return ( - - ); + return variantRoute(path, Component); + })} + {selectVariants.map((variant: string) => { + const path = `select/${variant}`; + const Component = require(`./select/${variant}`).default; + return variantRoute(path, Component); })} {textFieldVariants.map((variant: string) => { const path = `text-field/${variant}`; const Component = require(`./text-field/${variant}`).default; - return ( - - ); + return variantRoute(path, Component); })} {topAppBarVariants.map((variant: string) => { const path = `top-app-bar/${variant}`; const Component = require(`./top-app-bar/${variant}`).default; - return ( - - ); + return variantRoute(path, Component); })}
, diff --git a/test/screenshot/screenshot-test-urls.tsx b/test/screenshot/screenshot-test-urls.tsx index 2396f1fe8..532b2e5d1 100644 --- a/test/screenshot/screenshot-test-urls.tsx +++ b/test/screenshot/screenshot-test-urls.tsx @@ -1,6 +1,7 @@ import topAppBarVariants from './top-app-bar/variants'; import dialogVariants from './dialog/variants'; import drawerVariants from './drawer/variants'; +import selectVariants from './select/variants'; import textFieldVariants from './text-field/variants'; const urls = [ 'button', @@ -19,7 +20,6 @@ const urls = [ 'menu-surface', 'notched-outline', 'radio', - 'select', 'snackbar', 'tab', 'tab-bar', @@ -42,6 +42,10 @@ drawerVariants.forEach((variant: string) => { urls.push(`drawer/${variant}`); }); +selectVariants.forEach((variant: string) => { + urls.push(`select/${variant}`); +}); + textFieldVariants.forEach((variant: string) => { urls.push(`text-field/${variant}`); }); diff --git a/test/screenshot/select/enhanced.tsx b/test/screenshot/select/enhanced.tsx new file mode 100644 index 000000000..3d9984bdb --- /dev/null +++ b/test/screenshot/select/enhanced.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import {getSelects} from './index'; + +const EnhancedSelectScreenshotTest = () => { + return
{getSelects(true)}
; +}; + +export default EnhancedSelectScreenshotTest; diff --git a/test/screenshot/select/index.tsx b/test/screenshot/select/index.tsx index 998903be2..322ffa8e6 100644 --- a/test/screenshot/select/index.tsx +++ b/test/screenshot/select/index.tsx @@ -1,87 +1,123 @@ import React from 'react'; -import Select, {SelectProps} from '../../../packages/select/index'; +import {Link} from 'react-router-dom'; +import selectVariants from './variants'; +import Select, {SelectIcon, SelectProps, Option, SelectHelperText} from '../../../packages/select/index'; +import '../../../packages/select/index.scss'; +import './index.scss'; + +const SelectScreenshotTest = () => { + return ( +
+ {selectVariants.map((variant, index) => ( +
+ {variant} +
+ ))} +
+ ); +}; + +interface SelectTestProps extends SelectProps { + enhanced: boolean; +} interface SelectTestState { - value: any + value: string; } -class SelectTest extends React.Component { - constructor(props: SelectProps) { +class SelectTest extends React.Component { + constructor(props: SelectTestProps) { super(props); this.state = {value: props.value || ''}; // eslint-disable-line react/prop-types } static defaultProps: Partial = { - box: false, className: '', disabled: false, floatingLabelClassName: '', - isRtl: false, lineRippleClassName: '', - nativeControlClassName: '', + selectClassName: '', notchedOutlineClassName: '', outlined: false, options: [], onChange: () => {}, + enhanced: false, } onChange = (evt: React.ChangeEvent) => ( this.setState({value: evt.target.value}) ); + onEnhancedChange = (_index: number, item: Element) => ( + this.setState({value: item.getAttribute('data-value') as string}) + ); + render() { const { disabled, id, - isRtl, + enhanced, ref, // eslint-disable-line no-unused-vars ...otherProps // eslint-disable-line react/prop-types } = this.props; return ( -
+
); } } -const variants = [{}, {box: true}, {outlined: true}]; -const rtlMap = [{}, {isRtl: true}]; +const variants = [{}, {outlined: true}]; +const leadingIconMap = [{}, { + leadingIcon: favorite, + key: 'favorite', +}]; const disabledMap = [{}, {disabled: true}]; +const requiredMap = [{}, {required: true}]; const valueMap = [{}, {value: 'pomsky'}]; +const helperTextMap = [ + {key: 'nohelpertext'}, + {helperText: Help me, key: 'persistent'}, +]; -const selects = variants.map((variant) => { - return rtlMap.map((isRtl) => { - return disabledMap.map((disabled) => { - return valueMap.map((value) => { - const props = Object.assign({}, variant, disabled, isRtl, value); - const valueKey = Object.keys(value)[0] || ''; - const variantKey = Object.keys(variant)[0] || ''; - const rtlKey = Object.keys(isRtl)[0] || ''; - const disabledKey = Object.keys(disabled)[0] || ''; - const key = `${variantKey}-${disabledKey}-${valueKey}--${rtlKey}`; - return ; +export const getSelects = (enhanced: boolean = false) => variants.map((variant) => { + return disabledMap.map((disabled) => { + return valueMap.map((value) => { + return requiredMap.map((required) => { + return helperTextMap.map((helperText) => { + return leadingIconMap.map((icon) => { + const props = Object.assign({}, variant, disabled, value, required, helperText, icon, {enhanced}); + const valueKey = Object.keys(value)[0] || ''; + const variantKey = Object.keys(variant)[0] || ''; + const disabledKey = Object.keys(disabled)[0] || ''; + const requiredKey = Object.keys(required)[0] || ''; + const key + = `${variantKey}-${disabledKey}-${valueKey}-${requiredKey} + -${helperText.key}-${icon.key}-${enhanced}`; + return ; + }); + }); }); }); }); }); -const SelectScreenshotTest = () => { - return
{selects}
; -}; export default SelectScreenshotTest; diff --git a/test/screenshot/select/nativeSelect.tsx b/test/screenshot/select/nativeSelect.tsx new file mode 100644 index 000000000..8ab821621 --- /dev/null +++ b/test/screenshot/select/nativeSelect.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import {getSelects} from './index'; + +const NativeSelectScreenshotTest = () => { + return
{getSelects()}
; +}; + +export default NativeSelectScreenshotTest; diff --git a/test/screenshot/select/variants.tsx b/test/screenshot/select/variants.tsx new file mode 100644 index 000000000..282edc3a5 --- /dev/null +++ b/test/screenshot/select/variants.tsx @@ -0,0 +1,4 @@ +export default [ + 'nativeSelect', + 'enhanced', +]; diff --git a/test/unit/menu/index.test.tsx b/test/unit/menu/index.test.tsx index c4d4a10e7..bceec3d36 100644 --- a/test/unit/menu/index.test.tsx +++ b/test/unit/menu/index.test.tsx @@ -203,22 +203,6 @@ test('handleOpen calls props.onOpen', () => { td.verify(onOpen(), {times: 1}); }); -test('handleOpen calls focuses on first list element', () => { - const div = document.createElement('div'); - document.body.append(div); - const options = {attachTo: div}; - const wrapper = mount( - - - - - , options); - coerceForTesting(wrapper.instance()).handleOpen!(); - assert.equal(document.activeElement, wrapper.find('.mdc-list-item').first().getDOMNode()); - wrapper.unmount(); - div.remove(); -}); - test('menu renders with tabindex=-1', () => { const wrapper = shallow(); assert.equal(wrapper.props().tabIndex, -1); diff --git a/test/unit/select/BaseSelect.test.tsx b/test/unit/select/BaseSelect.test.tsx new file mode 100644 index 000000000..ffaa11317 --- /dev/null +++ b/test/unit/select/BaseSelect.test.tsx @@ -0,0 +1,249 @@ +import * as React from 'react'; +import * as td from 'testdouble'; +import {assert} from 'chai'; +import {shallow} from 'enzyme'; +import {coerceForTesting} from '../helpers/types'; + +import {BaseSelect} from '../../../packages/select/BaseSelect'; +import NativeSelect from '../../../packages/select/NativeSelect'; +import EnhancedSelect from '../../../packages/select/EnhancedSelect'; +import {MDCSelectFoundation} from '@material/select/foundation'; + +suite('Base Select'); + +test('renders EnhancedSelect when props.enhanced is true', () => { + const wrapper = shallow(); + assert.equal(wrapper.find(EnhancedSelect).length, 1); +}); + +test('renders NativeSelect when props.enhanced is false', () => { + const wrapper = shallow(); + assert.equal(wrapper.find(NativeSelect).length, 1); +}); + +test('NativeSelect onFocus calls handleFocus', () => { + const handleFocus = td.func(); + const onFocus = td.func<(evt: React.FocusEvent) => void>(); + const foundation = coerceForTesting({handleFocus}); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const evt = coerceForTesting>({}); + nativeSelect.simulate('focus', evt); + td.verify(handleFocus(), {times: 1}); + td.verify(onFocus(evt), {times: 1}); +}); + +test('EnhancedSelect onFocus calls handleFocus', () => { + const handleFocus = td.func(); + const onFocus = td.func<(evt: React.FocusEvent) => void>(); + const foundation = coerceForTesting({handleFocus}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const evt = coerceForTesting>({}); + enhancedSelect.simulate('focus', evt); + td.verify(handleFocus(), {times: 1}); + td.verify(onFocus(evt), {times: 1}); +}); + +test('NativeSelect onBlur calls handleBlur', () => { + const handleBlur = td.func(); + const onBlur = td.func<(evt: React.FocusEvent) => void>(); + const foundation = coerceForTesting({handleBlur}); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const evt = coerceForTesting>({}); + nativeSelect.simulate('blur', evt); + td.verify(handleBlur(), {times: 1}); + td.verify(onBlur(evt), {times: 1}); +}); + +test('EnhancedSelect onBlur calls handleBlur', () => { + const handleBlur = td.func(); + const onBlur = td.func<(evt: React.FocusEvent) => void>(); + const foundation = coerceForTesting({handleBlur}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const evt = coerceForTesting>({}); + enhancedSelect.simulate('blur', evt); + td.verify(handleBlur(), {times: 1}); + td.verify(onBlur(evt), {times: 1}); +}); + +test('NativeSelect onTouchStart calls handleClick', () => { + const handleClick = td.func(); + const onTouchStart = td.func<(evt: React.TouchEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: { + touches: [{clientX}], + }, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + nativeSelect.simulate('touchstart', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onTouchStart(evt), {times: 1}); +}); + +test('EnhancedSelect onTouchStart calls handleClick', () => { + const handleClick = td.func(); + const onTouchStart = td.func<(evt: React.TouchEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: { + touches: [{clientX}], + }, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + enhancedSelect.simulate('touchstart', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onTouchStart(evt), {times: 1}); +}); + +test('NativeSelect onMouseDown calls handleClick', () => { + const handleClick = td.func(); + const onMouseDown = td.func<(evt: React.MouseEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + nativeSelect.simulate('mousedown', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onMouseDown(evt), {times: 1}); +}); + +test('EnhancedSelect onMouseDown calls handleClick', () => { + const handleClick = td.func(); + const onMouseDown = td.func<(evt: React.MouseEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + enhancedSelect.simulate('mousedown', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onMouseDown(evt), {times: 1}); +}); + +test('NativeSelect onClick calls handleClick', () => { + const handleClick = td.func(); + const onClick = td.func<(evt: React.MouseEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + nativeSelect.simulate('click', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onClick(evt), {times: 1}); +}); + +test('EnhancedSelect onClick calls handleClick', () => { + const handleClick = td.func(); + const onClick = td.func<(evt: React.MouseEvent) => void>(); + const foundation = coerceForTesting({handleClick}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + enhancedSelect.simulate('click', evt); + td.verify(handleClick(clientX - left), {times: 1}); + td.verify(onClick(evt), {times: 1}); +}); + +test('NativeSelect onKeyDown calls props.onKeyDown', () => { + const onKeyDown = td.func<(evt: React.KeyboardEvent) => void>(); + const wrapper = shallow(); + const nativeSelect = wrapper.find(NativeSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + nativeSelect.simulate('keydown', evt); + td.verify(onKeyDown(evt), {times: 1}); +}); + +test('EnhancedSelect onKeyDown calls handleKeydown', () => { + const handleKeydown = td.func(); + const onKeyDown = td.func<(evt: React.KeyboardEvent) => void>(); + const foundation = coerceForTesting({handleKeydown}); + const wrapper = shallow(); + const enhancedSelect = wrapper.find(EnhancedSelect); + const clientX = 100; + const left = 10; + const getBoundingClientRect = td.func(); + const currentTarget = coerceForTesting({getBoundingClientRect}); + const evt = coerceForTesting>({ + currentTarget, + nativeEvent: {clientX}, + }); + td.when(getBoundingClientRect()).thenReturn({left}); + + enhancedSelect.simulate('keydown', evt); + td.verify(handleKeydown(evt.nativeEvent), {times: 1}); + td.verify(onKeyDown(evt), {times: 1}); +}); + +test('props.selectClassName gets passed to props.className of NativeSelect', () => { + const wrapper = shallow(); + assert.equal(wrapper.find(NativeSelect).props().className, 'test-class-name'); +}); + +test('props.selectClassName gets passed to props.className of EnhancedSelect', () => { + const wrapper = shallow(); + assert.equal(wrapper.find(EnhancedSelect).props().className, 'test-class-name'); +}); diff --git a/test/unit/select/EnhancedSelect.test.tsx b/test/unit/select/EnhancedSelect.test.tsx new file mode 100644 index 000000000..91caac04d --- /dev/null +++ b/test/unit/select/EnhancedSelect.test.tsx @@ -0,0 +1,234 @@ +import * as React from 'react'; +import * as td from 'testdouble'; +import {assert} from 'chai'; +import {shallow, mount} from 'enzyme'; +import EnhancedSelect from '../../../packages/select/EnhancedSelect'; +import {Option} from '../../../packages/select/index'; +import {coerceForTesting} from '../helpers/types'; +import {MDCSelectFoundation} from '@material/select/foundation'; +import Menu from '../../../packages/menu/index'; + +suite('Enhanced Select'); + +const testEvt = { + test: 'test', + clientX: 20, + target: { + getBoundingClientRect: () => ({left: 15}), + value: 'value', + }, +}; + +test('#event.focus calls #props.onFocus', () => { + const onFocus = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('focus', testEvt); + td.verify(onFocus(coerceForTesting>(testEvt)), {times: 1}); +}); + +test('#event.click calls #props.onClick', () => { + const onClick = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('click', testEvt); + td.verify(onClick(coerceForTesting>(testEvt)), {times: 1}); +}); + + +test('#event.blur calls #props.onBlur', () => { + const onBlur = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('blur', testEvt); + td.verify(onBlur(coerceForTesting>(testEvt)), {times: 1}); +}); + +test('#event.mousedown calls #props.onMouseDown', () => { + const onMouseDown = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('mousedown', testEvt); + td.verify(onMouseDown(coerceForTesting>(testEvt)), {times: 1}); +}); + +test('#event.touchstart calls #props.onTouchStart', () => { + const onTouchStart = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('touchstart', testEvt); + td.verify(onTouchStart(coerceForTesting>(testEvt)), {times: 1}); +}); + +test('#event.keydown calls #props.onKeyDown', () => { + const onKeyDown = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + selectedTextEl.simulate('keydown', testEvt); + td.verify(onKeyDown(coerceForTesting>(testEvt)), {times: 1}); +}); + +test('#event.touchstart calls #props.onTouchStart', () => { + const onTouchStart = coerceForTesting>(td.func()); + const wrapper = shallow(); + const selectedTextEl = wrapper.find(MDCSelectFoundation.strings.SELECTED_TEXT_SELECTOR); + const evt = coerceForTesting>({ + test: 'test', + touches: [{clientX: 20}], + target: { + getBoundingClientRect: () => ({left: 15}), + value: 'value', + }, + }); + selectedTextEl.simulate('touchstart', evt); + td.verify(onTouchStart(evt), {times: 1}); +}); + +test('renders children', () => { + const wrapper = shallow( + + + + ); + assert.equal(wrapper.find(Option).length, 1); +}); + +test('state.selectedItem and state.selectedValue updates when props.value updates', () => { + const wrapper = mount( + + + + ); + wrapper.setProps({value: 'test'}); + const listItem = wrapper.find('.mdc-list-item').getDOMNode(); + assert.equal(wrapper.state().selectedItem, listItem); + assert.equal(wrapper.state().selectedValue, 'test'); + wrapper.unmount(); +}); + +test('state.selectedItem and state.selectedValue do not update when props.value updates with no Options', () => { + const wrapper = mount( + + ); + wrapper.setProps({value: 'test'}); + assert.equal(wrapper.state().selectedItem, null); + assert.equal(wrapper.state().selectedValue, ''); + wrapper.unmount(); +}); + +test('listElements returns Option element', () => { + const wrapper = mount( + + + + ); + const listElement = coerceForTesting(wrapper.instance().listElements)[0]; + assert.equal(listElement, wrapper.find('Option').getDOMNode()); + wrapper.unmount(); +}); + +test('Menu.onClose calls sets aria-expanded and calls props.closeMenu, foundation.handleBlur', () => { + const closeMenu = td.func<() => {}>(); + const handleBlur = td.func<() => {}>(); + const foundation = coerceForTesting({handleBlur}); + const wrapper = mount( + + + + ); + wrapper.setState({'aria-expanded': true}); + wrapper.find(Menu).props().onClose!(); + + td.verify(closeMenu(), {times: 1}); + td.verify(handleBlur(), {times: 1}); + assert.equal(wrapper.state()['aria-expanded'], undefined); + wrapper.unmount(); +}); + +test('Menu.onOpen calls sets aria-expanded sets list item to focus', () => { + const div = document.createElement('div'); + document.body.append(div); + const wrapper = mount( + + + , + {attachTo: div} + ); + wrapper.find(Menu).props().onOpen!(); + + assert.equal(document.activeElement, wrapper.find(Option).getDOMNode()); + assert.equal(wrapper.state()['aria-expanded'], true); + div.remove(); + wrapper.unmount(); +}); + +test('renders selectedText element aria-required if props.required true', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-required'), 'true'); + wrapper.unmount(); +}); + +test('renders selectedText element aria-expanded if state.aria-expanded true', () => { + const wrapper = mount(); + wrapper.setState({'aria-expanded': true}); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-expanded'), 'true'); + wrapper.unmount(); +}); + +test('renders selectedText element aria-expanded if state.aria-expanded "true"', () => { + const wrapper = mount(); + wrapper.setState({'aria-expanded': 'true'}); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-expanded'), 'true'); + wrapper.unmount(); +}); + +test('renders selectedText element aria-invalid if props.isInvalid is true', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-invalid'), 'true'); + wrapper.unmount(); +}); + +test('renders selectedText element aria-disabled if props.disabled true', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-disabled'), 'true'); + wrapper.unmount(); +}); + +test('renders selectedText element aria-disabled as false if props.disabled false', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('aria-disabled'), 'false'); + wrapper.unmount(); +}); + +test('renders selectedText element tabindex as 0', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('tabindex'), '0'); + wrapper.unmount(); +}); + +test('renders selectedText element tabindex as -1 if disabled', () => { + const wrapper = mount(); + const selectedText = wrapper.find('.mdc-select__selected-text').getDOMNode(); + assert.equal(selectedText.getAttribute('tabindex'), '-1'); + wrapper.unmount(); +}); + +test('renders selectedText with state.selectedItem trimed', () => { + const wrapper = mount( + + + + ); + assert.equal(wrapper.find('.mdc-select__selected-text').text(), 'MEOW MEOW'); + wrapper.unmount(); +}); diff --git a/test/unit/select/NativeControl.test.tsx b/test/unit/select/NativeControl.test.tsx deleted file mode 100644 index 93d535b01..000000000 --- a/test/unit/select/NativeControl.test.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import td from 'testdouble'; -import {assert} from 'chai'; -import {shallow, mount} from 'enzyme'; -import NativeControl from '../../../packages/select/NativeControl'; -import {coerceForTesting} from '../helpers/types'; - -suite('Select Native Input'); - -const testEvt = { - test: 'test', - clientX: 20, - target: { - getBoundingClientRect: () => ({left: 15}), - value: 'value', - }, -}; - -test('has mdc-select__native-control class', () => { - const wrapper = shallow(); - assert.isTrue(wrapper.hasClass('mdc-select__native-control')); -}); - -test('classNames adds classes', () => { - const wrapper = shallow(); - assert.isTrue(wrapper.hasClass('test-class-name')); -}); - -test('calls props.handleDisabled if props.disabled updates', () => { - const handleDisabled = coerceForTesting<(d: boolean) => void>(td.func()); - const wrapper = shallow(); - wrapper.setProps({disabled: true}); - td.verify(handleDisabled(true), {times: 1}); -}); - -test('#event.focus calls #foundation.handleFocus', () => { - const foundation = {handleFocus: td.func()}; - const wrapper = shallow(); - wrapper.simulate('focus', testEvt); - td.verify(foundation.handleFocus(testEvt), {times: 1}); -}); - -test('#event.focus calls #props.onFocus', () => { - const onFocus = coerceForTesting>(td.func()); - const wrapper = shallow(); - wrapper.simulate('focus', testEvt); - td.verify(onFocus(coerceForTesting>(testEvt)), {times: 1}); -}); - -test('#event.blur calls #foundation.handleBlur', () => { - const foundation = {handleBlur: coerceForTesting>(td.func())}; - const wrapper = shallow(); - wrapper.simulate('blur', testEvt); - td.verify(foundation.handleBlur(coerceForTesting>(testEvt)), {times: 1}); -}); - -test('#event.blur calls #props.onBlur', () => { - const onBlur = coerceForTesting>(td.func()); - const wrapper = shallow(); - wrapper.simulate('blur', testEvt); - td.verify(onBlur(coerceForTesting>(testEvt)), {times: 1}); -}); - -test('#event.change calls #props.onChange', () => { - const onChange = coerceForTesting>(td.func()); - const wrapper = shallow(); - wrapper.simulate('change', testEvt); - td.verify(onChange(coerceForTesting>(testEvt)), {times: 1}); -}); - -test('#event.mousedown calls #props.onMouseDown', () => { - const onMouseDown = coerceForTesting>(td.func()); - const wrapper = shallow(); - wrapper.simulate('mousedown', testEvt); - td.verify(onMouseDown(coerceForTesting>(testEvt)), {times: 1}); -}); - -test('#event.mousedown calls #props.setRippleCenter if target is nativeControl', () => { - const setRippleCenter = coerceForTesting<(rippleCenter: number) => void>(td.func()); - const wrapper = mount(); - wrapper.instance().nativeControl_ - = coerceForTesting>({current: testEvt.target}); - wrapper.simulate('mousedown', testEvt); - const left = testEvt.target.getBoundingClientRect().left; - td.verify(setRippleCenter(testEvt.clientX - left), {times: 1}); -}); - -test('#event.mousedown does not call #props.setRippleCenter if target is not nativeControl', () => { - const setRippleCenter = coerceForTesting<(rippleCenter: number) => void>(td.func()); - const wrapper = mount(); - wrapper.simulate('mousedown', testEvt); - const left = testEvt.target.getBoundingClientRect().left; - td.verify(setRippleCenter(testEvt.clientX - left), {times: 0}); -}); - -test('#event.touchstart calls #props.onTouchStart', () => { - const onTouchStart = coerceForTesting>(td.func()); - const wrapper = shallow(); - const evt = coerceForTesting>({ - test: 'test', - touches: [{clientX: 20}], - target: { - getBoundingClientRect: () => ({left: 15}), - value: 'value', - }, - }); - wrapper.simulate('touchstart', evt); - td.verify(onTouchStart(evt), {times: 1}); -}); - -test('#event.touchstart calls #props.setRippleCenter if target is nativeControl', () => { - const setRippleCenter = coerceForTesting<(rippleCenter: number) => void>(td.func()); - const wrapper = mount(); - const evt = { - test: 'test', - touches: [{clientX: 20}], - target: { - getBoundingClientRect: () => ({left: 15}), - value: 'value', - }, - }; - wrapper.instance().nativeControl_ = coerceForTesting>({current: evt.target}); - wrapper.simulate('touchstart', evt); - const left = evt.target.getBoundingClientRect().left; - td.verify(setRippleCenter(20 - left), {times: 1}); -}); - -test('#event.touchstart does not call #props.setRippleCenter if target is not nativeControl', () => { - const setRippleCenter = coerceForTesting<(rippleCenter: number) => void>(td.func()); - const wrapper = mount(); - const evt = { - test: 'test', - touches: [{clientX: 20}], - target: { - getBoundingClientRect: () => ({left: 15}), - value: 'value', - }, - }; - wrapper.simulate('touchstart', evt); - const left = evt.target.getBoundingClientRect().left; - td.verify(setRippleCenter(20 - left), {times: 0}); -}); - -test('renders children', () => { - const wrapper = shallow( - - - - ); - assert.equal(wrapper.find('option[value="test"]').length, 1); -}); diff --git a/test/unit/select/NativeSelect.test.tsx b/test/unit/select/NativeSelect.test.tsx new file mode 100644 index 000000000..2efa8b3b7 --- /dev/null +++ b/test/unit/select/NativeSelect.test.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import {assert} from 'chai'; +import {shallow} from 'enzyme'; +import NativeSelect from '../../../packages/select/NativeSelect'; + +suite('Select Native'); + +test('has mdc-select__native-control class', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.hasClass('mdc-select__native-control')); +}); + +test('classNames adds classes', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.hasClass('test-class-name')); +}); + +test('renders children', () => { + const wrapper = shallow( + + + + ); + assert.equal(wrapper.find('option[value="test"]').length, 1); +}); diff --git a/test/unit/select/Option.test.tsx b/test/unit/select/Option.test.tsx index b6bfd6d45..4b664ea48 100644 --- a/test/unit/select/Option.test.tsx +++ b/test/unit/select/Option.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import {assert} from 'chai'; import {shallow, mount} from 'enzyme'; -import Option from '../../../packages/select/Option'; +import {Option} from '../../../packages/select/index'; import {MenuListItem} from '../../../packages/menu/index'; suite('Select Options'); diff --git a/test/unit/select/helper-text/index.test.tsx b/test/unit/select/helper-text/index.test.tsx index f54839f51..bb59a253f 100644 --- a/test/unit/select/helper-text/index.test.tsx +++ b/test/unit/select/helper-text/index.test.tsx @@ -28,10 +28,9 @@ test('renders with persistent class when props.persistent is true', () => { assert.isTrue(wrapper.hasClass('mdc-select-helper-text--persistent')); }); -test.only('calls setHelperTextFoundation with foundation', () => { +test('calls setHelperTextFoundation with foundation', () => { const setHelperTextFoundation = td.func<(foundation?: MDCSelectHelperTextFoundation) => void>(); shallow(); - // TODO: change Object to MDCSelectHelperTextFoundation in PR 823 td.verify(setHelperTextFoundation(td.matchers.isA(Object)), {times: 1}); }); diff --git a/test/unit/select/icon/index.test.tsx b/test/unit/select/icon/index.test.tsx index e104d018d..8366c656e 100644 --- a/test/unit/select/icon/index.test.tsx +++ b/test/unit/select/icon/index.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as td from 'testdouble'; import {assert} from 'chai'; import {shallow, mount} from 'enzyme'; -import {SelectIcon} from '../../../../packages/select/icon/index'; +import {SelectIcon} from '../../../../packages/select/index'; import {MDCSelectIconFoundation} from '@material/select'; suite('Select Icon'); @@ -20,7 +20,6 @@ test('renders with a test class name', () => { test('calls setIconFoundation with foundation', () => { const setIconFoundation = td.func<(foundation?: MDCSelectIconFoundation) => void>(); shallow(); - // TODO: change Object to MDCSelectHelperTextFoundation in PR 823 td.verify(setIconFoundation(td.matchers.isA(Object)), {times: 1}); }); diff --git a/test/unit/select/index.test.tsx b/test/unit/select/index.test.tsx index ad6332ed1..395e3b5fe 100644 --- a/test/unit/select/index.test.tsx +++ b/test/unit/select/index.test.tsx @@ -1,36 +1,47 @@ -import React from 'react'; -import td from 'testdouble'; +import * as React from 'react'; +import * as td from 'testdouble'; import {assert} from 'chai'; import {mount, shallow} from 'enzyme'; -import Select from '../../../packages/select/index'; -import NativeControl from '../../../packages/select/NativeControl'; +import Select, {SelectHelperText} from '../../../packages/select/index'; +import {MDCSelectFoundation} from '@material/select/foundation'; +import {BaseSelect} from '../../../packages/select/BaseSelect'; +import {SelectIcon} from '../../../packages/select/index'; import {coerceForTesting} from '../helpers/types'; +import FloatingLabel from '../../../packages/floating-label/index'; +import LineRipple from '../../../packages/line-ripple/index'; +import NotchedOutline from '../../../packages/notched-outline'; +import {MDCSelectHelperTextFoundation} from '@material/select/helper-text/foundation'; +import {MDCSelectIconFoundation} from '@material/select/icon/foundation'; suite('Select'); +const fakeClassName = 'my-added-class'; +const fakeSecondClassName = 'my-other-class'; + test('has mdc-select class', () => { const wrapper = shallow( ); - assert.isTrue(wrapper.hasClass('test-class-name')); + assert.isTrue(wrapper.childAt(0).hasClass('test-class-name')); }); test('creates foundation', () => { const wrapper = mount); - assert.exists(wrapper.instance().foundation); + assert.exists(wrapper.state().foundation); }); test('#foundation.handleChange gets called when state.value updates', () => { const wrapper = shallow); - wrapper.instance().foundation.handleChange = td.func(); + const handleChange = td.func<() => void>(); + wrapper.setState({foundation: coerceForTesting({handleChange})}); const value = 'value'; wrapper.setState({value}); - td.verify(wrapper.instance().foundation.handleChange(), {times: 1}); + td.verify(wrapper.state().foundation!.handleChange(true), {times: 1}); }); test('state.value updates when props.value changes', () => { @@ -42,85 +53,224 @@ test('state.value updates when props.value changes', () => { test('#componentWillUnmount destroys foundation', () => { const wrapper = shallow); - const foundation = wrapper.instance().foundation; - foundation.destroy = td.func(); + const foundation = wrapper.state().foundation!; + foundation.destroy = td.func<() => void>(); wrapper.unmount(); td.verify(foundation.destroy(), {times: 1}); }); test('props.outlined will add mdc-select--outlined', () => { const wrapper = shallow(); - assert.isTrue(wrapper.hasClass('mdc-select--disabled')); + assert.isTrue(wrapper.childAt(0).hasClass('mdc-select--disabled')); +}); + +test('props.required will add mdc-select--required', () => { + const wrapper = shallow(); - assert.isTrue(wrapper.hasClass('mdc-select--box')); +test('props.leadingIcon will add mdc-select--with-leading-icon', () => { + const wrapper = shallow(); wrapper.setState({classList: new Set(['best-class-name'])}); - assert.isTrue(wrapper.hasClass('best-class-name')); + assert.isTrue(wrapper.childAt(0).hasClass('best-class-name')); }); test('#adapter.addClass adds to state.classList', () => { const wrapper = shallow); - wrapper.instance().adapter.addClass('my-added-class'); - assert.isTrue(wrapper.state().classList.has('my-added-class')); + wrapper.instance().adapter.addClass(fakeClassName); + assert.isTrue(wrapper.state().classList.has(fakeClassName)); +}); + +test('#adapter.addClass cleans up classesBeingAdded', () => { + const wrapper = shallow); + wrapper.instance().adapter.addClass(fakeClassName); + assert.isFalse(wrapper.instance().classesBeingAdded.has(fakeClassName)); }); test('#adapter.removeClass removes from state.classList', () => { - const wrapper = shallow); - wrapper.setState({classList: new Set(['my-added-class'])}); - wrapper.instance().adapter.removeClass('my-added-class'); - assert.isFalse(wrapper.state().classList.has('my-added-class')); + const wrapper = shallow); + wrapper.setState({classList: new Set([fakeClassName])}); + wrapper.instance().adapter.removeClass(fakeClassName); + assert.isFalse(wrapper.state().classList.has(fakeClassName)); +}); + +test('#adapter.removeClass cleans up classesBeingRemoved', () => { + const wrapper = shallow); + wrapper.setState({classList: new Set([fakeClassName])}); + wrapper.instance().adapter.removeClass(fakeClassName); + assert.isFalse(wrapper.instance().classesBeingRemoved.has(fakeClassName)); +}); + +test('back to back calls to removeClass result in removing both classes', () => { + const wrapper = shallow); + wrapper.setState({classList: new Set([fakeClassName, fakeSecondClassName])}); + wrapper.instance().adapter.removeClass(fakeClassName); + wrapper.instance().adapter.removeClass(fakeSecondClassName); + assert.isFalse(wrapper.state().classList.has(fakeClassName)); + assert.isFalse(wrapper.state().classList.has(fakeSecondClassName)); }); test('#adapter.hasClass returns true if the string is in state.classList', () => { - const wrapper = shallow); - wrapper.setState({classList: new Set(['my-added-class'])}); - assert.isTrue(wrapper.instance().adapter.hasClass('my-added-class')); + const wrapper = shallow); + wrapper.setState({classList: new Set([fakeClassName])}); + assert.isTrue(wrapper.instance().adapter.hasClass(fakeClassName)); +}); + +test('#adapter.hasClass returns true if the string is in classesBeingAdded', () => { + const wrapper = shallow); + wrapper.instance().classesBeingAdded.add(fakeClassName); + assert.isTrue(wrapper.instance().adapter.hasClass(fakeClassName)); }); test('#adapter.hasClass returns false if the string is not in state.classList', () => { - const wrapper = shallow); - assert.isFalse(wrapper.instance().adapter.hasClass('my-added-class')); + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.hasClass(fakeClassName)); }); -test('#adapter.isRtl returns true if props.isRtl is true', () => { - const wrapper = mount); - assert.isTrue(wrapper.instance().foundation.adapter_.isRtl()); +test('#adapter.hasClass returns false if the string is included in classesBeingRemoved', () => { + const wrapper = shallow); + assert.isTrue(wrapper.instance().adapter.hasClass(fakeClassName)); + wrapper.instance().classesBeingRemoved.add(fakeClassName); + assert.isFalse(wrapper.instance().adapter.hasClass(fakeClassName)); }); -test('#adapter.isRtl returns false if parent is not dir="rtl"', () => { - const wrapper = mount); - assert.isFalse(wrapper.instance().foundation.adapter_.isRtl()); +test('#adapter.setRippleCenter', () => { + const wrapper = shallow); + wrapper.instance().adapter.setRippleCenter(23); + assert.equal(wrapper.state().lineRippleCenter, 23); }); -test('adapter.getValue returns state.value', () => { - const wrapper = shallow); +test('#adapter.getValue returns state.value', () => { + const wrapper = shallow); const value = 'value'; wrapper.setState({value}); assert.equal(wrapper.instance().adapter.getValue(), value); }); +test('#adapter.setValue sets state.value', () => { + const wrapper = shallow); + const value = 'value'; + wrapper.instance().adapter.setValue(value); + assert.equal(wrapper.state().value, value); +}); + +test('#adapter.setDisabled sets state.disabled', () => { + const wrapper = shallow); + wrapper.instance().adapter.setDisabled(true); + assert.equal(wrapper.state().disabled, true); +}); + +test('#adapter.closeMenu sets state.open to false', () => { + const wrapper = shallow); + wrapper.instance().adapter.closeMenu(); + assert.isFalse(wrapper.state().open); +}); + +test('#adapter.openMenu sets state.open to true', () => { + const wrapper = shallow); + wrapper.instance().adapter.openMenu(); + assert.isTrue(wrapper.state().open); +}); + +test('#adapter.isMenuOpen for nativeSelect always returns false', () => { + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.isMenuOpen()); + wrapper.setState({open: true}); + assert.isFalse(wrapper.instance().adapter.isMenuOpen()); +}); + +test('#adapter.checkValidity for nativeSelect returns nativeControl.checkValidity()', () => { + const checkValidity = td.func(); + td.when(checkValidity()).thenReturn(true); + const wrapper = shallow); + wrapper.instance().nativeControl = { + current: coerceForTesting({checkValidity}), + }; + assert.isTrue(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.checkValidity for nativeSelect returns false if there is not nativeSelect', () => { + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.setValid for nativeSelect adds invalid class when isValid = false', () => { + const wrapper = shallow); + wrapper.instance().adapter.setValid(false); + assert.isTrue(wrapper.state().classList.has(MDCSelectFoundation.cssClasses.INVALID)); +}); + +test('#adapter.setValid for nativeSelect removes invalid class when isValid = true', () => { + const wrapper = shallow); + wrapper.setState({classList: new Set([MDCSelectFoundation.cssClasses.INVALID])}); + wrapper.instance().adapter.setValid(true); + assert.isFalse(wrapper.state().classList.has(MDCSelectFoundation.cssClasses.INVALID)); +}); + +test('#adapter.isMenuOpen for enhancedSelect returns true when state.open = true', () => { + const wrapper = shallow); + wrapper.setState({open: true}); + assert.isTrue(wrapper.instance().adapter.isMenuOpen()); +}); + +test('#adapter.isMenuOpen for enhancedSelect returns false when state.open = false', () => { + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.isMenuOpen()); +}); + +test('#adapter.checkValidity for enhancedSelect returns true if state.value exists & props.required=true', () => { + const wrapper = shallow); + assert.isTrue(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.checkValidity for enhancedSelect returns false if no state.value & props.required=true', () => { + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.checkValidity for enhancedSelect returns true props.disabled=true', () => { + const wrapper = shallow); + assert.isTrue(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.checkValidity for enhancedSelect returns false if no state.value & props.required=true', () => { + const wrapper = shallow); + assert.isFalse(wrapper.instance().adapter.checkValidity()); +}); + +test('#adapter.setValid for enhancedSelect sets state.isInvalid to false and ' + +'removes invalid class when isValid=true', () => { + const wrapper = shallow); + wrapper.setState({classList: new Set([MDCSelectFoundation.cssClasses.INVALID])}); + wrapper.instance().adapter.setValid(true); + assert.isFalse(wrapper.state().isInvalid); + assert.isFalse(wrapper.state().classList.has(MDCSelectFoundation.cssClasses.INVALID)); +}); + +test('#adapter.setValid for enhancedSelect sets state.isInvalid to true and ' + +'adds invalid class when isValid=false', () => { + const wrapper = shallow); + wrapper.instance().adapter.setValid(false); + assert.isTrue(wrapper.state().isInvalid); + assert.isTrue(wrapper.state().classList.has(MDCSelectFoundation.cssClasses.INVALID)); +}); + test('#adapter.floatLabel set state.labelIsFloated', () => { const wrapper = shallow); wrapper.instance().adapter.floatLabel(true); assert.isTrue(wrapper.state().labelIsFloated); }); -test('#adapter.hasLabel returns true if label exists', () => { - const wrapper = shallow); - assert.isTrue(wrapper.instance().adapter.hasLabel()); -}); - test('#adapter.getLabelWidth returns state.labelWidth', () => { const wrapper = shallow); const labelWidth = 59; @@ -141,19 +291,16 @@ test('#adapter.deactivateBottomLine sets state.activeLineRipple to false', () => assert.isFalse(wrapper.state().activeLineRipple); }); -test('NativeControl.props.setRippleCenter sets state.lineRippleCenter', () => { - const wrapper = shallow); - wrapper - .children() - .first() - .props() - .setRippleCenter(123); - assert.equal(wrapper.state().lineRippleCenter, 123); +test('#adapter.notifyChange calls props.afterChange with the updated value', () => { + const afterChange = td.func<(value: string) => void>(); + const wrapper = shallow); + wrapper.instance().adapter.notifyChange('test'); + td.verify(afterChange('test')); }); test('#adapter.notchOutline sets state.outlineIsNotched to true', () => { const wrapper = shallow); - wrapper.instance().adapter.notchOutline(); + wrapper.instance().adapter.notchOutline(0); assert.isTrue(wrapper.state().outlineIsNotched); }); @@ -174,88 +321,71 @@ test('#adapter.hasOutline returns false if props.outlined is false', () => { assert.isFalse(wrapper.instance().adapter.hasOutline()); }); +test('renders dropdown icon', () => { + const wrapper = shallow(favorite} />); + assert.equal(wrapper.childAt(0).childAt(0).type(), 'i'); + assert.equal(wrapper.childAt(0).childAt(1).type(), 'i'); +}); + test('renders notchedOutline if props.outlined is true', () => { const wrapper = shallow(); - const LineRipplePackage = require('../../../packages/line-ripple'); - assert.equal(wrapper.childAt(2).type(), LineRipplePackage.default); + assert.equal(wrapper.childAt(0).childAt(3).type(), LineRipple); }); -test('renders NativeControl for select', () => { +test('renders BaseSelect for select', () => { const wrapper = shallow(); - const FloatingLabelPackage = require('../../../packages/floating-label'); - assert.equal(wrapper.childAt(1).type(), FloatingLabelPackage.default); + assert.equal(wrapper.childAt(0).childAt(2).type(), FloatingLabel); +}); + +test('renders no FloatingLabel if props.label does not exists', () => { + const wrapper = shallow( + const wrapper = mount( + (((((({options}); + const wrapper = mount(); assert.equal(wrapper.find('option').length, 1); }); @@ -266,31 +396,31 @@ test('renders options passed as children', () => { ); - const wrapper = shallow(); + const wrapper = mount(); assert.equal(wrapper.find('option').length, 2); }); test('renders options passed as array of 1 string', () => { - const wrapper = shallow(); assert.equal(wrapper.find('option[value="opt 1"]').length, 1); }); test('renders options passed as array of strings', () => { - const wrapper = shallow( + const wrapper = mount( ); assert.equal(wrapper.find('option[value="opt-1"]').length, 1); }); test('renders options passed as array of objects', () => { - const wrapper = shallow( + const wrapper = mount( ); - assert.isTrue(wrapper.childAt(1).hasClass(className)); + assert.isTrue(wrapper.childAt(0).childAt(2).hasClass(className)); }); test('updates float prop with state.labelIsFloated', () => { const wrapper = shallow( ); - assert.isTrue(wrapper.childAt(2).hasClass(className)); + assert.isTrue(wrapper.childAt(0).childAt(3).hasClass(className)); }); test('updates active prop with state.activeLineRipple', () => { const wrapper = shallow( ); - assert.isTrue(wrapper.childAt(2).hasClass(className)); + assert.isTrue(wrapper.childAt(0).childAt(2).hasClass(className)); }); test('updates notch prop with state.outlineIsNotched', () => { const wrapper = shallow(); wrapper.setState({labelWidth: 55}); - assert.equal(wrapper.childAt(2).props().notchWidth, 55); + assert.equal(wrapper.childAt(0).childAt(2).props().notchWidth, 55); +}); + +test('createFoundation instantiates a new foundation', () => { + const wrapper = shallow); + const currentFoundation = wrapper.state().foundation; + wrapper.instance().createFoundation(); + assert.notEqual(currentFoundation, wrapper.state().foundation); +}); + +test('update to state.helperTextFoundation creates a new foundation', () => { + const wrapper = shallow); + const destroy = wrapper.state().foundation!.destroy = td.func<() => {}>(); + wrapper.setState({helperTextFoundation: coerceForTesting({})}); + assert.exists(wrapper.state().foundation); + td.verify(destroy(), {times: 1}); +}); + +test('update to state.iconFoundation creates a new foundation', () => { + const wrapper = shallow); + const destroy = wrapper.state().foundation!.destroy = td.func<() => {}>(); + wrapper.setState({iconFoundation: coerceForTesting({})}); + assert.exists(wrapper.state().foundation); + td.verify(destroy(), {times: 1}); +}); + +test('leadingIcon.props.setIconFoundation() updates state.iconFoundation', () => { + const wrapper = mount} />); + const foundation = coerceForTesting({}); + wrapper.childAt(0).childAt(0).props().setIconFoundation!(foundation); + assert.equal(wrapper.state().iconFoundation, foundation); +}); + +test('leadingIcon.props.setHelperTextFoundation() updates state.helperTextFoundation', () => { + const wrapper = mount} />); + const foundation = coerceForTesting({}); + wrapper.childAt(1).props().setHelperTextFoundation!(coerceForTesting(foundation)); + assert.equal(wrapper.state().helperTextFoundation, foundation); });