main

mattermost/focalboard

Last updated at: 29/12/2023 09:42

valueSelector.tsx

TLDR

This file, valueSelector.tsx, contains a React component called ValueSelector. This component is a value selector that allows users to select and create options from a dropdown menu. It includes various sub-components, such as labels, icons, and menus, to enhance its functionality.

Classes

ValueSelector

The ValueSelector class is a React component that provides a value selector with a dropdown menu. It allows users to select options from a list or create new ones. It includes sub-components for labels, icons, and menus to enhance its functionality.

Methods

None

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {useIntl} from 'react-intl'
import {ActionMeta, OnChangeValue} from 'react-select'
import {FormatOptionLabelMeta} from 'react-select/base'
import CreatableSelect from 'react-select/creatable'

import {CSSObject} from '@emotion/serialize'

import {IPropertyOption} from '../blocks/board'
import {Constants} from '../constants'

import {getSelectBaseStyle} from '../theme'

import Menu from './menu'
import MenuWrapper from './menuWrapper'
import IconButton from './buttons/iconButton'
import OptionsIcon from './icons/options'
import DeleteIcon from './icons/delete'
import CloseIcon from './icons/close'
import Label from './label'

import './valueSelector.scss'

type Props = {
    options: IPropertyOption[]
    value?: IPropertyOption | IPropertyOption[]
    emptyValue: string
    onCreate: (value: string) => void
    onChange: (value: string | string[]) => void
    onChangeColor: (option: IPropertyOption, color: string) => void
    onDeleteOption: (option: IPropertyOption) => void
    isMulti?: boolean
    onDeleteValue?: (value: IPropertyOption) => void
    onBlur?: () => void
}

type LabelProps = {
    option: IPropertyOption
    meta: FormatOptionLabelMeta<IPropertyOption>
    onChangeColor: (option: IPropertyOption, color: string) => void
    onDeleteOption: (option: IPropertyOption) => void
    onDeleteValue?: (value: IPropertyOption) => void
    isMulti?: boolean
}

const ValueSelectorLabel = (props: LabelProps): JSX.Element => {
    const {option, onDeleteValue, meta, isMulti} = props
    const intl = useIntl()
    if (meta.context === 'value') {
        let className = onDeleteValue ? 'Label-no-padding' : 'Label-single-select'
        if (!isMulti) {
            className += ' Label-no-margin'
        }
        return (
            <Label
                color={option.color}
                className={className}
            >
                <span className='Label-text'>{option.value}</span>
                {onDeleteValue &&
                    <IconButton
                        onClick={() => onDeleteValue(option)}
                        icon={<CloseIcon/>}
                        title='Clear'
                        className='margin-left delete-value'
                    />
                }
            </Label>
        )
    }
    return (
        <div
            className='value-menu-option'
            role='menuitem'
        >
            <div className='label-container'>
                <Label color={option.color}>{option.value}</Label>
            </div>
            <MenuWrapper stopPropagationOnToggle={true}>
                <IconButton
                    title={intl.formatMessage({id: 'ValueSelectorLabel.openMenu', defaultMessage: 'Open menu'})}
                    icon={<OptionsIcon/>}
                />
                <Menu position='left'>
                    <Menu.Text
                        id='delete'
                        icon={<DeleteIcon/>}
                        name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
                        onClick={() => props.onDeleteOption(option)}
                    />
                    <Menu.Separator/>
                    {Object.entries(Constants.menuColors).map(([key, color]: [string, string]) => (
                        <Menu.Color
                            key={key}
                            id={key}
                            name={color}
                            onClick={() => props.onChangeColor(option, key)}
                        />
                    ))}
                </Menu>
            </MenuWrapper>
        </div>
    )
}

const valueSelectorStyle = {
    ...getSelectBaseStyle(),
    option: (provided: CSSObject, state: {isFocused: boolean}): CSSObject => ({
        ...provided,
        background: state.isFocused ? 'rgba(var(--center-channel-color-rgb), 0.1)' : 'rgb(var(--center-channel-bg-rgb))',
        color: state.isFocused ? 'rgb(var(--center-channel-color-rgb))' : 'rgb(var(--center-channel-color-rgb))',
        padding: '8px',
    }),
    control: (): CSSObject => ({
        border: 0,
        width: '100%',
        margin: '0',
    }),
    valueContainer: (provided: CSSObject): CSSObject => ({
        ...provided,
        padding: '0 8px',
        overflow: 'unset',
    }),
    singleValue: (provided: CSSObject): CSSObject => ({
        ...provided,
        position: 'static',
        top: 'unset',
        transform: 'unset',
    }),
    placeholder: (provided: CSSObject): CSSObject => ({
        ...provided,
        color: 'rgba(var(--center-channel-color-rgb), 0.4)',
    }),
    multiValue: (provided: CSSObject): CSSObject => ({
        ...provided,
        margin: 0,
        padding: 0,
        backgroundColor: 'transparent',
    }),
    multiValueLabel: (provided: CSSObject): CSSObject => ({
        ...provided,
        display: 'flex',
        paddingLeft: 0,
        padding: 0,
    }),
    multiValueRemove: (): CSSObject => ({
        display: 'none',
    }),
    menu: (provided: CSSObject): CSSObject => ({
        ...provided,
        width: 'unset',
        background: 'rgb(var(--center-channel-bg-rgb))',
        minWidth: '260px',
    }),
}

function ValueSelector(props: Props): JSX.Element {
    const intl = useIntl()
    return (
        <CreatableSelect
            noOptionsMessage={() => intl.formatMessage({id: 'ValueSelector.noOptions', defaultMessage: 'No options. Start typing to add the first one!'})}
            aria-label={intl.formatMessage({id: 'ValueSelector.valueSelector', defaultMessage: 'Value selector'})}
            captureMenuScroll={true}
            maxMenuHeight={1200}
            isMulti={props.isMulti}
            isClearable={true}
            styles={valueSelectorStyle}
            formatOptionLabel={(option: IPropertyOption, meta: FormatOptionLabelMeta<IPropertyOption>) => (
                <ValueSelectorLabel
                    option={option}
                    meta={meta}
                    isMulti={props.isMulti}
                    onChangeColor={props.onChangeColor}
                    onDeleteOption={props.onDeleteOption}
                    onDeleteValue={props.onDeleteValue}
                />
            )}
            className='ValueSelector'
            classNamePrefix='ValueSelector'
            options={props.options}
            getOptionLabel={(o: IPropertyOption) => o.value}
            getOptionValue={(o: IPropertyOption) => o.id}
            onChange={(value: OnChangeValue<IPropertyOption, true | false>, action: ActionMeta<IPropertyOption>): void => {
                if (action.action === 'select-option' || action.action === 'pop-value') {
                    if (Array.isArray(value)) {
                        props.onChange((value as IPropertyOption[]).map((option) => option.id))
                    } else {
                        props.onChange((value as IPropertyOption).id)
                        props.onBlur?.()
                    }
                } else if (action.action === 'clear') {
                    props.onChange('')
                }
            }}
            onKeyDown={(event) => {
                if (event.key === 'Escape') {
                    props.onBlur?.()
                }
            }}
            onBlur={props.onBlur}
            onCreateOption={props.onCreate}
            autoFocus={true}
            value={props.value || null}
            closeMenuOnSelect={!props.isMulti}
            placeholder={props.emptyValue}
            hideSelectedOptions={false}
            defaultMenuIsOpen={true}
            menuIsOpen={props.isMulti}
            blurInputOnSelect={!props.isMulti}
        />
    )
}

export default React.memo(ValueSelector)