material ui dialog automatically closes on pressing tab key - javascript

I have a react project and am using material-ui v3. I have an appBar which contains a menu with some menuItems, upon clicking the menuItem I'm opening a Dialog which contains a form, now everything seems good until I fill the first input box and press tab to switch to another and as soon as I press tab the dialog automatically closes.
Below are relevant code snippets.
header.js
<header>
<AppBar>
<Toolbar>
<Typography variant="title" color="inherit" className={classes.flex} component={Link} to='/'>
{appName}
</Typography>
<Avatar className={classes.orangeAvatar}>
<Button
color="primary"
aria-owns={anchorEl ? 'simple-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
>
{user && user.username[0] || "-"}
</Button>
</Avatar>
<Menu
id="simple-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
>
<ChangePassword
{...this.props}
>
{({ onClick }) => {
return (
<MenuItem onClick={onClick}>
Change password
</MenuItem>
);
}}
</ChangePassword>
<MenuItem onClick={async e => {
this.handleClose(e);
await window.localStorage.clear();
client.resetStore();
window.location.href = "/";
}}
>
<InputIcon className={classes.icon} /> Logout
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
</header>
ChangePassword.js
class ChangePassword extends React.PureComponent {
state = {
open: false,
};
handleClose = () => {
this.setState({ open: false });
};
handleOpen = () => {
this.setState({ open: true });
};
render() {
const { open } = this.state;
const {
classes,
history,
negativeHandler = e => this.handleClose(),
positiveHandler = e => null,
postMutation = e => null,
children
} = this.props;
const title = "Change password",
content = "Change password of this user.";
return (
<Mutation mutation={UPDATE_USER_PASSWORD}>
{(mutate, { loading, error, data }) => {
return (
<React.Fragment>
{children({ onClick: this.handleOpen })}
{
open ? (
<Dialog
fullScreen={false}
open={open}
onClose={negativeHandler}
aria-labelledby={title}
>
<Form
onSubmit={e => {
positiveHandler(mutate, e)
.then((data) => {
if (postMutation) {
postMutation(data);
this.handleClose(e);
}
else {
history.goBack()
}
})
}}
>
<DialogTitle id={title}>{title}</DialogTitle>
<DialogContent>
<DialogContentText>
{content}
</DialogContentText>
{
getFormJSX(defaults)
}
</DialogContent>
<DialogActions>
{
loading ? <CircularProgress className={classes.progress} /> : null
}
<Button onClick={negativeHandler} color="primary">Cancel</Button>
<Button size="large" type="submit" disabled={loading}>Confirm</Button>
</DialogActions>
</Form>
</Dialog>
) : null
}
</React.Fragment>
);
}}
</Mutation>
);
}}
export default withStyles(styles)(ChangePassword);
The getFormJSX(defaults) method simple generates dynamic form based upon defaults object, the return value only contains form controls and not the tag itself. Beside everything work fine on a regular form in the rest of my app except or other dialogs. This problem only occurs when the dialog is inside an menuItem which is inside a menu which is inside the appBar. Please let me know if I can provide anything thing else to support my question.

Related

test react form submit with conditions which button clicked

React beginner on testing, i'm testing form submits, i have tested already few form submits successfully, but this one has 'Accept' button AND 'Delete' button and both uses same 'handleSubmit' function so thats why i have conditions inside that function, depending on which clicked. I need some advice on how to test form submit when
”Accept” button is clicked ?
English is not my mother language so could be mistakes. If any question just ask me.
My test will pass only if i delete one of those buttons but i need both.
test:
import React from "react";
import {
render,
screen,
RenderResult,
cleanup,
fireEvent,
getByRole,
getByDisplayValue,
} from "#testing-library/react";
const onFormSubmit = jest.fn();
const currentCamera = "0-0-0-2";
describe("Testing component", () => {
const AddCamera = () =>
render(
<Provider store={store}>
<MemoryRouter>
<CameraForm onSubmit={onFormSubmit} key={"cameraform-" + currentCamera} />
</MemoryRouter>
</Provider>
);
test("Testing Add Camera", () => {
AddCamera();
const AddName = screen.getByTestId(/^AddName/i);
expect(AddName).toBeInTheDocument();
fireEvent.change(AddName, { target: { value: "Camera 2" } });
expect(AddName).toHaveValue("Camera 2");
fireEvent.submit(screen.getByTestId("renderAddForm"));
expect(onFormSubmit).toHaveBeenCalled();
});
});
code:
import {
...
} from "#material-ui/core/";
const handleSubmit = (event) => {
event.preventDefault();
if (state.button === 1) {
if (!Camera) {
return;
}
const { name} = state;
refresh();
dispatch(
addCamera(site.identifier, Camera.identifier, {
name
})
);
externalOnSubmit();
}
if (state.button === 2) {
event.preventDefault();
if (!Camera) {
return;
}
refresh();
dispatch(
deleteCamera(site.identifier, Camera.identifier)
);
externalOnSubmit();
}
};
const renderAdd = () => {
const { helperText, error, name } = state;
return (
<React.Fragment>
<Box sx={boxStyle} data-testid="CameraForm">
<form
data-testid="renderAddForm"
onSubmit={handleSubmit}>
<div className="handle">
<Box>
<Button
onClick={nulll}
aria-label="close-settings-popup">
<Close />
</Button>
</Box>
</div>
<div>
<FormGroup>
<Box>
<FormControl>
<TextField
helperText={helperText.name}
error={error.name}
inputProps={{
"data-testid": "AddName",
}}
InputProps={{
className: classes.underline,
}}
InputLabelProps={{
className: classes.inputLabelColor,
}}
required={true}
type="text"
id="name"
value={name}
onChange={handleChange}
label='Name'
></TextField>
</FormControl>
</Box>
</FormGroup>
</div>
{!state.readOnly ? (
<div>
<Button
onClick={() => (state.button = 2)}
type="submit"
>
<Trans i18nKey="form.delete">Delete</Trans>
</Button>
<Button
data-testid="submitButton"
onClick={() => (state.button = 1)}
type="submit"
variant="contained"
color="primary"
disabled={
!currentSite ||
Object.values(state.error).some((v) => {
return v === true;
})
}
className={classes.button_basic}
startIcon={<Done />}
>
Accept
</Button>
</div>
) : (
""
)}
</form>
</Box>
</React.Fragment>
);
};

React - Close MUI drawer from nested menu

I'm using this excellent example (Nested sidebar menu with material ui and Reactjs) to build a dynamic nested menu for my application. On top of that I'm trying to go one step further and put it into a Material UI appbar/temporary drawer. What I'd like to achieve is closing the drawer when the user clicks on one of the lowest level item (SingleLevel) however I'm having a tough time passing the toggleDrawer function down to the menu. When I handle the click at SingleLevel I consistently get a 'toggle is not a function' error.
I'm relatively new to this so I'm sure it's something easy and obvious. Many thanks for any answers/comments.
EDIT: Here's a sandbox link
https://codesandbox.io/s/temporarydrawer-material-demo-forked-v11ur
Code is as follows:
Appbar.js
export default function AppBar(props) {
const [drawerstate, setDrawerstate] = React.useState(false);
const toggleDrawer = (state, isopen) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setDrawerstate({ ...state, left: isopen });
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" color="secondary">
<Toolbar>
<IconButton
size="large"
edge="start"
color="primary"
aria-label="menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<img src={logo} alt="logo" />
</Toolbar>
<Drawer
anchor='left'
open={drawerstate['left']}
onClose={toggleDrawer('left', false)}
>
<Box>
<AppMenu toggleDrawer={toggleDrawer} />
</Box>
</Drawer>
</AppBar>
</Box >
)
}
Menu.js
export default function AppMenu(props) {
return MenuItemsJSON.map((item, key) => <MenuItem key={key} item={item} toggleDrawer={props.toggleDrawer} />);
}
const MenuItem = ({ item, toggleDrawer }) => {
const MenuComponent = hasChildren(item) ? MultiLevel : SingleLevel;
return <MenuComponent item={item} toggleDrawer={toggleDrawer} />;
};
const SingleLevel = ({ item, toggleDrawer }) => {
const [toggle, setToggle] = React.useState(toggleDrawer);
return (
<ListItem button onClick={() => { toggle('left', false) }}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} secondary={item.description} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
You shouldn't call a react hook inside of any function that is not a react component. Please see React Rules of Hooks
What you could do instead is pass setToggle directly into the Drawer component as a prop and do something like this for it's onClick attribute:
onClick={() => setToggle(<value>)}

How do I put a Button inside a Tab where indicator will stay in the correct tab?

How do I put a button inside the Tab? If I'll click Homepage, it does go in the correct tab with the correct line indicator. However, if I'll click "Profile", the indicator will go to the log out button. How do I correct this where the indicator will stay on the correct tab? This is what it looks like if I'll go to the Profile Tab, the indicator will directly go to the Logout button.
const Header = (props) => {
const { currentUser } = props;
const [value, setValue] = React.useState(0);
const [open, setOpen] = React.useState(false);
const handleChange = (event, newValue) => {
setValue(newValue);
};
// for the tab to stay on the correct path/page even if it was reloaded
useEffect(() => {
let path = window.location.pathname;
if (path === "/" && value !== 0) setValue(0);
else if (path === "/login" && value !== 1) setValue(1);
else if (path === "/registration" && value !== 2) setValue(2);
else if (path === "/profile" && value !== 2) setValue(2);
}, [value]);
const isMatch = useMediaQuery(theme.breakpoints.down("md"));
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<AppBar>
{/* */}
<Toolbar
variant="dense">
{isMatch ? (
<h1>
<div>
<MobileviewComponent />
</div>
</h1>
) : (
<div>
<Grid>
<Tabs
value={value}
onChange={handleChange}
>
{currentUser && (
<Tab
disableRipple
label="Homepage"
to="/"
component={Link}
/>
)}
{currentUser && (
<Tab
disableRipple
label="Profile"
to="/profile"
component={Link}
/>
)}
{currentUser && (
<Button color="inherit" onClick={handleClickOpen}>
Logout
</Button>
)}
{!currentUser && (
<Tab
disableRipple
label="Homepage"
to="/"
component={Link}
/>
)}
{!currentUser && (
<Tab
disableRipple
label="Login"
to="/login"
component={Link}
/>
)}
</Tabs>
</Grid>
</div>
)}
</Toolbar>
</AppBar>
</div>
);
};
you are setting state as 2 for both /registration and /profile so when you click on /profile tab value is set to 2 and I guess you are material-ui so the tabs count starts from 0, hence 2 indicates 3rd item in tabs and that is your logout button, hence it is highlighted

Sharing component states with useState() for active link in Nav

I'm trying to learn react and fairly new to the framework. I am trying to create a simple navbar component wih material-ui that is responsive (will show all links on medium devices and up, and open a side drawer on small devices). I have most of it setup to my liking, however, the issue I am currently having, is getting and setting the active link according to the page I am on.
It seems to works correctly on the medium devices and up, but when transitioning to a smaller device, the link is not updated correctly, as it will keep the active link from the medium screen set, while updating the side drawer active link.
Navbar.js
const Navbar = () => {
const classes = useStyles();
const pathname = window.location.pathname;
const path = pathname === '' ? '' : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
return (
<>
<HideNavOnScroll>
<AppBar position="fixed">
<Toolbar component="nav" className={classes.navbar}>
<Container maxWidth="lg" className={classes.navbarDisplayFlex}>
<List>
<ListItem
button
component={RouterLink}
to="/"
selected={selectedItem === ''}
onClick={event => handleItemClick(event, '')}
>
<ListItemText className={classes.item} primary="Home" />
</ListItem>
</List>
<Hidden smDown>
<List
component="nav"
aria-labelledby="main navigation"
className={classes.navListDisplayFlex}
>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === 'account/login'}
onClick={event => handleItemClick(event, 'account/login')}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/register"
selected={selectedItem === 'account/register'}
onClick={event => handleItemClick(event, 'account/register')}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</Hidden>
<Hidden mdUp>
<SideDrawer />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</HideNavOnScroll>
<Toolbar id="scroll-to-top-anchor" />
<ScrollToTop>
<Fab aria-label="Scroll back to top">
<NavigationIcon />
</Fab>
</ScrollToTop>
</>
)
}
SideDrawer.js
const SideDrawer = () => {
const classes = useStyles();
const [state, setState] = useState({ right: false });
const pathname = window.location.pathname;
const path = pathname === "" ? "" : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
const toggleDrawer = (anchor, open) => (event) => {
if (
event &&
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const drawerList = (anchor) => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer(anchor, false)}
onKeyDown={toggleDrawer(anchor, false)}
>
<List component="nav">
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/login"}
onClick={(event) => handleItemClick(event, "account/login")}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/register"}
onClick={(event) => handleItemClick(event, "account/register")}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</div>
);
return (
<React.Fragment>
<IconButton
edge="start"
aria-label="Menu"
onClick={toggleDrawer("right", true)}
>
<Menu fontSize="large" style={{ color: "white" }} />
</IconButton>
<Drawer
anchor="right"
open={state.right}
onClose={toggleDrawer("right", false)}
>
{drawerList("right")}
</Drawer>
</React.Fragment>
);
};
Code Sandbox - https://codesandbox.io/s/async-water-yx90j
I came across this question on SO: Is it possible to share states between components using the useState() hook in React?, which suggests that I need to lift the state up to a common ancestor component, but I don't quite understand how to apply this in my situation.
I would suggest to put aside for a moment your code and do a playground for this lifting state comprehension. Lifting state is the basic strategy to share state between unrelated components. Basically at some common ancestor is where the state and setState will live. there you can pass down as props to its children:
const Parent = () => {
const [name, setName] = useState('joe')
return (
<>
<h1>Parent Component</h1>
<p>Child Name is {name}</p>
<FirstChild name={name} setName={setName} />
<SecondChild name={name} setName={setName} />
</>
)
}
const FirstChild = ({name, setName}) => {
return (
<>
<h2>First Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Mary')}>My Name is Mary</button>
</>
)
}
const SecondChild = ({name, setName}) => {
return (
<>
<h2>Second Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Joe')}>My Name is Joe</button>
</>
)
}
As you can see, there is one state only, one source of truth. State is located at Parent and it passes down to its children. Now, sometimes it can be troublesome if you need your state to be located at some far GreatGrandParent. You would have to pass down each child until get there, which is annoying. if you found yourself in this situation you can use React Context API. And, for most complicated state management, there are solutions like redux.

Show hide multiple tooltips React material

I'm using React material framework in one of my projects. I'm trying to add multiple controlled tooltips which are going to be visible only when their respective state is visible.
Unfortunately, right now I'm stuck because I'm sharing the same state with multiple components hence all the tooltips are visible once you hover on any one of them. Is there any way to do so ? I think this can be done by array.
P.S there are going to be multiple parent components inside a page each having three set of tooltip i.e Edit, Delete, View
class ControlledTooltips extends React.Component {
state = {
open: false,
};
handleTooltipClose = () => {
this.setState({ open: false });
};
handleTooltipOpen = () => {
this.setState({ open: true });
};
render() {
return (
<div>
<Tooltip
enterDelay={300}
id="tooltip-controlled"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="Edit"
>
<IconButton aria-label="Delete">
<Edit />
</IconButton>
</Tooltip>
<Tooltip
enterDelay={300}
id="tooltip-controlled"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="view"
>
<IconButton aria-label="view">
<Visibility />
</IconButton>
</Tooltip>
<Tooltip
enterDelay={300}
id="tooltip-controlleded"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="Delete"
>
<IconButton aria-label="Delete">
<DeleteOutlined />
</IconButton>
</Tooltip>
</div>
);
}
}
codesandbox link
Any help will be appreciated :)
I would advise against complicating your component state too much. In my opinion, each component should control a very precise part of its state.
What I would recommend is to create a custom tooltip that will handle the state for each element. You can build your 3 Edit, Delete, View width them.
class TooltipCustom extends React.Component {
state = {
open: false
};
toggleState = () => {
this.setState({ open: !this.state.open });
};
render() {
return (
<IconButton aria-label={this.props.title}>
<Tooltip
enterDelay={300}
leaveDelay={300}
onClose={this.toggleState}
onOpen={this.toggleState}
open={this.state.open}
placement="bottom"
title={this.props.title}
>
{this.props.children}
</Tooltip>
</IconButton>
);
}
}
const Delete = () => (
<TooltipCustom title="delete">
<DeleteIcon />
</TooltipCustom>
);
const Edit = () => (
<TooltipCustom title="Edit">
<EditIcon />
</TooltipCustom>
);
const View = () => (
<TooltipCustom title="View">
<VisibilityIcon />
</TooltipCustom>
);
const ControlledTooltips = () => (
<div>
<Delete />
<Edit />
<View />
</div>
);
In case someone is looking for an answer. As suggested by #Einar Ólafsson, I made a custom tooltip wrapper which had all three tooltips inside it.
Tooltip name which was needed to be shown was passed to handleTooltipOpen() and handleTooltipClose() function. Inside this function, I changed the state of the individual tooltip.
class ControlledTooltips extends React.Component {
state = {
edit: false,
delete: false,
view: false
};
handleTooltipClose = (name) => {
this.setState({ [name]: false });
};
handleTooltipOpen = (name) => {
this.setState({ [name]: true });
};
render() {
return (
<div>
<Tooltip
id="tooltip-controlled-delete"
onClose={() => this.handleTooltipClose("delete")}
onOpen={() => this.handleTooltipOpen("delete")}
open={this.state.delete}
placement="bottom"
title="Delete"
>
<IconButton name="delete" aria-label="Delete">
<DeleteOutlined name="delete" />
</IconButton>
</Tooltip>
<Tooltip
id="tooltip-controlled-edit"
onClose={() => this.handleTooltipClose("edit")}
onOpen={() => this.handleTooltipOpen("edit")}
open={this.state.edit}
placement="bottom"
title="edit"
>
<IconButton name="edit" aria-label="edit">
<Edit />
</IconButton>
</Tooltip>
<Tooltip
id="tooltip-controlled-view"
onClose={() => this.handleTooltipClose("view")}
onOpen={() => this.handleTooltipOpen("view")}
open={this.state.view}
placement="bottom"
title="view"
>
<IconButton name="view" aria-label="view">
<Visibility />
</IconButton>
</Tooltip>
</div>
);
}
}

Categories

Resources