From cec58cb9c2fc8234e614f76cf6a83fdcb9e93232 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Sat, 27 Apr 2019 10:05:45 -0700 Subject: [PATCH 1/6] feat(select): add icon --- packages/select/icon/README.md | 23 +++++ packages/select/icon/index.tsx | 120 +++++++++++++++++++++++++++ test/unit/select/icon/index.test.tsx | 78 +++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 packages/select/icon/README.md create mode 100644 packages/select/icon/index.tsx create mode 100644 test/unit/select/icon/index.test.tsx diff --git a/packages/select/icon/README.md b/packages/select/icon/README.md new file mode 100644 index 000000000..34ebd53b2 --- /dev/null +++ b/packages/select/icon/README.md @@ -0,0 +1,23 @@ +# React Select Icon + +MDC React Select Icon is a React Component which uses MDC [MDC Select Icon](https://github.com/material-components/material-components-web/tree/master/packages/mdc-select/icon/)'s CSS and foundation JavaScript. + +## Usage + +```js +import {SelectIcon} from '@material/react-select/icon/index'; + +const MyComponent = () => { + return ( + + favorite + + ); +} +``` + +## Props + +Prop Name | Type | Description +--- | --- | --- +tag | keyof React.ReactHTML | Sets the element tag. Defaults to ``. diff --git a/packages/select/icon/index.tsx b/packages/select/icon/index.tsx new file mode 100644 index 000000000..78e2f88e4 --- /dev/null +++ b/packages/select/icon/index.tsx @@ -0,0 +1,120 @@ +// 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 React from 'react'; +import classnames from 'classnames'; +import {MDCSelectIconAdapter} from '@material/select/icon/adapter'; +import {MDCSelectIconFoundation} from '@material/select/icon/foundation'; + +export interface SelectIconProps extends React.HTMLProps { + setIconFoundation?: (foundation?: MDCSelectIconFoundation) => void; + tag?: keyof React.ReactHTML; +} + +interface ElementAttributes { + 'tabindex'?: number; + role?: string; +}; + +interface SelectIconState extends ElementAttributes {}; + +export class SelectIcon extends React.Component { + foundation?: MDCSelectIconFoundation; + + state: SelectIconState = { + 'tabindex': undefined, + 'role': undefined, + }; + + static defaultProps = { + tag: 'i', + }; + + componentDidMount() { + const {setIconFoundation} = this.props; + this.foundation = new MDCSelectIconFoundation(this.adapter); + this.foundation.init(); + setIconFoundation && setIconFoundation(this.foundation); + } + + componentWillUnmount() { + const {setIconFoundation} = this.props; + if (this.foundation) { + this.foundation.destroy(); + setIconFoundation && setIconFoundation(undefined); + } + } + + get adapter(): MDCSelectIconAdapter { + return { + getAttr: (attr: keyof ElementAttributes) => { + if (this.state[attr] !== undefined) { + return (this.state[attr] as ElementAttributes[keyof ElementAttributes])!.toString(); + } + const reactAttr = attr === 'tabindex' ? 'tabIndex' : attr; + if (this.props[reactAttr] !== undefined) { + return (this.props[reactAttr])!.toString(); + } + return null; + }, + setAttr: (attr: keyof ElementAttributes, value: ElementAttributes[keyof ElementAttributes]) => { + this.setState((prevState) => ({ + ...prevState, + [attr]: value, + })); + }, + removeAttr: (attr: keyof ElementAttributes) => { + this.setState((prevState) => ({...prevState, [attr]: null})); + }, + setContent: () => { + // not implmenting because developer should would never call `setContent()` + }, + // the adapter methods below are effectively useless since React + // handles events and width differently + registerInteractionHandler: () => undefined, + deregisterInteractionHandler: () => undefined, + notifyIconAction: () => undefined, + }; + } + + render() { + const { + tag: Tag, + setIconFoundation, // eslint-disable-line no-unused-vars + children, + className, + ...otherProps + } = this.props; + const {tabindex: tabIndex, role} = this.state; + return ( + // @ts-ignore https://github.com/Microsoft/TypeScript/issues/28892 + + {children} + + ); + } +} diff --git a/test/unit/select/icon/index.test.tsx b/test/unit/select/icon/index.test.tsx new file mode 100644 index 000000000..28886f66c --- /dev/null +++ b/test/unit/select/icon/index.test.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import * as td from 'testdouble'; +import {assert} from 'chai'; +import {shallow, mount} from 'enzyme'; +import {SelectIcon} from '../../../../packages/select'; +import {MDCSelectIconFoundation} from '@material/select'; + +suite('Select Icon'); + +test('renders with mdc-select-helper-text class', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.hasClass('mdc-select__icon')); +}); + +test('renders with a test class name', () => { + const wrapper = shallow(); + assert.isTrue(wrapper.hasClass('test-class')); +}); + +test('calls setIconFoundation with foundation', () => { + const setIconFoundation = td.func<(foundation?: MDCSelectIconFoundation) => void>(); + shallow(); + td.verify(setIconFoundation(td.matchers.isA(MDCSelectIconFoundation)), {times: 1}); +}); + +test('#componentWillUnmount destroys foundation', () => { + const wrapper = mount(); + const foundation = wrapper.instance().foundation!; + foundation.destroy = td.func<() => void>(); + wrapper.unmount(); + td.verify(foundation.destroy(), {times: 1}); +}); + +test('#adapter.setAttr should update state', () => { + const wrapper = shallow(); + wrapper.instance().adapter.setAttr('role', 'menu'); + assert.equal(wrapper.state().role, 'menu'); +}); + +test('#adapter.removeAttr should update state', () => { + const wrapper = shallow(); + wrapper.setState({role: 'menu'}); + wrapper.instance().adapter.removeAttr('role'); + assert.equal(wrapper.state().role, null); +}); + +test('renders with tabindex from state.tabindex', () => { + const wrapper = mount(); + wrapper.setState({'tabindex': 1}); + assert.equal(wrapper.getDOMNode().getAttribute('tabindex'), '1'); +}); + +test('#adapter.getAttr returns the correct value of role', () => { + const wrapper = mount(); + assert.equal(wrapper.instance().adapter.getAttr('role'), 'menu'); +}); + +test('#adapter.getAttr returns the correct value of tabindex', () => { + const wrapper = mount(); + assert.equal(wrapper.instance().adapter.getAttr('tabindex'), '1'); +}); + +test('#adapter.getAttr returns the correct value of role if it exists on state.role', () => { + const wrapper = mount(); + wrapper.setState({role: 'menu'}); + assert.equal(wrapper.instance().adapter.getAttr('role'), 'menu'); +}); + +test('renders with role from state.role', () => { + const wrapper = mount(); + wrapper.setState({'role': 'true'}); + assert.equal(wrapper.getDOMNode().getAttribute('role'), 'true'); +}); + +test('renders children', () => { + const wrapper = mount(MEOW); + assert.equal(wrapper.text(), 'MEOW'); +}); From 2ddfa5061dfbc61a059e5610219908a267971741 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Sat, 27 Apr 2019 10:12:30 -0700 Subject: [PATCH 2/6] fix: update select --- package.json | 2 +- packages/select/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e5fd7650c..542143ebb 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@material/notched-outline": "^1.1.1", "@material/radio": "^1.1.0", "@material/ripple": "^1.0.0", - "@material/select": "^0.40.1", + "@material/select": "^1.1.1", "@material/snackbar": "^1.0.0", "@material/switch": "^1.0.0", "@material/tab": "^1.0.0", diff --git a/packages/select/package.json b/packages/select/package.json index c9a01731d..17283de20 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -19,7 +19,7 @@ "@material/react-floating-label": "^0.11.0", "@material/react-line-ripple": "^0.11.0", "@material/react-notched-outline": "^0.11.0", - "@material/select": "^0.40.1", + "@material/select": "^1.1.1", "classnames": "^2.2.6", "react": "^16.4.2" }, From cccbff0dafa7a2f4cbae7459742e1686398af286 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Sat, 27 Apr 2019 10:31:45 -0700 Subject: [PATCH 3/6] fix: test --- test/unit/select/icon/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/select/icon/index.test.tsx b/test/unit/select/icon/index.test.tsx index 28886f66c..5cd94344a 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'; +import {SelectIcon} from '../../../../packages/select/icon/index'; import {MDCSelectIconFoundation} from '@material/select'; suite('Select Icon'); From d83209f1c4ef7525a3ad55fc233178e1ff321223 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 29 Apr 2019 10:21:23 -0700 Subject: [PATCH 4/6] fix: test --- test/screenshot/golden.json | 2 +- test/unit/select/icon/index.test.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index 7e905c010..bd69d13ae 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -27,7 +27,7 @@ "text-field/textArea": "b4c009f5f7637f7103380a2dd93bd6387d1ccc22d031323e6f0472949ce35881", "text-field/standard": "61cf0ebade2a09263d3a015c26cde28b3b3e67ab9d9bb4c494ac4823b9e8000b", "text-field/fullWidth": "26fa1e96054939384efb6427d93967bdbbc05ecc00bf7e4f13ab17cbe3e367fb", - "text-field/outlined": "91e95a9bfb4e3f75ba9bb6a7ccf6a379b944d4960aaffd4ca2e4026a3f3daa71", + "text-field/outlined": "0f4dc80c6390656292c2b551cb636169ff8b1e72f717709a3bd3508d638706d6", "text-field/refTest": "742fe55ba0f3ca11c74beef5ea9737e2eaec37d9c8524552f3b06c6cb25f4157", "top-app-bar/fixed": "7a2dd6318d62ac2eabd66f1b28100db7c15840ccb753660065fa9524db6435d6", "top-app-bar/prominent": "2506ed2dd5f370c7bab69315d2daebd58b443d2b9e32bbaec762e40a8736309b", diff --git a/test/unit/select/icon/index.test.tsx b/test/unit/select/icon/index.test.tsx index 5cd94344a..e104d018d 100644 --- a/test/unit/select/icon/index.test.tsx +++ b/test/unit/select/icon/index.test.tsx @@ -20,7 +20,8 @@ test('renders with a test class name', () => { test('calls setIconFoundation with foundation', () => { const setIconFoundation = td.func<(foundation?: MDCSelectIconFoundation) => void>(); shallow(); - td.verify(setIconFoundation(td.matchers.isA(MDCSelectIconFoundation)), {times: 1}); + // TODO: change Object to MDCSelectHelperTextFoundation in PR 823 + td.verify(setIconFoundation(td.matchers.isA(Object)), {times: 1}); }); test('#componentWillUnmount destroys foundation', () => { From e785b6c530237712525b5c70b433029d8343e691 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 29 Apr 2019 10:33:19 -0700 Subject: [PATCH 5/6] fix: remove outlined screenshot --- test/screenshot/golden.json | 1 - test/screenshot/text-field/variants.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/test/screenshot/golden.json b/test/screenshot/golden.json index bd69d13ae..e9140580f 100644 --- a/test/screenshot/golden.json +++ b/test/screenshot/golden.json @@ -27,7 +27,6 @@ "text-field/textArea": "b4c009f5f7637f7103380a2dd93bd6387d1ccc22d031323e6f0472949ce35881", "text-field/standard": "61cf0ebade2a09263d3a015c26cde28b3b3e67ab9d9bb4c494ac4823b9e8000b", "text-field/fullWidth": "26fa1e96054939384efb6427d93967bdbbc05ecc00bf7e4f13ab17cbe3e367fb", - "text-field/outlined": "0f4dc80c6390656292c2b551cb636169ff8b1e72f717709a3bd3508d638706d6", "text-field/refTest": "742fe55ba0f3ca11c74beef5ea9737e2eaec37d9c8524552f3b06c6cb25f4157", "top-app-bar/fixed": "7a2dd6318d62ac2eabd66f1b28100db7c15840ccb753660065fa9524db6435d6", "top-app-bar/prominent": "2506ed2dd5f370c7bab69315d2daebd58b443d2b9e32bbaec762e40a8736309b", diff --git a/test/screenshot/text-field/variants.tsx b/test/screenshot/text-field/variants.tsx index dbf0457ac..aaabf4033 100644 --- a/test/screenshot/text-field/variants.tsx +++ b/test/screenshot/text-field/variants.tsx @@ -1,7 +1,6 @@ export default [ 'standard', 'fullWidth', - 'outlined', 'textArea', 'refTest', ]; From 9e7ddd1f863a6cc64b2a997bc8c43015ad648205 Mon Sep 17 00:00:00 2001 From: Matt Goo Date: Mon, 29 Apr 2019 13:27:14 -0700 Subject: [PATCH 6/6] Update README.md --- packages/select/icon/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/select/icon/README.md b/packages/select/icon/README.md index 34ebd53b2..be00e3190 100644 --- a/packages/select/icon/README.md +++ b/packages/select/icon/README.md @@ -20,4 +20,4 @@ const MyComponent = () => { Prop Name | Type | Description --- | --- | --- -tag | keyof React.ReactHTML | Sets the element tag. Defaults to ``. +tag | string (keyof React.ReactHTML) | Sets the element tag. Defaults to i which becomes``.