I am working on a customization of Material UI's Autocomplete. I've customized the Popper component to include a button at the end of the options list, however I need to make this button accessible via the keyboard (via the Tab button, for example).
I have tried adding the tabIndex=0
property to the button, but this doesn't change the default behavior - pressing Tab closed the Autocomplete.
What can I do to make the button reachable via keyboard commands?
Link to sandbox: https://codesandbox.io/s/compassionate-butterfly-f4rn8z?file=/demo.tsx:0-1072
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import Popper from "@mui/material/Popper";
import Button from "@mui/material/Button";
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ label: "The Shawshank Redemption", year: 1994 },
{ label: "The Godfather", year: 1972 },
{ label: "The Godfather: Part II", year: 1974 },
];
export default function ComboBox() {
const handleNotFound = () => {
alert("Not found");
};
return (
<Autocomplete
disablePortal
id="combo-box-demo"
options={top100Films}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Movie" />}
PopperComponent={(props) => (
<Popper {...props} placement="bottom">
{props.children}
<div>
<Button type="button" tabIndex={0} onMouseDown={handleNotFound}>
I don't see my movie
</Button>
</div>
</Popper>
)}
/>
);
}
I’m afraid in terms of ARIA there are not many options.
Authors MUST ensure the popup element associated with a
combobox
has a role oflistbox
…
In other words, the element that is hidden and shown is the listbox
, there is no way to add another container.
List boxes contain children whose role is
option
or elements whose role isgroup
which in turn contain children whose role isoption
.
This forbids any other role (except generic or none) inside the listbox
, so no button
can be placed inside the popup. i.e. listbox.
Authors MUST manage focus on the following container roles: […]
listbox
This practically forbids usage of Tab for navigation inside the popup, so tabindex="0"
is not an option.
So you’d need the popup to have another role than listbox
to break out of this logic. dialog
would be an option, but this requires a lot of rendering and event logic to get right.
I recommend adding an “I don't see my movie” option inside the listbox, and maybe grouping the actual movies in a group
. Or simply a button next to the combobox, to make things easier.
For better orientation, grouping options by found movies and other would help to distinguish “I don’t see my movie” from actual movies.
You then can use the onChange
event to react to the user picking that option.
const moviesGroup = "Matching Movies";
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
const top100Films = [
{ label: "The Shawshank Redemption", year: 1994, isMovie: moviesGroup },
{ label: "The Godfather", year: 1972, isMovie: moviesGroup },
{ label: "The Godfather: Part II", year: 1974, isMovie: moviesGroup },
];
const options = [
...top100Films,
{ label: "I don’t find my movie", isMovie: "Other Options" }
];
export default function ComboBox() {
const handleNotFound = () => {
alert("Not found");
}
const onChange = (_, o) => {
if (o.isMovie !== moviesGroup) {
handleNotFound();
}
}
return (
<Autocomplete
disablePortal
id="combo-box-demo"
options={options}
groupBy={(option) => option.isMovie}
onChange={onChange}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
);
}