Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[InputAction] Initial implementation of a InputAction component #8365

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/src/pages/demos/text-fields/ComposedTextField.js
Expand Up @@ -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: {
Expand Down Expand Up @@ -49,6 +51,18 @@ class ComposedTextField extends React.Component {
<Input id="name-error" value={this.state.name} onChange={this.handleChange} />
<FormHelperText>Error</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="name-error">Name</InputLabel>
<Input id="name-error" value={this.state.name} onChange={this.handleChange}>
<InputAdornment position="before">$</InputAdornment>
<InputAdornment position="after">
<IconButton>
<DeleteIcon />
</IconButton>
</InputAdornment>
</Input>
<FormHelperText>Input as an action</FormHelperText>
</FormControl>
</div>
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/Form/FormControl.js
Expand Up @@ -125,15 +125,19 @@ class FormControl extends React.Component<DefaultProps & Props, State> {
};

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,
Expand Down
53 changes: 50 additions & 3 deletions 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().
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -183,6 +188,9 @@ export const styles = (theme: Object) => {
fullWidth: {
width: '100%',
},
inputAction: {
paddingRight: theme.spacing.unit * 3,
},
};
};

Expand All @@ -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.
*/
Expand Down Expand Up @@ -426,6 +438,7 @@ class Input extends React.Component<DefaultProps & Props, State> {
const {
autoComplete,
autoFocus,
children: childrenProp,
classes,
className: classNameProp,
defaultValue,
Expand Down Expand Up @@ -461,6 +474,7 @@ class Input extends React.Component<DefaultProps & Props, State> {
let disabled = disabledProp;
let error = errorProp;
let margin = marginProp;
let hasInputAction = false;

if (muiFormControl) {
if (typeof disabled === 'undefined') {
Expand All @@ -474,6 +488,18 @@ class Input extends React.Component<DefaultProps & Props, State> {
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(
Expand All @@ -485,8 +511,10 @@ class Input extends React.Component<DefaultProps & Props, State> {
[classes.focused]: this.state.focused,
[classes.formControl]: muiFormControl,
[classes.inkbar]: !disableUnderline,
[classes.inputAction]: hasInputAction,
[classes.multiline]: multiline,
[classes.underline]: !disableUnderline,
[classes.adorned]: hasAdornments,
},
classNameProp,
);
Expand Down Expand Up @@ -537,6 +565,7 @@ class Input extends React.Component<DefaultProps & Props, State> {

return (
<div onBlur={this.handleBlur} onFocus={this.handleFocus} className={className} {...other}>
{beforeAdornments}
<InputComponent
autoComplete={autoComplete}
autoFocus={autoFocus}
Expand All @@ -556,6 +585,7 @@ class Input extends React.Component<DefaultProps & Props, State> {
rows={rows}
{...inputProps}
/>
{afterAdornments}
</div>
);
}
Expand All @@ -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 => <Input {...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);
7 changes: 7 additions & 0 deletions src/Input/InputAction.d.ts
@@ -0,0 +1,7 @@
import { StyledComponent } from "..";

export interface InputActionProps {
component?: React.ReactType;
}

export default class InputAction extends StyledComponent<InputActionProps> {}
53 changes: 53 additions & 0 deletions 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,
/**
*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing

*/
component?: ElementType,
};

function InputAction(props: Default & Props) {
const { children, component, classes, className, ...other } = props;

const Component = component || 'div';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React provides a mechanism for default, let's use it, it automatically added in the docs.


return (
<Component {...other} className={classNames(classes.root, className)}>
{children}
</Component>
);
}

InputAction.muiName = 'InputAction';

export default withStyles(styles, { name: 'MuiInputAction' })(InputAction);
38 changes: 38 additions & 0 deletions 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('<InputAction />', () => {
let shallow;
let classes;

before(() => {
shallow = createShallow({ untilSelector: InputAction });
classes = getClasses(<InputAction />);
});

it('should render a div', () => {
const wrapper = shallow(<InputAction />);
assert.strictEqual(wrapper.name(), 'div');
assert.strictEqual(wrapper.hasClass(classes.root), true);
});

it('should render with the user and root classes', () => {
const wrapper = shallow(<InputAction className="woofInputAction" />);
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(<InputAction other="woofInputAction" />);
assert.strictEqual(wrapper.prop('other'), 'woofInputAction');
});

it('should render Chidren', () => {
const wrapper = shallow(<InputAction>Foo</InputAction>);
assert.strictEqual(wrapper.childAt(0).node, 'Foo');
});
});
56 changes: 56 additions & 0 deletions 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 (
<Component className={classNames(classes.root, className)} {...other}>
{children}
</Component>
);
}

InputAdornment.muiName = 'InputAdornment';
InputAdornment.defaultProps = {
component: 'div',
};

export default withStyles(styles, { name: 'MuiInputAction' })(InputAdornment);
2 changes: 2 additions & 0 deletions 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';