MultiSelect box popover keeps jumping when scroll or select items
Codepen https://codesandbox.io/s/material-demo-e5j8h
import React from \"react\";
i
First, thank you Ryan Cogswell, for great explanation on why this is happening and also how to solve it. I was trying to solve the issue of Select jumping during multiple selection, and was able to implement fix thanks to your answer. One thing I wanted to add was for other developers using typescript like myself, if you implement the above solution directly you will run into
" Type '{ PaperProps: { style: { float: string; minWidth: number; display: string; flexWrap: string; flexDirection: string; }; }; variant: string; getContentAnchorEl: null; }' is not assignable to type 'Partial'. Types of property 'variant' are incompatible. Type 'string' is not assignable to type '"menu" | "selectedMenu" | undefined'. TS2322 "
if you are having this type compatibility issue, declaring the MenuProps directly like below will fix it.
<Select
labelId="demo-mutiple-checkbox-label"
id="demo-mutiple-checkbox"
multiple
value={personName}
onChange={handleChange}
input={<Input />}
renderValue={selected => selected.join(", ")}
MenuProps={{
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
},
variant: "menu",
getContentAnchorEl: null
}}>
This, worked for my project, but please let me know if there are better solutions to this type compatibility issue.
The cause of the jumping is related to the "content anchor" functionality of the Menu
.
From https://material-ui.com/components/menus/#selected-menus (emphasis mine):
If used for item selection, when opened, simple menus attempt to vertically align the currently selected menu item with the anchor element, and the initial focus will be placed on the selected menu item. The currently selected menu item is set using the
selected
property (from ListItem). To use a selected menu item without impacting the initial focus or the vertical positioning of the menu, set thevariant
property tomenu
.
You can also find a similar note in the documentation of the variant prop.
The other relevant portion of the documentation is the description of the getContentAnchorEl prop of Popover:
This function is called in order to retrieve the content anchor element. It's the opposite of the
anchorEl
prop. The content anchor element should be an element inside the popover. It's used to correctly scroll and set the position of the popover. The positioning strategy tries to make the content anchor element just above the anchor element.
The default behavior of the Select
element is to use the Select
input element (the part that shows the selected item(s) when closed) as the "anchor element" and the last selected menu item as the "content anchor element". This means that when the Popover
is open, it tries to align the last selected menu item (within the Popover
) with the Select
input element (behind the Popover
).
In the case of using the multiple
property on the Select
, you have the potential to change the last selected item while the Popover
is open (for single select, it would typically close immediately after selecting something). Additionally, since not all the menu items fit at once, the last selected item is sometimes scrolled out of view which forces the Popover
to use slightly different logic for the vertical alignment.
The net effect of all this is the weird jumping demonstrated in your sandbox. You can fix this by forcing Popover
to use a contentAnchorOffset of zero by specifying getContentAnchorEl: null
as follows:
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
},
getContentAnchorEl: null
};
You may also want to add variant: "menu"
to get rid of some auto focus behavior which will cause it to automatically scroll to the last selected item. This is nice behavior for single select, but less useful and somewhat disorienting in a multi-select.
Setting variant: "menu"
is not sufficient (without getContentAnchorEl: null
) to get rid of the jumping. This would cause it to always use the first menu item as the content anchor which would result in less jumping, but it would still do some jumping due to the first menu item sometimes being scrolled out of view when changing selections.
Below is the full code for the modified version of your sandbox that no longer has any weird jumping (the only changes are to the MenuProps
):
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import ListItemText from "@material-ui/core/ListItemText";
import Select from "@material-ui/core/Select";
import Checkbox from "@material-ui/core/Checkbox";
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
maxWidth: 300
},
chips: {
display: "flex",
flexWrap: "wrap"
},
chip: {
margin: 2
},
noLabel: {
marginTop: theme.spacing(3)
}
}));
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
},
variant: "menu",
getContentAnchorEl: null
};
const names = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
export default function MultipleSelect() {
const classes = useStyles();
const [personName, setPersonName] = React.useState([]);
const handleChange = event => {
setPersonName(event.target.value);
};
return (
<div>
long text <br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
long text
<br />
<FormControl className={classes.formControl}>
<InputLabel id="demo-mutiple-checkbox-label">Tag</InputLabel>
<Select
labelId="demo-mutiple-checkbox-label"
id="demo-mutiple-checkbox"
multiple
value={personName}
onChange={handleChange}
input={<Input />}
renderValue={selected => selected.join(", ")}
MenuProps={MenuProps}
>
{names.map(name => (
<MenuItem key={name} value={name}>
<Checkbox checked={personName.indexOf(name) > -1} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
}