Material-UI - How to change transition of Customized Snackbar to Slide - reactjs

I'd like to change the transition of snackbar to Slide instead of Grow (the default behaviour), but I can't do that since I'm using snackbar with Alert.
This is the original demo from Material-UI:
https://codesandbox.io/s/e1dks
If I import this:
import Slide from '#material-ui/core/Slide';
import { TransitionProps } from '#material-ui/core/transitions';
Create this function:
function SlideTransition(props: TransitionProps) {
return <Slide {...props} direction="up" />;
}
And insert this attribute on Snackbar tag:
TransitionComponent={SlideTransition}
I have the error:
Cannot read property 'getBoundingClientRect' of null
Take a look the error when I try to use Snackbar with Alert and Slide at the same time
https://codesandbox.io/s/material-demo-ysub3
At https://material-ui.com/api/slide/ there is a warning that can help, but I didn't understand this:
A single child content element. ⚠️ Needs to be able to hold a ref.
I'm using React with Typescript.

Looking at your example, there is an error in the console:
Warning: Failed prop type: Invalid prop `children` supplied to `ForwardRef(Slide)`. Expected an element that can hold a ref. Did you accidentally use a plain function component for an element instead? For more information see https://material-ui.com/r/caveat-with-refs-guide
Following the "more information" link, it advises that you'll need to wrap your "plain function component" in React.forwardRef.
This results in the changing the Alert function from:
function Alert(props: AlertProps) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
to
const Alert = React.forwardRef((props, ref) => <MuiAlert elevation={6} variant="filled" {...props} ref={ref} />);
Once this change has been made, the code works as expected - with the alert sliding in from the bottom rather than popping into view.

Related

Passing empty props changes the Button color in ButtonGroup

I'm using a customized IconButton element with ButtonGroup.
While passing an empty props, the color and hover shape of element is also changed. I want to know why did this happen? For me, an HOC shouldn't have different render effect with an empty prop passed.
I hosted a minimal environment with GitHub Pages. The question is shown on the second and third chapter.
Using a new customized component.
Misbehavior: buttons are white and not in group.
See the hover highlight of second and third "G"
The component is based on IconButton without passing {...props}.
Using a new customized component. All good.
The component is also based on IconButton but passing {...props}.
Although the props is not passed, hence should be empty(?).
Maybe useStyles changed color, but the hover background shape is changed too.
Also I made a related issue on official repo, and in my Learning note
It's the opposite, if you don't pass the props to IconButton:
function IconButtonWrapper(props /* unused props */) {
return (
<IconButton>
<DeleteIcon />
</IconButton>
);
}
Then no styles are applied and the IconButton appears in plain white, so nothing changes.
Although the props is not passed
The props are passed automatically from the ButtonGroup, even if you don't pass anything. The way it works is that ButtonGroup clones the child component and pass its own set of props to them, so when you don't pass anything yourself like this:
<ButtonGroup variant="contained">
<IconButtonWrapper />
<IconButtonWrapper />
<IconButtonWrapper />
</ButtonGroup>
The child component still has props provided by the parent, you can see it by logging the props:
function IconButtonWrapper(props) {
console.log(props);
return (
<IconButton {...props}>
<DeleteIcon />
</IconButton>
);
}

Using Stateful React classes in typescipt

I am trying to create a Stateful class in which you can call methods such as createHeaderButton() where after calling it would update the state and re-render with these new updates in the component.
Im using Material-UI and so most of their styling utilizes Reacts hook API which of course classes cant use. Ive tried to get around this by using;
export default withStyles(useStyles)(HeaderBar)
Which exports the class separately with the Styles(withStyles(useStyles) useStyles as the defined styles) And the class(HeaderBar). Now the only issue is that i need to access the styles in my class. Ive found a JS example online that wont work for me because of the strong typed syntax of TS. Additionally When initializing my Class component in other places i try to get the ref=(ref:any)=>{} And with that call the create button methods when i get a response from my server, Which doesnt work because of this new way of exporting the class component!
Thanks for the help, Heres my component class: https://pastebin.pl/view/944070c7
And where i try to call it: https://pastebin.com/PVxhKFHJ
My personal opinion is that you should convert HeaderBar to a function component. The reason that it needs to be a class right now is so you can use a ref to call a class method to modify the buttons. But this is not a good design to begin with. Refs should be avoided in cases where you can use props instead. In this case, you can pass down the buttons as a prop. I think the cleanest way to pass them down is by using the special children prop.
Let's create a BarButton component to externalize the rendering of each button. This is basically your this.state.barButtons.forEach callback, but we are moving it outside of the HeaderBar component to keep our code flexible since the button doesn't depend on the HeaderBar (the header bar depends on the buttons).
What is a bar button and what does it need? It needs to have a label text and a callback function which we will call on click. I also allowed it to pass through any valid props of the material-ui Button component. Note that we could have used children instead of label and that's just down to personal preference.
You defined your ButtonState as a callback which takes the HTMLButtonElement as a prop, but none of the buttons shown here use this prop at all. But I did leave this be to keep your options open so that you have the possibility of using the button in the callback if you need it. Using e.currentTarget instead of e.target gets the right type for the element.
import Button, {ButtonProps as MaterialButtonProps} from "#material-ui/core/Button";
type ButtonState = (button: HTMLButtonElement) => void;
type BarButtonProps = {
label: string;
callback: ButtonState;
} & Omit<MaterialButtonProps, 'onClick'>
const BarButton = ({ label, callback, ...props }: BarButtonProps) => {
return (
<Button
color="inherit" // place first so it can be overwritten by props
onClick={(e) => callback(e.currentTarget)}
{...props}
>
{label}
</Button>
);
};
Our HeaderBar becomes a lot simpler. We need to render the home page button, and the rest of the buttons will come from props.childen. If we define the type of HeaderBar as FunctionComponent that includes children in the props (through a PropsWithChildren<T> type which you can also use directly).
Since it's now a function component, we can get the CSS classes from a material-ui hook.
const useStyles = makeStyles({
root: {
flexGrow: 1
},
menuButton: {
marginRight: 0
},
title: {
flexGrow: 1
}
});
const HeaderBar: FunctionComponent = ({ children }) => {
const classes = useStyles();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<HeaderMenu classes={classes} />
<Typography variant="h6" className={classes.title}>
<BarButton
callback={() => renderModule(<HomePage />)}
style={{ color: "white" }}
label="Sundt Memes"
/>
</Typography>
{children}
</Toolbar>
</AppBar>
</div>
);
};
Nothing up to this point has used state at all, BarButton and HeaderBar are purely for rendering. But we do need to determine whether to display "Log In" or "Log Out" based on the current login state.
I had said in my comment that the buttons would need to be stateful in the Layout component, but in fact we can just use state to store an isLoggedIn boolean flag which we get from the response of AuthVerifier (this could be made into its own hook). We decide which buttons to show based on this isLoggedIn state.
I don't know what this handle prop is all about, so I haven't optimized this at all. If this is tied to renderModule, we could use a state in Layout to store the contents, and pass down a setContents method to be called by the buttons instead of renderModule.
interface LayoutProp {
handle: ReactElement<any, any>;
}
export default function Layout(props: LayoutProp) {
// use a state to respond to an asynchronous response from AuthVerifier
// could start with a third state of null or undefined when we haven't gotten a response yet
const [isLoggedIn, setIsLoggedIn] = useState(false);
// You might want to put this inside a useEffect but I'm not sure when this
// needs to be re-run. On every re-render or just once?
AuthVerifier.verifySession((res) => setIsLoggedIn(res._isAuthenticated));
return (
<div>
<HeaderBar>
{isLoggedIn ? (
<BarButton
label="Log Out"
callback={() => new CookieManager("session").setCookie("")}
/>
) : (
<>
<BarButton
label="Log In"
callback={() => renderModule(<LogInPage />)}
/>
<BarButton
label="Sign Up"
callback={() => renderModule(<SignUpPage />)}
/>
</>
)}
</HeaderBar>
{props.handle}
</div>
);
}
I believe that this rewrite will allow you to use the material-ui styles that you want as well as improving code style, but I haven't actually been able to test it since it relies on so many other pieces of your app. So let me know if you have issues.

React Material UI Tooltips Disable Animation

I'm using React Material UI's Tooltip Component in my React application.
import Tooltip from "#material-ui/core/Tooltip";
...
...
<Tooltip title="Add" arrow>
<Button>Arrow</Button>
</Tooltip>
...
...
I want to disable the entry and exit animations. How can I achieve this in the latest version
You can use the TransitionComponent and the TransitionProps to solve this.
Use the Fade Transition component with timeout: 0 as the properties for the transition component:
import Tooltip from "#material-ui/core/Tooltip";
import Fade from "#material-ui/core/Fade";
...
<Tooltip
title="Add"
arrow
TransitionComponent={Fade}
TransitionProps={{ timeout: 0 }}
>
<Button>Arrow</Button>
</Tooltip>
Just disable/mock the transition component.
ie: render automatically the children like this:
const FakeTransitionComponent = ({ children }) => children;
<Tooltip
title="tooltip title"
TransitionComponent={FakeTransitionComponent}
// or TransitionComponent={({ children}) => children}
>
<h1>Hello CodeSandbox</h1>
</Tooltip>
Here is a codesandbox demo
I've used Incepter's solution, that is clean. If anyone is looking for a TypeScript solution here it is.
const FakeTransitionComponent = React.forwardRef<
HTMLDivElement,
TransitionProps & { children?: React.ReactElement<any, any> }
>(
(
{
appear,
onEnter,
onEntered,
onEntering,
onExit,
onExited,
onExiting,
...props
},
ref
) => {
props.in = undefined;
return <div {...props} ref={ref}></div>;
}
);
TransitionProps are not passed to the wrapper element, because they would all cause React warnings.
You can just pass React.Fragment as TransitionComponent
<Tooltip
title="tooltip title"
TransitionComponent={React.Fragment}
>
<h1>Hello CodeSandbox</h1>
</Tooltip>
Best option is to create a simple NoTransition component:
export const NoTransition = React.forwardRef<
React.ReactFragment,
TransitionProps
// eslint-disable-next-line #typescript-eslint/no-unused-vars
>(({ children }, ref) => {
return <>{ children }</>;
});
And do TransitionComponent={ NoTransition }.
Also, do not omit the ref param or you will get a warning too:
Warning: forwardRef render functions accept exactly two parameters: props and ref. Did you forget to use the ref parameter?
Just adding TransitionProps={{ timeout: 0 }}, without setting TransitionComponent, will also work, but there's no need to render the default TransitionComponent in that case.
Some of the options proposed in other answers will throw errors or warnings:
Doing TransitionComponent={ Fragment } will result in the following warning:
Warning: Invalid prop appear supplied to React.Fragment. React.Fragment can only have key and children props.
Doing TransitionComponent={ ({ children }) => children } will result in this other warnings (might change a bit depending if you are using a Tooltip, Modal or other component):
Warning: Failed prop type: Invalid prop children supplied to ForwardRef(Modal). Expected an element that can hold a ref. Did you accidentally use a plain function component for an element instead? For more information see https://mui.com/r/caveat-with-refs-guide
Warning: Failed prop type: Invalid prop children supplied to ForwardRef(ModalUnstyled). Expected an element that can hold a ref. Did you accidentally use a plain function component for an element instead? For more information see https://mui.com/r/caveat-with-refs-guide
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Check the render method of Unstable_TrapFocus.
And potentially also this error:
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
I'm using "#mui/material": "^5.5.2".

How to use override Button using Box component in Material-UI?

I've been trying to understand and write code on the Box component in material-UI. (https://material-ui.com/components/box/#box)
I've been trying to override a Button component the two ways it describes in the documentation, but I have no idea how. When I run the code segment using both methods, the button appears but no color change. Then when I try to add an extra Button underneath the clone element code segment I get an error saying 'Cannot read property 'className' of undefined'.
<Box color="primary" clone>
<Button>Click</Button>
<Button>Click</Button>
</Box>
When I add a Button component underneath in the second render props way, the first button just disappears from the DOM completely.
<Box color="secondary">
{props => <Button {...props} > Click </Button>}
<Button color="secondary">Click</Button>
</Box>
Would appreciate an explanation of how overriding underlying DOM elements work.
There are a few issues with the code you've shown in your question.
primary and secondary are not valid colors within the palette. They are valid options for the color prop of Button, but here you are trying to reference colors within the theme's palette object. For this purpose, you need primary.main and secondary.main (which is what Button uses when you specify <Button color="primary">).
Box only supports a single child when using the clone property and it only supports a single child when using the render props approach. In both of your examples you have two children.
Here is the Material-UI source code that deals with the clone option:
if (clone) {
return React.cloneElement(children, {
className: clsx(children.props.className, className),
...spread,
});
}
This is creating a new child element that combines the className generated by Box with any existing class name on the child. It gets at this existing class name via children.props.className, but when there are multiple children then children will be an array of elements and will not have a props property so you get the error:
Cannot read property 'className' of undefined
Here is the Material-UI source code that deals with the render props approach:
if (typeof children === 'function') {
return children({ className, ...spread });
}
When you have more than one child, then typeof children === 'function' will not be true and it won't use the render props approach. In this case, both children just get normal react rendering and trying to render a function doesn't render anything.
Below is a working example that fixes all of these problems by using a single Button child in the clone case and a single function child in the render props case (a function that then renders two Button elements).
import React from "react";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
export default function App() {
return (
<>
<Box color="primary.main" clone>
<Button>Click</Button>
</Box>
<Box color="secondary.main">
{props => (
<>
<Button {...props}> Click </Button>
<Button color="secondary">Click</Button>
</>
)}
</Box>
</>
);
}

Material-ui's Switch component onChange handler is not firing

I've put some Switches in an app and they work fine. Then I put the Switches in another app, but they don't work when clicked.
Both apps are using the same component. Here it is working in one app:
And here's the other app, not working:
In the second app, the onChange handler doesn't seem to ever fire.
The code in both apps looks like the following:
<Switch
checked={(console.log('checked:', status === 'visible'), status === 'visible')}
onChange={(e, c) => console.log('event:', e, c)}
/>
In the first app I see the output of those console.logs, while in the second app I only see the initial console.log of the checked prop, but I never see any of the onChange prop.
I checked if any ancestor elements have click handlers, and I didn't find any that are returning false, calling stopPropagation, or calling preventDefault.
Notice in the gif that when I click, the ripple effect still works, so click handling is obviously still working.
Any ideas why onChange may not be firing?
UPDATE! I replaced the switches with regular <input type="checkbox"> elements, and it works great! See:
Looks to me like something is wrong with material-ui's <Switch> component. I have a hunch that I will investigate when I get a chance: there might be more than one React singleton in the application. I'll be back to post an update.
I think, this is a weird fix and it is working smoothly for me. So, instead of handleChange I am using handleClick. I am not using event here, instead I am passing a string which is obviously the name of the state or id in case of arrays.
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
handleToggle = (name: string) => {
this.setState({ active: !this.state.active });
};
I tried handleChange, but the problem still persists. I hope this will get fixed soon.
I've had the same issue with Checkbox and Switch in the WordPress admin area.
Turns out, there was global CSS rule like:
input[type="checkbox"] {
height: 1rem;
width: 1rem;
}
Clicking the upper left corner of the element works, though.
As a solution, I reset some styles in my app root.
EDIT: Nevermind, I just put my whole app into shadow DOM. There are a few gotchas, I'll list them here:
You have to provide a custom insertion point for Material-UI style elements inside the shadow DOM. In general, you have to make nothing gets put outside of you shadow DOM.
You have to load/link the font inside the shadow DOM and outside the shadow DOM (the light DOM).
Use ScopedCssBaseline instead of the global reset.
Dialogs have to have their container prop specified.
This is how I've set things up with Material-UI:
// configure-shadow-dom.js
import { create } from 'jss';
import { jssPreset } from '#material-ui/core/styles';
const shadowHostId = 'my-app-root-id'
export const appRoot = document.createElement('div')
appRoot.setAttribute('id', 'app-root')
const styleInsertionPoint = document.createComment('jss-insertion-point')
export const jss = create({
...jssPreset(),
insertionPoint: styleInsertionPoint,
})
const robotoFontLink = document.createElement('link')
robotoFontLink.setAttribute('rel', 'stylesheet')
robotoFontLink.setAttribute('href', 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap')
const shadowHost = document.getElementById(shadowHostId)
shadowHost.attachShadow({ mode: 'open' })
const shadowRoot = shadowHost.shadowRoot
shadowRoot.appendChild(robotoFontLink)
shadowRoot.appendChild(styleInsertionPoint)
shadowRoot.appendChild(appRoot)
// index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import ScopedCssBaseline from '#material-ui/core/ScopedCssBaseline';
import { StylesProvider } from '#material-ui/core/styles';
import App from './App';
import { jss, appRoot } from './configure-shadow-dom';
ReactDOM.render(
<React.StrictMode>
<StylesProvider jss={jss}>
<ScopedCssBaseline>
<App />
</ScopedCssBaseline>
</StylesProvider>
</React.StrictMode>,
appRoot
);
It turns out that in my case, there was a CSS in the page, something like
.some-component { pointer-events: none; }
.some-component * { pointer-events: auto; }
where .some-component was containing my material-ui buttons and switches. I had to manually set pointer-events to all (or some value, I don't remember at at the moment) for the elements inside the switches.
So, that's one thing to look out for: check what pointer-events style is doing.
in Switch component, changing onChange to onClick worked for me:
<Switch
checked={this.state.active}
onClick={() => this.handleToggle('active')}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>
still running into similar issues getting an event from the switch. here's a quick fix using internal state (you should be using hooks now and functional components, if not you are missing out, but you can use setState to do what i'm showing here in a class component.)
const [switchChecked, setSwitchChecked] = useState(false);
.......
<Switch checked={switchChecked} onChange={() => {setSwitchChecked(!switchChecked);}}/>
you'll need to have a value for switchChecked in the components local state.
Try this one checked or unchecked passed as a second argument.
<Switch
checked={this.state.active}
onClick={(e,flag) => this.handleToggle('active', flag)}
value="active"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/>```
The issue is that you have to sniff the checked attribute from the event: event.target.checked
Full solution:
import { Switch } from '#material-ui/core';
import { useState } from 'react';
function App() {
const [checked, setChecked] = useState(false);
const switchHandler = (event) => {
//THIS IS THE SOLUTION - use event.target.checked to get value of switch
setChecked(event.target.checked);
};
return (
<div>
<Switch checked={checked} onChange={switchHandler} />
</div>
);
}
export default App;

Resources