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

Commit e9e29a3

Browse files
author
Matt Goo
committed
feat(icon-button): typescript support (#496)
1 parent fee5968 commit e9e29a3

File tree

6 files changed

+136
-112
lines changed

6 files changed

+136
-112
lines changed

packages/checkbox/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
// THE SOFTWARE.
22+
2223
import * as React from 'react';
2324
import * as classnames from 'classnames';
2425
// @ts-ignore no mdc .d.ts file

packages/icon-button/IconToggle.js renamed to packages/icon-button/IconToggle.tsx

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,23 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
// THE SOFTWARE.
2222

23-
import React, {Component} from 'react';
23+
import * as React from 'react';
2424
import classnames from 'classnames';
25-
import PropTypes from 'prop-types';
2625

27-
export default class IconToggle extends Component {
28-
render() {
29-
const {isOn, className, children} = this.props;
30-
const classes = classnames(
31-
'mdc-icon-button__icon',
32-
{'mdc-icon-button__icon--on': isOn},
33-
className,
34-
);
35-
return (
36-
<div className={classes}>
37-
{children}
38-
</div>
39-
);
40-
}
26+
export interface IconToggleProps {
27+
className?: string;
28+
isOn?: boolean;
4129
}
4230

43-
IconToggle.propTypes = {
44-
children: PropTypes.node,
45-
className: PropTypes.string,
46-
isOn: PropTypes.bool,
31+
const IconToggle: React.FunctionComponent<IconToggleProps> = ({
32+
isOn = false, className = '', children = '', // eslint-disable-line react/prop-types
33+
}) => {
34+
const classes = classnames(
35+
'mdc-icon-button__icon',
36+
{'mdc-icon-button__icon--on': isOn},
37+
className
38+
);
39+
return <div className={classes}>{children}</div>;
4740
};
4841

49-
IconToggle.defaultProps = {
50-
children: '',
51-
className: '',
52-
isOn: false,
53-
};
42+
export default IconToggle;

packages/icon-button/index.js renamed to packages/icon-button/index.tsx

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,57 @@
2020
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2121
// THE SOFTWARE.
2222

23-
import React, {Component} from 'react';
23+
import * as React from 'react';
2424
import classnames from 'classnames';
25-
import PropTypes from 'prop-types';
26-
import {withRipple} from '@material/react-ripple';
25+
import * as Ripple from '@material/react-ripple';
26+
// no mdc .d.ts file
27+
// @ts-ignore
2728
import {MDCIconButtonToggleFoundation} from '@material/icon-button/dist/mdc.iconButton';
2829
import IconToggle from './IconToggle';
30+
const ARIA_PRESSED = 'aria-pressed';
2931

30-
const {strings} = MDCIconButtonToggleFoundation;
32+
interface ElementAttributes {
33+
// from HTMLAttributes
34+
[ARIA_PRESSED]?: boolean | 'false' | 'mixed' | 'true';
35+
}
36+
37+
type IconButtonTypes = HTMLButtonElement | HTMLAnchorElement;
38+
export interface IconButtonBaseProps extends ElementAttributes {
39+
isLink?: boolean;
40+
};
41+
42+
interface IconButtonBaseState extends ElementAttributes {
43+
classList: Set<string>;
44+
};
45+
46+
export interface IconButtonProps<T extends IconButtonTypes>
47+
extends Ripple.InjectedProps<T>, IconButtonBaseProps, React.HTMLProps<T> {};
48+
49+
class IconButtonBase<T extends IconButtonTypes> extends React.Component<
50+
IconButtonProps<T>,
51+
IconButtonBaseState
52+
> {
53+
foundation = MDCIconButtonToggleFoundation;
3154

32-
class IconButtonBase extends Component {
33-
constructor(props) {
55+
constructor(props: IconButtonProps<T>) {
3456
super(props);
3557
this.state = {
3658
classList: new Set(),
37-
[strings.ARIA_PRESSED]: props[strings.ARIA_PRESSED],
59+
[ARIA_PRESSED]: props[ARIA_PRESSED],
3860
};
3961
}
4062

63+
static defaultProps = {
64+
className: '',
65+
initRipple: () => {},
66+
isLink: false,
67+
onClick: () => {},
68+
unbounded: true,
69+
};
70+
4171
componentDidMount() {
42-
this.foundation_ = new MDCIconButtonToggleFoundation(this.adapter);
43-
this.foundation_.init();
72+
this.foundation = new MDCIconButtonToggleFoundation(this.adapter);
73+
this.foundation.init();
4474
}
4575

4676
get classes() {
@@ -51,23 +81,30 @@ class IconButtonBase extends Component {
5181

5282
get adapter() {
5383
return {
54-
addClass: (className) =>
84+
addClass: (className: string) =>
5585
this.setState({classList: this.state.classList.add(className)}),
56-
removeClass: (className) => {
86+
removeClass: (className: string) => {
5787
const classList = new Set(this.state.classList);
5888
classList.delete(className);
5989
this.setState({classList});
6090
},
61-
hasClass: (className) => this.classes.split(' ').includes(className),
62-
setAttr: (attr, value) => this.setState({[attr]: value}),
91+
hasClass: (className: string) => this.classes.split(' ').includes(className),
92+
setAttr: this.updateState,
6393
};
6494
}
6595

66-
handleClick_ = (e) => {
67-
this.props.onClick(e);
68-
this.foundation_.handleClick();
96+
updateState = (key: keyof IconButtonBaseState, value: string | boolean) => {
97+
this.setState((prevState) => ({
98+
...prevState,
99+
[key]: value,
100+
}));
69101
}
70102

103+
handleClick_ = (e: React.MouseEvent<T>) => {
104+
this.props.onClick!(e);
105+
this.foundation.handleClick();
106+
};
107+
71108
render() {
72109
const {
73110
children,
@@ -77,52 +114,26 @@ class IconButtonBase extends Component {
77114
className,
78115
onClick,
79116
unbounded,
80-
[strings.ARIA_PRESSED]: ariaPressed,
117+
[ARIA_PRESSED]: ariaPressed,
81118
/* eslint-enable no-unused-vars */
82119
...otherProps
83120
} = this.props;
84121

85122
const props = {
86123
className: this.classes,
87124
ref: initRipple,
88-
[strings.ARIA_PRESSED]: this.state[strings.ARIA_PRESSED],
125+
[ARIA_PRESSED]: this.state[ARIA_PRESSED],
89126
onClick: this.handleClick_,
90127
...otherProps,
91128
};
92-
93129
if (isLink) {
94-
return (
95-
<a {...props}>
96-
{children}
97-
</a>
98-
);
130+
return <a {...props as IconButtonProps<HTMLAnchorElement>}>{children}</a>;
99131
}
100-
101-
return (
102-
<button {...props}>
103-
{children}
104-
</button>
105-
);
132+
return <button {...props as IconButtonProps<HTMLButtonElement>}>{children}</button>;
106133
}
107134
}
108135

109-
IconButtonBase.propTypes = {
110-
children: PropTypes.node,
111-
className: PropTypes.string,
112-
initRipple: PropTypes.func,
113-
isLink: PropTypes.bool,
114-
onClick: PropTypes.func,
115-
unbounded: PropTypes.bool,
116-
};
117-
118-
IconButtonBase.defaultProps = {
119-
children: '',
120-
className: '',
121-
initRipple: () => {},
122-
isLink: false,
123-
onClick: () => {},
124-
unbounded: true,
125-
};
136+
const IconButton = Ripple.withRipple<IconButtonProps<IconButtonTypes>, IconButtonTypes>(IconButtonBase);
126137

127-
export default withRipple(IconButtonBase);
138+
export default IconButton;
128139
export {IconToggle, IconButtonBase};

test/screenshot/icon-button/index.js renamed to test/screenshot/icon-button/index.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import React from 'react';
1+
import * as React from 'react';
22
import MaterialIcon from '../../../packages/material-icon/index';
33
import '../../../packages/icon-button/index.scss';
44
import './index.scss';
5-
65
import IconButton, {IconToggle} from '../../../packages/icon-button/index';
76

8-
class IconButtonTest extends React.Component {
7+
class IconButtonTest extends React.Component<{}, {}> {
98
render() {
109
return (
1110
<div>
@@ -22,7 +21,6 @@ class IconButtonTest extends React.Component {
2221
<IconButton isLink>
2322
<MaterialIcon icon='favorite' />
2423
</IconButton>
25-
2624
<IconButton disabled>
2725
<MaterialIcon icon='favorite' />
2826
</IconButton>
@@ -39,5 +37,4 @@ class IconButtonTest extends React.Component {
3937
);
4038
}
4139
}
42-
4340
export default IconButtonTest;

test/unit/icon-button/IconToggle.test.js renamed to test/unit/icon-button/IconToggle.test.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import * as React from 'react';
22
import {assert} from 'chai';
33
import {shallow} from 'enzyme';
44
import {IconToggle} from '../../../packages/icon-button/index';
@@ -21,22 +21,46 @@ test('has icon button on icon class if props.isOn is true', () => {
2121
});
2222

2323
test('renders icon', () => {
24-
const wrapper = shallow(<IconToggle>
25-
<i className='test-icon' />
26-
</IconToggle>);
27-
assert.equal(wrapper.children().first().type(), 'i');
24+
const wrapper = shallow(
25+
<IconToggle>
26+
<i className='test-icon' />
27+
</IconToggle>
28+
);
29+
assert.equal(
30+
wrapper
31+
.children()
32+
.first()
33+
.type(),
34+
'i'
35+
);
2836
});
2937

3038
test('renders svg', () => {
31-
const wrapper = shallow(<IconToggle>
32-
<svg className='test-svg' />
33-
</IconToggle>);
34-
assert.equal(wrapper.children().first().type(), 'svg');
39+
const wrapper = shallow(
40+
<IconToggle>
41+
<svg className='test-svg' />
42+
</IconToggle>
43+
);
44+
assert.equal(
45+
wrapper
46+
.children()
47+
.first()
48+
.type(),
49+
'svg'
50+
);
3551
});
3652

3753
test('renders img', () => {
38-
const wrapper = shallow(<IconToggle>
39-
<img className='test-img' />
40-
</IconToggle>);
41-
assert.equal(wrapper.children().first().type(), 'img');
54+
const wrapper = shallow(
55+
<IconToggle>
56+
<img className='test-img' />
57+
</IconToggle>
58+
);
59+
assert.equal(
60+
wrapper
61+
.children()
62+
.first()
63+
.type(),
64+
'img'
65+
);
4266
});

0 commit comments

Comments
 (0)