I'm working on adding arrow key functionality to a react-select input dropdown with submenus. I am able to open the submenu, however I don't know how to be able focus an option from the submenu.
As you can see in the image, the submenu parent is selected. And I want to be able to focus the options on the right.
export const CustomOption = () => {
const [showNestedOptions, setShowNestedOptions] = useState(false);
const [subMenuOpen, setSubMenuOpen] = useState(null);
// this is used to give styling when an option is hovered over or chosen with arrow key
const [isFocusedState, setIsFocusedState] = useState(false);
// this basically opens the submenu on ArrowRight keyboard click and gives it isFocusedState(true) which is used to render the SELECTED submenu
const handleKeys = (e: any) => {
if (e.key === 'ArrowRight') {
!props.isMobile && setShowNestedOptions(true);
e.setIsFocusedState(true);
} else if (e.key === 'ArrowLeft') {
!props.isMobile && setShowNestedOptions(false);
e.setIsFocusedState(false);
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeys);
return () => {
return window.removeEventListener('keydown', handleKeys);
};
}, []);
// this does the same but on mouseOver (hover)
const handleMouseOver = (e: any) => {
!props.isMobile && setShowNestedOptions(true);
setIsFocusedState(true);
};
const handleMouseLeave = (e: any) => {
!props.isMobile && setShowNestedOptions(false);
setIsFocusedState(false);
};
return (
<Box>
{props.data.nestedOptions ? (
<Box
onMouseLeave={handleMouseLeave}
onMouseOver={handleMouseOver}
onKeyDown={handleKeys}
onClick={() => setIsFocusedState(!isFocusedState)}
>
<MainOption
renderOption={props.renderOption}
option={props.data}
hasNestedOptions={true}
setSubMenuOpen={() => setSubMenuOpen(props.data.value)}
selectOption={selectOption}
isFocused={isFocusedState}
/>
{showNestedOptions && (
<Box>
{(isFocusedState || props.isFocused) &&
map(props.data.nestedOptions, (nestedOption, index: number) => {
const isFirst = index === 0;
const value = props.getOptionValue?.(nestedOption) || props.value;
const label = props.getOptionLabel?.(nestedOption) || props.label;
const nestedInnerProps = innerProps;
nestedInnerProps.onClick = (e: React.ChangeEvent<HTMLInputElement>) =>
selectOption(props.data.nestedOptions.find((o: SelectOption) => o.label === e.target.textContent));
const optionProps = {
...props,
data: { ...nestedOption, parentValue: subMenuOpen },
value: value,
label: label,
children: label,
innerProps: { ...nestedInnerProps, parentValue: subMenuOpen },
};
// here the submenu is rendered and opened
return (
<Box
key={index}
>
<Option {...optionProps} key={value} isFocused={false}>
<Center>
{label}
</Center>
</Option>
</Box>
);
})}
</Box>
)}
</Box>
) : (
// if there's no submenu, simply render the list of options
<Option {...props}>
<MainOption
isMobile={props.isMobile}
option={props.data}
getOptionValue={props.getOptionValue}
renderOption={props.renderOption}
wrapperOptionArg={props.wrapperOptionArg}
getOptionLabel={props.getOptionLabel}
/>
</Option>
)}
</Box>
);
};
I have tried to add onKeyDown to change the isFocused prop conidtionally, like so but somehow it only works on mouseOver and it sets the condition inappropriately ( for all options, or none at all )
return (
<Box
key={index}
>
<Option
{...optionProps}
key={value}
isFocused={props.data.nestedOptions[0] ? true : false}
<Center>
{label}
</Center>
</Option>
</Box>
Unfortunately there's not much information about this certain keyboard functionality that I was able to find online.
In short, how to focus the first element of a submenu when ArrowRight is already used to open the submenu?
Related
I use useRef and This is part of my code:
useEffect(() => {
setRange(props.instrument);
let data = getValues(props.instrument);
tooltipRef.current.innerHTML = ReactDOMServer.renderToString(
showTooltip(data.LastTradePrice)
);
},[]);
and
const setRange = (instrument) => {
let tooltip = tooltipRef.current;
const rangeInfo = getValues(instrument);
tooltip.children[0].style.left = `50%`;
tooltip.children[0].children[0].style.fill =
rangeInfo.LastTradePrice > rangeInfo.PreviousDayPrice
? `${theme.palette.color.green}`
: `${theme.palette.color.red}`;
tooltip.setAttribute("title", rangeInfo.LastTradePrice);
}
and
const showTooltip = (data) => {
return (
<ThemeProvider theme={theme}>
<Grid
item
className={classes.currentPrice}
>
<LocationIcon
className={clsx(
classes.currentPriceIcon,
device.isMobile && classes.currentPriceIconMobile
)}
></LocationIcon>
</Grid>
</ThemeProvider>
);
};
and
return (
<Grid item ref={tooltipRef}></Grid >
)
By default, it shows me a tooltip.How can I apply the style I want to this tooltip?
This is the default:
For example, how can I change the tooltip backgroundcolor?
I have a problem. I want to make buttons section, where user can click buttons to filter some content. When user click on 'all' button, all other should be turn off (change its color to initial, not active) in this moment. Also, user can check multiple buttons.
I can't get how to do this.
Example of JSON:
{
title: 'All',
id: 53,
},
{
title: 'Im a parent',
icon: <Parent />,
id: 0,
},
{
title: 'I live here',
icon: <ILiveHere />,
id: 2,
},
example of code: https://codesandbox.io/s/sleepy-haze-35htx?file=/src/App.js
Its wrong, I know. I tried some solutions, but I guess I can't get how to do it correctly.
With this code I can do active multiple buttons, but I can't get how to make conditions like
if (item.title === 'all){
TURN_OFF_ANY_OTHER_BTNS
}
I guess I should store checked buttons in temporary array to make these operations.
Will be really thankfull for help.
Is this something you would like?
const SocialRole = ({ item, selected, setSelected }) => {
let style =
[...selected].indexOf(item.id) !== -1
? { color: "red" }
: { color: "blue" };
return (
<button
style={style}
onClick={() => {
if (item.id === 53) {
setSelected(null);
} else {
setSelected(item.id);
}
}}
>
{item.icon}
<h1>{item.title}</h1>
</button>
);
};
export default function App() {
// We keep array of selected item ids
const [selected, setSelected] = useState([roles[0]]);
const addOrRemove = (item) => {
const exists = selected.includes(item);
if (exists) {
return selected.filter((c) => {
return c !== item;
});
} else {
return [...selected, item];
}
};
return (
<div>
{roles.map((item, index) => (
<SocialRole
key={index}
item={item}
selected={selected}
setSelected={(id) => {
if (id === null) setSelected([]);
else {
setSelected(addOrRemove(id));
}
}}
/>
))}
</div>
);
}
If I understand your problem, I think this is what you are looking for:
const roles = [
{
title: "All",
id: 53
},
{
title: "I live here",
id: 0
},
{
title: "I live here too",
id: 2
}
];
// button
const SocialRole = ({ item, selected, setSelected }) => {
const isActive = selected === item.title || selected === 'All';
return (
<button
style={isActive ? { color: "red" } : { color: "blue" }}
onClick={() => setSelected(item.title)}
>
{item.icon}
<h1>{item.title}</h1>
</button>
);
};
export default function App() {
const [selected, setSelected] = useState(roles[0].title);
return (
<div>
{roles.map((item, index) => (
<SocialRole
key={index}
item={item}
selected={selected}
setSelected={setSelected}
/>
))}
</div>
);
}
The problem was you were setting a new state into each button, when you should just use the state from the App.
I almost give up on this bug.
I simply just can't type "S" into the search input.
The keyboard works fine.
Sandbox below.
https://codesandbox.io/s/jolly-raman-61zbx?file=/src/App.js
Code from sandbox:
import {
Box,
FormControl,
InputAdornment,
ListItem,
Menu,
TextField,
withStyles
} from "#material-ui/core";
import {
clamp,
difference,
includes,
intersection,
join,
map,
union
} from "lodash";
import ArrowDropDownIcon from "#material-ui/icons/ArrowDropDown";
import { FixedSizeList } from "react-window";
import Fuse from "fuse.js";
import React from "react";
import SearchIcon from "#material-ui/icons/Search";
import memoize from "memoize-one";
const styles = {
popoverPaper: {
width: "100%"
}
};
const fuseOptions = {
includeScore: true
};
const initialSearchState = {
searchValue: ""
};
const getCheckedList = (list) => (!!list ? list : []);
class VirtualisedSelector extends React.Component {
constructor(props) {
super(props);
this.state = {
anchorEl: null,
...initialSearchState
};
}
getSearchList = memoize((list, searchValue) => {
const fuse = new Fuse(list, fuseOptions);
return !!searchValue ? map(fuse.search(searchValue), (o) => o.item) : list;
});
handleSearch = (event) => {
this.setState({
...this.state,
searchValue: event.target.value
});
};
getCleanSelectedValues = memoize((currentSelectedValues, list) =>
intersection(currentSelectedValues, list)
);
getNewMultipleValue = (currentSelectedValues, clickedItemValue) =>
includes(currentSelectedValues, clickedItemValue)
? difference(currentSelectedValues, [clickedItemValue])
: union(currentSelectedValues, [clickedItemValue]);
getNewSingleValue = (currentSelectedValues, clickedItemValue) =>
includes(currentSelectedValues, clickedItemValue) ? [] : [clickedItemValue];
getNewValue = (currentSelectedValues, clickedItemValue, list, multiple) =>
multiple
? this.getNewMultipleValue(
this.getCleanSelectedValues(currentSelectedValues, list),
clickedItemValue
)
: this.getNewSingleValue(currentSelectedValues, clickedItemValue);
handleChange = (currentSelectedValues, list, multiple) => (event) => {
this.setState({
anchorEl: null
});
const clickedItemValue = event.target.getAttribute("value");
const newValue = this.getNewValue(
currentSelectedValues,
clickedItemValue,
list,
multiple
);
this.props.onSelect(newValue);
};
getTextFieldDisplayValue = (value, list, labelMap) =>
join(
map(this.getCleanSelectedValues(value, list), (e) => labelMap[e] ?? e),
", "
);
handleMenuOpen = (event) => {
this.setState({ anchorEl: event.currentTarget });
};
handleMenuClose = (event) => {
this.setState({ anchorEl: null, ...initialSearchState });
};
renderRow = () => ({ index, style }) => {
const { multiple, value, list, labelMap } = this.props;
const { searchValue } = this.state;
const searchList = this.getSearchList(list, searchValue);
const cleanSelectedValues = this.getCleanSelectedValues(value, list);
const listItem = searchList[index];
return (
<ListItem
value={listItem}
key={listItem}
selected={includes(value, listItem)}
onClick={this.handleChange(cleanSelectedValues, list, multiple)}
style={style}
>
{labelMap?.[listItem] || listItem}
</ListItem>
);
};
render() {
const {
value,
list,
name,
label,
labelMap,
required = false,
classes
} = this.props;
const { searchValue } = this.state;
const formControlClassNames = this.props?.classes?.formControl;
const searchList = this.getSearchList(getCheckedList(list), searchValue);
const nItems = searchList.length;
const stringHeight = 46;
const fixedSizeListHeight = stringHeight * clamp(nItems, 1, 10);
return (
<FormControl className={formControlClassNames} fullWidth>
<TextField
onClick={this.handleMenuOpen}
variant="outlined"
required={required}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<ArrowDropDownIcon />
</InputAdornment>
)
}}
value={this.getTextFieldDisplayValue(value, list, labelMap)}
multiline
type="text"
name={name}
label={label}
fullWidth
/>
<Menu
anchorEl={this.state.anchorEl}
keepMounted
open={Boolean(this.state.anchorEl)}
onClose={this.handleMenuClose}
PopoverClasses={{ paper: classes.popoverPaper }}
>
{/* Search */}
<Box p={1}>
<TextField
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
type="text"
label={"Search"}
value={searchValue}
onChange={this.handleSearch}
fullWidth
/>
</Box>
{/* Virtualized list */}
<FixedSizeList
height={fixedSizeListHeight}
width={"100%"}
itemSize={stringHeight}
itemCount={nItems}
>
{this.renderRow()}
</FixedSizeList>
</Menu>
</FormControl>
);
}
}
VirtualisedSelector.defaultProps = {
value: "",
list: [],
name: "",
label: "",
labelMap: {},
required: false
};
export default withStyles(styles)(VirtualisedSelector);
The main issue is that you shouldn't be using Menu for this. Menu assumes that it has MenuItem children and has accessibility functionality geared towards that assumption. The behavior you are seeing is caused by the functionality that tries to navigate to menu items by typing the character that the menu item's text starts with. In your case, it is finding the text of the label "Search", and then it is moving focus to that "menu item" (which is why you then get a focus outline on the div containing your TextField). If you change the label to "Type Here", you'll find the "s" works, but "t" doesn't.
My recommendation would be to use Popover directly (the lower-level component which Menu delegates to for the main functionality you are using from it). Another option would be to use the Autocomplete component since you seem to be trying to use Menu and the pop-up TextField to do your own custom version of what the Autocomplete component provides.
Apparently, this is an issue in MenuList of Material UI. The Menu component internally uses MenuList, so it also has the issue.
You can work around this behaviour by preventing the bubbling of the event of the TextField (as stated in the Github thread).
<TextField
variant="outlined"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
type="text"
label={"Search"}
value={searchValue}
onChange={this.handleSearch}
onKeyDown={(event) => event.stopPropagation()}
fullWidth
/>
I want to close all other open SubMenu when the user opens a SubMenu.
Can anyone help me to find a solution?
My code:
Menu.tsx ->
const Menu: React.FC = ({ data }) => {
return (
<nav className={styles.headerMenu}>
{
data.map((item, index) => <MenuItem key={index} item={item} /> )
}
</nav>
)
}
MenuItem.tsx ->
const MenuItem: React.FC = ({ item }) => {
let [subMenuOpened, setSubMenuOpened] = useState<boolean>(false);
const switchMenu = (condition) => setSubMenuOpened(condition !== null ? condition : !subMenuOpened)
const SubMenu: React.FC = () => { /* Code to render submenu */ }
return (
<section onMouseEnter={()=> item.subMenu && switchMenu(true)} onMouseLeave={() => item.subMenu && switchMenu(false)}>
<a href={item.href}>{item.title}</a>
//Render SubMenu if item has submenu and it is open
{ (item.subMenu && subMenuOpened) && <SubMenu /> }
</section>
)
}
An example of what I mean
#Nikki9696 answered your question in a comment:
Generally, if a sibling needs to know about other sibling state, you
move the state up to the parent. So, you'd keep track of which submenu
was open in the Menu component, not the MenuItem, and close all but
the selected one there.
so I will show you an example of what they meant:
const Menu: React.FC = ({ data }) => {
const keys = data.map(function(item,key) { return { key:key, close: null, item: item}});
onOpen=(key)=>{
// this will close all menus except the current one
keys.forEach(x=>{
if (x.key !== key && x.close !== null)
x.close(); // Close should be set by child eg MenuItem.
})
}
return (
<nav className={styles.headerMenu}>
{
keys.map((item, index) => <MenuItem onOpen={onOpen} key={index} item={item} /> )
}
</nav>
)
}
const MenuItem: React.FC = ({ item, onOpen }:{item: any, onOpen:Function}) => {
let [subMenuOpened, setSubMenuOpened] = useState<boolean>(false);
item.close = ()=> { setSubMenuOpened(false); } /// this is so the parent will trigger close
const switchMenu = (condition) => setSubMenuOpened(condition !== null ? condition : !subMenuOpened)
useEffect(() =>{
if (subMenuOpened)
onOpen(item.key); // trigger parent
}, [subMenuOpened]);
const SubMenu: React.FC = () => { /* Code to render submenu */ }
return (
<section onMouseEnter={()=> item.item.subMenu && switchMenu(true)} onMouseLeave={() => item.subMenu && switchMenu(false)}>
<a href={item.item.href}>{item.item.title}</a>
//Render SubMenu if item has submenu and it is open
{ (item.item.subMenu && subMenuOpened) && <SubMenu /> }
</section>
)
}
This is the code for a custom MultiSelect component I'm writing. I want each button to have class="selected" when that value is selected.
import React from 'react'
import './styles.scss'
export default function MultiSelect({
name = '',
options = [],
onChange = () => {},
}) {
const clickOption = e => {
e.preventDefault()
onChange(
options.map(o => {
if (o.value === e.target.value) o.selected = !o.selected
return o
}),
)
}
return (
<div className="multiselect" name={name}>
{options.map(option => (
<button
key={option.value}
value={option.value}
onClick={e => clickOption(e)}
{/* here */}
className={option.selected ? 'selected' : ''}
>
{option.name}
</button>
))}
</div>
)
}
The class name never displays as selected, and doesn't change when option.selected changes. When I add {option.selected ? 'selected' : ''} under {option.name} inside the button as raw text, it displays and changes as expected.
When I change it to either of the following, it works:
<button className={`${option.selected ? 'selected' : ''}`}>
<!-- OR -->
<button className={'' + (option.selected ? 'selected' : '')}>
Can anybody explain why plain old className={option.selected ? 'selected' : ''} isn't working?
I'm going to analyze your solution.
className={option.selected ? 'selected' : ''} could be rewrite to className={option.selected && 'selected' }if the property is defined the operation result will be 'selected' for operator precedence, javascript always evaluate from left to right.
MultiSelect is a stateless component so your options props come from the hight order component, one way is an onClick event send as a parameter the id of the option, and in the parent change the value of the option.
import React, { useState } from 'react';
import './styles.scss';
const MultiSelect = ({
name = '',
options = [],
onChange = () => {},
}) => {
const handleClick = (id) => () => onChange(id);
return (
<div className="multiselect" name={name}>
{options.map((option) => (
<button
key={option.value}
value={option.value}
onClick={handleClick(option.id)}
className={option.selected && 'selected'}
>
{option.name}
</button>
))}
</div>
);
};
const Parent = ({ }) => {
const [options, setOptions] = useState([{
id: 1,
value: 1,
selected: true,
name: 'Hello',
},
{
id: 2,
value: 2,
selected: false,
name: 'World',
},
]);
const handleChange = (id) => {
const option = options.find((option) => option.id === id);
setOptions(
...options.filter((option) => option.id !== id),
{
...option,
selected: !option.selected,
},
);
};
return <MultiSelect options={options} onChange={handleChange} name="Example" />;
};