Source

src/components/Interface/ControlPopover.jsx

import React, { useState } from 'react'
import styled from 'styled-components'
import { ComboBox, Icon, StyledDropDown, StyledSlider } from '..'
import colors from '../../config/colors'
import {} from '..'
import {
    Stack,
    Typography,
    InputLabel,
    Popover,
    MenuItem,
    Switch,
    Select,
    TextField,
    Checkbox,
} from '@mui/material'

/**
 * Outer container (div) for the popover
 *
 * @category HelperComponents
 * @param {Object} props
 * @param {React.ReactNode} props.children - Inner content
 * @param {boolean} props.inline - Boolean to display block or absolute
 * @param {number} props.left - LEFT positioning for the button + container
 * @param {number} props.right - RIGHT positioning for the button + container
 * @param {number} props.top - TOP positioning for the button + container
 * @param {number} props.bottom - BOTTOM positioning for the button + container
 * @param {number} props.size - Width and height of the button in REM
 * @param {string} props.color - Color of button
 * @component
 */
const PopoverContainer = styled.div`
    position: ${({ inline }) => (inline ? 'block' : 'absolute')};
    left: ${(props) =>
        props.left !== undefined
            ? typeof props.left === 'string'
                ? props.left
                : 0
            : 'initial'};
    bottom: ${(props) =>
        props.bottom !== undefined
            ? typeof props.bottom === 'string'
                ? props.bottom
                : 0
            : 'initial'};
    right: ${(props) =>
        props.right !== undefined
            ? typeof props.right === 'string'
                ? props.right
                : 0
            : 'initial'};
    top: ${(props) =>
        props.top !== undefined
            ? typeof props.top === 'string'
                ? props.top
                : 0
            : 'initial'};
    width: ${({ size }) => size}rem;
    height: ${({ size }) => size}rem;
    z-index: 500;
    overflow-y: visible;
    button {
        width: 100%;
        height: 100%;
        border: none;
        background: none;
        cursor: pointer;
        opacity: 0.6;
        transition: 250ms all;
        svg {
            width: 60%;
            height: 60%;
            stroke: ${(props) => props.color};
            fill: none;
            path {
                fill: ${(props) => props.color};
            }
        }
        &:hover {
            opacity: 1;
        }
    }
`

const PopoverContent = styled.div`
    display: flex;
    flex-direction: column;
    background: ${colors.gray};
    border: 1px solid ${colors.yellow};
    color: ${colors.white};
    padding: 1em;
    overflow-x: hidden;
    overflow-y: visible;
`

const H3 = ({ content }) => <h3>{content}</h3>
const P = ({ content }) => <p>{content}</p>
const Label = ({ content }) => <label>{content}</label>

/**
 * Select / dropdown control called as a composition in ControlPopover
 *
 * @category HelperComponents
 * @example
 *     function MyComponent() {
 *         return (
 *             <ControlPopover
 *                 bottom
 *                 left
 *                 controlElements={[
 *                     {
 *                         type: 'select',
 *                         action: (value) => console.log(value),
 *                         value: { value: '1', label: 'Option 1' },
 *                         content: {
 *                             label: 'My Options',
 *                             items: [
 *                                 {
 *                                     text: 'Option 1',
 *                                     value: '1',
 *                                 },
 *                                 {
 *                                     text: 'Option 2',
 *                                     value: '2',
 *                                 },
 *                             ],
 *                         },
 *                     },
 *                 ]}
 *             />
 *         )
 *     }
 *
 * @param {Object} props.content Content {label: string, items: {value: any,
 *   text: string|null, label: string|null}[]}
 * @param {string} props.content.label String label for the control
 * @param {Object[]} props.content.items Array of objects with value, text, and
 *   label If using nested, add subItems to each item with the same structure
 * @param {Object | string[]} props.value Value of the control as {value: any,
 *   label: any}. When using `multiple`, an array of value strings.
 * @param {function} props.action Function to call on change
 * @param {boolean} props.multiple Boolean to allow multiple selections. Use
 *   SelectMultiControl
 * @param {boolean} props.nested If true, nests rows of checkbox options
 * @param {boolean} props.autocomplete If true, uses autocomplete search instead
 *   of select
 * @param {boolean} props.active Boolean passed to StyledDropDown for
 *   highlighted state overload props passed to MuiSelect
 *   https://mui.com/material-ui/react-select/
 * @component
 */
const SelectControl = (
    { content, value, action, active = false, multiple, nested, autocomplete },
    rest
) => {
    if (autocomplete) {
        return (
            <ComboBox
                MenuProps={{ id: 'variableMenu' }}
                value={value}
                setValue={action}
                options={content.items}
                label={content.label}
                {...rest}
            />
        )
    } else {
        return (
            <StyledDropDown
                style={{ marginTop: '1.5em', width: '100%' }}
                active={active}
            >
                <InputLabel htmlFor="variableSelect">
                    {content.label}
                </InputLabel>
                <Select
                    MenuProps={{ id: 'variableMenu' }}
                    value={value}
                    multiple={multiple || nested}
                    onChange={action}
                    {...rest}
                >
                    {nested
                        ? content.items.map((item, index) => (
                              <Stack
                                  key={index}
                                  direction="column"
                                  alignItems="center"
                                  sx={{
                                      padding: '.5em 1em 0 1em',
                                      borderBottom: '1px solid white',
                                  }}
                              >
                                  <Typography fontWeight="bold">
                                      {item.text || item.label}
                                  </Typography>
                                  <Stack direction="row">
                                      {item.subItems.map(
                                          (subItem, subIndex) => (
                                              <MenuItem
                                                  key={subIndex}
                                                  value={subItem.value}
                                                  onClick={() =>
                                                      action(subItem.value)
                                                  }
                                              >
                                                  <Checkbox
                                                      key={subIndex}
                                                      checked={
                                                          value.indexOf(
                                                              subItem.value
                                                          ) > -1
                                                      }
                                                  />
                                                  {subItem.text ||
                                                      subItem.label}
                                              </MenuItem>
                                          )
                                      )}
                                  </Stack>
                              </Stack>
                          ))
                        : content.items.map((item, index) => (
                              <MenuItem key={index} value={item.value}>
                                  {item.text || item.label}
                              </MenuItem>
                          ))}
                </Select>
            </StyledDropDown>
        )
    }
}

const SelectMultiControl = (props) => <SelectControl {...props} multiple />
const SelectNestedMultiControl = (props) => (
    <SelectControl {...props} nested multiple />
)
const ComboBoxControl = (props) => <SelectControl {...props} autocomplete />

const StyledSwitch = styled.div`
    margin: 0 5px;
    @media (max-width: 960px) {
        margin: 0;
    }
    p {
        color: white;
        display: inline;
        text-align: center;
    }
    span.MuiSwitch-track {
        background-color: ${colors.lightgray};
    }
    .MuiSwitch-colorSecondary.Mui-checked {
        color: ${colors.lightblue};
    }
    .MuiSwitch-colorSecondary.Mui-checked + .MuiSwitch-track {
        background-color: ${colors.lightblue};
    }
    .MuiSwitch-colorSecondary:hover {
        background-color: ${colors.lightblue}55;
    }
`

/**
 * Switch on-off component for control popover
 *
 * @category HelperComponents
 * @example
 *     function MyComponent() {
 *         return (
 *             <ControlPopover
 *                 controlElements={[
 *                     {
 *                         type: 'switch',
 *                         action: (value) => console.log(value),
 *                         value: true,
 *                         content: 'My Switch',
 *                     },
 *                 ]}
 *             />
 *         )
 *     }
 *
 * @param {string} content Label for switch
 * @param {boolean} value Value of switch
 * @param {function} action Function to call on change
 * @component
 */
const SwitchControl = ({ content, value, action }) => (
    <StyledSwitch>
        <Switch
            checked={value}
            onChange={action}
            name="log chart switch"
            inputProps={{ 'aria-label': 'secondary checkbox' }}
        />
        <p>{content}</p>
    </StyledSwitch>
)

const StyledSliderContainer = styled.div`
    span.MuiSlider-rail {
        display: initial;
    }
`

const SliderControl = ({ content, value, action }) => (
    <StyledSliderContainer>
        <label>{content.label}</label>
        <StyledSlider {...{ value, ...content }} onChange={action} />
    </StyledSliderContainer>
)

const StyledTextField = styled(TextField)`
    label.MuiInputLabel-root {
        color: ${colors.white};
    }
    input.MuiInput-input:before,
    .MuiInputBase-input {
        border-bottom: 1px solid ${colors.white};
    }
`

const CloseButton = styled.button`
    position: absolute;
    top: -0.5em;
    right: -0.125em;
    padding: 0.75em;
    background: none;
    color: white;
    border: none;
    font-size: 1rem;
    cursor: pointer;
`

const TextInputControl = ({ content, value, action }) => (
    <StyledTextField
        fullWidth
        id="standard-basic"
        variant="standard"
        value={value}
        onChange={action}
    />
)

export const ControlElementMapping = {
    header: H3,
    helperText: P,
    label: Label,
    select: SelectControl,
    switch: SwitchControl,
    slider: SliderControl,
    comboBox: ComboBoxControl,
    textInput: TextInputControl,
    selectMulti: SelectMultiControl,
    selectNestMulti: SelectNestedMultiControl,
    // geocoder: Geocoder,
    // size: Size,
}
/**
 * A popover that contains a list of controls for a given component Can be
 * positioned in any corner
 *
 * @category Components/Interface
 * @example
 *     function MyComponent() {
 *         return (
 *             <ControlPopover
 *                 size={4}
 *                 top
 *                 bottom
 *                 iconColor="blue"
 *                 controlElements={[
 *                     {
 *                         type: 'header',
 *                         content: 'My Header',
 *                     },
 *                     {
 *                         type: 'helperText',
 *                         content: 'My Helper Text',
 *                     },
 *                     {
 *                         type: 'label',
 *                         content: 'My Label',
 *                     },
 *                     {
 *                         type: 'select',
 *                         content: {
 *                             label: 'My Select',
 *                             items: [
 *                                 { value: 'value1', label: 'Text 1' },
 *                                 { value: 'value2', label: 'Text 2' },
 *                                 { value: 'value3', label: 'Text 3' },
 *                             ],
 *                             value: { value: 'value1', label: 'Text 1' },
 *                             action: (value) => console.log(value),
 *                         },
 *                     },
 *                     {
 *                         type: 'switch',
 *                         action: (value) => console.log(value),
 *                         value: true,
 *                         content: 'My Switch',
 *                     },
 *                     {
 *                         type: 'slider',
 *                         content: {
 *                             min: 0,
 *                             max: 100,
 *                             step: 1,
 *                         },
 *                         value: 50,
 *                         action: (value) => console.log(value),
 *                     },
 *                     {
 *                         type: 'comboBox',
 *                         content: {
 *                             items: [
 *                                 { value: 'value1', label: 'Text 1' },
 *                                 { value: 'value2', label: 'Text 2' },
 *                             ],
 *                         },
 *                         value: { value: 'value1', label: 'Text 1' },
 *                         action: (value) => console.log(value),
 *                     },
 *                 ]}
 *             />
 *         )
 *     }
 *
 * @param {Object} props
 * @param {number} props.size Size of clickable icon in REM
 * @param {boolean} props.inline Display inline or absolutely positioned
 * @param {boolean} props.top If true, position on top of parent
 * @param {boolean} props.bottom If true, position on bottom of parent
 * @param {boolean} props.left If true, position on left of parent
 * @param {boolean} props.right If true, position on right of parent
 * @param {Object[]} props.controlElements Array of control elements to display
 *   Typically {type: string, content: string|Object[], value:
 *   boolean|Object|string[], action: function} Available types: header,
 *   helperText, label, select, switch, slider, comboBox, textInput,
 *   selectMulti, selectNestMulti
 * @param {string} props.iconColor Color of icon (optional)
 * @param {string} props.className Class name for styling (optional)
 * @component
 */
function ControlsPopover({
    size = 2,
    inline = false,
    controlElements = [],
    top,
    bottom,
    left,
    right,
    iconColor,
    className,
}) {
    const [anchorEl, setAnchorEl] = useState(null)

    const handleClick = (event) => {
        setAnchorEl(event.currentTarget)
    }

    const handleClose = () => {
        setAnchorEl(null)
    }

    const id = !!anchorEl ? 'simple-popover' : undefined

    return (
        <PopoverContainer
            size={size}
            inline={inline}
            className={className}
            top={top}
            bottom={bottom}
            left={left}
            right={right}
            color={iconColor || colors.yellow}
        >
            <button
                aria-describedby={id}
                variant="contained"
                onClick={handleClick}
                title="Open Settings"
            >
                <Icon symbol="settings" />
            </button>
            <Popover
                id={id}
                open={!!anchorEl}
                anchorEl={anchorEl}
                onClose={handleClose}
                anchorOrigin={{
                    vertical: 'top',
                    horizontal: 'left',
                }}
                transformOrigin={{
                    vertical: 'top',
                    horizontal: 'right',
                }}
                style={{
                    padding: '1em',
                    overflow: 'hidden',
                }}
            >
                <PopoverContent>
                    {controlElements.map((elementProps, idx) => {
                        if (ControlElementMapping[elementProps.type]) {
                            const El = ControlElementMapping[elementProps.type]
                            return <El key={idx} {...elementProps} />
                        } else {
                            return null
                        }
                    })}
                </PopoverContent>
                {!!anchorEl && (
                    <CloseButton onClick={handleClose} title="Close Panel">
                        ×
                    </CloseButton>
                )}
            </Popover>
        </PopoverContainer>
    )
}
export default ControlsPopover