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

[Select] Migrate NativeSelect to emotion #24698

Merged
merged 15 commits into from
Apr 8, 2021
3 changes: 2 additions & 1 deletion docs/pages/api-docs/native-select.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"input": { "type": { "name": "element" }, "default": "<Input />" },
"inputProps": { "type": { "name": "object" } },
"onChange": { "type": { "name": "func" } },
"sx": { "type": { "name": "object" } },
"value": { "type": { "name": "any" } },
"variant": {
"type": {
Expand Down Expand Up @@ -37,6 +38,6 @@
"filename": "/packages/material-ui/src/NativeSelect/NativeSelect.js",
"inheritance": { "component": "Input", "pathname": "/api/input/" },
"demos": "<ul><li><a href=\"/components/selects/\">Selects</a></li></ul>",
"styledComponent": false,
"styledComponent": true,
"cssComponent": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"input": "An <code>Input</code> element; does not have to be a material-ui specific <code>Input</code>.",
"inputProps": "&lt;a href=&quot;<a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes&quot;&gt;Attributes&lt;/a&gt;\">https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Attributes&quot;&gt;Attributes&lt;/a&gt;</a> applied to the <code>select</code> element.",
"onChange": "Callback fired when a menu item is selected.<br><br><strong>Signature:</strong><br><code>function(event: object) =&gt; void</code><br><em>event:</em> The event source of the callback. You can pull out the new value by accessing <code>event.target.value</code> (string).",
"sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the <a href=\"/system/basics/#the-sx-prop\">`sx` page</a> for more details.",
"value": "The <code>input</code> value. The DOM API casts this to a string.",
"variant": "The variant to use."
},
Expand Down
7 changes: 6 additions & 1 deletion packages/material-ui/src/NativeSelect/NativeSelect.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import { InternalStandardProps as StandardProps } from '..';
import { SxProps } from '@material-ui/system';
import { InternalStandardProps as StandardProps, Theme } from '..';
import { InputProps } from '../Input';
import { NativeSelectInputProps } from './NativeSelectInput';

Expand Down Expand Up @@ -58,6 +59,10 @@ export interface NativeSelectProps
* You can pull out the new value by accessing `event.target.value` (string).
*/
onChange?: NativeSelectInputProps['onChange'];
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The `input` value. The DOM API casts this to a string.
*/
Expand Down
13 changes: 9 additions & 4 deletions packages/material-ui/src/NativeSelect/NativeSelect.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import NativeSelectInput from './NativeSelectInput';
import withStyles from '../styles/withStyles';
import formControlState from '../FormControl/formControlState';
import useFormControl from '../FormControl/useFormControl';
import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown';
import Input from '../Input';
import useThemeProps from '../styles/useThemeProps';

export const styles = (theme) => ({
/* Styles applied to the select component `root` class. */
Expand Down Expand Up @@ -62,7 +62,7 @@ export const styles = (theme) => ({
},
/* Styles applied to the select component `selectMenu` class. */
selectMenu: {
height: 'auto', // Resets for multiple select with chips
height: 'auto', // Resets for multipile select with chips
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
height: 'auto', // Resets for multipile select with chips
height: 'auto', // Resets for multiple select with chips

minHeight: '1.4375em', // Required for select\text-field height consistency
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
Expand Down Expand Up @@ -111,7 +111,8 @@ const defaultInput = <Input />;
/**
* An alternative to `<Select native />` with a much smaller bundle size footprint.
*/
const NativeSelect = React.forwardRef(function NativeSelect(props, ref) {
const NativeSelect = React.forwardRef(function NativeSelect(inProps, ref) {
const props = useThemeProps({ name: 'MuiNativeSelect', props: inProps });
const {
children,
classes,
Expand Down Expand Up @@ -182,6 +183,10 @@ NativeSelect.propTypes /* remove-proptypes */ = {
* You can pull out the new value by accessing `event.target.value` (string).
*/
onChange: PropTypes.func,
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.object,
/**
* The `input` value. The DOM API casts this to a string.
*/
Expand All @@ -194,4 +199,4 @@ NativeSelect.propTypes /* remove-proptypes */ = {

NativeSelect.muiName = 'Select';

export default withStyles(styles, { name: 'MuiNativeSelect' })(NativeSelect);
export default NativeSelect;
15 changes: 6 additions & 9 deletions packages/material-ui/src/NativeSelect/NativeSelect.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as React from 'react';
import { expect } from 'chai';
import { getClasses, createMount, createClientRender, describeConformance } from 'test/utils';
import { createMount, createClientRender, describeConformanceV5 } from 'test/utils';
import Input, { inputClasses } from '../Input';
import NativeSelect from './NativeSelect';
import classes from './nativeSelectClasses';

describe('<NativeSelect />', () => {
let classes;

const mount = createMount();
const render = createClientRender();
const defaultProps = {
Expand All @@ -21,16 +20,14 @@ describe('<NativeSelect />', () => {
],
};

before(() => {
classes = getClasses(<NativeSelect {...defaultProps} />);
});

describeConformance(<NativeSelect {...defaultProps} />, () => ({
describeConformanceV5(<NativeSelect {...defaultProps} />, () => ({
classes,
inheritComponent: Input,
mount,
render,
refInstanceof: window.HTMLDivElement,
skip: ['componentProp', 'rootClass'],
muiName: 'MuiNativeSelect',
skip: ['componentProp', 'componentsProp', 'rootClass', 'themeVariants', 'themeStyleOverrides'],
}));

it('should render a native select', () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/material-ui/src/NativeSelect/NativeSelectInput.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as React from 'react';
import { SxProps } from '@material-ui/system';
import { Theme } from '..';

export interface NativeSelectInputProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
disabled?: boolean;
IconComponent?: React.ElementType;
inputRef?: React.Ref<HTMLSelectElement>;
variant?: 'standard' | 'outlined' | 'filled';
sx?: SxProps<Theme>;
}

declare const NativeSelectInput: React.ComponentType<NativeSelectInputProps>;
Expand Down
150 changes: 125 additions & 25 deletions packages/material-ui/src/NativeSelect/NativeSelectInput.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,145 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { refType } from '@material-ui/utils';
import { refType, deepmerge } from '@material-ui/utils';
import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled';
import capitalize from '../utils/capitalize';
import nativeSelectClasses, { getNativeSelectUtilitiyClasses } from './nativeSelectClasses';
import experimentalStyled from '../styles/experimentalStyled';

export const overridesResolver = (props, styles) => {
const { styleProps } = props;
return deepmerge(styles.root, {
...styles.select,
...styles[styleProps.variant],
[`& .${nativeSelectClasses.icon}`]: {
...styles.icon,
...(styleProps.variant && styles[`icon${capitalize(styleProps.variant)}`]),
...(styleProps.open && styles.iconOpen),
},
});
};

const useUtilityClasses = (styleProps) => {
const { classes, variant, disabled, open } = styleProps;

const slots = {
root: ['root', 'select', variant, disabled && 'disabled'],
icon: ['icon', `icon${capitalize(variant)}`, open && 'iconOpen', disabled && 'disabled'],
};

return composeClasses(slots, getNativeSelectUtilitiyClasses, classes);
};

const SelectRoot = experimentalStyled(
'select',
{},
{ name: 'MuiNativeSelect', slot: 'Root', overridesResolver },
)(({ styleProps, theme }) => ({
MozAppearance: 'none', // Reset
WebkitAppearance: 'none', // Reset
// When interacting quickly, the text can end up selected.
// Native select can't be selected either.
userSelect: 'none',
borderRadius: 0, // Reset
minWidth: 16, // So it doesn't collapse.
cursor: 'pointer',
'&:focus': {
// Show that it's not an text input
backgroundColor:
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.05)' : 'rgba(255, 255, 255, 0.05)',
borderRadius: 0, // Reset Chrome style
},
// Remove IE11 arrow
'&::-ms-expand': {
display: 'none',
},
'&.Mui-disabled': {
cursor: 'default',
},
'&[multiple]': {
height: 'auto',
},
'&:not([multiple]) option, &:not([multiple]) optgroup': {
backgroundColor: theme.palette.background.paper,
},
// Bump specificity to allow extending custom inputs
'&&': {
paddingRight: 24,
},
...(styleProps.variant === 'filled' && {
'&&': {
paddingRight: 32,
},
}),
...(styleProps.variant === 'outlined' && {
borderRadius: theme.shape.borderRadius,
'&:focus': {
borderRadius: theme.shape.borderRadius, // Reset the reset for Chrome style
},
'&&': {
paddingRight: 32,
},
}),
...(styleProps.selectMenu && {
height: 'auto', // Resets for multpile select with chips
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
height: 'auto', // Resets for multpile select with chips
height: 'auto', // Resets for multiple select with chips

minHeight: '1.4375em', // Required for select\text-field height consistency
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}),
}));

const IconRoot = experimentalStyled(
'svg',
{},
{ name: 'MuiNativeSelect', slot: 'Icon' },
)(({ styleProps, theme }) => ({
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input and to support wrapping tags..
position: 'absolute',
right: 0,
top: 'calc(50% - 12px)', // Center vertically
pointerEvents: 'none', // Don't block pointer events on the select under the icon.
color: theme.palette.action.active,
'&.Mui-disabled': {
color: theme.palette.action.disabled,
},
...(styleProps.open && {
right: 7,
}),
...(styleProps.variant === 'filled' && {
right: 7,
}),
...(styleProps.variant === 'outlined' && {
right: 7,
}),
}));

/**
* @ignore - internal component.
*/
const NativeSelectInput = React.forwardRef(function NativeSelectInput(props, ref) {
const {
classes,
className,
const { className, disabled, IconComponent, inputRef, variant = 'standard', ...other } = props;

const styleProps = {
...props,
disabled,
IconComponent,
inputRef,
variant = 'standard',
...other
} = props;
variant,
};

const classes = useUtilityClasses(styleProps);
return (
<React.Fragment>
<select
className={clsx(
classes.root, // TODO v5: merge root and select
classes.select,
classes[variant],
{
[classes.disabled]: disabled,
},
className,
)}
<SelectRoot
styleProps={styleProps}
className={clsx(classes.root, className)}
disabled={disabled}
ref={inputRef || ref}
{...other}
/>
{props.multiple ? null : (
<IconComponent
className={clsx(classes.icon, classes[`icon${capitalize(variant)}`], {
[classes.disabled]: disabled,
})}
/>
<IconRoot as={IconComponent} styleProps={styleProps} className={classes.icon} />
)}
</React.Fragment>
);
Expand All @@ -55,7 +155,7 @@ NativeSelectInput.propTypes = {
* Override or extend the styles applied to the component.
* See [CSS API](#css) below for more details.
*/
classes: PropTypes.object.isRequired,
classes: PropTypes.object,
/**
* The CSS class name of the select element.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createMount, describeConformance, createClientRender } from 'test/utils';
import { createMount, describeConformanceV5, createClientRender } from 'test/utils';
import NativeSelectInput from './NativeSelectInput';

describe('<NativeSelectInput />', () => {
Expand All @@ -25,10 +25,11 @@ describe('<NativeSelectInput />', () => {
],
};

describeConformance(<NativeSelectInput {...defaultProps} onChange={() => {}} />, () => ({
describeConformanceV5(<NativeSelectInput {...defaultProps} onChange={() => {}} />, () => ({
mount,
only: ['refForwarding'],
refInstanceof: window.HTMLSelectElement,
muiName: 'MuiNativeSelectInput',
}));

it('should render a native select', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/material-ui/src/NativeSelect/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default } from './NativeSelect';
export * from './NativeSelect';
export { default as nativeSelectClasses } from './nativeSelectClasses';
Copy link
Member

Choose a reason for hiding this comment

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

Convention with the other files

Suggested change
export { default as nativeSelectClasses } from './nativeSelectClasses';
export { default as nativeSelectClasses } from './nativeSelectClasses';

export * from './nativeSelectClasses';
2 changes: 2 additions & 0 deletions packages/material-ui/src/NativeSelect/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { default } from './NativeSelect';
export { default as nativeSelectClasses } from './nativeSelectClasses';
Copy link
Member

Choose a reason for hiding this comment

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

Convention with the other files

Suggested change
export { default as nativeSelectClasses } from './nativeSelectClasses';
export { default as nativeSelectClasses } from './nativeSelectClasses';

export * from './nativeSelectClasses';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NativeSelectClassKey } from './NativeSelect';

declare const nativeSelectClasses: Record<NativeSelectClassKey, string>;

export function getNativeSelectUtilityClasses(slot: string): string;

export default nativeSelectClasses;
21 changes: 21 additions & 0 deletions packages/material-ui/src/NativeSelect/nativeSelectClasses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled';

export function getNativeSelectUtilitiyClasses(slot) {
return generateUtilityClass('MuiNativeSelect', slot);
}

const nativeSelectClasses = generateUtilityClasses('MuiNativeSelect', [
'root',
'select',
'filled',
'outlined',
'selectMenu',
'disabled',
'icon',
'iconOpen',
'iconFilled',
'iconOutlined',
'nativeInput',
]);

export default nativeSelectClasses;