|
| 1 | +import Button, { ButtonProps } from "@material-ui/core/Button" |
| 2 | +import ButtonGroup from "@material-ui/core/ButtonGroup" |
| 3 | +import ClickAwayListener from "@material-ui/core/ClickAwayListener" |
| 4 | +import Grow from "@material-ui/core/Grow" |
| 5 | +import MenuItem from "@material-ui/core/MenuItem" |
| 6 | +import MenuList from "@material-ui/core/MenuList" |
| 7 | +import Paper from "@material-ui/core/Paper" |
| 8 | +import Popper from "@material-ui/core/Popper" |
| 9 | +import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown" |
| 10 | +import React, { useRef, useState } from "react" |
| 11 | + |
| 12 | +interface SplitButtonOptions<T> { |
| 13 | + /** |
| 14 | + * label is shown in the SplitButton UI |
| 15 | + */ |
| 16 | + label: string |
| 17 | + /** |
| 18 | + * value is any value for this option |
| 19 | + */ |
| 20 | + value: T |
| 21 | +} |
| 22 | + |
| 23 | +export interface SplitButtonProps<T> extends Pick<ButtonProps, "color" | "disabled" | "startIcon"> { |
| 24 | + /** |
| 25 | + * onClick is called with the selectedOption |
| 26 | + */ |
| 27 | + onClick: (selectedOption: T) => void |
| 28 | + /** |
| 29 | + * options is a list of options |
| 30 | + */ |
| 31 | + options: SplitButtonOptions<T>[] |
| 32 | + /** |
| 33 | + * textTransform is applied to the primary button text. Defaults to |
| 34 | + * uppercase |
| 35 | + */ |
| 36 | + textTransform?: React.CSSProperties["textTransform"] |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * SplitButton is a button with a primary option and a dropdown with secondary |
| 41 | + * options. |
| 42 | + * @remark The primary option is the 0th index (first option) in the array. |
| 43 | + * @see https://mui.com/components/button-group/#split-button |
| 44 | + */ |
| 45 | +export const SplitButton = <T,>({ |
| 46 | + color, |
| 47 | + disabled, |
| 48 | + onClick, |
| 49 | + options, |
| 50 | + startIcon, |
| 51 | + textTransform, |
| 52 | +}: SplitButtonProps<T>): ReturnType<React.FC> => { |
| 53 | + const [isPopperOpen, setIsPopperOpen] = useState<boolean>(false) |
| 54 | + |
| 55 | + const anchorRef = useRef<HTMLDivElement>(null) |
| 56 | + const displayedLabel = options[0].label |
| 57 | + |
| 58 | + const handleClick = () => { |
| 59 | + onClick(options[0].value) |
| 60 | + } |
| 61 | + const handleClose = (e: React.MouseEvent<Document, MouseEvent>) => { |
| 62 | + if (anchorRef.current && anchorRef.current.contains(e.target as HTMLElement)) { |
| 63 | + return |
| 64 | + } |
| 65 | + setIsPopperOpen(false) |
| 66 | + } |
| 67 | + const handleSelectOpt = (e: React.MouseEvent<HTMLLIElement, MouseEvent>, opt: number) => { |
| 68 | + onClick(options[opt].value) |
| 69 | + setIsPopperOpen(false) |
| 70 | + } |
| 71 | + const handleTogglePopper = () => { |
| 72 | + setIsPopperOpen((prevOpen) => !prevOpen) |
| 73 | + } |
| 74 | + |
| 75 | + return ( |
| 76 | + <> |
| 77 | + <ButtonGroup aria-label="split button" color={color} ref={anchorRef} variant="contained"> |
| 78 | + <Button disabled={disabled} onClick={handleClick} startIcon={startIcon} style={{ textTransform }}> |
| 79 | + {displayedLabel} |
| 80 | + </Button> |
| 81 | + <Button |
| 82 | + aria-controls={isPopperOpen ? "split-button-menu" : undefined} |
| 83 | + aria-expanded={isPopperOpen ? "true" : undefined} |
| 84 | + aria-label="select merge strategy" |
| 85 | + aria-haspopup="menu" |
| 86 | + disabled={disabled} |
| 87 | + size="small" |
| 88 | + onClick={handleTogglePopper} |
| 89 | + > |
| 90 | + <ArrowDropDownIcon /> |
| 91 | + </Button> |
| 92 | + </ButtonGroup> |
| 93 | + |
| 94 | + <Popper |
| 95 | + anchorEl={anchorRef.current} |
| 96 | + disablePortal |
| 97 | + open={isPopperOpen} |
| 98 | + role={undefined} |
| 99 | + style={{ zIndex: 1 }} |
| 100 | + transition |
| 101 | + > |
| 102 | + {({ TransitionProps, placement }) => ( |
| 103 | + <Grow |
| 104 | + {...TransitionProps} |
| 105 | + style={{ |
| 106 | + transformOrigin: placement === "bottom" ? "center top" : "center bottom", |
| 107 | + }} |
| 108 | + > |
| 109 | + <Paper> |
| 110 | + <ClickAwayListener onClickAway={handleClose}> |
| 111 | + <MenuList id="split-button-menu"> |
| 112 | + {options.map((opt, idx) => ( |
| 113 | + <MenuItem key={opt.label} onClick={(e) => handleSelectOpt(e, idx)}> |
| 114 | + {opt.label} |
| 115 | + </MenuItem> |
| 116 | + ))} |
| 117 | + </MenuList> |
| 118 | + </ClickAwayListener> |
| 119 | + </Paper> |
| 120 | + </Grow> |
| 121 | + )} |
| 122 | + </Popper> |
| 123 | + </> |
| 124 | + ) |
| 125 | +} |
0 commit comments