Using chakra-ui ToolTip with a floating button - reactjs

I am attempting to create a floating "emergency exit" button for my React typescript application which will immediately take the user to weather.com. I'm having no trouble creating the button, but the requirements call for a tooltip when hovering over the button. Since we use chakra-ui throughout the product, using the Tooltip component they provide seems natural to me.
My first attempt looks like this:
Button.tsx
import React from "react";
import { Button as ChakraButton, ButtonProps } from "#chakra-ui/react";
interface Props extends ButtonProps {
buttonColor: string;
}
const Button: React.FC<Props> = ({
buttonColor,
children,
...restProps
}: Props) => (
<ChakraButton
backgroundColor={buttonColor}
color="white"
_hover={{
background: buttonColor
}}
_active={{
background: buttonColor
}}
padding="15px 30px"
height="auto"
fontSize="sm"
minWidth="200px"
borderRadius="100px"
fontFamily="AvenirBold"
{...restProps}
>
{children}
</ChakraButton>
);
export default Button;
EmergencyExitButton.tsx
import styled from "#emotion/styled";
import React from "react";
import Button from "./Button";
import { Tooltip } from "#chakra-ui/react";
const StyledButton = styled(Button)`
z-index: 99999;
position: fixed;
margin-left: calc(50% - 100px);
margin-top: 5px;
`;
export const EmergencyExitButton: React.FC = ({ children }) => {
const handleClick = () => {
window.open("https://weather.com", "_self");
};
return (
<>
<Tooltip
width="100%"
label="Immediately exit to the Weather Channel. Unsaved changes will be lost."
placement="bottom"
bg="black"
color="white"
>
<StyledButton buttonColor="#CC0000" onClick={handleClick}>
Emergency Exit
</StyledButton>
</Tooltip>
{children}
</>
);
};
When I insert this button into the application and hover over it, the tooltip appears in the top left corner of the screen and doesn't go away when you move the pointer away from the button. (codesandbox: https://codesandbox.io/s/objective-rain-z5szs7)
After consulting the chakra-ui documentation on Tooltip, I realized that I should be using a forwardRef for the wrapped component, so I modified EmergencyExitButton to look like this:
import * as React from "react";
import Button from "./Button";
import { Tooltip } from "#chakra-ui/react";
const EmergencyButton = React.forwardRef<HTMLDivElement>((props, ref) => {
const handleClick = () => {
window.open("https://weather.com", "_self");
};
return (
<div
ref={ref}
style={{
zIndex: 99999,
position: "fixed",
marginLeft: "calc(75% - 100px)",
marginTop: "5px"
}}
>
<Button buttonColor="#CC0000" onClick={handleClick}>
EmergencyExit
</Button>
</div>
);
});
EmergencyButton.displayName = "EmergencyButton";
export const EmergencyExitButton: React.FC = ({ children }) => (
<>
<Tooltip
width="100%"
label="Immediately exit to the Weather Channel. Unsaved changes will be lost."
placement="bottom"
bg="black"
color="white"
hasArrow
style={{ zIndex: 99999 }}
>
<EmergencyButton />
</Tooltip>
{children}
</>
);
In this iteration, the tooltip doesn't appear at all. (codesandbox: https://codesandbox.io/s/kind-voice-i230ku)
I would really appreciate any advice or ideas on how to make this work.
Edited to fix the code a little.

I figured it out. It turns out that instead of creating a forwardRef, I just needed to wrap the button in a span tag.
import React from 'react';
import Button from './Button';
import { Tooltip } from '#chakra-ui/react';
export const EmergencyExitButton: React.FC = ({ children }) => {
const handleClick = () => {
window.open('https://weather.com', '_self');
};
return (
<>
<Tooltip
width='100%'
label='Immediately exit to the Weather Channel. Unsaved changes will be lost.'
placement='bottom'
bg='black'
color='white'
>
<span style={{ zIndex: 99999, position: 'fixed', marginLeft: 'calc(50% - 100px)', marginTop: '5px'}}>
<Button buttonColor='#CC0000' onClick={handleClick}>Emergency Exit</Button>
</span>
</Tooltip>
{children}
</>
);
};

I think the prop shouldWrapChildren could be used in this case.
See https://chakra-ui.com/docs/components/tooltip/props

Related

Add a tooltip to MUI Badge content?

I want to add a tooltip to my MUI Badge component.
I tried wrapping the badge with a ToolTip component from MUI but tooltip text also displays when the children are hovered, I'd like it to only appear when the Badge itself is hovered.
I have also tried using the primitive title prop on the badge component but this has the same issue.
Does anyone know of a better way to add a tooltip to a Badge component?
my usage:
<Badge
title={'Click to view more info'} // not ideal as the tooltip shows when the children are hovered too
badgeContent={getTotalVulnerabilitiesCount()}
showZero={false}
>
{children}
</Badge>
You're very close, badgeContent prop also accepts a ReactNode so you can put the Badge content inside a Tooltip without affecting the other component:
<Badge
color="primary"
badgeContent={
<Tooltip title="Delete">
<span>1</span>
</Tooltip>
}
>
<MailIcon color="action" />
</Badge>
I ended up building my own badge component, its not too long either so good solution imo. If anyone has feedback for the code please let me know :)
import React from 'react';
import { makeStyles, Tooltip } from '#material-ui/core';
const useStyles = makeStyles({
badgeStyles: {
minHeight: '24px',
minWidth: '24px',
position: 'absolute',
top: '-12px',
left: 'calc(100% - 12px)',
color: 'white',
borderRadius: '50%',
backgroundColor: 'tomato',
padding: '3px',
fontSize: '.75rem'
}
});
const Badge = props => {
const {
children,
showZero,
...badgeContentProps
} = props;
return (
<span>
{children}
{
(showZero || props.badgeContent !== 0) && (
<BadgeComponent {...badgeContentProps}/>
)
}
</span>
);
};
const BadgeComponent = props => {
const classes = useStyles();
const {
badgeContent,
badgeClasses,
onClick,
tooltipText,
tooltipPlacement
} = props;
// If no tooltiptext provided render without Tooltip
if(tooltipText == null) return (
<span
className = {`${badgeClasses ?? ''} ${classes.badgeStyles}`}
onClick={onClick ? onClick : undefined}
>
{badgeContent}
</span>
);
// Render with Tooltip
return (
<Tooltip title={tooltipText} placement={tooltipPlacement}>
<span
className = {`${badgeClasses} ${classes.notifyCount}`}
onClick={onClick ? onClick : undefined}
>
{badgeContent}
</span>
</Tooltip>
);
};
export default Badge;

Material-ui Backdrop not covering buttons and other elements

How do I get the Backdrop to cover the button?
No matter what I do, the buttons appear as if they were above the backdrop, I can't them move them behind it.
See code:
import React from "react";
import { Backdrop, Button } from "#material-ui/core";
import CircularProgress from "#material-ui/core/CircularProgress";
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({});
export default function App() {
const classes = useStyles();
const [test, setTest] = React.useState(true);
return (
<div className={classes.parent}>
<Backdrop className={classes.backdrop} open={test}>
<CircularProgress color="inherit" />
</Backdrop>
<Button
variant="contained"
color="secondary"
onClick={() => {
setTest(test ? false : true);
}}
>
I should be behind the backdrop (click me)
</Button>
</div>
);
}
You need to give the backdrop a higher z-index
const useStyles = makeStyles((theme) => ({ backdrop: { zIndex: theme.zIndex.drawer + 1, color: '#fff', }, }));
This should fix it, just check the docs.

get child state in stateless component using hooks

README:
Hi! I'm new to using React, and even newer to using hooks so please correct me if my verbiage is incorrect. In fact, I was struggling to even google my issue/come up with a title for this post - how would I best put this problem into words?
Question :
I have a root component which contains a table in its state, and I'm using Material UI and react-csv to create a NavBar with a save button that can save the table. Material UI makes use of hooks; I know if my NavBar component was stateful I could write data={this.props.table} to get the table, but I was wondering how I would download the table given the current framework? Is it possible?
CodePen: https://codesandbox.io/embed/old-dust-88mrp
Root Component:
import React from "react";
import ReactDOM from "react-dom";
import NavBar from "./NavBar";
import "./styles.css";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
table: "this is a table"
};
}
render() {
return (
<div>
<NavBar />
<div>{this.state.table}</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
NavBar:
[I tried to simplify code as much as possible!]
import React from "react";
import { withStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import IconButton from "#material-ui/core/IconButton";
import Menu from "#material-ui/core/Menu";
import MenuItem from "#material-ui/core/MenuItem";
import ListItemText from "#material-ui/core/ListItemText";
import SaveIcon from "#material-ui/icons/Save";
import Tooltip from "#material-ui/core/Tooltip";
import { CSVLink } from "react-csv";
const StyledMenu = withStyles({
paper: {
border: "1px solid #d3d4d5"
}
})(props => (
<Menu
elevation={0}
getContentAnchorEl={null}
anchorOrigin={{
vertical: "bottom",
horizontal: "center"
}}
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
{...props}
/>
));
const StyledMenuItem = withStyles(theme => ({
root: {
"&:focus": {
backgroundColor: theme.palette.primary.main,
"& .MuiListItemIcon-root, & .MuiListItemText-primary": {
color: theme.palette.common.white
}
}
}
}))(MenuItem);
export default function PrimarySearchAppBar() {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = event => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div>
<AppBar position="static">
<Toolbar>
<div>
<Tooltip disableFocusListener title="Save">
<IconButton size="medium" onClick={handleClick} color="inherit">
<SaveIcon />
</IconButton>
</Tooltip>
<StyledMenu
id="customized-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
<StyledMenuItem>
{/* In stateful components I could put this.props.table here,
but how does this translate to a stateless component? */}
<CSVLink data={"this is a test"}>
<ListItemText primary="Data" />
</CSVLink>
</StyledMenuItem>
</StyledMenu>
</div>
</Toolbar>
</AppBar>
</div>
);
}
Thanks for any advice/help!!
<NavBar table={this.state.table}/>
export default function PrimarySearchAppBar({table}) {
<CSVLink data={table}>
}

Button components inside ButtonGroup

I'm using React 16.9.0 and Material-UI 4.4.2 and I'm having the following issue.
I want to render a ButtonGroup with Button elements inside it but these buttons come from other custom components which return a Button render with a Modal view linked to the button. Thing is, I can't make them look like a ButtonGroup with the same style since it seems like the Button elements only take the "grouping" styling but not the "visual" styling.
Example code to reproduce behaviour:
<ButtonGroup variant="outlined">
<AModal/>
<BModal/>
<CModal/>
</ButtonGroup>
As you can see, the render output does not look as expected. Bare in mind that I'm defining the buttons with the outlined variant since if not they just render as Text Buttons.
Any help is much appreciated
Adding AModal as requested:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import { Button } from '#material-ui/core';
import Modal from '#material-ui/core/Modal';
import Backdrop from '#material-ui/core/Backdrop';
import Fade from '#material-ui/core/Fade';
import InnerModalComponent from './InnerModalComponent';
const useStyles = makeStyles((theme) => ({
modal: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
paper: {
backgroundColor: theme.palette.background.paper,
border: '2px solid #000',
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3),
},
}));
export default function AModal() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
function handleOpen() {
setOpen(true);
}
function handleClose() {
setOpen(false);
}
return (
<div>
<Button variant="contained" onClick={handleOpen}> A </Button>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
className={classes.modal}
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{ timeout: 500 }}
>
<Fade in={open}>
<div className={classes.paper}>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
justifyContent: 'center',
}}
>
<InnerModalComponent/>
</div>
<Button variant="contained" color="secondary" style={{ marginTop: '10px' }}> Button inside Modal</Button>
</div>
</Fade>
</Modal>
</div>
);
}
There are two main issues:
You are adding a div around each of your buttons. This will interfere a little with the styling. Change this to a fragment (e.g. <> or <React.Fragment>) instead.
The way that ButtonGroup works is by cloning the child Button elements and adding props to control the styling. When you introduce a custom component in between, you need to pass through to the Button any props not used by your custom component.
Here is a working example:
import React from "react";
import ReactDOM from "react-dom";
import ButtonGroup from "#material-ui/core/ButtonGroup";
import Button from "#material-ui/core/Button";
import Modal from "#material-ui/core/Modal";
const AModal = props => {
return (
<>
<Button {...props}>A</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
const OtherModal = ({ buttonText, ...other }) => {
return (
<>
<Button {...other}>{buttonText}</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
// I don't recommend this approach due to maintainability issues,
// but if you have a lint rule that disallows prop spreading, this is a workaround.
const AvoidPropSpread = ({
className,
disabled,
color,
disableFocusRipple,
disableRipple,
fullWidth,
size,
variant
}) => {
return (
<>
<Button
className={className}
disabled={disabled}
color={color}
disableFocusRipple={disableFocusRipple}
disableRipple={disableRipple}
fullWidth={fullWidth}
size={size}
variant={variant}
>
C
</Button>
<Modal open={false}>
<div>Hello Modal</div>
</Modal>
</>
);
};
function App() {
return (
<ButtonGroup>
<AModal />
<OtherModal buttonText="B" />
<AvoidPropSpread />
</ButtonGroup>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

How to add padding and margin to all Material-UI components?

I need to add padding or margin to some of Material-UI components, but could not find an easy way to do it. Can I add these properties to all components? something like this:
<Button color="default" padding={10} margin={5}>
I know that this is possible using pure CSS and classes but I want to do it the Material-UI way.
You can use de "Spacing" in a BOX component just by importing the component first:
import Box from '#material-ui/core/Box';
The Box component works as a "Wrapper" for the component you want to "Modify" the spacing.
then you can use the next properties on the component:
The space utility converts shorthand margin and padding props to margin and padding CSS declarations. The props are named using the format {property}{sides}.
Where property is one of:
m - for classes that set margin
p - for classes that set padding
Where sides is one of:
t - for classes that set margin-top or padding-top
b - for classes that set margin-bottom or padding-bottom
l - for classes that set margin-left or padding-left
r - for classes that set margin-right or padding-right
x - for classes that set both *-left and *-right
y - for classes that set both *-top and *-bottom
blank - for classes that set a margin or padding on all 4 sides of the element
as an example:
<Box m={2} pt={3}>
<Button color="default">
Your Text
</Button>
</Box>
Material-UI's styling solution uses JSS at its core. It's a high performance JS to CSS compiler which works at runtime and server-side.
import { withStyles} from '#material-ui/core/styles';
const styles = theme => ({
buttonPadding: {
padding: '30px',
},
});
function MyButtonComponent(props) {
const { classes } = props;
return (
<Button
variant="contained"
color="primary"
className={classes.buttonPadding}
>
My Button
</Button>
);
}
export default withStyles(styles)(MyButtonComponent);
You can inject styles with withStyle HOC into your component. This is how it works and it's very much optimized.
EDITED: To apply styles across all components you need to use createMuiTheme and wrap your component with MuiThemeprovider
const theme = createMuiTheme({
overrides: {
MuiButton: {
root: {
margin: "10px",
padding: "10px"
}
}
}
});
<MuiThemeProvider theme={theme}>
<Button variant="contained" color="primary">
Custom CSS
</Button>
<Button variant="contained" color="primary">
MuiThemeProvider
</Button>
<Button variant="contained" color="primary">
Bootstrap
</Button>
</MuiThemeProvider>
In Material-UI v5, one can change the button style using the sx props. You can see the margin/padding system properties and its equivalent CSS property here.
<Button sx={{ m: 2 }} variant="contained">
margin
</Button>
<Button sx={{ p: 2 }} variant="contained">
padding
</Button>
<Button sx={{ pt: 2 }} variant="contained">
padding top
</Button>
<Button sx={{ px: 2 }} variant="contained">
padding left, right
</Button>
<Button sx={{ my: 2 }} variant="contained">
margin top, bottom
</Button>
The property shorthands like m or p are optional if you want to quickly prototype your component, you can use normal CSS properties if you want your code more readable.
The code below is equivalent to the above but use CSS properties:
<Button sx={{ margin: 2 }} variant="contained">
margin
</Button>
<Button sx={{ padding: 2 }} variant="contained">
padding
</Button>
<Button sx={{ paddingTop: 2 }} variant="contained">
padding top
</Button>
<Button sx={{ paddingLeft: 3, paddingRight: 3 }} variant="contained">
padding left, right
</Button>
<Button sx={{ marginTop: 2, marginBottom: 2 }} variant="contained">
margin top, bottom
</Button>
Live Demo
import Box from '#material-ui/core/Box';
<Box m={1} p={2}>
<Button color="default">
Your Text
</Button>
</Box>
We can use makeStyles of material-ui to achieve this without using Box component.
Create a customSpacing function like below.
customSpacing.js
import { makeStyles } from "#material-ui/core";
const spacingMap = {
t: "Top", //marginTop
b: "Bottom",//marginBottom
l: "Left",//marginLeft
r: "Right",//marginRight
a: "", //margin (all around)
};
const Margin = (d, x) => {
const useStyles = makeStyles(() => ({
margin: () => {
// margin in x-axis(left/right both)
if (d === "x") {
return {
marginLeft: `${x}px`,
marginRight: `${x}px`
};
}
// margin in y-axis(top/bottom both)
if (d === "y") {
return {
marginTop: `${x}px`,
marginBottom: `${x}px`
};
}
return { [`margin${spacingMap[d]}`]: `${x}px` };
}
}));
const classes = useStyles();
const { margin } = classes;
return margin;
};
const Padding = (d, x) => {
const useStyles = makeStyles(() => ({
padding: () => {
if (d === "x") {
return {
paddingLeft: `${x}px`,
paddingRight: `${x}px`
};
}
if (d === "y") {
return {
paddingTop: `${x}px`,
paddingBottom: `${x}px`
};
}
return { [`padding${spacingMap[d]}`]: `${x}px` };
}
}));
const classes = useStyles();
const { padding } = classes;
return padding;
};
const customSpacing = () => {
return {
m: Margin,
p: Padding
};
};
export default customSpacing;
Now import above customSpacing function into your Component and use it like below.
App.js
import React from "react";
import "./styles.css";
import customSpacing from "./customSpacing";
const App = () => {
const { m, p } = customSpacing();
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2
style={{ background: "red" }}
className={`${m("x", 20)} ${p("x", 2)}`}
>
Start editing to see some magic happen!
</h2>
</div>
);
};
export default App;
click to open codesandbox
We can use makeStyles or styles props on the Typography component to give margin until version 4.0.
I highly recommend to use version 5.0 of material ui and on this version Typography is having margin props and it makes life easy.
Specific for "padding-top" (10px) using Global style
Read this!
import React from "react";
import { Container, makeStyles, Typography } from "#material-ui/core";
import { Home } from "#material-ui/icons";
const useStyles = makeStyles((theme) => ({
container: {
paddingTop: theme.spacing(10),
},
}));
const LeftBar = () => {
const classes = useStyles();
return (
<Container className={classes.container}>
<div className={classes.item}>
<Home className={classes.icon} />
<Typography className={classes.text}>Homepage</Typography>
</div>
</Container>
);
};
export default LeftBar;
<Button color="default" p=10px m='5px'>
set initial spacing first in the themeprovider i.e the tag enclosing you app entry. It should look like this
import { createMuiTheme } from '#material-ui/core/styles';
import purple from '#material-ui/core/colors/purple';
import green from '#material-ui/core/colors/green';
const theme = createMuiTheme({
palette: {
primary: {
main: purple[500],
},
secondary: {
main: green[500],
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<LandingPage />
</ThemeProvider>
);
}
that's it. so add the theme section to the code and use margin/padding as you wish
const theme = {
spacing: 8,
}
<Box m={-2} /> // margin: -16px;
<Box m={0} /> // margin: 0px;
<Box m={0.5} /> // margin: 4px;
<Box m={2} /> // margin: 16px;
you can use "margin" or "m" for short same applies to padding
or
const theme = {
spacing: value => value ** 2,
}
<Box m={0} /> // margin: 0px;
<Box m={2} /> // margin: 4px;
or
<Box m="2rem" /> // margin: 2rem;
<Box mx="auto" /> // margin-left: auto; margin-right: auto

Resources