React JS Material UI Select IconComponent (Dropdown Icon) avoid rotating - reactjs

By default, in React JS Material UI's Select component, when we provide a custom IconComponent, it gets turned upside down when user has selected the dropdown / Select component.
Sample code:
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={Search}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
...
I did a sneaky thing to remove "MuiSelect-iconOpen" from the className when calling IconComponent.
Sample Code after my fix:
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={({ className }) => {
className = className.replace("MuiSelect-iconOpen", "")
return <Search className={className} />
}}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
....
Now is there a better way to do this without replacing the className?

My current solution is to overwrite the original iconOpen class provided by the Material-UI Select.
....
import { makeStyles } from "#material-ui/core";
const useStyles = makeStyles((theme) => ({
iconOpen: {
transform: 'rotate(0deg)',
},
}));
....
export const MyCompo: FC<> = () => {
const classes = useStyles();
return (
<Select
multiple
variant="outlined"
MenuProps={CustomMenuProps}
IconComponent={Search}
classes={{
iconOpen: classes.iconOpen,
}}
renderValue={(selected) => (selected as string[]).join(', ')}
{...props}
>
....

<Select
value={values.phoneCode}
onChange={handleChange("phoneCode")}
inputProps={{ "aria-label": "Without label" }}
IconComponent={(_props) => {
const rotate = _props.className.toString().includes("iconOpen");
return (
<div
style={{
position: "absolute",
cursor: "pointer",
pointerEvents: "none",
right: 10,
height: "15px",
width: "15px",
transform: rotate ? "rotate(180deg)" : "none",
}}
>
<ArrowDown />
</div>
);
}}
>
....

It does not rotate if you use arrow function: IconComponent={()=> <YourIcon/>}

Related

How to stop modal from closing when clicking on a select option?

I've made a custom filter for MUI's datagrid, the filter has two select's which allow you to filter by the column and filter type. The selects are quite big and endup outside the modal, when clicking on an option the whole modal closes, how can I prevent this from happening?
I've used this tutorial - Detect click outside React component to detect clicks outside the filter.
The code below shows the filter and I've also made an codesandbox example here - https://codesandbox.io/s/awesome-panka-g92vhn?file=/src/DataGridCustomFilter.js:0-6708
any help would be appreciated
import React, { useState, useEffect, useRef } from "react";
import {
Button,
Stack,
FormControl,
InputLabel,
Select,
MenuItem,
Paper,
Grid,
IconButton,
TextField,
ClickAwayListener
} from "#material-ui/core";
import FilterListIcon from "#mui/icons-material/FilterList";
import AddIcon from "#mui/icons-material/Add";
import CloseIcon from "#mui/icons-material/Close";
import { useForm, useFieldArray, Controller } from "react-hook-form";
import { columns } from "./columns";
const filterTypes = {
string: ["contains", "equals", "starts with", "ends with", "is any of"],
int: ["contains", "equals", "less than", "greater than"]
};
function FilterRow({
len,
setOpen,
field,
control,
columns,
index,
handleRemoveFilter
}) {
return (
<Grid container spacing={0}>
<Grid
item
md={1}
style={{
display: "flex",
alignSelf: "flex-end",
alignItems: "center"
}}
>
<IconButton
size="small"
onClick={() => {
if (len === 1) {
setOpen(false);
} else {
console.log(index, "---");
handleRemoveFilter(index);
}
}}
>
<CloseIcon style={{ fontSize: "20px" }} />
</IconButton>
</Grid>
<Grid item md={4}>
<Controller
name={`filterForm.${index}.column`}
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl variant="standard" sx={{ width: "100%" }}>
<InputLabel>Column</InputLabel>
<Select
value={value}
onChange={onChange}
label="Column"
defaultValue=""
>
{columns.map((a) => {
return a.exclude_filter === true ? null : (
<MenuItem value={a.headerName}>{a.headerName}</MenuItem>
);
})}
</Select>
</FormControl>
)}
/>
</Grid>
<Grid item md={3}>
<Controller
name={`filterForm.${index}.filter`}
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl variant="standard" sx={{ width: "100%" }}>
<InputLabel>Filter</InputLabel>
<Select
value={value}
onChange={onChange}
label="Filter"
defaultValue=""
>
{filterTypes.string.map((a) => {
return <MenuItem value={a}>{a}</MenuItem>;
})}
</Select>
</FormControl>
)}
/>
</Grid>
<Grid item md={4}>
<Controller
name={`filterForm.${index}.value`}
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl>
<TextField
onChange={onChange}
value={value}
label="Value"
variant="standard"
/>
</FormControl>
)}
/>
{/* )} */}
</Grid>
</Grid>
);
}
function DataGridCustomFilter() {
const { control, handleSubmit } = useForm();
const { fields, append, remove } = useFieldArray({
control,
name: "filterForm"
});
const [open, setOpen] = useState(false);
const onSubmit = (data) => {};
useEffect(() => {
if (fields.length === 0) {
append({
column: "ID",
filter: filterTypes.string[0],
value: ""
});
}
}, [fields]);
const [clickedOutside, setClickedOutside] = useState(false);
const myRef = useRef();
const handleClickOutside = (e) => {
if (myRef.current && !myRef.current.contains(e.target)) {
setClickedOutside(true);
setOpen(!open);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
});
return (
<>
<Button
startIcon={<FilterListIcon />}
size="small"
onClick={() => {
setOpen(!open);
}}
// disabled={isDisabled}
>
FILTERS
</Button>
{open ? (
<div ref={myRef}>
<Paper
style={{
width: 550,
padding: 10,
zIndex: 1300,
position: "absolute",
inset: "0px auto auto 0px",
margin: 0,
display: "block"
// transform: "translate3d(160.556px, 252.222px, 0px)",
}}
variant="elevation"
elevation={5}
>
<form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={0.5}>
<div style={{ maxHeight: 210, overflow: "scroll" }}>
{fields.map((field, index) => {
return (
<div style={{ paddingBottom: 5 }}>
<FilterRow
len={fields.length}
control={control}
setOpen={setOpen}
field={field}
columns={columns}
handleRemoveFilter={() => remove(index)}
{...{ control, index, field }}
// handleClickAway={handleClickAway}
/>
</div>
);
})}
</div>
<div style={{ marginTop: 10, paddingLeft: 40 }}>
<Stack direction="row" spacing={1}>
<Button size="small" startIcon={<AddIcon />}>
ADD FILTER
</Button>
<Button size="small" type="submit">
{fields.length > 1 ? "APPLY FILTERS" : "APPLY FILTER"}
</Button>
</Stack>
</div>
</Stack>
</form>
</Paper>
</div>
) : null}
</>
);
}
export default DataGridCustomFilter;
So far I've tried MUI's ClickAwayListener and the example above, both seem to give the same result
DataGrid component uses NativeSelect. I have checked your codesandbox and tried replacing Select to NativeSelect and MenuItem to Option. filter is working properly. below is sample code for update.
...
<NativeSelect
value={value}
onChange={onChange}
label="Column"
defaultValue=""
>
{columns.map((a) => {
return a.exclude_filter === true ? null : (
<option value={a.headerName}>{a.headerName}</option >
);
})}
</NativeSelect>
...

Edit material UI style programatically

I'm trying to make an input expand when I press the search button, but I can't change the material-ui style after using makeStyles.
Is there a way I can do it?
Thanks
Sample code:
const useStyles = makeStyles((theme) => ({
searchInput: {
width: 0,
padding: 0,
border: "none",
},
}));
function ExpandableSearchBar() {
const classes = useStyles();
function onClick() {
// change style of the input class
// classes.searchInput
}
return (
<div className="component-wrapper">
<IconButton onClick={onClick} aria-label="Search">
<SearchIcon />
</IconButton>
<input className={classes.searchInput} type="text" />
</div>
);
}
You can set a state to toggle className of the input. Your code should look like -
const useStyles = makeStyles((theme) => ({
searchInput: {
width: 0,
padding: 0,
border: "none",
},
expandedSearchInput: {
width: "5rem" // or, you can set it as per your need
// add other styles of expanded input
}
}));
function ExpandableSearchBar() {
const [isExpanded,setExpand] = useState(false);
const classes = useStyles();
function toggleSearchInput(){
setExpand(val => !val);
}
return (
<div className="component-wrapper">
<IconButton onClick={toggleSearchInput} aria-label="Search">
<SearchIcon />
</IconButton>
<input
className={isExpanded ? classes.expandedSearchInput : classes.searchInput}
type="text" />
</div>
);
}
Note - Have a look on the className of the <input/>. It will change to classes.expandedSearchInput when the isExpanded is set to true.

Autocomplete - How to put all values in one row

I need to put all the values and input field for multi autocomplete on one line. But the input field sliding down.
My code:
const { filter, classes, options } = this.props;
const style = filter && filter.value !== '' ? {backgroundColor: 'lavender'} : {};
return (
<TableFilterRow.Cell { ...this.props } className={ classes.cell } style={ style }>
<Autocomplete
options={options}
value={options.filter(option => filter.includes(option.value)) || []}
getOptionLabel={option => option.label}
multiple={true}
fullWidth
disableClearable={true}
onChange={this.handleFilter}
renderOption={option => (
<React.Fragment>
<Checkbox
color="primary"
checked={filter.includes(option.value) || false}
/>
{option.label}
</React.Fragment>
)}
renderTags={values => values.map(option => option.label).join(', ')}
renderInput={(params) => (
<TextField
{...params}
fullWidth
margin="dense"
/>
)}
/>
</TableFilterRow.Cell>
Result:
How can I put all the values and input field for multi autocomplete on one line?
Use style hook API to select input and apply flexWrap and overflow CSS properties:
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
root: {
"& > .MuiAutocomplete-root .MuiFormControl-root .MuiInputBase-root": {
flexWrap: "nowrap",
overflowX: "scroll" // or "hidden"
}
}
}));
then, on component use on className:
...
const classes = useStyles();
return (
<div className={classes.root}>
<Autocomplete
...
See this working on codesandbox here.

How do I provide a container for popover component?

I decided to use react-simple-popover for my popover needs. Documentation is here: https://github.com/dbtek/react-popover. The problem is that the documentation is for stateful components (class components). The container prop takes in this of the current class component, but I am using stateless component with hooks. What do I pass in place of the "this" keyword in this way?
import React, { useRef, useState } from "react";
import styled from "styled-components";
import Popover from "react-simple-popover";
import { HelpQuestion } from "../UI/Common";
const Wrapper = styled.div`
margin-bottom: 1rem;
`;
export const TextInput = styled.input`
border: 1px solid
${(props) => (props.dark ? "#37394B" : props.theme.lightGray)};
padding: 0.5rem 1rem;
width: 100%;
border-radius: 10px;
color: ${(props) => (props.dark ? "white" : "black")};
background-color: ${(props) => (props.dark ? "#37394B" : "white")};
`;
export default function TextField({
label,
value,
onChange,
onClick,
error,
type,
placeholder,
help,
dark,
disabled,
}) {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const popoverRef = useRef(null);
const componentRef = useRef(null);
return (
<Wrapper ref={componentRef}>
<div style={{ display: "flex", alignItems: "center" }}>
<TextInput
value={value}
onChange={!disabled ? onChange : () => {}}
onClick={onClick}
placeholder={placeholder}
type={type}
dark={dark}
disabled={disabled}
/>
{help && (
<div
style={{ marginLeft: "0.5rem" }}
onClick={() => setPopoverOpen(!isPopoverOpen)}
ref={popoverRef}
>
<HelpQuestion className="fas fa-question" />
</div>
)}
</div>
{error && <Error>{error}</Error>}
<Popover
placement="left"
container={componentRef.current} //doesnt work
target={popoverRef}
show={isPopoverOpen}
onHide={() => setPopoverOpen(false)}
>
<p>{help}</p>
</Popover>
</Wrapper>
);
}
How do I provide a container for popover component?
What do I pass in place of the "this" keyword in this way?
No, I don't think you can. View the source.
const Popover = props => {
if (
ReactDOM.findDOMNode(props.container) &&
ReactDOM.findDOMNode(props.container).parentElement.parentElement !==
document.body
) {
ReactDOM.findDOMNode(props.container).style.position = 'relative';
}
return (
<Overlay
show={props.show}
onHide={props.onHide}
placement={props.placement}
container={props.container}
target={p => ReactDOM.findDOMNode(props.target)}
rootClose={props.hideWithOutsideClick}
>
<PopoverContent
showArrow={props.showArrow}
arrowStyle={props.arrowStyle}
innerStyle={props.style}
style={props.containerStyle}
>
{props.children}
</PopoverContent>
</Overlay>
);
};
It doesn't appear to be well maintained and it's using ReactDOM.findDOMNode which is practically deprecated. I tried this and also tried a small class-based component wrapper. Nothing worked. Each time the reported error referred to the current ref value (componentRef.current) not being a react component.
For what its worth I suggest using a more functional component friendly Popover component. Here's an example using Material-UI's Popover component.
function TextField({
label,
value,
onChange,
onClick,
error,
type,
placeholder,
help,
dark,
disabled
}) {
const [isPopoverOpen, setPopoverOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const clickHandler = (e) => {
setPopoverOpen((open) => !open);
setAnchorEl(e.target);
};
return (
<Wrapper>
<div style={{ display: "flex", alignItems: "center" }}>
<TextInput
value={value}
onChange={!disabled ? onChange : () => {}}
onClick={onClick}
placeholder={placeholder}
type={type}
dark={dark}
disabled={disabled}
/>
{help && (
<div style={{ marginLeft: "0.5rem" }} onClick={clickHandler}>
<HelpOutlineIcon />
</div>
)}
</div>
{error && <Error>{error}</Error>}
<Popover
anchorEl={anchorEl}
open={isPopoverOpen}
onClose={() => setPopoverOpen(false)}
anchorOrigin={{
vertical: "center",
horizontal: "left"
}}
transformOrigin={{
vertical: "center",
horizontal: "right"
}}
>
<p>{help}</p>
</Popover>
</Wrapper>
);
}

Click on radio button and display the corresponding value inside the radio button as selected in react js material UI design

I am new to react js material UI design.
What I want is something like below.
If I clicked the first radio button , its value 1 should be displayed inside the selected radio button.
I could only implement the standalone radio button using the following.
import React from 'react';
import { withStyles } from '#material-ui/core/styles';
import { green } from '#material-ui/core/colors';
import Radio from '#material-ui/core/Radio';
const GreenRadio = withStyles({
root: {
color: green[400],
'&$checked': {
color: green[600],
},
},
checked: {},
})((props) => <Radio color="default" {...props} />);
export default function RadioButtons() {
const [selectedValue, setSelectedValue] = React.useState('a');
const handleChange = (event) => {
setSelectedValue(event.target.value);
};
return (
<div>
<Radio
checked={selectedValue === 'a'}
onChange={handleChange}
value="a"
name="radio-button-demo"
inputProps={{ 'aria-label': 'A' }}
/>
<Radio
checked={selectedValue === 'b'}
onChange={handleChange}
value="b"
name="radio-button-demo"
inputProps={{ 'aria-label': 'B' }}
/>
<GreenRadio
checked={selectedValue === 'c'}
onChange={handleChange}
value="c"
name="radio-button-demo"
inputProps={{ 'aria-label': 'C' }}
/>
<Radio
checked={selectedValue === 'd'}
onChange={handleChange}
value="d"
color="default"
name="radio-button-demo"
inputProps={{ 'aria-label': 'D' }}
/>
<Radio
checked={selectedValue === 'e'}
onChange={handleChange}
value="e"
color="default"
name="radio-button-demo"
inputProps={{ 'aria-label': 'E' }}
size="small"
/>
</div>
);
}
How can I get the value inside the selected button?
It is a bit of a hack but a combination of (s)css and custom data elements does the trick. First, I added a custom attribute with the value of the field to it (data-test, you will obviously have a better idea for a name than me):
<Radio
checked={selectedValue === "e"}
onChange={handleChange}
value="e"
data-test="e"
color="default"
name="radio-button-demo"
inputProps={{ "aria-label": "E" }}
size="small"
/>
Then I used this scss:
.Mui-checked {
position: relative;
.MuiSvgIcon-root + .MuiSvgIcon-root {
opacity: 0;
}
&::after {
content: attr(data-test);
position: absolute;
top: 4px;
}
}
Mui-checked is the class MaterialUI gives the element when it is checked. As this is a span, it can have pseudo elements. So basically I am accessing the custom data element and setting the content property to it's value. .MuiSvgIcon-root.PrivateRadioButtonIcon-layer-6 is the selector to get the default dot in the middle of the checkbox. This gets hidden.
You would have to play around with the styles a bit to fit your use case and you can find a demo here.
EDIT:
Here is the css, I also updated the selector to improve it's stability:
.App {
font-family: sans-serif;
text-align: center;
}
.Mui-checked {
position: relative;
}
.Mui-checked::after {
content: attr(data-test);
position: absolute;
top: 4px;
}
.MuiSvgIcon-root + .MuiSvgIcon-root {
opacity: 0;
}
Here is the solution I came up with: I built a custom component from ButtonBase.
It works fine but is less clever than #Gh05d. On the other hand it avoids writing global style or digging into Material-UI's internals:
const useStyle = makeStyles((theme) => ({
root: {
margin: 4,
borderRadius: "50%"
},
checked: {
height: 30,
width: 30,
backgroundColor: "#0a2",
color: "#fff",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: "bold"
},
notchecked: {
height: 26,
width: 26,
borderRadius: "50%",
border: "2px solid #00000066",
color: "#ffffff00"
}
}));
function MyButton(props) {
const { children, checked, ...rest } = props;
const classes = useStyle();
return (
<ButtonBase {...rest} className={classes.root}>
<div className={clsx(checked ? classes.checked : classes.notchecked)}>
{children}
</div>
</ButtonBase>
);
}
function App() {
const [selectedValue, setSelectedValue] = React.useState("a");
const handleChange = (letter) => (event) => {
setSelectedValue(letter);
};
return (
<div>
<MyButton
checked={selectedValue === "a"}
onClick={handleChange("a")}
value="a"
>
A
</MyButton>
<MyButton
checked={selectedValue === "b"}
onClick={handleChange("b")}
value="b"
>
B
</MyButton>
<MyButton
checked={selectedValue === "c"}
onClick={handleChange("c")}
value="c"
>
C
</MyButton>
</div>
);
}

Resources