Skip to content

Commit c2fcaf7

Browse files
authored
feat: add RadioField component, rework RadioInput (#537)
* refactor(inputs/radio): to have consistent API with the other input components * feat: add experimental component <StylesProvider> for applying global styles (as replacement of reset.css) * feat: add RadioField component * docs(fields/radio): update readmes * test(inputs/radio): use unicode chars for fruits * test(inputs/radio): remove fruits * Revert "feat: add experimental component <StylesProvider> for applying global styles (as replacement of reset.css)" This reverts commit d3a9e26. * refactor(inputs/radio): keep Text.Body wrapper to RadioInput.Option children * fix(inputs/radio): wrapper text component to use 100% width * fix: export RadioField * fix(grid): font-family for VRT * fix(fields/radio): remove placeholder vrt, add readonly vrt * refactor(inputs/radio): do not use text component as a wrapper, manually apply styles
1 parent 9f420c5 commit c2fcaf7

15 files changed

+929
-156
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# RadioField
2+
3+
#### Description
4+
5+
A controlled radio input component with validation states and a label.
6+
7+
## Usage
8+
9+
```js
10+
import { RadioField } from '@commercetools-frontend/ui-kit';
11+
12+
<RadioField
13+
title="Fruits"
14+
name="fruits"
15+
value="apple"
16+
onChange={event => alert(event.target.value)}
17+
>
18+
<RadioInput.Option value="apple">{'Apple'}</RadioInput.Option>
19+
<RadioInput.Option value="banana">{'Banana'}</RadioInput.Option>
20+
</RadioField>;
21+
```
22+
23+
#### Properties
24+
25+
##### RadioField
26+
27+
| Props | Type | Required | Values | Default | Description |
28+
| ---------------------- | ------------------ | :------: | ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29+
| `id` | `string` | - | - | - | Used as HTML `id` property. An `id` is auto-generated when it is not specified. |
30+
| `horizontalConstraint` | `object` | | `m`, `l`, `xl`, `scale` | `scale` | Horizontal size limit of the input fields. |
31+
| `errors` | `object` | - | - | - | A map of errors. Error messages for known errors are rendered automatically. Unknown errors will be forwarded to `renderError`. |
32+
| `renderError` | `function` | - | - | - | Called with custom errors, as `renderError(key, error)`. This function can return a message which will be wrapped in an `ErrorMessage`. It can also return `null` to show no error. |
33+
| `isRequired` | `bool` | - | - | `false` | Indicates if the value is required. Shows an the "required asterisk" if so. |
34+
| `touched` | `bool` | - | - | `false` | Indicates whether the field was touched. Errors will only be shown when the field was touched. |
35+
| `name` | `string` | - | - | - | Used as HTML `name` of the input component. property |
36+
| `value` | `string` || - | - | Value of the input component. |
37+
| `onChange` | `func` | - | - | - | Called with an event containing the new value. Required when input is not read only. Parent should pass it back as `value`. |
38+
| `onBlur` | `func` | - | - | - | Called when input is blurred |
39+
| `onFocus` | `func` | - | - | - | Called when input is focused |
40+
| `isDisabled` | `bool` | - | - | `false` | Indicates that the input cannot be modified (e.g not authorised, or changes currently saving). |
41+
| `isReadOnly` | `bool` | - | - | `false` | Indicates that the field is displaying read-only content |
42+
| `direction` | `string` | - | `stack` \| `inline` | `stack` | Rendering direction of the radio `RadioInput.Option`s |
43+
| `directionProps` | `object` | - | - | `{ scale: "m" }` | Passes props of the `Spacings.Stack` or `Spacings.Inline`, dependeing on the chosen direction |
44+
| `children` | `node` || - | - | At least one `RadioInput.Option` component or another node (mixed children are allowed) |
45+
| `title` | `string` or `node` || - | - | Title of the label |
46+
| `hint` | `string` or `node` | - | - | - | Hint for the label. Provides a supplementary but important information regarding the behaviour of the input (e.g warn about uniqueness of a field, when it can only be set once), whereas `description` can describe it in more depth. Can also receive a `hintIcon`. |
47+
| `description` | `string` or `node` | - | - | - | Provides a description for the title. |
48+
| `onInfoButtonClick` | `function` | - | - | - | Function called when info button is pressed. Info button will only be visible when this prop is passed. |
49+
| `hintIcon` | `node` | - | - | - | Icon to be displayed beside the hint text. Will only get rendered when `hint` is passed as well. |
50+
| `badge` | `node` | - | - | - | Badge to be displayed beside the label. Might be used to display additional information about the content of the field (E.g verified email) |
51+
52+
The component further forwards all `data-` attributes to the underlying `RadioInput` component.
53+
54+
##### RadioInput.Option
55+
56+
See `RadioInput` README for the list of props.
57+
58+
##### `errors`
59+
60+
This object is a key-value map. The `renderError` prop will be called for each entry with the key and the value. The return value will be rendered inside an `ErrorMessage` component underneath the input.
61+
62+
The `RadioField` supports some errors out of the box. Return `undefined` from `renderError` for these and the default errors will be shown instead. This prevents consumers from having to reimplement the same error messages for known errors while still keeping the flexibility of showing custom error messages for them.
63+
64+
When the `key` is known, and when the value is truthy, and when `renderError` returned `undefined` for that error entry, then the `RadioField` will render an appropriate error automatically.
65+
66+
Known error keys are:
67+
68+
- `missing`: tells the user that this field is required
69+
70+
### Main Functions and use cases are:
71+
72+
- Single option selection field in login forms
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './radio-field';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import requiredIf from 'react-required-if';
4+
import Constraints from '../../constraints';
5+
import Spacings from '../../spacings';
6+
import FieldLabel from '../../field-label';
7+
import RadioInput from '../../inputs/radio-input';
8+
import getFieldId from '../../../utils/get-field-id';
9+
import createSequentialId from '../../../utils/create-sequential-id';
10+
import FieldErrors from '../../field-errors';
11+
import filterDataAttributes from '../../../utils/filter-data-attributes';
12+
13+
const sequentialId = createSequentialId('radio-field-');
14+
15+
const hasErrors = errors => errors && Object.values(errors).some(Boolean);
16+
17+
class RadioField extends React.Component {
18+
static displayName = 'RadioField';
19+
20+
static propTypes = {
21+
// RadioField
22+
id: PropTypes.string,
23+
horizontalConstraint: PropTypes.oneOf(['m', 'l', 'xl', 'scale']),
24+
errors: PropTypes.shape({
25+
missing: PropTypes.bool,
26+
}),
27+
renderError: PropTypes.func,
28+
isRequired: PropTypes.bool,
29+
touched: PropTypes.bool,
30+
31+
// RadioInput
32+
name: PropTypes.string,
33+
value: PropTypes.string.isRequired,
34+
onChange: requiredIf(PropTypes.func, props => !props.isReadOnly),
35+
onBlur: PropTypes.func,
36+
onFocus: PropTypes.func,
37+
isDisabled: PropTypes.bool,
38+
isReadOnly: PropTypes.bool,
39+
direction: PropTypes.oneOf(['stack', 'inline']),
40+
directionProps: PropTypes.object,
41+
children: PropTypes.node.isRequired,
42+
43+
// LabelField
44+
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
45+
hint: requiredIf(
46+
PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
47+
props => props.hintIcon
48+
),
49+
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
50+
onInfoButtonClick: PropTypes.func,
51+
hintIcon: PropTypes.node,
52+
badge: PropTypes.node,
53+
};
54+
55+
static defaultProps = {
56+
horizontalConstraint: 'scale',
57+
};
58+
59+
state = {
60+
// We generate an id in case no id is provided by the parent to attach the
61+
// label to the input component.
62+
id: this.props.id,
63+
};
64+
65+
static getDerivedStateFromProps = (props, state) => ({
66+
id: getFieldId(props, state, sequentialId),
67+
});
68+
69+
render() {
70+
const hasError = this.props.touched && hasErrors(this.props.errors);
71+
return (
72+
<Constraints.Horizontal constraint={this.props.horizontalConstraint}>
73+
<Spacings.Stack scale="xs">
74+
<FieldLabel
75+
title={this.props.title}
76+
hint={this.props.hint}
77+
description={this.props.description}
78+
onInfoButtonClick={this.props.onInfoButtonClick}
79+
hintIcon={this.props.hintIcon}
80+
badge={this.props.badge}
81+
hasRequiredIndicator={this.props.isRequired}
82+
htmlFor={this.state.id}
83+
/>
84+
<RadioInput.Group
85+
id={this.state.id}
86+
name={this.props.name}
87+
value={this.props.value}
88+
onChange={this.props.onChange}
89+
onBlur={this.props.onBlur}
90+
onFocus={this.props.onFocus}
91+
isDisabled={this.props.isDisabled}
92+
isReadOnly={this.props.isReadOnly}
93+
hasError={hasError}
94+
horizontalConstraint={this.props.horizontalConstraint}
95+
direction={this.props.direction}
96+
directionProps={this.props.directionProps}
97+
{...filterDataAttributes(this.props)}
98+
>
99+
{this.props.children}
100+
</RadioInput.Group>
101+
<FieldErrors
102+
errors={this.props.errors}
103+
isVisible={hasError}
104+
renderError={this.props.renderError}
105+
/>
106+
</Spacings.Stack>
107+
</Constraints.Horizontal>
108+
);
109+
}
110+
}
111+
112+
export default RadioField;

0 commit comments

Comments
 (0)