Custom Menu Item in MUI Data Grid Toolbar - reactjs

I'm currently using the Data Grid Toolbar (a feature of the Material-UI Data Grid component) because I want the Column Show/Hide component, but I also want to add my own menu item in the form of an IconButton with a Menu that opens when clicked. The issue is when you click said button, the Toolbar appears to re-render, which causes the Menu to lose its anchor and render in the upper left. Is there a special way to get an anchor within the Data Grid Toolbar for the Menu popper to appear in the correct location?
function CustomToolbar() {
return (
<GridToolbarContainer>
<Box
height="65px"
width="100%"
display="flex"
flexDirection="row"
justifyContent="center"
>
<Box width="300px" display="flex" justifyContent="flex-start" alignItems="center">
<GridToolbarColumnsButton sx={{ ml: 2 }} />
</Box>
<Box width="100%" alignSelf="center" textAlign="center">
<Typography sx={{ flex: "1 1 100%" }} variant="h6" component="div">
Title Goes Here
</Typography>
</Box>
<Box width="300px" display="flex" justifyContent="flex-end" alignItems="center">
<Tooltip title="Filter">
<IconButton
color="primary"
component="span"
disabled={loading}
sx={{ mr: 2 }}
onClick={handleMenuClick}
>
<FilterList />
</IconButton>
</Tooltip>
<Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={() => handleClose(menuState, filters)}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
PaperProps={MenuProps}
>
<MenuItem /> //Clipped
<MenuItem /> //Clipped
<MenuItem /> //Clipped
</Menu>
</Box>
</Box>
</GridToolbarContainer>
);
}

You must create the Toolbar component outside of your component that declares the DataGrid, and get the properties you need through the DataGrid's componentsProps property.
GridToolbarCustom Component:
type Props = {
selectionModel: GridSelectionModel;
}
const GridToolbarCustom = ({ selectionModel }: Props) => {
const [anchorElMenu, setAnchorElMenu] = useState<null | HTMLButtonElement>(null);
const openMenu = Boolean(anchorElMenu);
return (
<GridToolbarContainer>
<Grid container item xs>
{/* default buttons */}
<GridToolbarColumnsButton />
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
<GridToolbarExport />
</Grid>
<Grid>
<Button
variant="contained"
size="small"
disabled={selectionModel.length === 0}
startIcon={<MoreVertIcon />}
onClick={(event: MouseEvent<HTMLButtonElement>) => {
setAnchorElMenu(event.currentTarget);
}}
>
Actions
</Button>
<Menu
id="menu-options"
anchorEl={anchorElMenu}
open={openMenu}
onClose={() => {
setAnchorElMenu(null);
}}
>
<MenuItem /> //Clipped
<MenuItem /> //Clipped
<MenuItem /> //Clipped
</Menu>
</Grid>
</GridToolbarContainer>
);
}
export default GridToolbarCustom;
MyComponent:
import GridToolbarCustom from './GridToolbarCustom';
const MyComponent = () => {
const [selectionModel, setSelectionModel] = useState<GridSelectionModel>([]);
return (
<DataGrid
//Clipped
components={{
Toolbar: GridToolbarCustom,
}}
componentsProps={{
toolbar: {
selectionModel,
},
}}
checkboxSelection
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
/>
);
};

Related

Creating custom actions header

I have problem tracking updated state from fullcalendar in my custom actions header.
I have ref to fullcalendar in parent component that I'm passing as a prop to this child. Then I render calendar itself. OnClicks work but calApi.getApi().getDate() doesn't update value when It changes.
const Actions: FC<ActionsProps> = ({ calApi }) => {
const { t } = useTranslation();
const onToday = () => calApi.getApi().today();
const changeView = (changedView: CalendarView) => calApi.getApi().changeView(changedView);
const onPrev = () => calApi.getApi().prev();
const onNext = () => calApi.getApi().next();
return (
<Grid container spacing={3} alignItems="center" justifyContent="space-between">
<Grid item>
<Tooltip arrow placement="top" title={t('Previous Day').toString()}>
<IconButton color="primary" onClick={onPrev}>
<ArrowBackTwoToneIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title={t('Today').toString()}>
<IconButton
color="primary"
sx={{
mx: 1
}}
onClick={onToday}
>
<TodayTwoToneIcon />
</IconButton>
</Tooltip>
<Tooltip arrow placement="top" title={t('Next Day').toString()}>
<IconButton color="primary" onClick={onNext}>
<ArrowForwardTwoToneIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid
item
sx={{
display: { xs: 'none', sm: 'inline-block' }
}}
>
<Typography variant="h3" color="text.primary">
{format(calApi.getApi().getDate(), 'MMMM yyyy')}
</Typography>
</Grid>
<Grid
item
sx={{
display: { xs: 'none', sm: 'inline-block' }
}}
>
{viewOptions.map((viewOption) => {
const Icon = viewOption.icon;
return (
<Tooltip key={viewOption.value} arrow placement="top" title={t(viewOption.label).toString()}>
<IconButton
color={
viewOption.value === calApi.getApi().getCurrentData().currentViewType
? 'primary'
: 'secondary'
}
onClick={() => changeView?.(viewOption.value)}
>
<Icon />
</IconButton>
</Tooltip>
);
})}
</Grid>
</Grid>
);
};

Chakra ui - page shell - how to populate the page based on sidebar menu

I'm trying to figure out how to use the Chakra UI page shell, so that I can add components to render in the main segment of the shell, for each sidebar menu item.
The sidebar menu has NavButton items as follows:
<NavButton label="Settings" />
I'm trying to find a way to add a component (say, Settings) which has the content to render in the main segment of the shell.
How can I do that?
The full dashboard is as follows:
import {
// As,
Avatar,
Box,
BoxProps,
Button,
ButtonProps,
chakra,
Container,
Divider,
Drawer,
DrawerContent,
DrawerOverlay,
Flex,
Heading,
HStack,
Icon,
Input,
InputLeftElement,
InputGroup,
// Progress,
// SimpleGrid,
Stack,
Text,
useBreakpointValue,
useColorModeValue,
useDisclosure,
IconButton,
IconButtonProps
} from '#chakra-ui/react'
import * as React from 'react'
import { FiDownloadCloud } from 'react-icons/fi'
// import { CgWorkAlt } from 'react-icons/cg'
import {
// FiBarChart2,
// FiBookmark,
// FiCheckSquare,
// FiHelpCircle,
// FiHome,
FiSearch,
// FiSettings
} from 'react-icons/fi'
const Card = (props: BoxProps) => (
<Box
minH="3xs"
bg="bg-surface"
boxShadow={useColorModeValue('sm', 'sm-dark')}
borderRadius="lg"
{...props}
/>
)
const Bar = chakra('span', {
baseStyle: {
display: 'block',
pos: 'absolute',
w: '1.25rem',
h: '0.125rem',
rounded: 'full',
bg: 'currentcolor',
mx: 'auto',
insetStart: '0.125rem',
transition: 'all 0.12s',
},
})
const ToggleIcon = (props: { active: boolean }) => {
const { active } = props
return (
<Box
className="group"
data-active={active ? '' : undefined}
as="span"
display="block"
w="1.5rem"
h="1.5rem"
pos="relative"
aria-hidden
pointerEvents="none"
>
<Bar top="0.4375rem" _groupActive={{ top: '0.6875rem', transform: 'rotate(45deg)' }} />
<Bar bottom="0.4375rem" _groupActive={{ bottom: '0.6875rem', transform: 'rotate(-45deg)' }} />
</Box>
)
}
interface ToggleButtonProps extends IconButtonProps {
isOpen: boolean
}
const ToggleButton = (props: ToggleButtonProps) => {
const { isOpen, ...iconButtonProps } = props
return (
<IconButton
position="relative"
variant="unstyled"
size="sm"
color={isOpen ? 'white' : 'muted'}
zIndex="skipLink"
icon={<ToggleIcon active={isOpen} />}
{...iconButtonProps}
/>
)
}
interface UserProfileProps {
name: string
// image: string
title: string
}
const UserProfile = (props: UserProfileProps) => {
const { name, title } = props
return (
<HStack spacing="3" ps="2">
<Avatar name={name} boxSize="10" />
<Box>
<Text fontWeight="medium" fontSize="sm">
{name}
</Text>
<Text color="muted" fontSize="sm">
{title}
</Text>
</Box>
</HStack>
)
}
const Sidebar = () => (
<Flex as="section" minH="100vh" bg="bg-canvas" maxW="100%" p="0">
<Flex
flex="1"
bg="brand.white"
// overflowY="auto"
maxW="100%" p="0"
// maxW={{ base: 'full', sm: 'xs' }}
py={{ base: '6', sm: '8' }}
pl={4}
pr={6}
mr={4}
// px={{ base: '4', sm: '6' }}
>
<Stack justify="space-between" spacing="1">
<Stack spacing={{ base: '5', sm: '6' }} shouldWrapChildren>
<InputGroup>
<InputLeftElement pointerEvents="none">
<Icon as={FiSearch} color="muted" boxSize="5" />
</InputLeftElement>
<Input placeholder="Search" />
</InputGroup>
<Stack spacing="1" >
<NavButton label="Home" fontWeight="normal"/>
<NavButton label="Dashboard" aria-current="page" fontWeight="normal" />
<NavButton label="Tasks" fontWeight="normal" />
<NavButton label="ddd" fontWeight="normal"/>
</Stack>
</Stack>
<Stack spacing={{ base: '5', sm: '6' }}>
<Stack spacing="1">
<NavButton label="Library" fontWeight="normal" />
<NavButton label="Help" fontWeight="normal"/>
<NavButton label="Settings" fontWeight="normal"/>
</Stack>
{/* <Box bg="bg-subtle" px="4" py="5" borderRadius="lg">
<Stack spacing="4">
<Stack spacing="1">
<Text fontSize="sm" fontWeight="medium">
Almost there
</Text>
<Text fontSize="sm" color="muted">
Fill in some more information about you and your person.
</Text>
</Stack>
<Progress value={80} size="sm" aria-label="Profile Update Progress" />
<HStack spacing="3">
<Button variant="link" size="sm">
Dismiss
</Button>
<Button variant="link" size="sm" colorScheme="blue">
Update profile
</Button>
</HStack>
</Stack>
</Box> */}
<Divider />
<UserProfile
name="Asdfon"
// image="adsf"
title="adsf, asdf"
/>
</Stack>
</Stack>
</Flex>
</Flex>
)
const Navbar = () => {
const { isOpen, onToggle, onClose } = useDisclosure()
return (
<Box
width="full"
maxW="100%"
py="4"
px={0}
// px={{ base: '4', md: '8' }}
bg="bg-surface"
boxShadow={useColorModeValue('sm', 'sm-dark')}
>
<Flex justify="space-between">
<ToggleButton isOpen={isOpen} aria-label="Open Menu" onClick={onToggle} />
<Drawer
isOpen={isOpen}
placement="left"
onClose={onClose}
isFullHeight
preserveScrollBarGap
// Only disabled for showcase
trapFocus={false}
>
<DrawerOverlay />
<DrawerContent>
<Sidebar />
</DrawerContent>
</Drawer>
</Flex>
</Box>
)
}
interface NavButtonProps extends ButtonProps {
// icon: As
label: string
}
const NavButton = (props: NavButtonProps) => {
const { label, ...buttonProps } = props
return (
<Button variant="ghost" justifyContent="start" {...buttonProps}>
<HStack spacing="3">
{/* <Icon as={icon} boxSize="6" color="subtle" /> */}
<Text>{label}</Text>
</HStack>
</Button>
)
}
const DashBase = () => {
const isDesktop = useBreakpointValue({ base: false, lg: true })
return (
<Flex
as="section"
direction={{ base: 'column', lg: 'row' }}
height="100vh"
bg="white"
mb="100px"
overflowY="auto"
minW="100%"
px={0}
mx="0px"
// minW="120em"
// margin="auto"
>
{isDesktop ? <Sidebar /> : <Navbar />}
<Container py="8" flex="1">
<Stack spacing={{ base: '8', lg: '6' }}>
<Stack
spacing="4"
direction={{ base: 'column', lg: 'row' }}
// justify="space-between"
align={{ base: 'start', lg: 'center' }}
>
<Stack spacing="1">
<Heading size={useBreakpointValue({ base: 'xs', lg: 'sm' })} fontWeight="medium">
Dashboard
</Heading>
<Text color="muted">All important metrics at a glance</Text>
</Stack>
<HStack spacing="3">
<Button variant="secondary" leftIcon={<FiDownloadCloud fontSize="1.25rem" />}>
Download
</Button>
<Button variant="primary">Create</Button>
</HStack>
</Stack>
{/* <Stack spacing={{ base: '5', lg: '6' }} maxW="100%">
<SimpleGrid columns={{ base: 1, md: 3 }} gap="6">
<Card />
<Card />
<Card />
</SimpleGrid>
</Stack> */}
<Card minH="sm" />
</Stack>
</Container>
</Flex>
)
}
export default DashBase;
You could wrap your NavButton in Link
<Link href="/settings">
<NavButton label="Home" fontWeight="normal"/>
</Link>
In pages/settings.tsx, have a component that renders your settings page.
In _app.tsx, pass Component as a child to Sidebar
<Sidebar>
<Component {...pageProps} />
</Sidebar>
In Sidebar, add a param for children. Component will be in the children props. Render children prop after the menu.
const Sidebar = ({ children }) => (
...
</Flex>
{children}
</Flex>
)

React MultiCascader DropDown displayed in background not in Dialog

I'm currently facing a issue with the MultiCascader from rsuite.
I planned to put the cascader inside a CustomDialog. The selection is showh however the dropdown to select new items is not shown in the Dialog itself but in the background. See attached screenshots.
Code of which is rendered inside the CustomDialog
return (
<>
<Grid container item xs={12} spacing={8}>
<Grid item xs={12}>
<Typography sx={{ color: 'black' }}>Please select skills!</Typography>
</Grid>
<Grid container item xs={12}>
<Box style={{ display: 'block', width:800 }}>
<MultiCascader
style={{ width: 800 }}
placeholder="Select Skills"
data={serviceThesaurus}
menuWidth={350}
onCheck={(value) => handleSelect(value)}
uncheckableItemValues={notSelectList}
defaultValue={serviceList}
onClean={handleClear}
placement="autoVerticalStart"
/>
</Box>
</Grid>
<Grid item xs={12}>
<Box sx={{ border: 1 }} minHeight="50px" minWidth="200px">
{skillList.map((service, ind) => (
// eslint-disable-next-line react/no-array-index-key
<Box margin="1%" key={ind}>
<Typography>{service}</Typography>
</Box>
))}
</Box>
</Grid>
</Grid>
Code of the custom Dialog:
const useStyles = makeStyles({
dialogPaper: {
minHeight: '97%',
minWidth: '50%',
},
});
interface ComponentProps {
children: ReactNode;
handleClose: () => void;
handleSave: (() => void) | undefined;
open: boolean;
}
export default function CustomDialog({
open,
handleClose,
handleSave,
children,
}: ComponentProps) {
const classes = useStyles();
const { t } = useTranslation('common');
return (
<Box
sx={{
backgroundColor: 'background.default',
display: 'flex',
flexDirection: 'column',
height: '100%',
justifyContent: 'center',
}}
>
<Dialog
open={open}
onClose={handleClose}
scroll="paper"
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
classes={{ paper: classes.dialogPaper }}
>
<DialogTitle id="scroll-dialog-title">{t('preferences')}</DialogTitle>
<DialogContent dividers>
<DialogContentText
id="scroll-dialog-description"
// ref={descriptionElementRef}
tabIndex={-1}
>
{children}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
{t('cancelButton')}
</Button>
{handleSave && (
<Button onClick={handleSave} variant="contained" color="primary">
{t('saveButton')}
</Button>
)}
</DialogActions>
</Dialog>
</Box>
);
}
Picture of the situation

Opening Dialog drawer half way

I'm trying to trigger a dialog box to open when a button is clicked so that it takes up half the height of the blue area as shown in the picture below, while the remaining top half will have the backdrop. Currently i'm unable to force the dialog box to open up in the specific area (blue area), instead it takes up the fullscreen and i'm not sure how to change that. I've added a sample code in codesanbox to show what I have done to try to do this: https://codesandbox.io/s/material-demo-rryul
This is an image showing what I would like to achieve
export default function CenteredGrid() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div className={classes.root}>
<Grid container spacing={1}>
<Grid item xs={6}>
<Paper style={{ height: 500, background: "blue" }}>
<div>
<Button
variant="outlined"
color="white"
onClick={handleClickOpen}
>
Open full-screen dialog
</Button>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
Sound
</Typography>
<Button autoFocus color="inherit" onClick={handleClose}>
save
</Button>
</Toolbar>
</AppBar>
<List>
<ListItem button>
<ListItemText
primary="Phone ringtone"
secondary="Titania"
/>
</ListItem>
<Divider />
<ListItem button>
<ListItemText
primary="Default notification ringtone"
secondary="Tethys"
/>
</ListItem>
</List>
</Dialog>
</div>
</Paper>
</Grid>
<Grid item xs={6}>
<Paper style={{ height: 500, background: "purple" }} />
</Grid>
</Grid>
</div>
);
}
If anything is unclear, just let me know
The material ui Dialog is rendered as portal. As per doc, Modal component's props are also available to Dialog.
So you can use the Modal's container prop in Dialog and provide a target which indicates where to display the portal.
The container will have the portal children appended to it.
By default, it uses the body of the top-level document object,
Working demo (codesandbox)
dialog style
dialog: {
background: "orange",
position: "relative !important",
height: "100%",
width: "100%",
padding: "100px 0px 0px 0px", //change this based on your needs
backgroundColor: "rgba(0,0,0,0.6)"
}
dialog jsx
...
const container = React.useRef(null);
...
...
<Dialog
container={container.current}//<---here
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
className={classes.dialog}
>
...

Material ui anchor without hooks

I have a menu component that pops open in a table. When I copy the material ui example into the cell in the table it works perfectly.
https://codesandbox.io/s/gitl9
The material ui example uses hooks and I want to change it to a class component and use redux.
When I made the change the pop-up menu does not align beside the the button you press anymore.
The anchorEl attribute is responsible for passing the location of the button that has been called.
I added these attributes allow me to move the menu pop but it does not align with button that you click to open the menu.
const options = ["View", "Edit", "Delete"];
const ITEM_HEIGHT = 48;
class ActionsOptionMenu extends Component {
state = { anchorEl: null };
render() {
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
>
<MoreVertIcon />
</IconButton>
<Menu
getContentAnchorEl={null}
anchorOrigin={{
height: "54px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: `0 ${padding} 0 ${padding}`,
margin: "0 auto 7px auto"
}}
transformOrigin={{ vertical: "top", horizontal: "right" }}
id='long-menu'
anchorEl={anchorEl}
keepMounted
open={true}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 120
}
}}
>
{options.map(option => (
<MenuItem key={option}>{option}</MenuItem>
))}
</Menu>
</div>
);
}
}
I solved the issue doing it this way.
render() {
const { open } = this.state;
return (
<div>
<IconButton
aria-label='more'
aria-controls='long-menu'
aria-haspopup='true'
buttonRef={node => {
this.anchorEl = node;
}}
onClick={event => this.handleClick(event)}
>
<MoreVertIcon />
</IconButton>
<Popper
open={open}
anchorEl={this.anchorEl}
transition
disablePortal
style={{ zIndex: 100 }}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
id='menu-list-grow'
style={{
zIndex: 1001,
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<Paper>
<ClickAwayListener
onClickAway={event => this.handleClose(event)}
>
<MenuList>
<MenuItem onClick={event => this.handleClose(event)}>
Profile
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
My account
</MenuItem>
<MenuItem onClick={event => this.handleClose(event)}>
Logout
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}

Resources