UNPKG

grommet

Version:

focus on the essential experience

501 lines (493 loc) 21.5 kB
"use strict"; exports.__esModule = true; exports.SelectMultipleContainer = void 0; var _react = _interopRequireWildcard(require("react")); var _Search = require("grommet-icons/icons/Search"); var _utils = require("../../utils"); var _Box = require("../Box"); var _Button = require("../Button"); var _CheckBox = require("../CheckBox"); var _InfiniteScroll = require("../InfiniteScroll"); var _Keyboard = require("../Keyboard"); var _Text = require("../Text"); var _TextInput = require("../TextInput"); var _SelectionSummary = require("./SelectionSummary"); var _StyledSelect = require("../Select/StyledSelect"); var _utils2 = require("../Select/utils"); var _EmptySearchOption = require("../Select/EmptySearchOption"); var _MessageContext = require("../../contexts/MessageContext"); var _useThemeValue2 = require("../../utils/useThemeValue"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } 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); } var SelectMultipleContainer = exports.SelectMultipleContainer = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) { var _theme$select$icons, _theme$selectMultiple2; var allOptions = _ref.allOptions, _ref$children = _ref.children, children = _ref$children === void 0 ? null : _ref$children, disabledProp = _ref.disabled, disabledKey = _ref.disabledKey, dropHeight = _ref.dropHeight, _ref$emptySearchMessa = _ref.emptySearchMessage, emptySearchMessage = _ref$emptySearchMessa === void 0 ? 'No matches found' : _ref$emptySearchMessa, help = _ref.help, icon = _ref.icon, id = _ref.id, labelKey = _ref.labelKey, limit = _ref.limit, messages = _ref.messages, onChange = _ref.onChange, onClose = _ref.onClose, onKeyDown = _ref.onKeyDown, onMore = _ref.onMore, onSearch = _ref.onSearch, optionIndexesInValue = _ref.optionIndexesInValue, options = _ref.options, _ref$replace = _ref.replace, replace = _ref$replace === void 0 ? true : _ref$replace, searchPlaceholder = _ref.searchPlaceholder, search = _ref.search, setSearch = _ref.setSearch, usingKeyboard = _ref.usingKeyboard, _ref$value = _ref.value, value = _ref$value === void 0 ? [] : _ref$value, valueKey = _ref.valueKey, showSelectedInline = _ref.showSelectedInline; var _useThemeValue = (0, _useThemeValue2.useThemeValue)(), theme = _useThemeValue.theme; // the currently active option based on keyboard navigation // or mouse hover, -1 means no active option var _useState = (0, _react.useState)(-1), activeIndex = _useState[0], setActiveIndex = _useState[1]; var _useState2 = (0, _react.useState)(usingKeyboard), keyboardNavigation = _useState2[0], setKeyboardNavigation = _useState2[1]; var _useContext = (0, _react.useContext)(_MessageContext.MessageContext), format = _useContext.format; var searchRef = (0, _react.useRef)(); var optionsRef = (0, _react.useRef)(); var _useState3 = (0, _react.useState)(disabledProp), disabled = _useState3[0], setDisabled = _useState3[1]; // the node of the currently active option, as activeIndex changes // this is updated too and the useEffect below ensures the // active option remains in keyboard focus since we're // following roving tab index pattern var activeRef = (0, _react.useRef)(); var _useState4 = (0, _react.useState)(), showA11yLimit = _useState4[0], setShowA11yLimit = _useState4[1]; var clearRef = (0, _react.useRef)(); var isDisabled = (0, _utils2.useDisabled)(disabled, disabledKey, options, valueKey || labelKey); var searchIcon = ((_theme$select$icons = theme.select.icons) == null ? void 0 : _theme$select$icons.search) || /*#__PURE__*/_react["default"].createElement(_Search.Search, { "aria-hidden": true }); // for keyboard/screenreader, keep the active option in focus (0, _react.useEffect)(function () { var _activeRef$current; if (activeIndex) (_activeRef$current = activeRef.current) == null || _activeRef$current.focus(); }, [activeIndex]); // set initial focus (0, _react.useEffect)(function () { // need to wait for Drop to be ready var timer = setTimeout(function () { var clearButton = clearRef.current; if (clearButton && clearButton.focus) { (0, _utils.setFocusWithoutScroll)(clearButton); } else if (searchRef && searchRef.current) { var searchInput = searchRef.current; if (searchInput && searchInput.focus) { (0, _utils.setFocusWithoutScroll)(searchInput); } } else if (activeRef.current) { (0, _utils.setFocusWithoutScroll)(activeRef.current); } else if (optionsRef.current) { setActiveIndex(0); } }, 100); // Drop should be open after 100ms return function () { return clearTimeout(timer); }; }, []); (0, _react.useEffect)(function () { var optionsNode = optionsRef.current; if (optionsNode != null && optionsNode.children) { var optionNode = optionsNode.children[activeIndex]; if (optionNode) optionNode.focus(); } }, [activeIndex]); var isSelected = (0, _react.useCallback)(function (index) { var result; var optionVal = (0, _utils2.getOptionValue)(index, options, valueKey || labelKey); if (value) { if (value.length === 0) { result = false; } else if (typeof value[0] !== 'object') { result = value.indexOf(optionVal) !== -1; } else if (valueKey) { result = value.some(function (valueItem) { var valueValue = typeof valueKey === 'function' ? valueKey(valueItem) : valueItem[valueKey]; return valueValue === optionVal; }); } } return result; }, [value, valueKey, options, labelKey]); var selectOption = (0, _react.useCallback)(function (index) { return function (event) { if (onChange) { var nextOptionIndexesInValue = optionIndexesInValue.slice(0); var allOptionsIndex = (0, _utils2.getOptionIndex)(allOptions, options[index], valueKey || labelKey); var valueIndex = optionIndexesInValue.indexOf(allOptionsIndex); if (valueIndex === -1 && (!limit || (value == null ? void 0 : value.length) < limit)) { nextOptionIndexesInValue.push(allOptionsIndex); } else { nextOptionIndexesInValue.splice(valueIndex, 1); } var nextValue = nextOptionIndexesInValue.map(function (i) { return valueKey && valueKey.reduce ? (0, _utils2.applyKey)(allOptions[i], valueKey) : allOptions[i]; }); var nextSelected = nextOptionIndexesInValue; onChange(event, { option: options[index], value: nextValue, selected: nextSelected }); } }; }, [labelKey, limit, onChange, optionIndexesInValue, options, allOptions, valueKey, value]); var onNextOption = (0, _react.useCallback)(function (event) { event.preventDefault(); var nextActiveIndex = activeIndex + 1; // checking activeIndex > -1 ensures arrow keys don't // move focus when select all/clear all button or search input // are focused if (nextActiveIndex !== (options == null ? void 0 : options.length) && activeIndex > -1) { setActiveIndex(nextActiveIndex); setKeyboardNavigation(true); } }, [activeIndex, options]); var onPreviousOption = (0, _react.useCallback)(function (event) { event.preventDefault(); var nextActiveIndex = activeIndex - 1; // checking activeIndex > -1 ensures arrow keys don't // move focus when select all/clear all button or search input // are focused if (activeIndex > -1) { setActiveIndex(Math.max(nextActiveIndex, 0)); setKeyboardNavigation(true); } }, [activeIndex]); var onKeyDownOption = (0, _react.useCallback)(function (event) { if (!onSearch) { var nextActiveIndex = options.findIndex(function (e) { var label; if (typeof e === 'object') { label = e.label || (0, _utils2.applyKey)(e, labelKey); } else { label = e; } return typeof label === 'string' && label.charAt(0).toLowerCase() === event.key.toLowerCase(); }); if (nextActiveIndex >= 0) { event.preventDefault(); setActiveIndex(nextActiveIndex); setKeyboardNavigation(true); } } if (onKeyDown) { onKeyDown(event); } }, [onKeyDown, options, onSearch, labelKey]); var onActiveOption = (0, _react.useCallback)(function (index) { return function () { if (!keyboardNavigation) setActiveIndex(index); }; }, [keyboardNavigation]); var onSelectOption = (0, _react.useCallback)(function (event) { if (!isDisabled(activeIndex) && activeIndex >= 0 && activeIndex < (options == null ? void 0 : options.length)) { event.preventDefault(); // prevent submitting forms selectOption(activeIndex)(event); } }, [activeIndex, selectOption, options, isDisabled]); var customSearchInput = theme.select.searchInput; var SelectTextInput = customSearchInput || _TextInput.TextInput; var selectOptionsStyle = theme.select.options ? _extends({}, theme.select.options.box, theme.select.options.container) : {}; // handle when limit is reached (0, _react.useEffect)(function () { var originallyDisabled = function originallyDisabled(index) { var option = allOptions[index]; var result; if (disabledKey) { result = (0, _utils2.applyKey)(option, disabledKey); } else if (Array.isArray(disabledProp)) { if (typeof disabledProp[0] === 'number') { result = disabledProp.indexOf(index) !== -1; } else { result = (0, _utils2.getOptionIndex)(disabledProp, (0, _utils2.getOptionValue)(index, options, valueKey || labelKey), valueKey || labelKey) !== -1; } } return result; }; if (value && limit) { if (value.length === limit) { var newDisabled = [].concat(disabledProp); // disable everything that is not selected for (var i = 0; i < (options == null ? void 0 : options.length); i += 1) { if (!isSelected(i) && !originallyDisabled(i)) { newDisabled.push(options[i]); } } if (usingKeyboard) setShowA11yLimit('Selected. Maximum selection limit reached.'); setDisabled(newDisabled); } else { if (usingKeyboard) setShowA11yLimit(undefined); setDisabled(disabledProp); } } }, [isSelected, value, limit, disabledProp, allOptions, disabledKey, labelKey, options, usingKeyboard, valueKey]); // reset showA11yLimit after announcement is read (0, _react.useEffect)(function () { if (showA11yLimit !== undefined) { setTimeout(function () { setShowA11yLimit(undefined); }, 2000); // value chosen based on length of a11yLimit message } }, [showA11yLimit]); var summaryContent = /*#__PURE__*/_react["default"].createElement(_SelectionSummary.SelectionSummary, { allOptions: allOptions, clearRef: clearRef, disabled: disabled, disabledKey: disabledKey, isSelected: isSelected, labelKey: labelKey, limit: limit, messages: messages, onChange: onChange, onMore: onMore, options: options, search: search, setActiveIndex: setActiveIndex, showSelectedInline: showSelectedInline, value: value, valueKey: valueKey }); var helpContent; if (help) { var _theme$selectMultiple; if (typeof help === 'string') helpContent = /*#__PURE__*/_react["default"].createElement(_Box.Box, { flex: false, pad: (_theme$selectMultiple = theme.selectMultiple) == null || (_theme$selectMultiple = _theme$selectMultiple.help) == null || (_theme$selectMultiple = _theme$selectMultiple.container) == null ? void 0 : _theme$selectMultiple.pad }, /*#__PURE__*/_react["default"].createElement(_Text.Text, { size: "small" }, help));else helpContent = /*#__PURE__*/_react["default"].createElement(_Box.Box, { flex: false }, help); } if (showSelectedInline) summaryContent = /*#__PURE__*/_react["default"].createElement(_Box.Box, { direction: "row", justify: "between", flex: false }, summaryContent, /*#__PURE__*/_react["default"].createElement(_Box.Box, null, /*#__PURE__*/_react["default"].createElement(_Button.Button, { fill: "vertical", onClick: onClose, a11yTitle: "Close Select" }, icon))); return /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, { onEnter: onSelectOption, onSpace: onSelectOption, onUp: onPreviousOption, onDown: onNextOption, onKeyDown: onKeyDownOption, onEsc: onClose }, /*#__PURE__*/_react["default"].createElement(_StyledSelect.StyledContainer, { ref: ref, id: id ? id + "__select-drop" : undefined, dropHeight: dropHeight, a11yTitle: format({ id: 'selectMultiple.selectDrop', messages: messages }) }, summaryContent, onSearch && /*#__PURE__*/_react["default"].createElement(_Box.Box, { pad: !customSearchInput ? (_theme$selectMultiple2 = theme.selectMultiple) == null || (_theme$selectMultiple2 = _theme$selectMultiple2.search) == null ? void 0 : _theme$selectMultiple2.pad : undefined, flex: false }, /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, { onEnter: function onEnter(event) { onNextOption(event); } }, /*#__PURE__*/_react["default"].createElement(SelectTextInput, { a11yTitle: format({ id: 'selectMultiple.search', messages: messages }), focusIndicator: !customSearchInput, size: "small", ref: searchRef, type: "search", value: search || '', placeholder: searchPlaceholder, onChange: function onChange(event) { var nextSearch = event.target.value; setSearch(nextSearch); setActiveIndex(-1); onSearch(nextSearch); }, onFocus: function onFocus() { return setActiveIndex(-1); }, icon: searchIcon }))), helpContent, (options == null ? void 0 : options.length) > 0 ? /*#__PURE__*/_react["default"].createElement(_StyledSelect.OptionsContainer, { "aria-label": format({ id: 'selectMultiple.optionsA11y', messages: messages }), role: "listbox", tabIndex: "-1", ref: optionsRef, "aria-multiselectable": true, onMouseMove: function onMouseMove() { return setKeyboardNavigation(false); }, selectMultiple: true // internal prop }, /*#__PURE__*/_react["default"].createElement(_InfiniteScroll.InfiniteScroll, { items: options, step: theme.select.step, onMore: onMore, replace: replace, show: activeIndex !== -1 ? activeIndex : undefined }, function (option, index, optionRef) { var optionDisabled = isDisabled(index); var optionSelected = value ? (0, _utils2.arrayIncludes)(value, valueKey && valueKey.reduce ? (0, _utils2.applyKey)(option, valueKey) : option, valueKey || labelKey) : false; var optionActive = activeIndex === index; var optionLabel = (0, _utils2.getOptionLabel)(index, options, labelKey || valueKey); // Determine whether the label is done as a child or // as an option Button kind property. var child; var textComponent = false; if (children) { child = children(option, index, options, { active: optionActive, disabled: optionDisabled, selected: optionSelected }); if (typeof child === 'string' || child.props && child.props.children && typeof child.props.children === 'string') textComponent = true; } else { var _theme$selectMultiple3; child = /*#__PURE__*/_react["default"].createElement(_CheckBox.CheckBox, { label: /*#__PURE__*/_react["default"].createElement(_Box.Box, { alignSelf: "center", width: "100%", align: "start" }, optionLabel), pad: (_theme$selectMultiple3 = theme.selectMultiple) == null || (_theme$selectMultiple3 = _theme$selectMultiple3.option) == null ? void 0 : _theme$selectMultiple3.pad, tabIndex: "-1", checked: optionSelected, disabled: optionDisabled, inert: _utils2.inertTrueValue, containerProps: { // in Firefox when we have inert set, the checkbox // click event gets swallowed by the checkbox. // We need the click event to go the the button // around the checkbox so we use pointerEvents = // none. For code clarity we decided an inline // style made sense here. style: { pointerEvents: 'none' } } }); } if (!children && search) { var searchText = search.toLowerCase(); if (typeof optionLabel === 'string' && optionLabel.toLowerCase().indexOf(searchText) >= 0) { var _theme$selectMultiple4; // code to bold search term in matching options var boldIndex = optionLabel.toLowerCase().indexOf(searchText); var childBeginning = optionLabel.substring(0, boldIndex); var childBold = optionLabel.substring(boldIndex, boldIndex + searchText.length); childBold = /*#__PURE__*/_react["default"].createElement("b", null, childBold); var childEnd = optionLabel.substring(boldIndex + searchText.length); child = /*#__PURE__*/_react["default"].createElement(_CheckBox.CheckBox, { label: /*#__PURE__*/_react["default"].createElement(_Box.Box, { alignSelf: "center", width: "100%", align: "start", direction: "row" }, /*#__PURE__*/_react["default"].createElement(_Text.Text, null, childBeginning, childBold, childEnd)), pad: (_theme$selectMultiple4 = theme.selectMultiple) == null || (_theme$selectMultiple4 = _theme$selectMultiple4.option) == null ? void 0 : _theme$selectMultiple4.pad, tabIndex: "-1", checked: optionSelected, disabled: optionDisabled, inert: _utils2.inertTrueValue, containerProps: { // in Firefox when we have inert set, the checkbox // click event gets swallowed by the checkbox. // We need the click event to go the the button // around the checkbox so we use pointerEvents = // none. For code clarity we decided an inline // style made sense here. style: { pointerEvents: 'none' } } }); } } // if we have a child, turn on plain, and hoverIndicator return /*#__PURE__*/_react["default"].createElement(_StyledSelect.SelectOption, { a11yTitle: format({ id: optionSelected ? 'selectMultiple.optionSelected' : 'selectMultiple.optionNotSelected', messages: messages, values: { optionLabel: optionLabel } }) // lint isn't flagging this but we shouldn't use index // as a key see no-array-index-key lint rule , key: index // merge optionRef and activeRef , ref: function ref(node) { // eslint-disable-next-line no-param-reassign if (optionRef) optionRef.current = node; if (optionActive) activeRef.current = node; }, tabIndex: optionSelected || activeIndex === index || // when nothing is selected and entering listbox // first option should be focused value.length === 0 && activeIndex === -1 && index === 0 ? '0' : '-1', role: "option", id: "option" + index, "aria-setsize": options.length, "aria-posinset": index + 1, "aria-selected": optionSelected, focusIndicator: usingKeyboard, "aria-disabled": optionDisabled || undefined, plain: !child ? undefined : true, align: "start", kind: !child ? 'option' : undefined, active: optionActive, selected: optionSelected, onMouseOver: !optionDisabled ? onActiveOption(index) : undefined, onClick: !optionDisabled ? selectOption(index) : undefined, onFocus: function onFocus() { return setActiveIndex(index); }, textComponent: textComponent }, child); })) : /*#__PURE__*/_react["default"].createElement(_EmptySearchOption.EmptySearchOption, { emptySearchMessage: emptySearchMessage, selectOptionsStyle: selectOptionsStyle, theme: theme }), usingKeyboard && showA11yLimit && /*#__PURE__*/_react["default"].createElement(_Box.Box, { height: "0px", width: "0px", overflow: "hidden" // announce when we reach the limit of items // that can be selected , "aria-live": "assertive", role: "alert" }, showA11yLimit))); });