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

Commit e22ac2a

Browse files
author
Matt Goo
committed
feat(select): add helper text (#824)
1 parent abaa146 commit e22ac2a

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

packages/select/helper-text/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# React Select Helper Text
2+
3+
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.
4+
5+
## Usage
6+
7+
```js
8+
import {SelectHelperText} from '@material/react-select/helper-text/index';
9+
10+
const MyComponent = () => {
11+
return (
12+
<SelectHelperText>
13+
Really fun helper text
14+
</SelectHelperText>
15+
);
16+
}
17+
```
18+
19+
## Props
20+
21+
Prop Name | Type | Description
22+
--- | --- | ---
23+
persistent | boolean | Adds the `.mdc-select-helper-text--persistent` class to keep the helper text always visible.

packages/select/helper-text/index.tsx

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 * as React from 'react';
24+
import classnames from 'classnames';
25+
import {MDCSelectHelperTextAdapter} from '@material/select/helper-text/adapter';
26+
import {MDCSelectHelperTextFoundation} from '@material/select/helper-text/foundation';
27+
28+
export interface SelectHelperTextProps extends React.HTMLProps<HTMLParagraphElement> {
29+
persistent?: boolean;
30+
setHelperTextFoundation?: (foundation?: MDCSelectHelperTextFoundation) => void;
31+
}
32+
33+
interface ElementAttributes {
34+
'aria-hidden'?: boolean | 'false' | 'true';
35+
role?: string;
36+
};
37+
38+
interface SelectHelperTextState extends ElementAttributes {
39+
classList: Set<string>;
40+
};
41+
42+
export class SelectHelperText extends React.Component<SelectHelperTextProps, SelectHelperTextState> {
43+
foundation?: MDCSelectHelperTextFoundation;
44+
45+
state: SelectHelperTextState = {
46+
'aria-hidden': undefined,
47+
'role': undefined,
48+
'classList': new Set(),
49+
};
50+
51+
componentDidMount() {
52+
const {setHelperTextFoundation} = this.props;
53+
this.foundation = new MDCSelectHelperTextFoundation(this.adapter);
54+
this.foundation.init();
55+
setHelperTextFoundation && setHelperTextFoundation(this.foundation);
56+
}
57+
58+
componentWillUnmount() {
59+
const {setHelperTextFoundation} = this.props;
60+
if (this.foundation) {
61+
this.foundation.destroy();
62+
setHelperTextFoundation && setHelperTextFoundation(undefined);
63+
}
64+
}
65+
66+
get classes() {
67+
const {className, persistent} = this.props;
68+
const {classList} = this.state;
69+
return classnames('mdc-select-helper-text', Array.from(classList), className, {
70+
'mdc-select-helper-text--persistent': persistent,
71+
});
72+
}
73+
74+
get adapter(): MDCSelectHelperTextAdapter {
75+
return {
76+
addClass: (className: string) => {
77+
const classList = new Set(this.state.classList);
78+
classList.add(className);
79+
this.setState({classList});
80+
},
81+
removeClass: (className: string) => {
82+
const classList = new Set(this.state.classList);
83+
classList.delete(className);
84+
this.setState({classList});
85+
},
86+
hasClass: (className: string) => {
87+
return this.classes.split(' ').includes(className);
88+
},
89+
setAttr: (attr: keyof ElementAttributes, value: ElementAttributes[keyof ElementAttributes]) => {
90+
this.setState((prevState) => ({
91+
...prevState,
92+
[attr]: value,
93+
}));
94+
},
95+
removeAttr: (attr: keyof ElementAttributes) => {
96+
this.setState((prevState) => ({...prevState, [attr]: null}));
97+
},
98+
setContent: () => {
99+
// not implmenting because developer should would never call `setContent()`
100+
},
101+
};
102+
}
103+
104+
render() {
105+
const {
106+
children,
107+
/* eslint-disable no-unused-vars */
108+
persistent,
109+
className,
110+
setHelperTextFoundation,
111+
/* eslint-enable no-unused-vars */
112+
...otherProps
113+
} = this.props;
114+
const {
115+
'aria-hidden': ariaHidden,
116+
role,
117+
} = this.state;
118+
119+
return (
120+
<p
121+
className={this.classes}
122+
aria-hidden={ariaHidden}
123+
role={role}
124+
{...otherProps}
125+
>
126+
{children}
127+
</p>
128+
);
129+
}
130+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 {SelectHelperText} from '../../../../packages/select/helper-text/index';
6+
import {MDCSelectHelperTextFoundation} from '@material/select/helper-text/foundation';
7+
8+
suite('Select Helper Text');
9+
10+
test('renders with mdc-select-helper-text class', () => {
11+
const wrapper = shallow(<SelectHelperText />);
12+
assert.isTrue(wrapper.hasClass('mdc-select-helper-text'));
13+
});
14+
15+
test('renders with a test class name', () => {
16+
const wrapper = shallow(<SelectHelperText className='test-class' />);
17+
assert.isTrue(wrapper.hasClass('test-class'));
18+
});
19+
20+
test('renders with class from state.classList', () => {
21+
const wrapper = shallow(<SelectHelperText />);
22+
wrapper.setState({classList: new Set(['test-class'])});
23+
assert.isTrue(wrapper.hasClass('test-class'));
24+
});
25+
26+
test('renders with persistent class when props.persistent is true', () => {
27+
const wrapper = shallow(<SelectHelperText persistent />);
28+
assert.isTrue(wrapper.hasClass('mdc-select-helper-text--persistent'));
29+
});
30+
31+
test.only('calls setHelperTextFoundation with foundation', () => {
32+
const setHelperTextFoundation = td.func<(foundation?: MDCSelectHelperTextFoundation) => void>();
33+
shallow(<SelectHelperText setHelperTextFoundation={setHelperTextFoundation} />);
34+
// TODO: change Object to MDCSelectHelperTextFoundation in PR 823
35+
td.verify(setHelperTextFoundation(td.matchers.isA(Object)), {times: 1});
36+
});
37+
38+
test('#componentWillUnmount destroys foundation', () => {
39+
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
40+
const foundation = wrapper.instance().foundation!;
41+
foundation.destroy = td.func<() => void>();
42+
wrapper.unmount();
43+
td.verify(foundation.destroy(), {times: 1});
44+
});
45+
46+
test('#adapter.addClass should add a class to state.classList', () => {
47+
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
48+
wrapper.instance().adapter.addClass('test-class');
49+
assert.isTrue(wrapper.state().classList.has('test-class'));
50+
});
51+
52+
test('#adapter.removeClass should remove a class to state.classList', () => {
53+
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
54+
wrapper.setState({classList: new Set(['test-class'])});
55+
wrapper.instance().adapter.removeClass('test-class');
56+
assert.isFalse(wrapper.state().classList.has('test-class'));
57+
});
58+
59+
test('#adapter.hasClass should return true if state.classList has class', () => {
60+
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
61+
wrapper.setState({classList: new Set(['test-class'])});
62+
assert.isTrue(wrapper.instance().adapter.hasClass('test-class'));
63+
});
64+
65+
test('#adapter.hasClass should return true if className has class', () => {
66+
const wrapper = shallow<SelectHelperText>(<SelectHelperText className='test-class' />);
67+
assert.isTrue(wrapper.instance().adapter.hasClass('test-class'));
68+
});
69+
70+
test('#adapter.setAttr should update state', () => {
71+
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
72+
wrapper.instance().adapter.setAttr('role', 'menu');
73+
assert.equal(wrapper.state().role, 'menu');
74+
});
75+
76+
test('#adapter.removeAttr should update state', () => {
77+
const wrapper = shallow<SelectHelperText>(<SelectHelperText />);
78+
wrapper.setState({role: 'menu'});
79+
wrapper.instance().adapter.removeAttr('role');
80+
assert.equal(wrapper.state().role, null);
81+
});
82+
83+
test('renders with aria-hidden from state.aria-hidden', () => {
84+
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
85+
wrapper.setState({'aria-hidden': 'true'});
86+
assert.equal(wrapper.getDOMNode().getAttribute('aria-hidden'), 'true');
87+
});
88+
89+
test('renders with role from state.role', () => {
90+
const wrapper = mount<SelectHelperText>(<SelectHelperText />);
91+
wrapper.setState({'role': 'true'});
92+
assert.equal(wrapper.getDOMNode().getAttribute('role'), 'true');
93+
});
94+
95+
test('renders children', () => {
96+
const wrapper = mount<SelectHelperText>(<SelectHelperText>MEOW</SelectHelperText>);
97+
assert.equal(wrapper.text(), 'MEOW');
98+
});

0 commit comments

Comments
 (0)