Skip to content

Commit

Permalink
[NativeSelect] New component
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed May 13, 2018
1 parent f94a2c2 commit a27cc45
Show file tree
Hide file tree
Showing 19 changed files with 525 additions and 306 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import InputLabel from '@material-ui/core/InputLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import NativeSelect from '@material-ui/core/NativeSelect';

const styles = theme => ({
root: {
Expand All @@ -21,7 +22,7 @@ const styles = theme => ({
},
});

class NativeSelect extends React.Component {
class NativeSelects extends React.Component {
state = {
age: '',
name: 'hai',
Expand Down Expand Up @@ -54,8 +55,7 @@ class NativeSelect extends React.Component {
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="age-native-helper">Age</InputLabel>
<Select
native
<NativeSelect
value={this.state.age}
onChange={this.handleChange('age')}
input={<Input id="age-native-helper" />}
Expand All @@ -64,12 +64,11 @@ class NativeSelect extends React.Component {
<option value={10}>Ten</option>
<option value={20}>Twenty</option>
<option value={30}>Thirty</option>
</Select>
</NativeSelect>
<FormHelperText>Some important helper text</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<Select
native
<NativeSelect
value={this.state.age}
onChange={this.handleChange('age')}
className={classes.selectEmpty}
Expand All @@ -78,13 +77,12 @@ class NativeSelect extends React.Component {
<option value={10}>Ten</option>
<option value={20}>Twenty</option>
<option value={30}>Thirty</option>
</Select>
</NativeSelect>
<FormHelperText>Without label</FormHelperText>
</FormControl>
<FormControl className={classes.formControl} disabled>
<InputLabel htmlFor="name-native-disabled">Name</InputLabel>
<Select
native
<NativeSelect
value={this.state.name}
onChange={this.handleChange('name')}
input={<Input id="name-native-disabled" />}
Expand All @@ -97,13 +95,12 @@ class NativeSelect extends React.Component {
<option value="olivier">Olivier</option>
<option value="kevin">Kevin</option>
</optgroup>
</Select>
</NativeSelect>
<FormHelperText>Disabled</FormHelperText>
</FormControl>
<FormControl className={classes.formControl} error>
<InputLabel htmlFor="name-native-error">Name</InputLabel>
<Select
native
<NativeSelect
value={this.state.name}
onChange={this.handleChange('name')}
input={<Input id="name-native-error" />}
Expand All @@ -116,26 +113,26 @@ class NativeSelect extends React.Component {
<option value="olivier">Olivier</option>
<option value="kevin">Kevin</option>
</optgroup>
</Select>
</NativeSelect>
<FormHelperText>Error</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="uncontrolled-native">Name</InputLabel>
<Select native defaultValue={30} input={<Input id="uncontrolled-native" />}>
<NativeSelect defaultValue={30} input={<Input id="uncontrolled-native" />}>
<option value="" />
<option value={10}>Ten</option>
<option value={20}>Twenty</option>
<option value={30}>Thirty</option>
</Select>
</NativeSelect>
<FormHelperText>Uncontrolled</FormHelperText>
</FormControl>
</div>
);
}
}

NativeSelect.propTypes = {
NativeSelects.propTypes = {
classes: PropTypes.object.isRequired,
};

export default withStyles(styles)(NativeSelect);
export default withStyles(styles)(NativeSelects);
2 changes: 1 addition & 1 deletion docs/src/pages/demos/selects/selects.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Menus are positioned over their emitting elements such that the currently select
As the user experience can be improved on mobile using the native select of the platform,
we allow such pattern.

{{"demo": "pages/demos/selects/NativeSelect.js"}}
{{"demo": "pages/demos/selects/NativeSelects.js"}}

## Multiple Select

Expand Down
4 changes: 2 additions & 2 deletions packages/material-ui/src/FormControl/FormControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ class FormControl extends React.Component {
const { children } = this.props;
if (children) {
React.Children.forEach(children, child => {
if (!isMuiElement(child, ['Input', 'Select'])) {
if (!isMuiElement(child, ['Input', 'Select', 'NativeSelect'])) {
return;
}

if (isFilled(child.props, true)) {
this.state.filled = true;
}

const input = isMuiElement(child, ['Select']) ? child.props.input : child;
const input = isMuiElement(child, ['Select', 'NativeSelect']) ? child.props.input : child;

if (input && isAdornedStart(input.props)) {
this.state.adornedStart = true;
Expand Down
19 changes: 19 additions & 0 deletions packages/material-ui/src/NativeSelect/NativeSelect.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { StandardProps } from '..';
import { InputProps } from '../Input';
import { MenuProps } from '../Menu';
import { NativeSelectInputProps } from './NativeSelectInput';

export interface NativeSelectProps
extends StandardProps<InputProps, NativeSelectClassKey, 'value' | 'onChange'>,
Pick<NativeSelectInputProps, 'onChange'> {
IconComponent?: React.ReactType;
input?: React.ReactNode;
value?: string | number;
}

export type NativeSelectClassKey = 'root' | 'select' | 'selectMenu' | 'disabled' | 'icon';

declare const NativeSelect: React.ComponentType<NativeSelectProps>;

export default NativeSelect;
129 changes: 129 additions & 0 deletions packages/material-ui/src/NativeSelect/NativeSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// @inheritedComponent Input

import React from 'react';
import PropTypes from 'prop-types';
import NativeSelectInput from './NativeSelectInput';
import withStyles from '../styles/withStyles';
import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown';
import Input from '../Input';

export const styles = theme => ({
root: {
position: 'relative',
width: '100%',
},
select: {
'-moz-appearance': 'none', // Reset
'-webkit-appearance': 'none', // Reset
// When interacting quickly, the text can end up selected.
// Native select can't be selected either.
userSelect: 'none',
paddingRight: theme.spacing.unit * 4,
width: `calc(100% - ${theme.spacing.unit * 4}px)`,
minWidth: theme.spacing.unit * 2, // So it doesn't collapse.
cursor: 'pointer',
'&:focus': {
// Show that it's not an text input
background:
theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.05)' : 'rgba(255, 255, 255, 0.05)',
borderRadius: 0, // Reset Chrome style
},
// Remove Firefox focus border
'&:-moz-focusring': {
color: 'transparent',
textShadow: '0 0 0 #000',
},
// Remove IE11 arrow
'&::-ms-expand': {
display: 'none',
},
'&$disabled': {
cursor: 'default',
},
},
selectMenu: {
width: 'auto', // Fix Safari textOverflow
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
minHeight: '1.1875em', // Reset (19px), match the native input line-height
},
disabled: {},
icon: {
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input.
position: 'absolute',
right: 0,
top: 'calc(50% - 12px)', // Center vertically
color: theme.palette.action.active,
'pointer-events': 'none', // Don't block pointer events on the select under the icon.
},
});

/**
* An alternative to `<Select native />` with a much smaller dependency graph.
*/
function NativeSelect(props) {
const { children, classes, IconComponent, input, inputProps, ...other } = props;

return React.cloneElement(input, {
// Most of the logic is implemented in `NativeSelectInput`.
// The `Select` component is a simple API wrapper to expose something better to play with.
inputComponent: NativeSelectInput,
inputProps: {
children,
classes,
IconComponent,
type: undefined, // We render a select. We can ignore the type provided by the `Input`.
...inputProps,
...(input ? input.props.inputProps : {}),
},
...other,
});
}

NativeSelect.propTypes = {
/**
* The option elements to populate the select with.
* Can be some `<option>` elements.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
* See [CSS API](#css-api) below for more details.
*/
classes: PropTypes.object.isRequired,
/**
* The icon that displays the arrow.
*/
IconComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
* An `Input` element; does not have to be a material-ui specific `Input`.
*/
input: PropTypes.element,
/**
* Properties applied to the `input` element.
* The properties are applied on the `select` element.
*/
inputProps: PropTypes.object,
/**
* Callback function fired when a menu item is selected.
*
* @param {object} event The event source of the callback.
* You can pull out the new value by accessing `event.target.value`.
*/
onChange: PropTypes.func,
/**
* The input value.
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

NativeSelect.defaultProps = {
IconComponent: ArrowDropDownIcon,
input: <Input />,
};

NativeSelect.muiName = 'NativeSelect';

export default withStyles(styles, { name: 'MuiNativeSelect' })(NativeSelect);
16 changes: 16 additions & 0 deletions packages/material-ui/src/NativeSelect/NativeSelectInput.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

export interface NativeSelectInputProps {
disabled?: boolean;
IconComponent?: React.ReactType;
inputRef?: (
ref: HTMLSelectElement | { node: HTMLInputElement; value: NativeSelectInputProps['value'] },
) => void;
name?: string;
onChange?: (event: React.ChangeEvent<HTMLSelectElement>, child: React.ReactNode) => void;
value?: string | number;
}

declare const NativeSelectInput: React.ComponentType<NativeSelectInputProps>;

export default NativeSelectInput;
90 changes: 90 additions & 0 deletions packages/material-ui/src/NativeSelect/NativeSelectInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

/**
* @ignore - internal component.
*/
function NativeSelectInput(props) {
const {
children,
classes,
className,
disabled,
IconComponent,
inputRef,
name,
onChange,
value,
...other
} = props;

return (
<div className={classes.root}>
<select
className={classNames(
classes.select,
{
[classes.disabled]: disabled,
},
className,
)}
name={name}
disabled={disabled}
onChange={onChange}
value={value}
ref={inputRef}
{...other}
>
{children}
</select>
<IconComponent className={classes.icon} />
</div>
);
}

NativeSelectInput.propTypes = {
/**
* The option elements to populate the select with.
* Can be some `<option>` elements.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
* See [CSS API](#css-api) below for more details.
*/
classes: PropTypes.object.isRequired,
/**
* The CSS class name of the select element.
*/
className: PropTypes.string,
/**
* If `true`, the select will be disabled.
*/
disabled: PropTypes.bool,
/**
* The icon that displays the arrow.
*/
IconComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
* Use that property to pass a ref callback to the native select element.
*/
inputRef: PropTypes.func,
/**
* Name attribute of the `select` or hidden `input` element.
*/
name: PropTypes.string,
/**
* Callback function fired when a menu item is selected.
*
* @param {object} event The event source of the callback.
* You can pull out the new value by accessing `event.target.value`.
*/
onChange: PropTypes.func,
/**
* The input value.
*/
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};

export default NativeSelectInput;
Loading

0 comments on commit a27cc45

Please sign in to comment.