Skip to content

Commit

Permalink
feat(select): add helper text (#824)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Goo committed Apr 30, 2019
1 parent abaa146 commit e22ac2a
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/select/helper-text/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# React Select Helper Text

MDC React Select Helper Text is a React Component which uses MDC [MDC Select Helper Text](https://github.com/material-components/material-components-web/tree/master/packages/mdc-select/helper-text/)'s CSS and foundation JavaScript.

## Usage

```js
import {SelectHelperText} from '@material/react-select/helper-text/index';

const MyComponent = () => {
return (
<SelectHelperText>
Really fun helper text
</SelectHelperText>
);
}
```

## Props

Prop Name | Type | Description
--- | --- | ---
persistent | boolean | Adds the `.mdc-select-helper-text--persistent` class to keep the helper text always visible.
130 changes: 130 additions & 0 deletions packages/select/helper-text/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 classnames from 'classnames';
import {MDCSelectHelperTextAdapter} from '@material/select/helper-text/adapter';
import {MDCSelectHelperTextFoundation} from '@material/select/helper-text/foundation';

export interface SelectHelperTextProps extends React.HTMLProps<HTMLParagraphElement> {
persistent?: boolean;
setHelperTextFoundation?: (foundation?: MDCSelectHelperTextFoundation) => void;
}

interface ElementAttributes {
'aria-hidden'?: boolean | 'false' | 'true';
role?: string;
};

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

export class SelectHelperText extends React.Component<SelectHelperTextProps, SelectHelperTextState> {
foundation?: MDCSelectHelperTextFoundation;

state: SelectHelperTextState = {
'aria-hidden': undefined,
'role': undefined,
'classList': new Set(),
};

componentDidMount() {
const {setHelperTextFoundation} = this.props;
this.foundation = new MDCSelectHelperTextFoundation(this.adapter);
this.foundation.init();
setHelperTextFoundation && setHelperTextFoundation(this.foundation);
}

componentWillUnmount() {
const {setHelperTextFoundation} = this.props;
if (this.foundation) {
this.foundation.destroy();
setHelperTextFoundation && setHelperTextFoundation(undefined);
}
}

get classes() {
const {className, persistent} = this.props;
const {classList} = this.state;
return classnames('mdc-select-helper-text', Array.from(classList), className, {
'mdc-select-helper-text--persistent': persistent,
});
}

get adapter(): MDCSelectHelperTextAdapter {
return {
addClass: (className: string) => {
const classList = new Set(this.state.classList);
classList.add(className);
this.setState({classList});
},
removeClass: (className: string) => {
const classList = new Set(this.state.classList);
classList.delete(className);
this.setState({classList});
},
hasClass: (className: string) => {
return this.classes.split(' ').includes(className);
},
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()`
},
};
}

render() {
const {
children,
/* eslint-disable no-unused-vars */
persistent,
className,
setHelperTextFoundation,
/* eslint-enable no-unused-vars */
...otherProps
} = this.props;
const {
'aria-hidden': ariaHidden,
role,
} = this.state;

return (
<p
className={this.classes}
aria-hidden={ariaHidden}
role={role}
{...otherProps}
>
{children}
</p>
);
}
}
98 changes: 98 additions & 0 deletions test/unit/select/helper-text/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as React from 'react';
import * as td from 'testdouble';
import {assert} from 'chai';
import {shallow, mount} from 'enzyme';
import {SelectHelperText} from '../../../../packages/select/helper-text/index';
import {MDCSelectHelperTextFoundation} from '@material/select/helper-text/foundation';

suite('Select Helper Text');

test('renders with mdc-select-helper-text class', () => {
const wrapper = shallow(<SelectHelperText />);
assert.isTrue(wrapper.hasClass('mdc-select-helper-text'));
});

test('renders with a test class name', () => {
const wrapper = shallow(<SelectHelperText className='test-class' />);
assert.isTrue(wrapper.hasClass('test-class'));
});

test('renders with class from state.classList', () => {
const wrapper = shallow(<SelectHelperText />);
wrapper.setState({classList: new Set(['test-class'])});
assert.isTrue(wrapper.hasClass('test-class'));
});

test('renders with persistent class when props.persistent is true', () => {
const wrapper = shallow(<SelectHelperText persistent />);
assert.isTrue(wrapper.hasClass('mdc-select-helper-text--persistent'));
});

test.only('calls setHelperTextFoundation with foundation', () => {
const setHelperTextFoundation = td.func<(foundation?: MDCSelectHelperTextFoundation) => void>();
shallow(<SelectHelperText setHelperTextFoundation={setHelperTextFoundation} />);
// TODO: change Object to MDCSelectHelperTextFoundation in PR 823
td.verify(setHelperTextFoundation(td.matchers.isA(Object)), {times: 1});
});

test('#componentWillUnmount destroys foundation', () => {
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
const foundation = wrapper.instance().foundation!;
foundation.destroy = td.func<() => void>();
wrapper.unmount();
td.verify(foundation.destroy(), {times: 1});
});

test('#adapter.addClass should add a class to state.classList', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
wrapper.instance().adapter.addClass('test-class');
assert.isTrue(wrapper.state().classList.has('test-class'));
});

test('#adapter.removeClass should remove a class to state.classList', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
wrapper.setState({classList: new Set(['test-class'])});
wrapper.instance().adapter.removeClass('test-class');
assert.isFalse(wrapper.state().classList.has('test-class'));
});

test('#adapter.hasClass should return true if state.classList has class', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
wrapper.setState({classList: new Set(['test-class'])});
assert.isTrue(wrapper.instance().adapter.hasClass('test-class'));
});

test('#adapter.hasClass should return true if className has class', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText className='test-class' />);
assert.isTrue(wrapper.instance().adapter.hasClass('test-class'));
});

test('#adapter.setAttr should update state', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
wrapper.instance().adapter.setAttr('role', 'menu');
assert.equal(wrapper.state().role, 'menu');
});

test('#adapter.removeAttr should update state', () => {
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
wrapper.setState({role: 'menu'});
wrapper.instance().adapter.removeAttr('role');
assert.equal(wrapper.state().role, null);
});

test('renders with aria-hidden from state.aria-hidden', () => {
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
wrapper.setState({'aria-hidden': 'true'});
assert.equal(wrapper.getDOMNode().getAttribute('aria-hidden'), 'true');
});

test('renders with role from state.role', () => {
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
wrapper.setState({'role': 'true'});
assert.equal(wrapper.getDOMNode().getAttribute('role'), 'true');
});

test('renders children', () => {
const wrapper = mount<SelectHelperText>(<SelectHelperText>MEOW</SelectHelperText>);
assert.equal(wrapper.text(), 'MEOW');
});

0 comments on commit e22ac2a

Please sign in to comment.