Skip to content
This repository was archived by the owner on Jan 14, 2025. It is now read-only.

Commit 311bdc2

Browse files
author
Matt Goo
authored
feat(select): add icon (#825)
1 parent 0a99bf9 commit 311bdc2

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed

packages/select/icon/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# React Select Icon
2+
3+
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.
4+
5+
## Usage
6+
7+
```js
8+
import {SelectIcon} from '@material/react-select/icon/index';
9+
10+
const MyComponent = () => {
11+
return (
12+
<SelectIcon className='material-icons'>
13+
favorite
14+
</SelectIcon>
15+
);
16+
}
17+
```
18+
19+
## Props
20+
21+
Prop Name | Type | Description
22+
--- | --- | ---
23+
tag | string (keyof React.ReactHTML) | Sets the element tag. Defaults to i which becomes`<i />`.

packages/select/icon/index.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// The MIT License
2+
//
3+
// Copyright (c) 2019 Google, Inc.
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
23+
import React from 'react';
24+
import classnames from 'classnames';
25+
import {MDCSelectIconAdapter} from '@material/select/icon/adapter';
26+
import {MDCSelectIconFoundation} from '@material/select/icon/foundation';
27+
28+
export interface SelectIconProps extends React.HTMLProps<HTMLElement> {
29+
setIconFoundation?: (foundation?: MDCSelectIconFoundation) => void;
30+
tag?: keyof React.ReactHTML;
31+
}
32+
33+
interface ElementAttributes {
34+
'tabindex'?: number;
35+
role?: string;
36+
};
37+
38+
interface SelectIconState extends ElementAttributes {};
39+
40+
export class SelectIcon extends React.Component<SelectIconProps, SelectIconState> {
41+
foundation?: MDCSelectIconFoundation;
42+
43+
state: SelectIconState = {
44+
'tabindex': undefined,
45+
'role': undefined,
46+
};
47+
48+
static defaultProps = {
49+
tag: 'i',
50+
};
51+
52+
componentDidMount() {
53+
const {setIconFoundation} = this.props;
54+
this.foundation = new MDCSelectIconFoundation(this.adapter);
55+
this.foundation.init();
56+
setIconFoundation && setIconFoundation(this.foundation);
57+
}
58+
59+
componentWillUnmount() {
60+
const {setIconFoundation} = this.props;
61+
if (this.foundation) {
62+
this.foundation.destroy();
63+
setIconFoundation && setIconFoundation(undefined);
64+
}
65+
}
66+
67+
get adapter(): MDCSelectIconAdapter {
68+
return {
69+
getAttr: (attr: keyof ElementAttributes) => {
70+
if (this.state[attr] !== undefined) {
71+
return (this.state[attr] as ElementAttributes[keyof ElementAttributes])!.toString();
72+
}
73+
const reactAttr = attr === 'tabindex' ? 'tabIndex' : attr;
74+
if (this.props[reactAttr] !== undefined) {
75+
return (this.props[reactAttr])!.toString();
76+
}
77+
return null;
78+
},
79+
setAttr: (attr: keyof ElementAttributes, value: ElementAttributes[keyof ElementAttributes]) => {
80+
this.setState((prevState) => ({
81+
...prevState,
82+
[attr]: value,
83+
}));
84+
},
85+
removeAttr: (attr: keyof ElementAttributes) => {
86+
this.setState((prevState) => ({...prevState, [attr]: null}));
87+
},
88+
setContent: () => {
89+
// not implmenting because developer should would never call `setContent()`
90+
},
91+
// the adapter methods below are effectively useless since React
92+
// handles events and width differently
93+
registerInteractionHandler: () => undefined,
94+
deregisterInteractionHandler: () => undefined,
95+
notifyIconAction: () => undefined,
96+
};
97+
}
98+
99+
render() {
100+
const {
101+
tag: Tag,
102+
setIconFoundation, // eslint-disable-line no-unused-vars
103+
children,
104+
className,
105+
...otherProps
106+
} = this.props;
107+
const {tabindex: tabIndex, role} = this.state;
108+
return (
109+
// @ts-ignore https://github.com/Microsoft/TypeScript/issues/28892
110+
<Tag
111+
className={classnames('mdc-select__icon', className)}
112+
role={role}
113+
tabIndex={tabIndex}
114+
{...otherProps}
115+
>
116+
{children}
117+
</Tag>
118+
);
119+
}
120+
}

test/unit/select/icon/index.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as React from 'react';
2+
import * as td from 'testdouble';
3+
import {assert} from 'chai';
4+
import {shallow, mount} from 'enzyme';
5+
import {SelectIcon} from '../../../../packages/select/icon/index';
6+
import {MDCSelectIconFoundation} from '@material/select';
7+
8+
suite('Select Icon');
9+
10+
test('renders with mdc-select-helper-text class', () => {
11+
const wrapper = shallow(<SelectIcon />);
12+
assert.isTrue(wrapper.hasClass('mdc-select__icon'));
13+
});
14+
15+
test('renders with a test class name', () => {
16+
const wrapper = shallow(<SelectIcon className='test-class' />);
17+
assert.isTrue(wrapper.hasClass('test-class'));
18+
});
19+
20+
test('calls setIconFoundation with foundation', () => {
21+
const setIconFoundation = td.func<(foundation?: MDCSelectIconFoundation) => void>();
22+
shallow(<SelectIcon setIconFoundation={setIconFoundation} />);
23+
// TODO: change Object to MDCSelectHelperTextFoundation in PR 823
24+
td.verify(setIconFoundation(td.matchers.isA(Object)), {times: 1});
25+
});
26+
27+
test('#componentWillUnmount destroys foundation', () => {
28+
const wrapper = mount<SelectIcon>(<SelectIcon />);
29+
const foundation = wrapper.instance().foundation!;
30+
foundation.destroy = td.func<() => void>();
31+
wrapper.unmount();
32+
td.verify(foundation.destroy(), {times: 1});
33+
});
34+
35+
test('#adapter.setAttr should update state', () => {
36+
const wrapper = shallow<SelectIcon>(<SelectIcon />);
37+
wrapper.instance().adapter.setAttr('role', 'menu');
38+
assert.equal(wrapper.state().role, 'menu');
39+
});
40+
41+
test('#adapter.removeAttr should update state', () => {
42+
const wrapper = shallow<SelectIcon>(<SelectIcon />);
43+
wrapper.setState({role: 'menu'});
44+
wrapper.instance().adapter.removeAttr('role');
45+
assert.equal(wrapper.state().role, null);
46+
});
47+
48+
test('renders with tabindex from state.tabindex', () => {
49+
const wrapper = mount<SelectIcon>(<SelectIcon />);
50+
wrapper.setState({'tabindex': 1});
51+
assert.equal(wrapper.getDOMNode().getAttribute('tabindex'), '1');
52+
});
53+
54+
test('#adapter.getAttr returns the correct value of role', () => {
55+
const wrapper = mount<SelectIcon>(<SelectIcon role='menu'/>);
56+
assert.equal(wrapper.instance().adapter.getAttr('role'), 'menu');
57+
});
58+
59+
test('#adapter.getAttr returns the correct value of tabindex', () => {
60+
const wrapper = mount<SelectIcon>(<SelectIcon tabIndex={1}/>);
61+
assert.equal(wrapper.instance().adapter.getAttr('tabindex'), '1');
62+
});
63+
64+
test('#adapter.getAttr returns the correct value of role if it exists on state.role', () => {
65+
const wrapper = mount<SelectIcon>(<SelectIcon />);
66+
wrapper.setState({role: 'menu'});
67+
assert.equal(wrapper.instance().adapter.getAttr('role'), 'menu');
68+
});
69+
70+
test('renders with role from state.role', () => {
71+
const wrapper = mount<SelectIcon>(<SelectIcon />);
72+
wrapper.setState({'role': 'true'});
73+
assert.equal(wrapper.getDOMNode().getAttribute('role'), 'true');
74+
});
75+
76+
test('renders children', () => {
77+
const wrapper = mount<SelectIcon>(<SelectIcon>MEOW</SelectIcon>);
78+
assert.equal(wrapper.text(), 'MEOW');
79+
});

0 commit comments

Comments
 (0)