Skip to content

Commit 72087a1

Browse files
authored
feat(components): Add a validation hint to the Select (#451)
* feat(components): add a validation hint tooltip to the Select component * feat(components): add error icon to invalid Select * fix(components): don't apply invalid styles when disabled
1 parent 42ce743 commit 72087a1

File tree

10 files changed

+207
-19
lines changed

10 files changed

+207
-19
lines changed

src/components/Input/Input.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import {
3131

3232
import Tooltip from '../Tooltip';
3333

34-
import { ReactComponent as ErrorIcon } from './error.svg';
35-
import { ReactComponent as WarningIcon } from './warning.svg';
36-
import { ReactComponent as ValidIcon } from './valid.svg';
34+
import { ReactComponent as ErrorIcon } from '../../icons/error.svg';
35+
import { ReactComponent as WarningIcon } from '../../icons/warning.svg';
36+
import { ReactComponent as ValidIcon } from '../../icons/valid.svg';
3737

3838
const containerBaseStyles = ({ theme }) => css`
3939
label: input__container;

src/components/Select/Select.js

+48-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import {
2626
} from '../../util/shared-prop-types';
2727
import { textMega, disableVisually } from '../../styles/style-helpers';
2828

29-
import { ReactComponent as ArrowsIcon } from './arrows.svg';
29+
import { ReactComponent as ArrowsIcon } from '../../icons/arrows.svg';
30+
import { ReactComponent as ErrorIcon } from '../../icons/error.svg';
31+
import Tooltip from '../Tooltip';
3032

3133
// HACK: Firefox includes the border-width in the overall height of the element
3234
// (despite box-sizing: border-box), so we have to force the height.
@@ -76,21 +78,29 @@ const selectInvalidStyles = ({ theme, invalid, disabled }) =>
7678
css`
7779
label: select--invalid;
7880
border-color: ${theme.colors.r300};
81+
padding-right: ${theme.spacings.zetta};
7982
`;
8083

81-
const iconBaseStyles = ({ theme }) => css`
84+
const suffixBaseStyles = ({ theme }) => css`
8285
label: select__icon;
8386
fill: ${theme.colors.n700};
8487
display: block;
8588
z-index: 40;
8689
pointer-events: none;
8790
position: absolute;
88-
${size(theme.spacings.kilo)};
89-
top: 50%;
90-
right: ${theme.spacings.kilo};
91-
transform: translateY(-50%);
91+
${size(theme.spacings.mega)};
92+
top: 1px;
93+
right: 1px;
94+
margin: ${theme.spacings.kilo};
9295
`;
9396

97+
const suffixInvalidStyles = ({ theme, invalid }) =>
98+
invalid &&
99+
css`
100+
label: select__icon--invalid;
101+
right: calc(1px + ${theme.spacings.giga});
102+
`;
103+
94104
const containerBaseStyles = ({ theme }) => css`
95105
label: select__container;
96106
color: ${theme.colors.n900};
@@ -146,6 +156,11 @@ const selectPrefixStyles = ({ theme, hasPrefix }) =>
146156
);
147157
`;
148158

159+
const tooltipBaseStyles = css`
160+
label: select__tooltip;
161+
right: 1px;
162+
`;
163+
149164
const SelectContainer = styled('div')`
150165
${containerBaseStyles};
151166
${containerNoMarginStyles};
@@ -159,8 +174,17 @@ const SelectElement = styled('select')`
159174
${selectPrefixStyles};
160175
`;
161176

162-
const Icon = styled(ArrowsIcon)`
163-
${iconBaseStyles};
177+
const SelectIcon = styled(ArrowsIcon)`
178+
${suffixBaseStyles};
179+
${suffixInvalidStyles};
180+
`;
181+
182+
const InvalidIcon = styled(ErrorIcon)`
183+
${suffixBaseStyles};
184+
`;
185+
186+
const SelectTooltip = styled(Tooltip)`
187+
${tooltipBaseStyles};
164188
`;
165189

166190
/**
@@ -172,19 +196,23 @@ const Select = ({
172196
disabled,
173197
noMargin,
174198
inline,
199+
invalid,
175200
options,
176201
children,
177202
renderPrefix: RenderPrefix,
203+
validationHint,
178204
...props
179205
}) => {
180206
const prefix = RenderPrefix && <RenderPrefix css={prefixStyles} />;
207+
const showInvalid = !disabled && invalid;
181208

182209
return (
183210
<SelectContainer {...{ noMargin, inline, disabled }}>
184211
{prefix}
185212
<SelectElement
186213
{...{
187214
...props,
215+
invalid,
188216
value,
189217
disabled,
190218
hasPrefix: !!prefix
@@ -206,7 +234,13 @@ const Select = ({
206234
</option>
207235
)))}
208236
</SelectElement>
209-
<Icon />
237+
<SelectIcon invalid={showInvalid} />
238+
{showInvalid && <InvalidIcon />}
239+
{!disabled && validationHint && (
240+
<SelectTooltip position={Tooltip.TOP} align={Tooltip.LEFT}>
241+
{validationHint}
242+
</SelectTooltip>
243+
)}
210244
</SelectContainer>
211245
);
212246
};
@@ -270,7 +304,11 @@ Select.propTypes = {
270304
* Render prop that should render a left-aligned overlay icon or element.
271305
* Receives a className prop.
272306
*/
273-
renderPrefix: PropTypes.func
307+
renderPrefix: PropTypes.func,
308+
/**
309+
* Warning or error message, displayed in a tooltip.
310+
*/
311+
validationHint: PropTypes.string
274312
};
275313

276314
Select.defaultProps = {

src/components/Select/Select.spec.js

+7
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ describe('Select', () => {
6262
expect(actual).toMatchSnapshot();
6363
});
6464

65+
it('should render with a tooltip when passed a validation hint', () => {
66+
const actual = create(
67+
<Select {...{ options }} validationHint="This field is required." />
68+
);
69+
expect(actual).toMatchSnapshot();
70+
});
71+
6572
/**
6673
* Accessibility tests.
6774
*/

src/components/Select/Select.story.js

+15-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import React from 'react';
1717
import { storiesOf } from '@storybook/react';
1818
import { withInfo } from '@storybook/addon-info';
1919
import { action } from '@storybook/addon-actions';
20-
import { boolean } from '@storybook/addon-knobs/react';
20+
import { boolean, text } from '@storybook/addon-knobs/react';
2121

2222
import { GROUPS } from '../../../.storybook/hierarchySeparators';
2323

@@ -71,6 +71,20 @@ storiesOf(`${GROUPS.FORMS}|Select`, module)
7171
onChange={action('Option selected')}
7272
disabled={boolean('Disabled', false)}
7373
invalid={boolean('Invalid', false)}
74+
validationHint={text('Validation hint', '')}
75+
/>
76+
))
77+
)
78+
.add(
79+
'Select invalid',
80+
withInfo()(() => (
81+
<Select
82+
name="select"
83+
options={options}
84+
onChange={action('Option selected')}
85+
disabled={boolean('Disabled', false)}
86+
invalid={boolean('Invalid', true)}
87+
validationHint={text('Validation hint', 'This field is required')}
7488
/>
7589
))
7690
)

src/components/Select/__snapshots__/Select.spec.js.snap

+131
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,133 @@ exports[`Select should render with a prefix when passed the prefix prop 1`] = `
174174
</div>
175175
`;
176176

177+
exports[`Select should render with a tooltip when passed a validation hint 1`] = `
178+
.circuit-4 {
179+
color: #212933;
180+
display: block;
181+
position: relative;
182+
margin-bottom: 16px;
183+
}
184+
185+
.circuit-0 {
186+
-webkit-appearance: none;
187+
-moz-appearance: none;
188+
appearance: none;
189+
background-color: #FFFFFF;
190+
border-width: 1px;
191+
border-style: solid;
192+
border-color: #D8DDE1;
193+
border-radius: 4px;
194+
box-shadow: inset 0 1px 2px 0 rgba(102,113,123,0.12);
195+
color: #212933;
196+
padding: 8px 32px 8px 12px;
197+
max-height: 42px;
198+
position: relative;
199+
width: 100%;
200+
z-index: 20;
201+
overflow-x: hidden;
202+
text-overflow: ellipsis;
203+
white-space: nowrap;
204+
font-size: 16px;
205+
line-height: 24px;
206+
}
207+
208+
.circuit-0:focus,
209+
.circuit-0:hover,
210+
.circuit-0:active {
211+
outline: none;
212+
}
213+
214+
.circuit-0:focus {
215+
border-color: #3388FF;
216+
}
217+
218+
.circuit-0:-moz-focusring {
219+
color: transparent;
220+
text-shadow: 0 0 0 #000;
221+
}
222+
223+
.circuit-2 {
224+
display: inline-block;
225+
width: 100%;
226+
max-width: 280px;
227+
min-width: 120px;
228+
background-color: #212933;
229+
color: #FFFFFF;
230+
border-radius: 4px;
231+
padding: 8px 12px;
232+
position: absolute;
233+
z-index: 31;
234+
-webkit-transition: opacity 0.3s;
235+
transition: opacity 0.3s;
236+
font-size: 13px;
237+
line-height: 20px;
238+
box-shadow: 0 0 0 1px rgba(12,15,20,0.02), 0 0 1px 0 rgba(12,15,20,0.06), 0 2px 2px 0 rgba(12,15,20,0.06);
239+
right: 50%;
240+
right: calc(50% - (16px + 4px));
241+
bottom: 100%;
242+
bottom: calc(100% + 12px);
243+
right: 1px;
244+
}
245+
246+
.circuit-2::after {
247+
display: block;
248+
content: '';
249+
width: 0;
250+
height: 0;
251+
position: absolute;
252+
border: 8px solid transparent;
253+
}
254+
255+
.circuit-2::after {
256+
right: 12px;
257+
}
258+
259+
.circuit-2::after {
260+
top: 100%;
261+
border-top-color: #212933;
262+
}
263+
264+
<div
265+
className="circuit-4 circuit-5"
266+
disabled={false}
267+
>
268+
<select
269+
className="circuit-0 circuit-1"
270+
disabled={false}
271+
>
272+
<option
273+
value=""
274+
>
275+
Select an option
276+
</option>
277+
<option
278+
value="1"
279+
>
280+
Option 1
281+
</option>
282+
<option
283+
value="2"
284+
>
285+
Option 2
286+
</option>
287+
<option
288+
value="3"
289+
>
290+
Option 3
291+
</option>
292+
</select>
293+
<div>
294+
arrows.svg
295+
</div>
296+
<div
297+
className="circuit-2 circuit-3"
298+
>
299+
This field is required.
300+
</div>
301+
</div>
302+
`;
303+
177304
exports[`Select should render with default styles 1`] = `
178305
.circuit-2 {
179306
color: #212933;
@@ -452,6 +579,7 @@ exports[`Select should render with invalid styles when passed the invalid prop 1
452579
font-size: 16px;
453580
line-height: 24px;
454581
border-color: #EA7A7A;
582+
padding-right: 56px;
455583
}
456584
457585
.circuit-0:focus,
@@ -501,6 +629,9 @@ exports[`Select should render with invalid styles when passed the invalid prop 1
501629
<div>
502630
arrows.svg
503631
</div>
632+
<div>
633+
error.svg
634+
</div>
504635
</div>
505636
`;
506637

src/components/Select/arrows.svg

-5
This file was deleted.

src/icons/arrows.svg

+3
Loading
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)