Material UI select dropdown arrow keys navigation not working - select

I added a textfield inside mui multiple select component , now keyboard arrow up and down list navigation is not working instead,on arrow onkeydown it focuses on the div itself (scrolling on arrow keydown).
Any help is appreciated (AutoComplete is not an option)
Have tried adding autoFocus on MenuItem itself , but that starts from the last list
import * as React from "react";
import { Theme, useTheme } from "#mui/material/styles";
import MenuItem from "#mui/material/MenuItem";
import TextField from "#mui/material/TextField";
import FormControl from "#mui/material/FormControl";
import Select, { SelectChangeEvent } from "#mui/material/Select";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250
}
}
};
const names = [
"Oliver Hansen",
"Van Henry",
"April Tucker",
"Ralph Hubbard",
"Omar Alexander",
"Carlos Abbott",
"Miriam Wagner",
"Bradley Wilkerson",
"Virginia Andrews",
"Kelly Snyder"
];
function getStyles(name: string, personName: string[], theme: Theme) {
return {
fontWeight:
personName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium
};
}
export default function MultipleSelect() {
const theme = useTheme();
const [personName, setPersonName] = React.useState<string[]>([]);
const handleChange = (event: SelectChangeEvent<typeof personName>) => {
const {
target: { value }
} = event;
setPersonName(
// On autofill we get a stringified value.
typeof value === "string" ? value.split(",") : value
);
};
return (
<div>
<FormControl sx={{ m: 1, width: 300 }}>
<Select
multiple
value={personName}
onChange={handleChange}
MenuProps={MenuProps}
onKeyDown={(e) => {
if (e.key !== "Escape") {
e.stopPropagation();
}
}}
>
<div style={{ textAlign: "center", margin: "20px 0px" }}>
<TextField placeholder="type..." />
</div>
{names.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, personName, theme)}
>
{name}
</MenuItem>
))}
</Select>
</FormControl>
</div>
);
}

It seems the issue is that the TextField element is a child of the Select element and is intercepting the keydown events that would normally be used to navigate the list of options. One way to fix this would be to use the onKeyDown prop on the TextField element to prevent it from intercepting the keydown events. Instead of calling e.stopPropagation(), you can call e.preventDefault() which will prevent the event from being handled by the TextField element and allow it to be handled by the Select element.
<TextField placeholder="type..." onKeyDown={e => e.preventDefault()}/>
Also to make sure that focus is on the first element in the list after the user opens the list you can use autoFocus prop on the first element of the list.
<MenuItem
key={names[0]}
value={names[0]}
style={getStyles(names[0], personName, theme)}
autoFocus
>
{names[0]}
</MenuItem>
This should allow the arrow keys to navigate the list as expected, while still allowing the user to type into the TextField.

Related

Material-UI: Restrict users from removing first characters in a TextField

I'm working with a TextField in Material UI. I need to restrict the user from deleting the first few words (initialValue) of a TextField. This is to ensure user responses start with specific text which is necessary in this case. Is this possible?
const initialValue = "This taught me"
const [response, setResponse] = useState(initialValue);
<TextField
multiline
minRows={3}
onChange={e => setResponse(e.target.value)}
value={response}
autoFocus={true}
inputRef={textInput}
/>
Converting my comment to an answer.
Instead off calling setResponse right away, call a custom function with some logic:
If the new value is shorter then initialValue, set initialValue as value
If the new value does not startsWith() initialValue, set the value again to the initialValue
// Get a hook function
const {useState} = React;
const Example = () => {
const initialValue = "This taught me"
const [response, setResponse] = useState(initialValue);
const onInput = (val) => {
if (val.length < initialValue.length || !val.startsWith(initialValue)) {
setResponse(initialValue);
} else {
setResponse(val)
}
}
return (
<div>
<input value={response} onChange={(e) => onInput(e.target.value)} />
</div>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
input { min-width: 300px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Material UI > Backdrop > only for some subcomponent of the page

Is there any way how to enhance a backdrop from example in https://material-ui.com/components/backdrop/ to show loading circle only above the single component (in case some page has more component), not above the whole page?
Thanks for reply.
Backdrop are fixed positioned by default, that's why it covers the whole page.
To achieve the result you want, we have to change its position to absolute and contain it inside an element with relative position — this element can be your component. If you're new in CSS positions check this docs from developer.mozilla.org.
Knowing all that, we can come up with the following codes
const useStyles = makeStyles({
parent: {
position: "relative",
width: 200,
height: 200,
backgroundColor: "red",
zIndex: 0,
},
backdrop: {
position: "absolute"
}
});
export default function App() {
const classes = useStyles();
return (
<div className={classes.parent}>
<Backdrop className={classes.backdrop} open={true}>
<CircularProgress color="inherit" />
</Backdrop>
</div>
);
}
Also we have to define z-index on either parent or backdrop element to make it work. Not sure why though.
I created a codesandbox for you to play with.
The Backdrop component of Material UI is set to position: 'fixed' by default, that's why it covers the whole page.
If you want it to reside and position itself like any other component typically on the DOM, all you have to do is to reset its position back to relative, for instance:
<Backdrop open={true} sx={{ position: 'relative' }}>
<CircularProgress color="inherit" />
</Backdrop>
and you don't need to change the parent component since it should be in your case see to relative by default if you're not changing it. But if you have crazy positions going in your app here and there, then you might consider changing that as well.
i have created a custom componenet that i use if i want to block only part of the UI:
"use strict";
/** external libraries */
import React from "react";
import Backdrop from "#mui/material/Backdrop";
import CircularProgress from "#mui/material/CircularProgress";
const BlockUi = ({open, onClose, children}) => {
return (
<div style={{"position": "relative"}}>
<Backdrop
sx={{color: "#FFFFFF", zIndex: (theme) => theme.zIndex.drawer + 1, "position": "absolute"}}
open={open}
onClick={() => onClose()}
>
<CircularProgress color="inherit"/>
</Backdrop>
{children}
</div>
);
}
export default BlockUi;
and i use it like this:
"use strict";
/** external libraries */
import React from "react";
import BlockUi from "./BlockUi";
const JsonForm = ({fields, onSubmit}) => {
const [loading, setLoading] = React.useState(false)
const stopLoading = () => {
setLoading(false)
}
return (
<div>
<BlockUi open={loading} onClose={stopLoading}>
<button type="submit" onClick={() => {
console.log(loading)
setLoading(true)
}}>Submit
</button>
</BlockUi>
</div>
);
}
export default JsonForm;

Select: Limit number of selected options

I'm using ANT Design's Select component in multiple select mode. After two options are selected (see screenshot) I'd like to prevent any more from being selected. The field should not be disabled, so that you can deselect an option and select another.
I've tried the onFocus event, but it doesn't provide an event that I could use to preventDefault or otherwise keep from opening the dropdown. I've also tried adding a ref and calling blur() whenever the onFocus event is called. This closes the dropdown, but it's still visible for a second.
Does anyone know of a way to accomplish this?
If 3 or more options selected then with a simple condition you can disable other options.
Store selected options in state and while displaying options disable them based on condition.
https://codesandbox.io/s/happy-leftpad-lu84g
Sample code
import React, { useState } from "react";
import { Select } from "antd";
const { Option } = Select;
const opts = ["a11", "b12", "c13", "d14", "e15"];
const Selectmultiple = () => {
const [optionsSelected, setOptionsSelected] = useState([]);
const handleChange = value => {
console.log(`selected ${value}`);
setOptionsSelected(value);
};
return (
<div>
<Select
mode="multiple"
style={{ width: "100%" }}
placeholder="Please select"
onChange={handleChange}
>
{opts.map(item => (
<Option
disabled={
optionsSelected.length > 1
? optionsSelected.includes(item)
? false
: true
: false
}
key={item}
>
{item}
</Option>
))}
</Select>
</div>
);
};
I solved this problem using "open" prop:
const isMaxValues = value.length === limit;
<Select
mode="multiple"
disabled={false}
{...(isMaxValues && { open: false, onDropdownVisibleChange: handleShowError })}
>
{renderOptions()}
</Select>
So you still able to remove/deselect some options
Also you can provide isMaxValues option to renderOptions method and disable Options to be selected(if you need dropdown to be visible)

Select all checkbox redux form

I want to check/ uncheck all checkboxes the moment I select Check All but can't make it work. I'm using material-ui components and redux-form. my plan is to grab checkAll field value using formValueSelector API and set checkbox A and B value based of that. Also tried using value prop but no luck still.
import React from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { Checkbox } from 'redux-form-material-ui';
let Form = (props) => {
return (
<form>
<Field name="checkAll" id="checkAll" label="Check All" component={ Checkbox } />
<Field name="a" label="A" component={ Checkbox } checked={ props.checkAll } />
<Field name="b" label="B" component={ Checkbox } checked={ props.checkAll } />
</form>
);
};
Form = reduxForm({
form: 'Form'
})(AddReturnModal);
// Decorate with connect to read form values
const selector = formValueSelector('Form'); // <-- same as form name
Form = connect(
(state) => {
const checkAll = selector(state, 'checkAll');
return {
checkAll
};
}
)(Form);
export default Form;
You could use change method. From docs:
change(field:String, value:any) : Function
Changes the value of a field in the Redux store. This is a bound action creator, so it returns nothing.
The only solution I see is to loop over the list of checkboxes and call change(checkboxName, value) on them.
#notgiorgi is right, to select all checkboxes, you can do this:
selectAll = () => {
const { change } = this.props
['a', 'b'].forEach((field) => change(field, true))
}
If you wanted a toggle, you might easily/cheaply/ignorantly be able to keep a rough reference to the selectAll state:
selectAllValue = false
selectAll = () => {
const { change } = this.props
['a', 'b'].forEach((field) => change(field, this.selectAllValue))
// toggle select all to select none
this.selectAllValue = !this.selectAllValue
}

How to add multiple event handlers to same event in React.js

All:
I wonder if it is possible that binding multiple event handlers to same event?
For example:
var LikeToggleButton = React.createClass({
render: function(){
(function toggle(){
this.setState({liked:!like});
}).bind(this);
return (
<div onClick={toggle}>TOGGLE LIKE</div>
);
}
});
Until this point everything seems normal, but I want to add another feature to that button, which is decide by other option:
For example, I have another switch component(could be anything like checkbox or radio button etc.) called "count toggle", which when enabled, the LikeToggleButton's button will be added another onClick handler which is start counting times of button clicked, I know it could be predesignd into the toggle function, but I just wonder if there is a way to append this part to onClick handler?
Thanks
If you want to have multiple callbacks executed when onClick is triggered, you can have them passed from outside, so you'll have access to them in the props object. Then execute them all (note: code not tested):
var LikeToggleButton = React.createClass({
toggle: function() {
this.setState({liked:!like});
},
handleClick: function(e) {
e.preventDefault();
this.toggle();
for (var i=0, l<this.props.callbacks.length; i<l; i++) {
this.props.callbacks[i].call();
}
},
render: function() {
return (
<div onClick={this.handleClick}>TOGGLE LIKE</div>
);
}
});
BUT, if you want to have components connected between them, you should not do that by calling methods inside handlers. Instead you should use an architectural pattern, where Flux is the obvious choice (but there are lots more).
Take a look to Flux, and here you have more choices.
For an extensible way that does't require the component to know about components that use it - save the onClick event before changing it.
This is highlights extracted from the actual working code:
button.jsx
class Button extends React.Component {
constructor(props) {
super(props);
this.state= { callback: false};
}
click(){
//do stuff here
if(this.state.callback) { this.state.callback.call(); }
}
render () {
this.state.callback = this.props.onClick; // save the onClick of previous handler
return (
<button { ...this.props } type={ this.props.type || "button" } onClick={ this.click.bind(this) } className = this.props.className } >
{ this.props.children }
</button>
);
}
}
export default Button;
Then in another component you can use the button and it can have it's own onClick handler:
class ItemButtons extends React.Component {
itemClick () {
//do something here;
}
render () {
const buttons = [
(
<Button onClick={ this.itemClick.bind(this) } className="item-button">
<span>Item-Button</span>
</Button>
)
];
return (<section>{ buttons }</section>);
}
export default ItemButtons;
To group multiple actions on an event
onMouseDown={(e) => { e.stopPropagation(); alert('hello'); }}
Maybe you can set multiple click event handlers on the same one target as described here: https://gist.github.com/xgqfrms-GitHub/a36b56ac3c0b4a7fe948f2defccf95ea#gistcomment-2136607
Code (copied from linke above):
<div style={{ display: 'flex' }}>
<div style={{
width: '270px',
background: '#f0f0f0',
borderRight: "30px solid red",
minHeight: ' 500px',
maxHeight: '700px',
overflowX: 'hidden',
overflowY: 'scroll',
}}
onClick={this.state.ClickHandler}
onClick={this.stateHandleClick}
className="sidebar-btn"
>
<button onClick={this.props.ClickHandler}>props</button>
<button onClick={(e) => this.props.ClickHandler}>props</button>
<button onClick={this.props.ClickHandler}>props</button>
<button onClick={this.state.ClickHandler}>state</button>
//...
</div>