-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into CTP-1585-add-prop=maxMenuHeight-to-selecti…
…nput
- Loading branch information
Showing
46 changed files
with
1,370 additions
and
771 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from './text-field'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.', | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.