-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6c88887
commit 25b4250
Showing
5 changed files
with
228 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { useState, useRef } from "react"; | ||
import Button from "@mui/material/Button"; | ||
import ButtonGroup, { type ButtonGroupProps } from "@mui/material/ButtonGroup"; | ||
import ClickAwayListener from "@mui/material/ClickAwayListener"; | ||
import Grow from "@mui/material/Grow"; | ||
import MenuItem from "@mui/material/MenuItem"; | ||
import MenuList from "@mui/material/MenuList"; | ||
import Paper from "@mui/material/Paper"; | ||
import Popper from "@mui/material/Popper"; | ||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; | ||
|
||
/** | ||
* A Mui ButtonGroup which will render a drop-down menu when more than one | ||
* option is provided to the `options` prop. | ||
* | ||
* - The "primary" option is rendered as a regular button next to the drop-down | ||
* icon button. | ||
* - Set any option as the "primary" option via the `isPrimary` key option key. | ||
* - If no option has `isPrimary: true`, the 0-index option is chosen as the | ||
* primary option. | ||
* - Use the `updatePrimaryOptionOnClick` prop to have a dynamic "primary" | ||
* option which updates upon menu-item selection. | ||
*/ | ||
export const ActionsButtonGroup = ({ | ||
options, | ||
updatePrimaryOnClick = true, | ||
variant = "contained", | ||
...buttonGroupProps | ||
}: ActionsButtonGroupProps) => { | ||
const anchorRef = useRef<HTMLDivElement>(null); | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
const [primaryOptionIndex, setPrimaryOptionIndex] = useState<number>(() => | ||
options.reduce((accum, { isPrimary = false }, idx) => (isPrimary ? idx : accum), 0) | ||
); | ||
|
||
const handleMenuItemClick = ( | ||
event: React.MouseEvent<HTMLLIElement, MouseEvent>, | ||
index: number | ||
) => { | ||
if (updatePrimaryOnClick) setPrimaryOptionIndex(index); | ||
options[index].handleClick(event); | ||
setIsOpen(false); | ||
}; | ||
|
||
const handleClose = (event: Event) => { | ||
if (!anchorRef.current || !anchorRef.current.contains(event.target as HTMLElement)) { | ||
setIsOpen(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
<ButtonGroup ref={anchorRef} variant={variant} {...buttonGroupProps}> | ||
<Button onClick={options[primaryOptionIndex].handleClick}> | ||
{options[primaryOptionIndex].label} | ||
</Button> | ||
<Button | ||
onClick={() => setIsOpen((prevOpen) => !prevOpen)} | ||
size="small" | ||
aria-haspopup="menu" | ||
{...(isOpen && { | ||
"aria-controls": ariaElementIDs.menu, | ||
"aria-expanded": "true", | ||
})} | ||
> | ||
<ArrowDropDownIcon /> | ||
</Button> | ||
</ButtonGroup> | ||
<Popper | ||
open={isOpen} | ||
anchorEl={anchorRef.current} | ||
role={undefined} | ||
transition | ||
disablePortal | ||
placement="bottom-end" | ||
style={{ zIndex: 1 }} | ||
> | ||
{({ TransitionProps }) => ( | ||
<Grow {...TransitionProps}> | ||
<Paper> | ||
<ClickAwayListener onClickAway={handleClose}> | ||
<MenuList id={ariaElementIDs.menu} autoFocusItem disablePadding> | ||
{options.map(({ label, isDisabled = false }, index) => ( | ||
<MenuItem | ||
key={label.replace(/\s/g, "-")} | ||
onClick={(event) => handleMenuItemClick(event, index)} | ||
selected={index === primaryOptionIndex} | ||
disabled={isDisabled} | ||
> | ||
{label} | ||
</MenuItem> | ||
))} | ||
</MenuList> | ||
</ClickAwayListener> | ||
</Paper> | ||
</Grow> | ||
)} | ||
</Popper> | ||
</> | ||
); | ||
}; | ||
|
||
const ariaElementIDs = { | ||
menu: "split-actions-button-menu", | ||
}; | ||
|
||
export type ActionsButtonGroupProps = { | ||
options: ActionsButtonGroupOptions; | ||
updatePrimaryOnClick?: boolean; | ||
} & ButtonGroupProps; | ||
|
||
export type ActionsButtonGroupOptions = Array<ActionsButtonGroupOption>; | ||
export interface ActionsButtonGroupOption { | ||
label: string; | ||
handleClick: React.MouseEventHandler<HTMLButtonElement | HTMLLIElement>; | ||
isPrimary?: boolean; | ||
isDisabled?: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { useNavigate } from "react-router-dom"; | ||
import Button from "@mui/material/Button"; | ||
import AddCircleIcon from "@mui/icons-material/AddCircle"; | ||
import { usePageLayoutContext } from "@app/PageLayoutContext/usePageLayoutContext"; | ||
import { MobileCreateItemButton } from "./MobileCreateItemButton"; | ||
|
||
/** | ||
* Layout-dependant CreateItemButton. | ||
*/ | ||
export const CreateItemButton = ({ createItemFormPath, buttonText }: CreateItemButtonProps) => { | ||
const nav = useNavigate(); | ||
const { isMobilePageLayout } = usePageLayoutContext(); | ||
|
||
const handleClickCreateItem: React.MouseEventHandler<HTMLButtonElement> = () => | ||
nav(createItemFormPath); | ||
|
||
return ( | ||
<> | ||
{isMobilePageLayout ? ( | ||
<MobileCreateItemButton onClick={handleClickCreateItem} /> | ||
) : ( | ||
<Button | ||
onClick={handleClickCreateItem} | ||
startIcon={<AddCircleIcon style={{ marginBottom: "0.12rem" }} />} | ||
style={{ | ||
height: "2rem", | ||
width: "14rem", | ||
paddingTop: "0.26rem", | ||
paddingBottom: "0.1rem", | ||
borderRadius: "1.5rem", | ||
}} | ||
> | ||
{buttonText} | ||
</Button> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export type CreateItemButtonProps = { | ||
createItemFormPath: string; | ||
buttonText: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { styled } from "@mui/material/styles"; | ||
import IconButton from "@mui/material/IconButton"; | ||
|
||
/** | ||
* An IconButton with a subtle yet distinguishable flash/shine animation created by a | ||
* gradient-background rotating around the border of a Mui IconButton. The animation | ||
* uses a custom cubic-bezier curve which gives it the appearance of inertial motion. | ||
* | ||
* - For circular icons with a "filled" background, pass `shouldInvertColors=true` | ||
* for a more preferable appearance. | ||
*/ | ||
export const FlashyIconButton = styled(IconButton, { | ||
shouldForwardProp: (propName: string) => propName !== "shouldInvertColors", | ||
})<{ shouldInvertColors?: boolean }>(({ theme: { palette }, shouldInvertColors = false }) => ({ | ||
height: "2rem", | ||
width: "2rem", | ||
color: shouldInvertColors ? palette.primary.main : palette.background.default, | ||
position: "relative", | ||
zIndex: 2, | ||
|
||
"&::before": { | ||
content: '""', | ||
position: "absolute", | ||
height: "2.25rem", | ||
width: "2.25rem", | ||
background: `conic-gradient(${palette.primary.dark} 75%, ${palette.primary.main}, ${palette.primary.dark})`, | ||
borderRadius: "50%", | ||
zIndex: -1, | ||
animation: "rotate 1.5s cubic-bezier(.14,.36,.94,.71) infinite", | ||
|
||
"@keyframes rotate": { | ||
from: { transform: "rotate(0deg)" }, | ||
to: { transform: "rotate(360deg)" }, | ||
}, | ||
}, | ||
|
||
"& svg": { | ||
fontSize: "2rem", | ||
borderRadius: "50%", | ||
...(shouldInvertColors | ||
? { | ||
backgroundColor: palette.background.default, | ||
border: "none", | ||
} | ||
: { | ||
backgroundColor: palette.primary.main, | ||
border: `3px solid ${palette.background.default}`, | ||
}), | ||
}, | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import AddCircleIcon from "@mui/icons-material/AddCircle"; | ||
import { FlashyIconButton } from "./FlashyIconButton"; | ||
|
||
export const MobileCreateItemButton = (props: MobileCreateItemButtonProps) => ( | ||
<FlashyIconButton shouldInvertColors {...props}> | ||
<AddCircleIcon /> | ||
</FlashyIconButton> | ||
); | ||
|
||
export type MobileCreateItemButtonProps = Omit< | ||
React.ComponentProps<typeof FlashyIconButton>, | ||
"shouldInvertColors" | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters