diff --git a/docs/src/pages/demos/text-fields/ComposedTextField.js b/docs/src/pages/demos/text-fields/ComposedTextField.js index 863d3188a7e5f9..193d9b55083eb8 100644 --- a/docs/src/pages/demos/text-fields/ComposedTextField.js +++ b/docs/src/pages/demos/text-fields/ComposedTextField.js @@ -3,8 +3,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from 'material-ui/styles'; -import Input, { InputLabel } from 'material-ui/Input'; +import IconButton from 'material-ui/IconButton'; +import Input, { InputLabel, InputAdornment } from 'material-ui/Input'; import { FormControl, FormHelperText } from 'material-ui/Form'; +import DeleteIcon from 'material-ui-icons/Delete'; const styles = theme => ({ container: { @@ -49,6 +51,18 @@ class ComposedTextField extends React.Component { Error + + Name + + $ + + + + + + + Input as an action + ); } diff --git a/src/Form/FormControl.js b/src/Form/FormControl.js index e7469102ae1adf..f33efd768e1a63 100644 --- a/src/Form/FormControl.js +++ b/src/Form/FormControl.js @@ -125,15 +125,19 @@ class FormControl extends React.Component { }; getChildContext() { - const { disabled, error, required, margin } = this.props; + const { disabled, error, required, margin, children: childrenProp } = this.props; const { dirty, focused } = this.state; + const children = React.Children.toArray(childrenProp); + const hasInputAction = children && children.some(value => isMuiElement(value, ['InputAction'])); + return { muiFormControl: { dirty, disabled, error, focused, + hasInputAction, margin, required, onDirty: this.handleDirty, diff --git a/src/Input/Input.js b/src/Input/Input.js index 5b8587e4545939..ea2cff94a340f0 100644 --- a/src/Input/Input.js +++ b/src/Input/Input.js @@ -1,11 +1,11 @@ // @flow weak import React from 'react'; -import type { ComponentType } from 'react'; +import type { Node, ComponentType } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import withStyles from '../styles/withStyles'; -import { isMuiComponent } from '../utils/reactHelpers'; +import { isMuiComponent, isMuiElement } from '../utils/reactHelpers'; import Textarea from './Textarea'; // Supports determination of isControlled(). @@ -59,6 +59,11 @@ export const styles = (theme: Object) => { color: theme.palette.input.inputText, paddingBottom: 2, }, + adorned: { + display: 'flex', + flexDirection: 'row', + alignItems: 'baseline', + }, formControl: { 'label + &': { marginTop: theme.spacing.unit * 2, @@ -183,6 +188,9 @@ export const styles = (theme: Object) => { fullWidth: { width: '100%', }, + inputAction: { + paddingRight: theme.spacing.unit * 3, + }, }; }; @@ -206,6 +214,10 @@ export type Props = { * If `true`, the input will be focused during the first mount. */ autoFocus?: boolean, + /** + * Any `InputAdornment` for this `Input` + */ + children: Node, /** * Useful to extend the style applied to components. */ @@ -426,6 +438,7 @@ class Input extends React.Component { const { autoComplete, autoFocus, + children: childrenProp, classes, className: classNameProp, defaultValue, @@ -461,6 +474,7 @@ class Input extends React.Component { let disabled = disabledProp; let error = errorProp; let margin = marginProp; + let hasInputAction = false; if (muiFormControl) { if (typeof disabled === 'undefined') { @@ -474,6 +488,18 @@ class Input extends React.Component { if (typeof margin === 'undefined') { margin = muiFormControl.margin; } + + hasInputAction = muiFormControl.hasInputAction; + } + + let beforeAdornments; + let afterAdornments; + let hasAdornments = false; + if (childrenProp) { + const children = React.Children.toArray(childrenProp); + beforeAdornments = children.filter(child => child.props.position === 'before'); + afterAdornments = children.filter(child => child.props.position === 'after'); + hasAdornments = true; } const className = classNames( @@ -485,8 +511,10 @@ class Input extends React.Component { [classes.focused]: this.state.focused, [classes.formControl]: muiFormControl, [classes.inkbar]: !disableUnderline, + [classes.inputAction]: hasInputAction, [classes.multiline]: multiline, [classes.underline]: !disableUnderline, + [classes.adorned]: hasAdornments, }, classNameProp, ); @@ -537,6 +565,7 @@ class Input extends React.Component { return (
+ {beforeAdornments} { rows={rows} {...inputProps} /> + {afterAdornments}
); } @@ -565,4 +595,21 @@ Input.contextTypes = { muiFormControl: PropTypes.object, }; -export default withStyles(styles, { name: 'MuiInput' })(Input); +let InputWrapper = Input; +if (process.env.NODE_ENV !== 'production') { + InputWrapper = props => ; + InputWrapper.PropTypes = { + children: (props, propName, componentName) => { + const prop = props[propName]; + const children = React.Children.toArray(prop); + + if (!children.every(child => isMuiElement(child, ['InputAdornment']))) { + return new Error(`${componentName} can only accept children of type \`InputAdornment\`.`); + } + + return null; + }, + }; +} + +export default withStyles(styles, { name: 'MuiInput' })(InputWrapper); diff --git a/src/Input/InputAction.d.ts b/src/Input/InputAction.d.ts new file mode 100644 index 00000000000000..18779011972f68 --- /dev/null +++ b/src/Input/InputAction.d.ts @@ -0,0 +1,7 @@ +import { StyledComponent } from ".."; + +export interface InputActionProps { + component?: React.ReactType; +} + +export default class InputAction extends StyledComponent {} diff --git a/src/Input/InputAction.js b/src/Input/InputAction.js new file mode 100644 index 00000000000000..d4c89296f4513e --- /dev/null +++ b/src/Input/InputAction.js @@ -0,0 +1,53 @@ +// @flow weak + +import React from 'react'; +import type { Node, ElementType } from 'react'; +import classNames from 'classnames'; +import withStyles from '../styles/withStyles'; + +export const styles = (theme: Object) => ({ + root: { + position: 'absolute', + right: -theme.spacing.unit * 2, + top: theme.spacing.unit, + }, +}); + +type Default = { + classes: Object, +}; + +export type Props = { + /** + * The content of the component, normally an `IconButton`. + */ + children?: Node, + /** + * Useful to extend the style applied to components. + */ + classes?: Object, + /** + * @ignore + */ + className?: string, + /** + * + */ + component?: ElementType, +}; + +function InputAction(props: Default & Props) { + const { children, component, classes, className, ...other } = props; + + const Component = component || 'div'; + + return ( + + {children} + + ); +} + +InputAction.muiName = 'InputAction'; + +export default withStyles(styles, { name: 'MuiInputAction' })(InputAction); diff --git a/src/Input/InputAction.spec.js b/src/Input/InputAction.spec.js new file mode 100644 index 00000000000000..7c2aa245285a34 --- /dev/null +++ b/src/Input/InputAction.spec.js @@ -0,0 +1,38 @@ +// @flow + +import React from 'react'; +import { assert } from 'chai'; +import { createShallow, getClasses } from '../test-utils'; +import InputAction from './InputAction'; + +describe('', () => { + let shallow; + let classes; + + before(() => { + shallow = createShallow({ untilSelector: InputAction }); + classes = getClasses(); + }); + + it('should render a div', () => { + const wrapper = shallow(); + assert.strictEqual(wrapper.name(), 'div'); + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); + + it('should render with the user and root classes', () => { + const wrapper = shallow(); + assert.strictEqual(wrapper.hasClass('woofInputAction'), true); + assert.strictEqual(wrapper.hasClass(classes.root), true); + }); + + it('should render with the user and root classes', () => { + const wrapper = shallow(); + assert.strictEqual(wrapper.prop('other'), 'woofInputAction'); + }); + + it('should render Chidren', () => { + const wrapper = shallow(Foo); + assert.strictEqual(wrapper.childAt(0).node, 'Foo'); + }); +}); diff --git a/src/Input/InputAdornment.js b/src/Input/InputAdornment.js new file mode 100644 index 00000000000000..00d158d6525a59 --- /dev/null +++ b/src/Input/InputAdornment.js @@ -0,0 +1,56 @@ +// @flow weak + +import React from 'react'; +import type { Node, ElementType } from 'react'; +import classNames from 'classnames'; +import withStyles from '../styles/withStyles'; + +export const styles = () => ({ + root: {}, +}); + +type Default = { + classes: Object, + component: ElementType, +}; + +export type Props = { + /** + * The content of the component, normally an `IconButton`. + */ + children?: Node, + /** + * Useful to extend the style applied to components. + */ + classes?: Object, + /** + * @ignore + */ + className?: string, + /** + * The component used for the root node. + * Either a string to use a DOM element or a component. + */ + component?: ElementType, + /** + * The position this adornment should appear relative to the `Input`. + */ + position: 'before' | 'after', +}; + +function InputAdornment(props: Default & Props) { + const { children, component: Component, classes, className, position, ...other } = props; + + return ( + + {children} + + ); +} + +InputAdornment.muiName = 'InputAdornment'; +InputAdornment.defaultProps = { + component: 'div', +}; + +export default withStyles(styles, { name: 'MuiInputAction' })(InputAdornment); diff --git a/src/Input/index.js b/src/Input/index.js index 57cf653aedcc62..d3a012a6df3311 100644 --- a/src/Input/index.js +++ b/src/Input/index.js @@ -1,4 +1,6 @@ // @flow export { default } from './Input'; +export { default as InputAction } from './InputAction'; +export { default as InputAdornment } from './InputAdornment'; export { default as InputLabel } from './InputLabel';