grommet
Version:
focus on the essential experience
498 lines (491 loc) • 19.7 kB
JavaScript
var _excluded = ["a11yTitle", "aria-label", "aria-labelledby", "alignSelf", "children", "defaultValue", "disabled", "disabledKey", "dropAlign", "dropHeight", "dropProps", "dropTarget", "emptySearchMessage", "focusIndicator", "gridArea", "help", "id", "icon", "labelKey", "limit", "margin", "messages", "name", "onBlur", "onChange", "onClick", "onClose", "onFocus", "onKeyDown", "onMore", "onOpen", "onSearch", "open", "options", "placeholder", "plain", "replace", "searchPlaceholder", "size", "sortSelectedOnClose", "value", "valueKey", "valueLabel", "showSelectedInline", "width"];
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
import React, { forwardRef, isValidElement, useCallback, useContext, useMemo, useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { controlBorderStyle, useKeyboard, useForwardedRef } from '../../utils';
import { Box } from '../Box';
import { DropButton } from '../DropButton';
import { Keyboard } from '../Keyboard';
import { FormContext } from '../Form/FormContext';
import { SelectMultipleValue } from './SelectMultipleValue';
import { SelectMultipleContainer } from './SelectMultipleContainer';
import { HiddenInput, SelectTextInput, StyledSelectDropButton } from '../Select/StyledSelect';
import { applyKey, getNormalizedValue, changeEvent, getSelectIcon, getIconColor, getDisplayLabelKey, arrayIncludes, inertTrueValue, selectInputId } from '../Select/utils';
import { DefaultSelectTextInput } from '../Select/DefaultSelectTextInput';
import { MessageContext } from '../../contexts/MessageContext';
import { SelectMultiplePropTypes } from './propTypes';
import { useThemeValue } from '../../utils/useThemeValue';
var StyledSelectBox = styled(Box).withConfig({
displayName: "SelectMultiple__StyledSelectBox",
componentId: "sc-18zwyth-0"
})(["", ";", ";", ";"], function (props) {
return !props.plainSelect && controlBorderStyle;
}, function (props) {
var _props$theme$select;
return (_props$theme$select = props.theme.select) == null || (_props$theme$select = _props$theme$select.control) == null ? void 0 : _props$theme$select.extend;
}, function (props) {
var _props$theme$select$c;
return props.open && ((_props$theme$select$c = props.theme.select.control) == null ? void 0 : _props$theme$select$c.open);
});
var SelectMultiple = /*#__PURE__*/forwardRef(function (_ref, ref) {
var a11yTitle = _ref.a11yTitle,
ariaLabel = _ref['aria-label'],
ariaLabelledByProp = _ref['aria-labelledby'],
alignSelf = _ref.alignSelf,
children = _ref.children,
defaultValue = _ref.defaultValue,
disabled = _ref.disabled,
disabledKey = _ref.disabledKey,
dropAlignProp = _ref.dropAlign,
dropHeight = _ref.dropHeight,
dropProps = _ref.dropProps,
dropTarget = _ref.dropTarget,
emptySearchMessage = _ref.emptySearchMessage,
focusIndicator = _ref.focusIndicator,
gridArea = _ref.gridArea,
help = _ref.help,
id = _ref.id,
icon = _ref.icon,
labelKey = _ref.labelKey,
limit = _ref.limit,
margin = _ref.margin,
messages = _ref.messages,
name = _ref.name,
onBlur = _ref.onBlur,
onChange = _ref.onChange,
onClick = _ref.onClick,
onClose = _ref.onClose,
onFocus = _ref.onFocus,
onKeyDown = _ref.onKeyDown,
onMore = _ref.onMore,
onOpen = _ref.onOpen,
onSearch = _ref.onSearch,
openProp = _ref.open,
optionsProp = _ref.options,
placeholder = _ref.placeholder,
plain = _ref.plain,
replace = _ref.replace,
searchPlaceholder = _ref.searchPlaceholder,
size = _ref.size,
_ref$sortSelectedOnCl = _ref.sortSelectedOnClose,
sortSelectedOnClose = _ref$sortSelectedOnCl === void 0 ? true : _ref$sortSelectedOnCl,
valueProp = _ref.value,
valueKey = _ref.valueKey,
valueLabel = _ref.valueLabel,
_ref$showSelectedInli = _ref.showSelectedInline,
showSelectedInline = _ref$showSelectedInli === void 0 ? false : _ref$showSelectedInli,
width = _ref.width,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
var _useThemeValue = useThemeValue(),
theme = _useThemeValue.theme,
passThemeFlag = _useThemeValue.passThemeFlag;
var inputRef = useRef();
var formContext = useContext(FormContext);
var _useContext = useContext(MessageContext),
format = _useContext.format;
var selectBoxRef = useRef();
var dropButtonRef = useForwardedRef(ref);
var usingKeyboard = useKeyboard();
var formFieldData = formContext == null ? void 0 : formContext.useFormField({});
var dropAlign = useMemo(function () {
return dropAlignProp || (showSelectedInline ? {
top: 'top',
right: 'right',
left: 'left'
} : {
top: 'bottom',
left: 'left'
});
}, [dropAlignProp, showSelectedInline]);
// value is used for what we receive in valueProp and the basis for
// what we send with onChange
// When 'valueKey' sets 'reduce', the value(s) here should match
// what the 'valueKey' would return for the corresponding
// selected option object.
// Otherwise, the value(s) should match the selected options.
var _formContext$useFormI = formContext.useFormInput({
name: name,
value: valueProp,
initialValue: defaultValue || ''
}),
value = _formContext$useFormI[0],
setValue = _formContext$useFormI[1];
var _useState = useState(),
ariaLabelledBy = _useState[0],
setAriaLabelledBy = _useState[1];
useEffect(function () {
if (formFieldData != null && formFieldData.inForm && id && !ariaLabel && !placeholder) {
var labelElement = document.getElementById("grommet-" + id + "__input__label");
if (labelElement) {
setAriaLabelledBy("grommet-" + id + "__input__label " + id);
}
}
}, [formFieldData == null ? void 0 : formFieldData.inForm, id, ariaLabel, placeholder]);
// normalizedValue is the value mapped with any valueKey applied
// When the options array contains objects, this property indicates how
// to retrieve the value of each option.
// If a string is provided, it is used as the key to retrieve a
// property of an option object.
// If a function is provided, it is called with the option and should
// return the value.
// If reduce is true, this value will be used for the 'value'
// delivered via 'onChange'.
var normalizedValue = useMemo(function () {
return getNormalizedValue(value, valueKey);
}, [value, valueKey]);
// search input value
var _useState2 = useState(),
search = _useState2[0],
setSearch = _useState2[1];
// All select option indices and values
var _useState3 = useState(optionsProp),
allOptions = _useState3[0],
setAllOptions = _useState3[1];
var _useState4 = useState(),
orderedOptions = _useState4[0],
setOrderedOptions = _useState4[1];
// Track changes to options property, except when options are being
// updated due to search activity. Allows option's initial index value
// to be referenced when filtered by search.
useEffect(function () {
if (!search) setAllOptions(optionsProp);
}, [optionsProp, search]);
useEffect(function () {
if (search && optionsProp && optionsProp.length > 0) {
var additionalOptions = [].concat(allOptions);
optionsProp.forEach(function (i) {
return !additionalOptions.some(function (j) {
return typeof i === 'object' ? applyKey(i, valueKey) === applyKey(j, valueKey) : i === j;
}) && additionalOptions.push(i);
});
if (allOptions.length !== additionalOptions.length) setAllOptions(additionalOptions);
}
}, [allOptions, optionsProp, search, valueKey]);
useEffect(function () {
if (sortSelectedOnClose) setOrderedOptions(optionsProp);
}, [optionsProp, sortSelectedOnClose]);
// the option indexes present in the value
var optionIndexesInValue = useMemo(function () {
var result = [];
if (!Array.isArray(normalizedValue)) {
return result;
}
normalizedValue.forEach(function (v) {
var index = allOptions.map(function (option) {
return applyKey(option, valueKey);
}).indexOf(v);
if (index !== -1) result.push(index);
});
return result;
}, [allOptions, valueKey, normalizedValue]);
var _useState5 = useState(openProp),
open = _useState5[0],
setOpen = _useState5[1];
useEffect(function () {
return setOpen(openProp);
}, [openProp]);
var onRequestOpen = useCallback(function () {
if (open) return;
setOpen(true);
if (onOpen) onOpen();
}, [onOpen, open]);
// On drop close if sortSelectedOnClose is true, sort options so that
// selected options appear first, followed by unselected options.
useEffect(function () {
if (sortSelectedOnClose && value && !open) {
var selectedOptions = optionsProp.filter(function (option) {
return arrayIncludes(value, valueKey && valueKey.reduce ? applyKey(option, valueKey) : option, valueKey || labelKey);
});
var unselectedOptions = optionsProp.filter(function (i) {
return !arrayIncludes(selectedOptions, i, valueKey || labelKey);
});
var nextOrderedOptions = selectedOptions.concat(unselectedOptions);
setOrderedOptions(nextOrderedOptions);
}
}, [labelKey, open, sortSelectedOnClose, optionsProp, value, valueKey]);
var onRequestClose = useCallback(function () {
setOpen(false);
if (onClose) onClose();
setSearch();
}, [onClose]);
var triggerChangeEvent = useCallback(function (nextValue) {
return changeEvent(inputRef, nextValue);
}, []);
var onSelectChange = useCallback(function (event, _ref2) {
var option = _ref2.option,
nextValue = _ref2.value;
// nextValue must not be of type object to set value directly on the
// input. if it is an object, then the user has not provided necessary
// props to reduce object option
if (typeof nextValue !== 'object' && nextValue !== event.target.value && inputRef.current) {
// select registers changing option as a click event or keydown.
// when in a form, we need to programatically trigger a change
// event in order for the change event to be registered upstream
// necessary for change validation in form
triggerChangeEvent(nextValue);
}
setValue(nextValue);
if (onChange) {
event.persist();
var adjustedEvent;
// support for native event used by Preact
if (event instanceof Event) {
adjustedEvent = new event.constructor(event.type, event);
Object.defineProperties(adjustedEvent, {
target: {
value: inputRef.current
},
value: {
value: nextValue
},
option: {
value: option
}
});
} else {
adjustedEvent = event;
adjustedEvent.target = inputRef.current;
adjustedEvent.value = nextValue;
adjustedEvent.option = option;
}
onChange(adjustedEvent);
}
}, [onChange, setValue, triggerChangeEvent]);
var SelectIcon = getSelectIcon(icon, theme, open);
// element to show, trumps inputValue
var selectValue = useMemo(function () {
var result;
if (valueLabel) {
result = value && valueLabel instanceof Function ? valueLabel(value) : valueLabel;
} else if ((value == null ? void 0 : value.length) > 0 && showSelectedInline) {
result = /*#__PURE__*/React.createElement(SelectMultipleValue, {
allOptions: allOptions,
disabled: disabled,
disabledKey: disabledKey,
dropButtonRef: dropButtonRef,
labelKey: labelKey,
messages: messages,
onRequestOpen: onRequestOpen,
onSelectChange: onSelectChange,
theme: theme,
value: value,
valueKey: valueKey
}, children);
}
return result;
}, [allOptions, children, disabled, disabledKey, dropButtonRef, labelKey, messages, onRequestOpen, onSelectChange, showSelectedInline, theme, value, valueKey, valueLabel]);
var displayLabelKey = useMemo(function () {
return getDisplayLabelKey(labelKey, allOptions, optionIndexesInValue, selectValue);
}, [labelKey, allOptions, optionIndexesInValue, selectValue]);
// text to show
// When the options array contains objects, this property indicates how
// to retrieve the value of each option.
// If a string is provided, it is used as the key to retrieve a
// property of an option object.
// If a function is provided, it is called with the option and should
// return the value.
// If reduce is true, this value will be used for the 'value'
// delivered via 'onChange'.
var inputValue = useMemo(function () {
if (!selectValue) {
if (optionIndexesInValue.length === 0) return '';
if (optionIndexesInValue.length === 1) return applyKey(allOptions[optionIndexesInValue[0]], labelKey);
// keeping messages.multiple for backwards compatibility
if (messages != null && messages.multiple && !messages.summarizedValue) {
return format({
id: 'select.multiple',
messages: messages
});
}
return format({
id: 'selectMultiple.summarizedValue',
messages: messages,
values: {
selected: optionIndexesInValue.length,
total: allOptions.length
}
});
}
return undefined;
}, [selectValue, optionIndexesInValue, allOptions, labelKey, format, messages]);
var iconColor = getIconColor(theme);
var displaySelectIcon = SelectIcon && /*#__PURE__*/React.createElement(Box, {
alignSelf: "center",
margin: theme.select.icons.margin,
width: {
min: 'auto'
}
}, /*#__PURE__*/isValidElement(SelectIcon) ? SelectIcon : /*#__PURE__*/React.createElement(SelectIcon, {
color: iconColor,
size: size
}));
var dropContent = /*#__PURE__*/React.createElement(SelectMultipleContainer, {
allOptions: allOptions,
disabled: disabled,
disabledKey: disabledKey,
dropHeight: dropHeight,
emptySearchMessage: emptySearchMessage,
help: help,
icon: displaySelectIcon,
id: id,
labelKey: labelKey,
limit: limit,
messages: messages,
onChange: onSelectChange,
onClose: onRequestClose,
onKeyDown: onKeyDown,
onMore: onMore,
onSearch: onSearch,
options: orderedOptions || optionsProp,
optionIndexesInValue: optionIndexesInValue,
replace: replace,
searchPlaceholder: searchPlaceholder,
search: search,
setSearch: setSearch,
usingKeyboard: usingKeyboard,
value: value,
valueKey: valueKey,
showSelectedInline: showSelectedInline
}, children);
var dropButtonProps = {
ref: dropButtonRef,
a11yTitle: (ariaLabel || a11yTitle || placeholder || format({
id: 'selectMultiple.open',
messages: messages
})) + ". " + format({
id: 'selectMultiple.selected',
values: {
selected: (value == null ? void 0 : value.length) || 0,
total: allOptions.length
}
}),
'aria-expanded': Boolean(open),
'aria-haspopup': 'listbox',
id: id,
disabled: disabled === true || undefined,
open: open,
focusIndicator: focusIndicator,
onFocus: onFocus,
onBlur: onBlur,
gridArea: gridArea,
margin: margin,
onOpen: onRequestOpen,
onClose: onRequestClose,
onClick: onClick,
plainSelect: plain,
plain: plain,
// Button should be plain
dropProps: dropProps,
dropContent: dropContent,
theme: theme
};
return /*#__PURE__*/React.createElement(Keyboard, {
onDown: onRequestOpen,
onUp: onRequestOpen
}, showSelectedInline ? /*#__PURE__*/React.createElement(StyledSelectBox, _extends({
disabled: disabled === true || undefined,
alignSelf: alignSelf,
direction: "row",
alignContent: "start",
background: theme.select.background,
ref: selectBoxRef,
flex: false,
plainSelect: plain,
width: width
}, passThemeFlag), /*#__PURE__*/React.createElement(Box, {
width: "100%"
}, /*#__PURE__*/React.createElement(DropButton, _extends({
fill: "horizontal",
alignSelf: "start"
}, dropButtonProps, {
dropAlign: dropAlign,
dropTarget: dropTarget || selectBoxRef.current
}), selectValue || displayLabelKey ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Box, {
direction: "row"
}, /*#__PURE__*/React.createElement(SelectTextInput, _extends({
a11yTitle: ariaLabel || a11yTitle,
defaultCursor: disabled === true || undefined,
focusIndicator: false,
id: id ? selectInputId(id) : undefined,
inert: inertTrueValue,
name: name,
width: "100%"
}, rest, {
tabIndex: "-1",
type: "text",
placeholder: !value || (value == null ? void 0 : value.length) === 0 ? placeholder || selectValue || displayLabelKey : format({
id: onMore ? 'selectMultiple.selected' : 'selectMultiple.selectedOfTotal',
messages: messages,
values: _extends({
selected: (value == null ? void 0 : value.length) || 0
}, !onMore ? {
total: allOptions.length
} : {})
}),
plain: true,
readOnly: true,
value: "",
theme: theme
})), displaySelectIcon), /*#__PURE__*/React.createElement(HiddenInput, {
type: "text",
name: name,
id: id ? selectInputId(id) : undefined,
inert: inertTrueValue,
value: inputValue,
ref: inputRef,
readOnly: true
})) : /*#__PURE__*/React.createElement(Box, {
direction: "row"
}, /*#__PURE__*/React.createElement(DefaultSelectTextInput, _extends({
a11yTitle: ariaLabel || a11yTitle,
disabled: disabled,
id: id,
inert: inertTrueValue,
name: name,
ref: inputRef,
placeholder: placeholder || 'Select',
value: inputValue,
size: size,
theme: theme
}, rest)), displaySelectIcon)), !open && (value == null ? void 0 : value.length) > 0 && (selectValue || displayLabelKey))) : /*#__PURE__*/React.createElement(Box, {
width: width
}, /*#__PURE__*/React.createElement(StyledSelectDropButton, _extends({}, dropButtonProps, {
dropAlign: dropAlign,
dropTarget: dropTarget,
alignSelf: alignSelf,
tabIndex: "0",
"aria-labelledby": ariaLabelledByProp || ariaLabelledBy
}), /*#__PURE__*/React.createElement(Box, {
align: "center",
direction: "row",
justify: "between",
background: theme.select.background
}, /*#__PURE__*/React.createElement(Box, {
direction: "row",
flex: true,
basis: "auto"
}, selectValue || displayLabelKey ? /*#__PURE__*/React.createElement(React.Fragment, null, selectValue || displayLabelKey, /*#__PURE__*/React.createElement(HiddenInput, {
type: "text",
name: name,
id: id ? selectInputId(id) : undefined,
inert: inertTrueValue,
value: inputValue,
ref: inputRef,
readOnly: true
})) : /*#__PURE__*/React.createElement(DefaultSelectTextInput, _extends({
a11yTitle: ariaLabel || a11yTitle,
disabled: disabled,
id: id,
inert: inertTrueValue,
name: name,
ref: inputRef,
placeholder: placeholder,
value: inputValue,
size: size,
theme: theme
}, rest))), displaySelectIcon))));
});
SelectMultiple.displayName = 'SelectMultiple';
SelectMultiple.propTypes = SelectMultiplePropTypes;
export { SelectMultiple };