Skip to content

Commit 97f772c

Browse files
authored
feat(SmartLabel): new comp (#55)
* feat(SmartLabel): new comp * Add unit tests * Add SmartLabel to index * Add more tests to smartLabel * Remove enum STATUS and add snapshot unit test * Revert unit test
1 parent b3671b7 commit 97f772c

File tree

7 files changed

+385
-0
lines changed

7 files changed

+385
-0
lines changed

Diff for: src/components/atoms/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export { default as Check } from './check';
33
export { default as ErrorMessage } from './errorMessage';
44
export { default as Invisible } from './invisible';
55
export { default as Label } from './label';
6+
export { default as SmartLabel } from './smartLabel';
67
export { default as Snackbars } from './snackbars';
78
export { default as TextInput } from './textInput';

Diff for: src/components/atoms/smartLabel/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# SmartLabel
2+
3+
<!-- STORY -->
4+
5+
<hr>
6+
7+
A label that only displays when the input has a value.
8+
9+
##### Import
10+
11+
```js
12+
import { SmartLabel } from 'react-helium';
13+
```
14+
15+
##### Usage
16+
17+
```jsx
18+
<SmartLabel forId="1" text="Hello" inputHasFocus={false} inputHasValue>
19+
<input type="text" id="1" value="world" />
20+
</SmartLabel>
21+
```
22+
23+
##### Required props
24+
25+
| Name | Type | Description |
26+
| --------------- | -------------- | --------------------------------------------------- |
27+
| `children` | `ReactElement` | e.g: any html tags |
28+
| `forId` | `string` | to specify which form element the label is bound to |
29+
| `text` | `string` | the smart label text |
30+
| `inputHasFocus` | `boolean` | |
31+
| `inputHasValue` | `boolean` | |
32+
33+
##### Optional props
34+
35+
| Name | Type | Default | Description |
36+
| ----------- | --------- | ------- | -------------------------------------------------------- |
37+
| `status` | `string` | `null` | can be of type "invalid", "valid", "modified", "caution" |
38+
| `maxWidth` | `boolean` | `false` | |
39+
| `required` | `boolean` | `false` | |
40+
| `hideLabel` | `boolean` | `false` | |

Diff for: src/components/atoms/smartLabel/index.tsx

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { FC, ReactElement, JSXElementConstructor } from 'react';
2+
3+
import Invisible from '../invisible';
4+
5+
import useStyles from './smartLabel.style';
6+
7+
import {
8+
STATUS_INVALID,
9+
STATUS_CAUTION,
10+
STATUS_VALID,
11+
STATUS_MODIFIED,
12+
} from '../../../constant/status';
13+
14+
interface Props {
15+
children: ReactElement<unknown, string | JSXElementConstructor<unknown>>;
16+
forId: string;
17+
text: string;
18+
inputHasFocus: boolean;
19+
inputHasValue: boolean;
20+
status?:
21+
| typeof STATUS_INVALID
22+
| typeof STATUS_CAUTION
23+
| typeof STATUS_VALID
24+
| typeof STATUS_MODIFIED;
25+
maxWidth?: boolean;
26+
required?: boolean;
27+
hideLabel?: boolean;
28+
}
29+
30+
export const SmartLabel: FC<Props> = ({
31+
children,
32+
forId,
33+
text,
34+
inputHasFocus,
35+
inputHasValue,
36+
status = null,
37+
maxWidth = false,
38+
required = false,
39+
hideLabel = false,
40+
}) => {
41+
const classes = useStyles();
42+
43+
const rootProps = {
44+
className: classes.root,
45+
'data-input-has-focus': inputHasFocus,
46+
'data-input-has-value': inputHasValue,
47+
'data-input-is-required': required,
48+
'data-input-is-invalid': status === STATUS_INVALID,
49+
'data-input-is-caution': status === STATUS_CAUTION,
50+
'data-input-is-valid': status === STATUS_VALID,
51+
'data-input-is-modified': status === STATUS_MODIFIED,
52+
'data-is-max-width': maxWidth,
53+
'data-testid': 'smartlabel',
54+
};
55+
56+
return (
57+
<label {...rootProps} htmlFor={forId}>
58+
<Invisible visible={!hideLabel}>
59+
<div className={classes.label}>
60+
<span>{text}</span>
61+
</div>
62+
</Invisible>
63+
{children}
64+
</label>
65+
);
66+
};
67+
68+
export default SmartLabel;
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { storiesOf } from '@storybook/react';
3+
import {
4+
boolean,
5+
text,
6+
withKnobs,
7+
optionsKnob as options,
8+
} from '@storybook/addon-knobs';
9+
import {
10+
STATUS_INVALID,
11+
STATUS_CAUTION,
12+
STATUS_VALID,
13+
STATUS_MODIFIED,
14+
} from '../../../constant/status';
15+
16+
import SmartLabel from '.';
17+
18+
import README from './README.md';
19+
20+
const stories = storiesOf('Atoms/SmartLabel', module);
21+
22+
stories.addDecorator(withKnobs);
23+
24+
stories.addParameters({
25+
readme: {
26+
content: README,
27+
},
28+
});
29+
30+
const statusOptions = {
31+
Invalid: STATUS_INVALID,
32+
Caution: STATUS_CAUTION,
33+
Valid: STATUS_VALID,
34+
Modified: STATUS_MODIFIED,
35+
};
36+
37+
stories.add('default', () => {
38+
const statusValue: unknown = options('Status', statusOptions, STATUS_VALID, {
39+
display: 'inline-radio',
40+
});
41+
42+
return (
43+
<SmartLabel
44+
forId="1"
45+
text={text('label text', '')}
46+
inputHasFocus={boolean('inputHasFocus', false)}
47+
inputHasValue={boolean('inputHasValue', false)}
48+
status={statusValue as any}
49+
maxWidth={boolean('maxWidth', false)}
50+
required={boolean('required', false)}
51+
hideLabel={boolean('hideLabel', false)}
52+
>
53+
<p>{text('Child', 'I am child')}</p>
54+
</SmartLabel>
55+
);
56+
});

Diff for: src/components/atoms/smartLabel/smartLabel.style.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { createUseStyles, Styles } from 'react-jss';
2+
3+
type Theme = {
4+
[key: string]: string;
5+
};
6+
7+
export default createUseStyles(
8+
(theme: Theme) =>
9+
({
10+
root: {
11+
display: 'flex',
12+
boxSizing: 'border-box',
13+
position: 'relative',
14+
width: '100%',
15+
backgroundColor: theme.white,
16+
borderRadius: 2,
17+
'&[data-is-max-width="true"]': {
18+
maxWidth: 300,
19+
},
20+
},
21+
label: {
22+
boxSizing: 'border-box',
23+
position: 'absolute',
24+
zIndex: 2,
25+
top: -6,
26+
left: 4,
27+
height: 13,
28+
lineHeight: `13px`,
29+
paddingLeft: 3,
30+
paddingRight: 3,
31+
fontSize: 10,
32+
fontWeight: 400,
33+
color: theme.grey3,
34+
whiteSpace: 'nowrap',
35+
overflow: 'hidden',
36+
transition: 'color 150ms linear 0ms',
37+
'& > span': {
38+
opacity: 0,
39+
transition: 'opacity 150ms linear 0ms',
40+
},
41+
'&::after': {
42+
content: '""',
43+
position: 'absolute',
44+
top: 6,
45+
height: 1,
46+
left: 0,
47+
right: 0,
48+
backgroundColor: theme.white1,
49+
zIndex: -1,
50+
opacity: 0,
51+
transform: `scaleX(0)`,
52+
transition: 'opacity 0ms linear 200ms, transform 50ms linear 150ms',
53+
},
54+
'[data-input-has-value="true"] > &': {
55+
'&::after': {
56+
opacity: 1,
57+
transform: `scaleX(1)`,
58+
transitionDelay: '0ms, 0ms',
59+
},
60+
'& > span': {
61+
opacity: 1,
62+
transitionDelay: '100ms',
63+
},
64+
},
65+
'[data-input-has-focus="true"]:not([data-input-is-invalid="true"]):not([data-input-is-caution="true"]):not([data-input-is-valid="true"]):not([data-input-is-modified="true"]) > &':
66+
{
67+
color: theme.teal1,
68+
},
69+
'[data-input-is-required="true"] > &::before': {
70+
content: '"* "',
71+
},
72+
'[data-input-is-invalid="true"] > &': {
73+
color: theme.warningRed,
74+
},
75+
'[data-input-is-caution="true"] > &': {
76+
color: theme.cautionOrange,
77+
},
78+
'[data-input-is-valid="true"] > &': {
79+
color: theme.validGreen,
80+
},
81+
'[data-input-is-modified="true"] > &': {
82+
color: theme.teal1,
83+
},
84+
},
85+
} as Styles)
86+
);

Diff for: src/components/atoms/smartLabel/smartLabel.test.tsx

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
5+
import { SmartLabel } from '.';
6+
7+
import {
8+
STATUS_VALID,
9+
STATUS_INVALID,
10+
STATUS_CAUTION,
11+
STATUS_MODIFIED,
12+
} from '../../../constant/status';
13+
14+
describe('SmartLabel', () => {
15+
let props;
16+
17+
beforeEach(() => {
18+
props = {
19+
text: 'Label text',
20+
forId: 'input_id_001',
21+
inputHasFocus: false,
22+
inputHasValue: false,
23+
required: false,
24+
};
25+
});
26+
27+
it('renders the component', () => {
28+
const { getByTestId, queryByTestId } = render(
29+
<SmartLabel {...props}>Hello world</SmartLabel>
30+
);
31+
32+
expect(queryByTestId('invisible-wrapper')).not.toBeInTheDocument();
33+
34+
expect(getByTestId('smartlabel')).toBeInTheDocument();
35+
});
36+
37+
it('renders a label in focus', () => {
38+
props.inputHasFocus = true;
39+
const { getByTestId } = render(
40+
<SmartLabel {...props}>Hello world</SmartLabel>
41+
);
42+
43+
expect(getByTestId('smartlabel').getAttribute('data-input-has-focus')).toBe(
44+
'true'
45+
);
46+
});
47+
48+
it('renders a label with a value', () => {
49+
props.inputHasValue = true;
50+
const { getByTestId } = render(
51+
<SmartLabel {...props}>Hello world</SmartLabel>
52+
);
53+
54+
expect(getByTestId('smartlabel').getAttribute('data-input-has-value')).toBe(
55+
'true'
56+
);
57+
});
58+
59+
it('renders a required label', () => {
60+
props.required = true;
61+
const { getByTestId } = render(
62+
<SmartLabel {...props}>Hello world</SmartLabel>
63+
);
64+
65+
expect(
66+
getByTestId('smartlabel').getAttribute('data-input-is-required')
67+
).toBe('true');
68+
});
69+
70+
it('renders a valid status', () => {
71+
props.status = STATUS_VALID;
72+
const { getByTestId } = render(
73+
<SmartLabel {...props}>Hello world</SmartLabel>
74+
);
75+
76+
expect(getByTestId('smartlabel').getAttribute('data-input-is-valid')).toBe(
77+
'true'
78+
);
79+
});
80+
81+
it('renders an invalid status', () => {
82+
props.status = STATUS_INVALID;
83+
const { getByTestId } = render(
84+
<SmartLabel {...props}>Hello world</SmartLabel>
85+
);
86+
87+
expect(
88+
getByTestId('smartlabel').getAttribute('data-input-is-invalid')
89+
).toBe('true');
90+
});
91+
92+
it('renders an caution status', () => {
93+
props.status = STATUS_CAUTION;
94+
const { getByTestId } = render(
95+
<SmartLabel {...props}>Hello world</SmartLabel>
96+
);
97+
98+
expect(
99+
getByTestId('smartlabel').getAttribute('data-input-is-caution')
100+
).toBe('true');
101+
});
102+
103+
it('renders a modified status', () => {
104+
props.status = STATUS_MODIFIED;
105+
const { getByTestId } = render(
106+
<SmartLabel {...props}>Hello world</SmartLabel>
107+
);
108+
109+
expect(
110+
getByTestId('smartlabel').getAttribute('data-input-is-modified')
111+
).toBe('true');
112+
});
113+
114+
it('renders with maxWidth', () => {
115+
props.maxWidth = true;
116+
const { getByTestId } = render(
117+
<SmartLabel {...props}>Hello world</SmartLabel>
118+
);
119+
120+
expect(getByTestId('smartlabel').getAttribute('data-is-max-width')).toBe(
121+
'true'
122+
);
123+
});
124+
125+
it('does not render the label text', () => {
126+
props.hideLabel = true;
127+
const { queryByTestId } = render(
128+
<SmartLabel {...props}>Hello world</SmartLabel>
129+
);
130+
131+
expect(queryByTestId('invisible-wrapper')).toBeInTheDocument();
132+
});
133+
});

Diff for: src/constant/status.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export const STATUS_INVALID = 'invalid';
22
export const STATUS_CAUTION = 'caution';
33
export const STATUS_VALID = 'valid';
4+
export const STATUS_MODIFIED = 'modified';

0 commit comments

Comments
 (0)