Skip to content

Commit

Permalink
Merge branch 'master' into CTP-1585-add-prop=maxMenuHeight-to-selecti…
Browse files Browse the repository at this point in the history
…nput
  • Loading branch information
islam3zzat authored Aug 31, 2018
2 parents f10d8b0 + 1df8bf9 commit e0345d4
Show file tree
Hide file tree
Showing 46 changed files with 1,370 additions and 771 deletions.
70 changes: 70 additions & 0 deletions fields/text-field/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# TextField

#### Description

A controlled text input component for single-line strings with validation
states.

## Usage

```js
import TextField from '@commercetools-frontend/ui-kit/fields/text-field';

<TextField title="Username" value="foo" onChange={value => alert(value)} />;
```

#### Properties

| Props | Type | Required | Values | Default | Description |
| ---------------------- | ------------------ | :------: | ---------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | - | - | - | Used as HTML `id` property. An `id` is auto-generated when it is not specified. |
| `horizontalConstraint` | `object` | | `xs`, `s`, `m`, `l`, `xl`, `scale` | `scale` | Horizontal size limit of the input fields. |
| `errors` | `object` | - | - | - | A map of errors. Error messages for known errors are rendered automatically. Unknown errors will be forwarded to `renderError`. |
| `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. |
| `isRequired` | `bool` | - | - | `false` | Indicates if the value is required. Shows an the "required asterisk" if so. |
| `isTouched` | `bool` | - | - | `false` | Indicates whether the field was touched. Errors will only be shown when the field was touched. |
| `name` | `string` | - | - | - | Used as HTML `name` of the input component. property |
| `value` | `string` || - | - | Value of the input component. |
| `onChange` | `func` | - | - | - | Called with an event containing the new value. Required when input is not read only. Parent should pass it back as `value`. |
| `onBlur` | `func` | - | - | - | Called when input is blurred |
| `onFocus` | `func` | - | - | - | Called when input is focused |
| `isAutofocussed` | `bool` | - | - | - | Focus the input on initial render |
| `isDisabled` | `bool` | - | - | `false` | Indicates that the input cannot be modified (e.g not authorised, or changes currently saving). |
| `isReadOnly` | `bool` | - | - | `false` | Indicates that the field is displaying read-only content |
| `placeholder` | `string` | - | - | - | Placeholder text for the input |
| `title` | `string` or `node` || - | - | Title of the label |
| `onInfoButtonClick` | `function` | - | - | - | Function called when info button is pressed. Info button will only be visible when this prop is passed. |
| `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`. |
| `hintIcon` | `node` | - | - | - | Icon to be displayed beside the hint text. Will only get rendered when `hint` is passed as well. |
| `description` | `string` or `node` | - | - | - | Provides a description for the title. |
| `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) |

The component further forwards all `data-` attributes to the underlying `input` component.

##### `errors`

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.

The `TextField` 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.

When the `key` is known, and when the value is truthy, and when `renderError` returned `undefined` for that error entry, then the `TextField` will render an appropriate error automatically.

Known error keys are:

- `missing`: tells the user that this field is required

### Static methods

#### `TextField.isEmpty`

Returns `true` when the value is considered empty, which is when the value is empty or consists of spaces only.

```js
TextField.isEmpty(''); // -> true
TextField.isEmpty(' '); // -> true
TextField.isEmpty('tree'); // -> false
```

### Main Functions and use cases are:

- Input field for single-line strings
1 change: 1 addition & 0 deletions fields/text-field/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './text-field';
9 changes: 9 additions & 0 deletions fields/text-field/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineMessages } from 'react-intl';

export default defineMessages({
missingRequiredField: {
id: 'UIKit.TextField.missingRequiredField',
description: 'Error message for missing required value',
defaultMessage: 'This field is required. Provide a value.',
},
});
99 changes: 99 additions & 0 deletions fields/text-field/text-field.form.story.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import { Formik } from 'formik';
import { IntlProvider } from 'react-intl';
import { storiesOf } from '@storybook/react';
import omitEmpty from 'omit-empty';
import { action } from '@storybook/addon-actions';
import { withKnobs, boolean, select } from '@storybook/addon-knobs';
import withReadme from 'storybook-readme/with-readme';
import Section from '../../.storybook/decorators/section';
import FormikBox from '../../.storybook/decorators/formik-box';
import PrimaryButton from '../../buttons/primary-button';
import SecondaryButton from '../../buttons/secondary-button';
import Spacings from '../../materials/spacings';
import Readme from './README.md';
import TextField from './text-field';

// Cool stuff to try in this story:
// - Click the "Username" label and see how the input is focused automatically
// - Type a username with a space in it
// - Type a username which exceeds ten characters
// - Type a username which exceeds ten characters and has a space in it
// - Play with the horizontalConstraint knob to see it influence the field

storiesOf('Examples|Forms/Fields', module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('TextField', () => (
<Section>
<IntlProvider locale="en">
<Formik
initialValues={{ userName: '' }}
validate={values => {
const errors = { userName: {} };
if (values.userName.trim().length === 0)
errors.userName.missing = true;
if (values.userName.trim().indexOf(' ') !== -1)
errors.userName.usesSpaces = true;
if (values.userName.trim().length > 10)
errors.userName.exceedsMaxLength = true;
return omitEmpty(errors);
}}
onSubmit={(values, formik, ...rest) => {
action('onSubmit')(values, formik, ...rest);
formik.resetForm(values);
}}
render={formik => (
<Spacings.Stack scale="l">
<TextField
title="Username"
description={
boolean('Use ridiculously long description', false)
? 'The name you will have on our platform. Chose it wisely as it can not be changed once it is set. Do not pick something with unicors or a superhero, everybody is doing that :) So once again, chose wisely since this is permantent.'
: 'The name you will have on our platform'
}
hint="No spaces allowed"
name="userName"
isRequired={true}
value={formik.values.userName}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isTouched={formik.touched.userName}
horizontalConstraint={select(
'horizontalConstraint',
['xs', 's', 'm', 'l', 'xl', 'scale'],
'm'
)}
errors={formik.errors.userName}
renderError={key => {
switch (key) {
// these could also use <FormattedMessage />
case 'usesSpaces':
return 'No spaces allowed';
case 'exceedsMaxLength':
return 'No more than 10 characters allowed';
default:
return null;
}
}}
/>
<Spacings.Inline>
<SecondaryButton
onClick={formik.handleReset}
isDisabled={formik.isSubmitting}
label="Reset"
/>
<PrimaryButton
onClick={formik.handleSubmit}
isDisabled={formik.isSubmitting || !formik.dirty}
label="Submit"
/>
</Spacings.Inline>
<hr />
<FormikBox formik={formik} />
</Spacings.Stack>
)}
/>
</IntlProvider>
</Section>
));
147 changes: 147 additions & 0 deletions fields/text-field/text-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import PropTypes from 'prop-types';
import has from 'lodash.has';
import uuid from 'uuid';
import { FormattedMessage } from 'react-intl';
import requiredIf from 'react-required-if';
import Constraints from '../../materials/constraints';
import Spacings from '../../materials/spacings';
import FieldLabel from '../../field-label';
import TextInput from '../../inputs/text-input';
import ErrorMessage from '../../messages/error-message';
import filterDataAttributes from '../../utils/filter-data-attributes';
import messages from './messages';

const isObject = obj => typeof obj === 'object';

const hasErrors = errors => Object.values(errors).some(Boolean);

class TextField extends React.Component {
static displayName = 'TextField';

static isEmpty = TextInput.isEmpty;

static propTypes = {
// TextField
id: PropTypes.string,
horizontalConstraint: PropTypes.oneOf(['xs', 's', 'm', 'l', 'xl', 'scale']),
errors: PropTypes.shape({
missing: PropTypes.bool,
}),
renderError: PropTypes.func,
isRequired: PropTypes.bool,
isTouched: PropTypes.bool,

// TextInput
name: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: requiredIf(PropTypes.func, props => !props.isReadOnly),
onBlur: PropTypes.func,
onFocus: PropTypes.func,
isAutofocussed: PropTypes.bool,
isDisabled: PropTypes.bool,
isReadOnly: PropTypes.bool,
placeholder: PropTypes.string,

// LabelField
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
hint: requiredIf(
PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
props => props.hintIcon
),
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
onInfoButtonClick: PropTypes.func,
hintIcon: PropTypes.node,
badge: PropTypes.node,
};

static defaultProps = {
horizontalConstraint: 'scale',
};

state = {
// We generate an id in case no id is provided by the parent to attach the
// label to the input component.
id: this.props.id,
};

static getDerivedStateFromProps = (props, state) => ({
id: (() => {
if (has(props, 'id')) return props.id;
if (state.id) return state.id;
return uuid();
})(),
});

renderErrors = () => {
if (!this.props.isTouched) return null;
if (!isObject(this.props.errors)) return null;

return (
Object.entries(this.props.errors)
// Only render errors which have truthy values, to avoid
// rendering an error for, e.g. { missing: false }
.filter(([, error]) => error)
.map(([key, error]) => {
// We might not a custom error render, so we fall back to null
// to enable the default errors to kick in
const errorComponent = this.props.renderError
? this.props.renderError(key, error)
: null;
// Render a custom error if one was provided.
// Custom errors take precedence over the known errors
if (errorComponent)
return <ErrorMessage key={key}>{errorComponent}</ErrorMessage>;
// Try to see if we know this error and render that error instead then
if (key === 'missing')
return (
<ErrorMessage key={key}>
<FormattedMessage {...messages.missingRequiredField} />
</ErrorMessage>
);
// Render nothing in case the error is not known and no custom error
// was returned
// The input element will still have the red border to indicate an
// error in this case.
return null;
})
);
};

render() {
return (
<Constraints.Horizontal constraint={this.props.horizontalConstraint}>
<Spacings.Stack scale="xs">
<FieldLabel
title={this.props.title}
hint={this.props.hint}
description={this.props.description}
onInfoButtonClick={this.props.onInfoButtonClick}
hintIcon={this.props.hintIcon}
badge={this.props.badge}
hasRequiredIndicator={this.props.isRequired}
htmlFor={this.state.id}
/>
<TextInput
id={this.state.id}
name={this.props.name}
value={this.props.value}
onChange={this.props.onChange}
onBlur={this.props.onBlur}
onFocus={this.props.onFocus}
isAutofocussed={this.props.isAutofocussed}
isDisabled={this.props.isDisabled}
isReadOnly={this.props.isReadOnly}
hasError={this.props.isTouched && hasErrors(this.props.errors)}
placeholder={this.props.placeholder}
horizontalConstraint="scale"
{...filterDataAttributes(this.props)}
/>
{this.renderErrors()}
</Spacings.Stack>
</Constraints.Horizontal>
);
}
}

export default TextField;
Loading

0 comments on commit e0345d4

Please sign in to comment.