Closed raymondsze closed 5 years ago
Here is what i did right now to make a Select
like component.
PS: I didn't extract the style to makeStyle, but rough idea..
Not sure whether it is right way to go.
const InputComponent = ({ className, 'aria-invalid': _, inputRef, ...props }: any) => (
<div tabIndex={0} ref={inputRef} className={className} style={{ height: '1.1875em', cursor: 'pointer' }}>
Option {props.value + 1}
<input {...props} type="hidden" />
<ArrowDownIcon
style={{ position: 'absolute', right: 0, top: 'calc(50% - 12px)' }}
color="action"
/>
</div>
);
const Dropdown = () => (
<Downshift>
{({
getToggleButtonProps,
getMenuProps,
getItemProps,
isOpen,
highlightedIndex,
selectedItem,
closeMenu,
}) => (
<div>
<MuiTextField
variant="filled"
label="Mock Dropdown"
value={selectedItem}
style={{ width: 160, cursor: 'pointer' }}
{...getToggleButtonProps()}
InputLabelProps={{ shrink: true }}
InputProps={{
inputRef,
inputComponent: InputComponent,
}}
/>
<Popper open={isOpen} anchorEl={inputRef.current} style={{ zIndex: 1300 }}>
<Grow in={isOpen}>
<Paper elevation={8} style={{ width: inputRef.current ? inputRef.current.clientWidth : undefined }}>
<List {...getMenuProps({}, { suppressRefError: true })}>
{new Array(2).fill(null).map((_, i) => (
<ListItem
{...getItemProps({ key: i, item: i, index: i })}
style={{ fontWeight: selectedItem === i ? 500 : 300 }}
selected={highlightedIndex === i}
button={true}
>
Option {i + 1}
</ListItem>
))}
</List>
</Paper>
</Grow>
</Popper>
</div>
)}
</Downshift>
);
A few things. Downshift is a render prop library. Downshift is telling you to draw itself.
I would highly recommend you bind onSelect
, onStateChange
, and stateReducer
to grok Downshift internal states. I believe have downshift has around 15 states. You can fine grain control over state change with stateReducer
.
https://github.com/downshift-js/downshift/blob/master/src/stateChangeTypes.js
/*
* All possible downshift states
* 0: "unknown"
* 1: "mouseUp"
* 2: "itemMouseEnter"
* 3: "keyDownArrowUp"
* 4: "keyDownArrowDown"
* 5: "keyDownEscape"
* 6: "keyDownEnter"
* 7: "clickItem"
* 8: "blurInput"
* 9: "changeInput"
* 10: "keyDownSpaceButton"
* 11: "clickButton"
* 12: "blurButton"
* 13: "controlledPropUpdatedSelectedItem"
* 14: "touchStart"
*/
On the other note: I would recommend you warp your drop down in a normal div tag. https://github.com/downshift-js/downshift/issues/443 Material UI breaks downshift expected ref behavior. You cannot select it on IOS.
Actually I would like to ask the ability to make the Select component Menu controllable. Here is the problems: Select ask me to pass the children, the children props is mandatory. But if I would like to use Downshift, the menu is rendered externally, I don't need any children there. Even the open and onOpen props are passed, this behaviour is still cannot be changed.
Here is the codes I finally work out to make it works. It's long but hope you get the idea what I trying to do.
import React, { useContext } from 'react';
import Downshift, { DownshiftProps, ControllerStateAndHelpers } from 'downshift';
import { StyledComponentProps } from '@material-ui/core/styles';
import { createStyles, withStyles, WithStyles } from '@material-ui/styles';
export * from 'downshift';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
const styles = createStyles({ root: { display: 'inline' }, });
export type DownshiftClassKey = keyof WithStyles
interface DownshiftWrapperProps extends Omit<DownshiftProps
export interface DownshiftWrapperInnerProps extends Omit<DownshiftWrapperProps, 'classes'>, WithStyles
const Context = React.createContext<ControllerStateAndHelpers
export const useDownshift = () => useContext(Context);
const stateReducer: DownshiftProps
const DownshiftWrapper = (props: DownshiftWrapperInnerProps) => {
const { id, children, classes, ...rest } = props;
return (
<Downshift
id={id}
labelId={${id}-label
}
inputId={${id}-input
}
menuId={${id}-menu
}
stateReducer={stateReducer}
{...rest}
{(stateAndHelpers) => (
{children} )} ); };
export type DownshiftProps = DownshiftWrapperProps;
export default withStyles(styles)(DownshiftWrapper) as React.ComponentType
2. Clone a SelectInput like component but children is not necessary (Here I added some custom styling to make it like MWC)
import React from 'react'; import clsx from 'clsx'; import { Theme, StyledComponentProps } from '@material-ui/core/styles'; import { createStyles, withStyles, WithStyles } from '@material-ui/styles'; import ArrowDropdown from '../internal/svg-icons/ArrowDropdown';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
const styles = (theme: Theme) => createStyles({
/ Styles applied to the Input
component root
class. /
root: {
position: 'relative',
width: '100%',
outline: 'none',
},
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: 32,
borderRadius: 0, // Reset
height: '1.1875em', // Reset (19px), match the native input line-height
width: 'calc(100% - 32px)',
minWidth: 16, // So it doesn't collapse.
cursor: 'pointer',
'&:focus': {
// Show that it's not an text input
backgroundColor:
theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.05)' : 'rgba(255, 255, 255, 0.05)',
borderRadius: 0, // Reset Chrome style
},
// Remove IE 11 arrow
'&::-ms-expand': {
display: 'none',
},
'&$disabled': {
cursor: 'default',
},
'&$open': {
backgroundColor: 'inherit',
cursor: 'default',
},
},
/ Styles applied to the Input
component if variant="filled"
. /
filled: {
width: 'calc(100% - 44px)',
},
/ Styles applied to the Input
component if variant="outlined"
. /
outlined: {
width: 'calc(100% - 46px)',
borderRadius: theme.shape.borderRadius,
},
/ Styles applied to the Input
component selectMenu
class. /
selectMenu: {
width: 'auto', // Fix Safari textOverflow
height: 'auto', // Reset
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
minHeight: '1.1875em', // Reset (19px), match the native input line-height
},
/ Styles applied to the Input
component disabled
class. /
disabled: {},
icon: {
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
// We use a position absolute over a flexbox in order to forward the pointer events
// to the input.
position: 'absolute',
right: 8,
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.
'&$open': {
color: theme.palette.primary.main,
transform: 'rotate(180deg) translateY(-5px)',
},
},
open: {},
});
export type SelectInputClassKey = keyof WithStyles
export interface SelectInputProps extends Omit<React.InputHTMLAttributes
export interface SelectInputInnerProps extends Omit<SelectInputProps, 'classes'>, WithStyles
const SelectInput = React.forwardRef<HTMLDivElement, SelectInputInnerProps>(
(function SelectInput(props: SelectInputInnerProps, ref: React.Ref
const buttonProps = {
className: clsx(
classes.select,
classes.selectMenu,
{
[classes.disabled]: disabled,
[classes.filled]: variant === 'filled',
[classes.outlined]: variant === 'outlined',
[classes.open]: open,
},
className,
),
onFocus,
onBlur,
onKeyDown,
onKeyUp,
disabled,
...SelectDisplayProps,
} as any;
return (
<div ref={ref} className={classes.root}>
<div
id={name ? `select-${name}` : undefined}
tabIndex={0}
ref={inputRef}
{...buttonProps}
>
{renderValue(value)}
<input
name={name}
value={Array.isArray(value) ? value.join(',') : value}
type="hidden"
{...rest as any}
/>
<ArrowDropdown
className={clsx(classes.icon, { [classes.open]: open })}
/>
</div>
</div>
)
}) as React.SFC
export default withStyles(styles)(SelectInput) as React.ComponentType
3. Write a TextFIeld that integrated with downshift hook, and make the inputComponent to SelectInput
import React from 'react'; import { SelectProps } from '@material-ui/core/Select'; import TextField, { TextFieldClassKey, StandardTextFieldProps, FilledTextFieldProps, OutlinedTextFieldProps, } from '@material-ui/core/TextField'; import { useDownshift } from '../Downshift'; import SelectInput from './SelectInput';
export type SelectFieldClassKey = TextFieldClassKey;
interface SelectBaseProps {
renderValue: SelectProps['renderValue'];
SelectDisplayProps?: React.HTMLAttributes
export interface StandardSelectFieldProps extends StandardTextFieldProps, SelectBaseProps {}; export interface OutlinedSelectFieldProps extends OutlinedTextFieldProps, SelectBaseProps {}; export interface FilledSelectFieldProps extends FilledTextFieldProps, SelectBaseProps {};
export type SelectFieldProps = StandardSelectFieldProps | OutlinedSelectFieldProps | FilledSelectFieldProps;
const SelectField: React.SFC
const open = (typeof openProp !== 'undefined') ? openProp : (downShift && downShift.isOpen); const value = (typeof valueProp !== 'undefined') ? valueProp : (downShift && downShift.selectedItem); const getToggleButtonProps = (downShift && downShift.getToggleButtonProps);
const handleKeyDown = (event: React.KeyboardEvent) => { (event.key === 'Enter') && downShift && downShift.toggleMenu(); }
return ( <TextField value={value} variant={variant as any} {...getToggleButtonProps && getToggleButtonProps({ onKeyDown: handleKeyDown, })} {...rest as any} InputProps={{ inputComponent: SelectInput, ...rest.InputProps, inputProps: { open, variant, renderValue, SelectDisplayProps, ...rest.InputProps && rest.InputProps.inputProps, }, }} /> ); };
export default SelectField;
4. Write a Select Popper that integrated with downshift hook
import React from 'react'; import Popper, { PopperProps } from '@material-ui/core/Popper'; import { PopoverProps } from '@material-ui/core/Popover'; import Paper, { PaperProps } from '@material-ui/core/Paper'; import Grow from '@material-ui/core/Grow'; import { StyledComponentProps } from '@material-ui/core/styles'; import { createStyles, withStyles, WithStyles } from '@material-ui/styles';
import { useDownshift } from '../Downshift';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export const styles = createStyles({ root: { zIndex: 1300, }, });
export type SelectPopperClassKey = keyof WithStyles
export interface SelectPopperProps extends Partial
export interface SelectPopperInnerProps extends Omit<SelectPopperProps, 'classes'>, WithStyles
const SelectPopper: React.SFC
return ( <Popper className={classes.root} open={!!open} anchorEl={anchorEl} {...rest}> <Transition in={open} timeout={transitionDuration} {...TransitionProps}
<Paper elevation={8} style={{ minWidth: anchorEl ? anchorEl.clientWidth : undefined, ...PaperProps && PaperProps.style }} {...getMenuProps && getMenuProps({}, { suppressRefError: true })} {...PaperProps}
{children} ); };
export default withStyles(styles)(SelectPopper) as React.ComponentType
5. Write a Leaf Menu Item
import React, { useEffect } from 'react'; import clsx from 'clsx'; import MenuItem, { MenuItemProps, MenuItemClassKey } from '@material-ui/core/MenuItem'; import { Theme, StyledComponentProps } from '@material-ui/core/styles'; import { createStyles, withStyles, WithStyles } from '@material-ui/styles'; import { useDownshift } from '../Downshift';
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export const styles = (theme: Theme) => createStyles({ root: {}, gutters: {}, selected: {}, });
export type SelectItemClassKey = keyof WithStyles
export interface SelectItemProps extends Omit<MenuItemProps, 'classes'> {
classes?: StyledComponentProps
export interface SelectItemInnerProps extends Omit<SelectItemProps, 'classes'>, WithStyles
const SelectItem: React.SFC
const downShift = useDownshift(); const highlighted = (typeof highlightedProp !== 'undefined') ? highlightedProp : (downShift && (downShift.highlightedIndex === index)); const selected = (typeof selectedProp !== 'undefined') ? selectedProp : (downShift && (downShift.selectedItem === value)); const getItemProps = downShift && downShift.getItemProps;
useEffect(() => { if (downShift && selected) downShift.setHighlightedIndex(index); }, [selected]);
const itemProps = { index, value, item: value, className, ...rest, };
return ( <MenuItem classes={classes} selected={highlighted} {...getItemProps && getItemProps({ index, item: value })} {...itemProps} /> ); }
export default withStyles(styles)(SelectItem) as React.ComponentType
6. Now I can compose my Select Component as I want
import React from 'react'; import List from '@material-ui/core/List'; import RootRef from '@material-ui/core/RootRef'; import SelectField, { SelectFieldClassKey, SelectFieldProps, } from '../SelectField'; import Downshift, { DownshiftProps } from '../Downshift/Downshift'; import SelectPopper, { SelectPopperProps } from '../SelectPopper';
export type DownshiftSelectClassKey = SelectFieldClassKey;
export type DownshiftSelectFieldProps = SelectFieldProps & { value: any; DownshiftProps?: DownshiftProps; SelectPopperProps?: SelectPopperProps; };
const DownshiftSelectField: React.SFC
const { id, value, DownshiftProps, SelectPopperProps, children: childrenProp, ...other } = props; const handleRef = (node: HTMLDivElement) => setPopperNode(node);
return ( <Downshift id={id} initialSelectedItem={value} {...DownshiftProps}
<SelectPopper anchorEl={popperNode} {...SelectPopperProps}>
{childrenProp}
); }
export default DownshiftSelectField;
And what I would like to get rid of is the `SelectInput` component.
I think `Context + Hook` combo would be the trend that I could prevent the problems introduced by RenderProps and Hoc especially the props drilling issue. Just like that current `material-ui` v4 `Radio` did.
Currently the `Select` component main issue is it assuming the children must contain a prop called `value`, and `Menu` also relying on children to make the keyboard event works.
It is not possible to integrate the `Select` component with other library like `Downshift`, `react-virtualized`, etc.
Actually if I use third party library like `Downshift`, the extra behaviour of `Select` component is mean nothing to me (it may be meaningful to others). So what I need is a static component that looks similar to what `Select` component but without any event binding (or optional). The closest solution is to pass a `endAdornment` with value `DropdownIcon` and mark the component prop to `button`.
@raymondsze Have you seen mui-downshift package yet? This is what I use.
@cvanem mui-downshift is good for general usage, but actually what I want is a generic solution that I can apply either the Downshift or Material-UI current MenuItem behaviour (Hook is one of possible solution) to any item I want. For example, modal select or even more complex selections ui component. Downshift is a library could achieve that, but not easy to integrate with MaterialUI "Select" component. Other component are fine, like the AutoComplete example shown in Material UI website.
Is this a proposal for different implementation of Select
, are features lacking or is the integration with downshift
lacking? Did you have a look at https://next.material-ui.com/components/autocomplete/#downshift?
@eps1lon Material UI have example to integrate with Downshift using TextField or equivalent.
However, Select
requires Menu
and MenuItem
to work. As I remember, the children is required for Select
Component. Let's say what if I don't want the Menu, but a popup Dialog with MenuItem inside the Dialog? I would like to preserve the Look & Feel of Select
component (Material Design) but with different behaviour (for example, integrate with Downshift).
You may say inputAdornment could do the Dropdown Icon, but I found that they are totally different and not easy to do the same.
So, I think it should be a proposal for different implementation of Select
(Maybe?).
Let's say what if I don't want the Menu, but a popup Dialog with MenuItem inside the Dialog? I would like to preserve the Look & Feel of Select component (Material Design) but with different behaviour (for example, integrate with Downshift).
That sounds like bad UX. Dialogs are usually very disruptive elements and should be used as rarely as possible. Do you have some mock implementation that I can play with? Right now I don't understand what issue you're trying to solve.
Let's say what if I don't want the Menu, but a popup Dialog with MenuItem inside the Dialog? I would like to preserve the Look & Feel of Select component (Material Design) but with different behaviour (for example, integrate with Downshift).
That sounds like bad UX. Dialogs are usually very disruptive elements and should be used as rarely as possible. Do you have some mock implementation that I can play with? Right now I don't understand what issue you're trying to solve.
Dialog is bad UX for desktop, but reasonable for Mobile device (What I mean here is 'full-screen' Dialog). You could simplify my issue as "How could we integrate with downshift to make a dropdown component with same looks and feel as Select component?".
So this is more of a usage question? Sounds like this is more appropriate for StackOverflow or Spectrum unless you have a conrete proposal for an integration demo. Otherwise I don't think this should be part of the Select component which has a pretty clear definition on the web. Making it work like a fullscreen dialog on mobile is not necessarily part of a select widget.
Nope, from my point of view. The current implementation of Select does not allow me to integrate with Downshift. Full screen dialog is just an example of usecase.
Basically, here is what I need.
I would like to use the Downshift behaviour with Material UI TextField (select=true) or Material UI Select Component. The reason behind is the default Select behaviour does not fit my need. We could integrate downshift with TextField for auto complete. Why we cant do the same with Select? I think its common as well for react-select or other libraries.
I tried to mark the MenuProps open to false without children for the Select component, the error I got was children is required.
Do with another approach, I remove select=true and just use TextField with endAdornment with dropdown icon, but extra css is needed to make it like Material Design. And I feel this is a bit hacky.
I will provide an example when I have time to do it.
I believe we should close for #13863.
@oliviertassinari I agree
Apart from
AutoComplete
,downshift
could be used to implement a dropdown component withgetToggleProps
.I would like to use
downshift
instead of the original implementation of Select (TextField) component.The main purpose I would like to have this feature is to implement
What I need is to keep the ui looks & feel of what Select have.
I have tried to set the
open
andonOpen
ofSelectProps
to make the open controlled by myself. ButSelect
force me to put at least one child to the component.Also, if I click on the
TextField
, theTextField
is highlighted as usual (focused state) but never get blur if I mark theopen
prop asfalse
.Another attempt is I try to implement a component looks similar to
Select
but withoutMenu
and any dropdown related event callback. But I found it is too hard to do except clone the codes from the@material-ui/core
package.Any advices / workaround on doing this? Or should we have a pure
Select
like component without any dropdown behaviour or pass a props toSelect
indicate that we would implement the dropdown ourself?And just a question. What is the design principle of Select and RadioGroup, since these component make use of React.cloneElement heavily and somehow assume the children must be MenuItem or FormControlLabel/Radio. Select is more complicated as it grab the children of MenuItem as default "renderValue", the "renderValue" behaviour could be different if multiple is specified as true due to
.join(',')
. And... we have RadioGroup but we don't have CheckboxGroup. If array value is rare, why we have Select that with "multiple" prop.....Thanks.
Expected Behavior 🤔
Current Behavior 😯
Examples 🌈
Context 🔦