@salesforce/design-system-react
Version:
Salesforce Lightning Design System for React
343 lines (321 loc) • 10.6 kB
JSX
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */
// # Input Component
// Implements the [Input design pattern](https://lightningdesignsystem.com/components/forms/#flavor-input) in React. Does not yet implement [fixed text](https://lightningdesignsystem.com/components/forms/#flavor-input-input-fixed-text).
// Based on SLDS v2.2.1
//
// ### React
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { shape } from 'airbnb-prop-types';
// ### classNames
// [github.com/JedWatson/classnames](https://github.com/JedWatson/classnames)
// This project uses `classnames`, "a simple javascript utility for conditionally
// joining classNames together."
import classNames from 'classnames';
// ### shortid
// [npmjs.com/package/shortid](https://www.npmjs.com/package/shortid)
// shortid is a short, non-sequential, url-friendly, unique id generator
import shortid from 'shortid';
// ## Children
import InputIcon from '../../icon/input-icon';
import InnerInput from './private/inner-input';
import Label from '../private/label';
// This component's `checkProps` which issues warnings to developers about properties when in development mode (similar to React's built in development tools)
import checkProps from './check-props';
import { FORMS_INPUT } from '../../../utilities/constants';
// ## InputDefinition
const Input = createReactClass({
// ### Display Name
// Always use the canonical component name as the React display name.
displayName: FORMS_INPUT,
// ### Prop Types
propTypes: {
'aria-activedescendant': PropTypes.string,
'aria-autocomplete': PropTypes.string,
/**
* An HTML ID that is shared with ARIA-supported devices with the
* `aria-controls` attribute in order to relate the input with
* another region of the page. An example would be a select box
* that shows or hides a panel.
*/
'aria-controls': PropTypes.string,
'aria-describedby': PropTypes.string,
'aria-expanded': PropTypes.bool,
'aria-haspopup': PropTypes.bool,
'aria-labelledby': PropTypes.string,
/**
* An HTML ID that is shared with ARIA-supported devices with the
* `aria-controls` attribute in order to relate the input with
* another region of the page. An example would be a search field
* that shows search results.
*/
'aria-owns': PropTypes.string,
'aria-required': PropTypes.bool,
/**
* **Assistive text for accessibility**
* * `label`: Visually hidden label but read out loud by screen readers.
* * `spinner`: Text for loading spinner icon.
*/
assistiveText: shape({
label: PropTypes.string,
spinner: PropTypes.string
}),
children: PropTypes.node,
/**
* Class names to be added to the outer container of the input.
*/
className: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object,
PropTypes.string
]),
/**
* Disables the input and prevents editing the contents.
*/
disabled: PropTypes.bool,
/**
* Message to display when the input is in an error state. When this is present, also visually highlights the component as in error.
*/
errorText: PropTypes.string,
/**
* Displays text or node to the left of the input. This follows the fixed text input UX pattern.
*/
fixedTextLeft: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
/**
* Displays text or node to the right of the input. This follows the fixed text input UX pattern.
*/
fixedTextRight: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
/**
* If true, loading spinner appears inside input on right hand side.
*/
hasSpinner: PropTypes.bool,
/**
* Left aligned icon, must be instace of `design-system-react/components/icon/input-icon`
*/
iconLeft: PropTypes.node,
/**
* Right aligned icon, must be instace of `design-system-react/components/icon/input-icon`
*/
iconRight: PropTypes.node,
/**
* Every input must have a unique ID in order to support keyboard navigation and ARIA support.
*/
id: PropTypes.string,
/**
* This callback exposes the input reference / DOM node to parent components. `<Parent inputRef={(inputComponent) => this.input = inputComponent} />
*/
inputRef: PropTypes.func,
/**
* Displays the value of the input statically. This follows the static input UX pattern.
*/
isStatic: PropTypes.bool,
/**
* This label appears above the input.
*/
label: PropTypes.string,
onBlur: PropTypes.func,
/**
* This callback fires when the input changes. The synthetic React event will be the first parameter to the callback. You will probably want to reference `event.target.value` in your callback. No custom data object is provided.
*/
onChange: PropTypes.func,
/**
* This event fires when the input is clicked.
*/
onClick: PropTypes.func,
onFocus: PropTypes.func,
onInput: PropTypes.func,
onInvalid: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyPress: PropTypes.func,
onKeyUp: PropTypes.func,
onSelect: PropTypes.func,
onSubmit: PropTypes.func,
/**
* Text that will appear in an empty input.
*/
placeholder: PropTypes.string,
minLength: PropTypes.string,
maxLength: PropTypes.string,
/**
* Name of the submitted form parameter.
*/
name: PropTypes.string,
/**
* Displays the value of the input as readOnly.
*/
readOnly: PropTypes.bool,
/**
* Highlights the input as a required field (does not perform any validation).
*/
required: PropTypes.bool,
/**
* The `<Input>` element includes support for all HTML5 types.
*/
type: PropTypes.oneOf([
'text',
'password',
'datetime',
'datetime-local',
'date',
'month',
'time',
'week',
'number',
'email',
'url',
'search',
'tel',
'color'
]),
/**
* The input is a controlled component, and will always display this value.
*/
value: PropTypes.string,
iconPosition: PropTypes.string,
inlineEditTrigger: PropTypes.node,
role: PropTypes.string
},
getDefaultProps () {
return {
type: 'text'
};
},
componentWillMount () {
// `checkProps` issues warnings to developers about properties (similar to React's built in development tools)
checkProps(FORMS_INPUT, this.props);
this.generatedId = shortid.generate();
if (this.props.errorText) {
this.generatedErrorId = shortid.generate();
}
},
getId () {
return this.props.id || this.generatedId;
},
getErrorId () {
return this.props['aria-describedby'] || this.generatedErrorId;
},
// This is convuluted to maintain backwards compatibility. Please remove deprecatedProps on next breaking change.
getIconRender (position, iconPositionProp) {
let icon;
/* eslint-disable react/prop-types */
const deprecatedProps = {
assistiveText:
(this.props[iconPositionProp] &&
this.props[iconPositionProp].props.assistiveText) ||
this.props.iconAssistiveText,
category:
(this.props[iconPositionProp] &&
this.props[iconPositionProp].props.category) ||
this.props.iconCategory,
name:
(this.props[iconPositionProp] &&
this.props[iconPositionProp].props.name) ||
this.props.iconName,
onClick:
(this.props[iconPositionProp] &&
this.props[iconPositionProp].props.onClick) ||
this.props.onIconClick
};
/* eslint-enable react/prop-types */
if (
this.props[iconPositionProp] &&
position &&
this.props[iconPositionProp]
) {
icon = React.cloneElement(this.props[iconPositionProp], {
iconPosition: `${position}`
});
} else if (deprecatedProps.name) {
icon = <InputIcon iconPosition={position} {...deprecatedProps} />;
}
return icon;
},
// ### Render
render () {
// this is a hack to make left the default prop unless overwritten by `iconPosition="right"`
const hasLeftIcon =
!!this.props.iconLeft ||
((this.props.iconPosition === 'left' ||
this.props.iconPosition === undefined) &&
!!this.props.iconName);
const hasRightIcon =
!!this.props.iconRight ||
(this.props.iconPosition === 'right' && !!this.props.iconName);
return (
<div
className={classNames(
'slds-form-element',
{
'slds-has-error': this.props.errorText
},
this.props.className
)}
>
<Label
assistiveText={this.props.assistiveText}
htmlFor={this.props.isStatic ? undefined : this.getId()}
label={this.props.label}
required={this.props.required}
variant={this.props.isStatic ? 'static' : 'base'}
/>
<InnerInput
aria-activedescendant={this.props['aria-activedescendant']}
aria-autocomplete={this.props['aria-autocomplete']}
aria-controls={this.props['aria-controls']}
aria-labelledby={this.props['aria-labelledby']}
aria-describedby={this.getErrorId()}
aria-expanded={this.props['aria-expanded']}
aria-owns={this.props['aria-owns']}
aria-required={this.props['aria-required']}
containerProps={{
className: 'slds-form-element__control'
}}
disabled={this.props.disabled}
fixedTextLeft={this.props.fixedTextLeft}
fixedTextRight={this.props.fixedTextRight}
hasSpinner={this.props.hasSpinner}
id={this.getId()}
iconLeft={hasLeftIcon ? this.getIconRender('left', 'iconLeft') : null}
iconRight={
hasRightIcon ? this.getIconRender('right', 'iconRight') : null
}
inlineEditTrigger={this.props.inlineEditTrigger}
isStatic={this.props.isStatic}
minLength={this.props.minLength}
maxLength={this.props.maxLength}
name={this.props.name}
onBlur={this.props.onBlur}
onChange={this.props.onChange}
onClick={this.props.onClick}
onFocus={this.props.onFocus}
onInput={this.props.onInput}
onInvalid={this.props.onInvalid}
onKeyDown={this.props.onKeyDown}
onKeyPress={this.props.onKeyPress}
onKeyUp={this.props.onKeyUp}
onSelect={this.props.onSelect}
onSubmit={this.props.onSubmit}
placeholder={this.props.placeholder}
inputRef={this.props.inputRef}
readOnly={this.props.readOnly}
required={this.props.required}
role={this.props.role}
spinnerAssistiveText={
this.props.assistiveText && this.props.assistiveText.spinner
}
type={this.props.type}
value={this.props.value}
/>
{this.props.errorText && (
<div id={this.getErrorId()} className="slds-form-element__help">
{this.props.errorText}
</div>
)}
{this.props.children}
</div>
);
}
});
export default Input;