From 4272f2534292d3ddccd5b5c92f5596c3410c019e Mon Sep 17 00:00:00 2001 From: James Moore Date: Sun, 24 Sep 2017 20:29:19 +0100 Subject: [PATCH 1/3] [InputAction] Initial implementation of a `InputAction` component --- .../demos/text-fields/ComposedTextField.js | 14 ++++- src/Form/FormControl.js | 6 ++- src/Input/Input.js | 7 +++ src/Input/InputAction.d.ts | 7 +++ src/Input/InputAction.js | 53 +++++++++++++++++++ src/Input/InputAction.spec.js | 38 +++++++++++++ src/Input/index.js | 1 + 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/Input/InputAction.d.ts create mode 100644 src/Input/InputAction.js create mode 100644 src/Input/InputAction.spec.js diff --git a/docs/src/pages/demos/text-fields/ComposedTextField.js b/docs/src/pages/demos/text-fields/ComposedTextField.js index 863d3188a7e5f9..727edc955e521c 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, InputAction } 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,16 @@ 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..34a16313389013 100644 --- a/src/Input/Input.js +++ b/src/Input/Input.js @@ -183,6 +183,9 @@ export const styles = (theme: Object) => { fullWidth: { width: '100%', }, + inputAction: { + paddingRight: theme.spacing.unit * 3, + }, }; }; @@ -461,6 +464,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 +478,8 @@ class Input extends React.Component { if (typeof margin === 'undefined') { margin = muiFormControl.margin; } + + hasInputAction = muiFormControl.hasInputAction; } const className = classNames( @@ -485,6 +491,7 @@ 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, }, 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/index.js b/src/Input/index.js index 57cf653aedcc62..e75de2a68ff84e 100644 --- a/src/Input/index.js +++ b/src/Input/index.js @@ -1,4 +1,5 @@ // @flow export { default } from './Input'; +export { default as InputAction } from './InputAction'; export { default as InputLabel } from './InputLabel'; From a822ae3902bdfdf2920b404181197b30effc7339 Mon Sep 17 00:00:00 2001 From: James Moore Date: Mon, 25 Sep 2017 08:26:18 +0100 Subject: [PATCH 2/3] Spiking InputAdornment --- .../demos/text-fields/ComposedTextField.js | 16 +++--- src/Input/Input.js | 44 ++++++++++++++- src/Input/InputAdornment.js | 56 +++++++++++++++++++ src/Input/index.js | 1 + 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/Input/InputAdornment.js diff --git a/docs/src/pages/demos/text-fields/ComposedTextField.js b/docs/src/pages/demos/text-fields/ComposedTextField.js index 727edc955e521c..193d9b55083eb8 100644 --- a/docs/src/pages/demos/text-fields/ComposedTextField.js +++ b/docs/src/pages/demos/text-fields/ComposedTextField.js @@ -4,7 +4,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { withStyles } from 'material-ui/styles'; import IconButton from 'material-ui/IconButton'; -import Input, { InputLabel, InputAction } from 'material-ui/Input'; +import Input, { InputLabel, InputAdornment } from 'material-ui/Input'; import { FormControl, FormHelperText } from 'material-ui/Form'; import DeleteIcon from 'material-ui-icons/Delete'; @@ -53,12 +53,14 @@ class ComposedTextField extends React.Component { Name - - - - - - + + $ + + + + + + Input as an action diff --git a/src/Input/Input.js b/src/Input/Input.js index 34a16313389013..90ba0d9ebc6685 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, @@ -209,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. */ @@ -429,6 +438,7 @@ class Input extends React.Component { const { autoComplete, autoFocus, + children: childrenProp, classes, className: classNameProp, defaultValue, @@ -482,6 +492,16 @@ class Input extends React.Component { 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( classes.root, { @@ -494,6 +514,7 @@ class Input extends React.Component { [classes.inputAction]: hasInputAction, [classes.multiline]: multiline, [classes.underline]: !disableUnderline, + [classes.adorned]: hasAdornments, }, classNameProp, ); @@ -544,6 +565,7 @@ class Input extends React.Component { return (
+ {beforeAdornments} { rows={rows} {...inputProps} /> + {afterAdornments}
); } @@ -572,4 +595,19 @@ 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\`.`); + } + }, + }; +} + +export default withStyles(styles, { name: 'MuiInput' })(InputWrapper); diff --git a/src/Input/InputAdornment.js b/src/Input/InputAdornment.js new file mode 100644 index 00000000000000..c5128e6d8a2a8b --- /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 = (theme: Object) => ({ + 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 e75de2a68ff84e..d3a012a6df3311 100644 --- a/src/Input/index.js +++ b/src/Input/index.js @@ -2,4 +2,5 @@ export { default } from './Input'; export { default as InputAction } from './InputAction'; +export { default as InputAdornment } from './InputAdornment'; export { default as InputLabel } from './InputLabel'; From a7c1268cd41f866244b3b79c52b13688def9a16d Mon Sep 17 00:00:00 2001 From: James Moore Date: Mon, 25 Sep 2017 08:35:40 +0100 Subject: [PATCH 3/3] Fix linter errors --- src/Input/Input.js | 2 ++ src/Input/InputAdornment.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Input/Input.js b/src/Input/Input.js index 90ba0d9ebc6685..ea2cff94a340f0 100644 --- a/src/Input/Input.js +++ b/src/Input/Input.js @@ -606,6 +606,8 @@ if (process.env.NODE_ENV !== 'production') { if (!children.every(child => isMuiElement(child, ['InputAdornment']))) { return new Error(`${componentName} can only accept children of type \`InputAdornment\`.`); } + + return null; }, }; } diff --git a/src/Input/InputAdornment.js b/src/Input/InputAdornment.js index c5128e6d8a2a8b..00d158d6525a59 100644 --- a/src/Input/InputAdornment.js +++ b/src/Input/InputAdornment.js @@ -5,7 +5,7 @@ import type { Node, ElementType } from 'react'; import classNames from 'classnames'; import withStyles from '../styles/withStyles'; -export const styles = (theme: Object) => ({ +export const styles = () => ({ root: {}, });