Skip to content

Commit

Permalink
feat(icon-button): typescript support (#496)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Goo committed Dec 27, 2018
1 parent a00e596 commit f4b24fc
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 112 deletions.
1 change: 1 addition & 0 deletions packages/checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// 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 * as classnames from 'classnames';
// no mdc .d.ts file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,23 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React, {Component} from 'react';
import * as React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';

export default class IconToggle extends Component {
render() {
const {isOn, className, children} = this.props;
const classes = classnames(
'mdc-icon-button__icon',
{'mdc-icon-button__icon--on': isOn},
className,
);
return (
<div className={classes}>
{children}
</div>
);
}
export interface IconToggleProps {
className?: string;
isOn?: boolean;
}

IconToggle.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
isOn: PropTypes.bool,
const IconToggle: React.FunctionComponent<IconToggleProps> = ({
isOn = false, className = '', children = '', // eslint-disable-line react/prop-types
}) => {
const classes = classnames(
'mdc-icon-button__icon',
{'mdc-icon-button__icon--on': isOn},
className
);
return <div className={classes}>{children}</div>;
};

IconToggle.defaultProps = {
children: '',
className: '',
isOn: false,
};
export default IconToggle;
107 changes: 59 additions & 48 deletions packages/icon-button/index.js → packages/icon-button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,57 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

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

const {strings} = MDCIconButtonToggleFoundation;
interface ElementAttributes {
// from HTMLAttributes
[ARIA_PRESSED]?: boolean | 'false' | 'mixed' | 'true';
}

type IconButtonTypes = HTMLButtonElement | HTMLAnchorElement;
export interface IconButtonBaseProps extends ElementAttributes {
isLink?: boolean;
};

interface IconButtonBaseState extends ElementAttributes {
classList: Set<string>;
};

export interface IconButtonProps<T extends IconButtonTypes>
extends Ripple.InjectedProps<T>, IconButtonBaseProps, React.HTMLProps<T> {};

class IconButtonBase<T extends IconButtonTypes> extends React.Component<
IconButtonProps<T>,
IconButtonBaseState
> {
foundation = MDCIconButtonToggleFoundation;

class IconButtonBase extends Component {
constructor(props) {
constructor(props: IconButtonProps<T>) {
super(props);
this.state = {
classList: new Set(),
[strings.ARIA_PRESSED]: props[strings.ARIA_PRESSED],
[ARIA_PRESSED]: props[ARIA_PRESSED],
};
}

static defaultProps = {
className: '',
initRipple: () => {},
isLink: false,
onClick: () => {},
unbounded: true,
};

componentDidMount() {
this.foundation_ = new MDCIconButtonToggleFoundation(this.adapter);
this.foundation_.init();
this.foundation = new MDCIconButtonToggleFoundation(this.adapter);
this.foundation.init();
}

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

get adapter() {
return {
addClass: (className) =>
addClass: (className: string) =>
this.setState({classList: this.state.classList.add(className)}),
removeClass: (className) => {
removeClass: (className: string) => {
const classList = new Set(this.state.classList);
classList.delete(className);
this.setState({classList});
},
hasClass: (className) => this.classes.split(' ').includes(className),
setAttr: (attr, value) => this.setState({[attr]: value}),
hasClass: (className: string) => this.classes.split(' ').includes(className),
setAttr: this.updateState,
};
}

handleClick_ = (e) => {
this.props.onClick(e);
this.foundation_.handleClick();
updateState = (key: keyof IconButtonBaseState, value: string | boolean) => {
this.setState((prevState) => ({
...prevState,
[key]: value,
}));
}

handleClick_ = (e: React.MouseEvent<T>) => {
this.props.onClick!(e);
this.foundation.handleClick();
};

render() {
const {
children,
Expand All @@ -77,52 +114,26 @@ class IconButtonBase extends Component {
className,
onClick,
unbounded,
[strings.ARIA_PRESSED]: ariaPressed,
[ARIA_PRESSED]: ariaPressed,
/* eslint-enable no-unused-vars */
...otherProps
} = this.props;

const props = {
className: this.classes,
ref: initRipple,
[strings.ARIA_PRESSED]: this.state[strings.ARIA_PRESSED],
[ARIA_PRESSED]: this.state[ARIA_PRESSED],
onClick: this.handleClick_,
...otherProps,
};

if (isLink) {
return (
<a {...props}>
{children}
</a>
);
return <a {...props as IconButtonProps<HTMLAnchorElement>}>{children}</a>;
}

return (
<button {...props}>
{children}
</button>
);
return <button {...props as IconButtonProps<HTMLButtonElement>}>{children}</button>;
}
}

IconButtonBase.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
initRipple: PropTypes.func,
isLink: PropTypes.bool,
onClick: PropTypes.func,
unbounded: PropTypes.bool,
};

IconButtonBase.defaultProps = {
children: '',
className: '',
initRipple: () => {},
isLink: false,
onClick: () => {},
unbounded: true,
};
const IconButton = Ripple.withRipple<IconButtonProps<IconButtonTypes>, IconButtonTypes>(IconButtonBase);

export default withRipple(IconButtonBase);
export default IconButton;
export {IconToggle, IconButtonBase};
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React from 'react';
import * as React from 'react';
import MaterialIcon from '../../../packages/material-icon/index';
import '../../../packages/icon-button/index.scss';
import './index.scss';

import IconButton, {IconToggle} from '../../../packages/icon-button/index';

class IconButtonTest extends React.Component {
class IconButtonTest extends React.Component<{}, {}> {
render() {
return (
<div>
Expand All @@ -22,7 +21,6 @@ class IconButtonTest extends React.Component {
<IconButton isLink>
<MaterialIcon icon='favorite' />
</IconButton>

<IconButton disabled>
<MaterialIcon icon='favorite' />
</IconButton>
Expand All @@ -39,5 +37,4 @@ class IconButtonTest extends React.Component {
);
}
}

export default IconButtonTest;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import {assert} from 'chai';
import {shallow} from 'enzyme';
import {IconToggle} from '../../../packages/icon-button/index';
Expand All @@ -21,22 +21,46 @@ test('has icon button on icon class if props.isOn is true', () => {
});

test('renders icon', () => {
const wrapper = shallow(<IconToggle>
<i className='test-icon' />
</IconToggle>);
assert.equal(wrapper.children().first().type(), 'i');
const wrapper = shallow(
<IconToggle>
<i className='test-icon' />
</IconToggle>
);
assert.equal(
wrapper
.children()
.first()
.type(),
'i'
);
});

test('renders svg', () => {
const wrapper = shallow(<IconToggle>
<svg className='test-svg' />
</IconToggle>);
assert.equal(wrapper.children().first().type(), 'svg');
const wrapper = shallow(
<IconToggle>
<svg className='test-svg' />
</IconToggle>
);
assert.equal(
wrapper
.children()
.first()
.type(),
'svg'
);
});

test('renders img', () => {
const wrapper = shallow(<IconToggle>
<img className='test-img' />
</IconToggle>);
assert.equal(wrapper.children().first().type(), 'img');
const wrapper = shallow(
<IconToggle>
<img className='test-img' />
</IconToggle>
);
assert.equal(
wrapper
.children()
.first()
.type(),
'img'
);
});

0 comments on commit f4b24fc

Please sign in to comment.