UNPKG

@kiwicom/orbit-components

Version:

Orbit-components is a React component library which provides developers with the easiest possible way of building Kiwi.com’s products.

396 lines (395 loc) 13.5 kB
"use client"; import _extends from "@babel/runtime/helpers/esm/extends"; import React from "react"; import styled, { css } from "styled-components"; import { groupOptions } from "./helpers"; import InputSelectOption from "./InputSelectOption"; import { StyledCloseButton, StyledDropdown, StyledLabel, StyledModalWrapper } from "./InputSelect.styled"; import CloseCircle from "../icons/CloseCircle"; import InputField from "../InputField"; import { useRandomIdSeed } from "../hooks/useRandomId"; import useClickOutside from "../hooks/useClickOutside"; import KEY_CODE from "../common/keyMaps"; import Box from "../Box"; import Text from "../Text"; import Stack from "../Stack"; import useMediaQuery from "../hooks/useMediaQuery"; import Modal, { ModalFooter } from "../Modal"; import ModalCloseButton from "../Modal/ModalCloseButton"; import Button from "../Button"; import Heading from "../Heading"; import { ModalSectionWrapper as ModalSection } from "../Modal/ModalSection"; import { ModalHeaderWrapper as ModalHeader } from "../Modal/ModalHeader"; const StyledModalSection = styled(ModalSection).withConfig({ displayName: "InputSelect__StyledModalSection", componentId: "sc-13l56eg-0" })(["padding:0;"]); const StyledModalHeader = styled(ModalHeader).withConfig({ displayName: "InputSelect__StyledModalHeader", componentId: "sc-13l56eg-1" })(["", ";"], ({ theme }) => css(["padding:", " !important;margin-bottom:0 !important;"], theme.orbit.spaceMedium)); const InputSelect = /*#__PURE__*/React.forwardRef(({ onChange, options, defaultSelected, prevSelected, prevSelectedLabel = "Previously selected", id, onFocus, label, showAll = true, showAllLabel = showAll ? "All options" : "Other options", help, error, onBlur, placeholder, labelClose = "Close", emptyState = "No results found.", onOptionSelect, onClose, disabled, maxHeight = "400px", maxWidth, onKeyDown, spaceAfter, ...props }, ref) => { const randomId = useRandomIdSeed(); const labelRef = React.useRef(null); const inputId = id || randomId("input"); const dropdownId = randomId("dropdown"); const dropdownRef = React.useRef(null); const [isOpened, setIsOpened] = React.useState(false); const [inputValue, setInputValue] = React.useState(defaultSelected ? options.find(opt => opt.value === defaultSelected.value)?.title : ""); const [selectedOption, setSelectedOption] = React.useState(defaultSelected || null); const [activeIdx, setActiveIdx] = React.useState(0); const [activeDescendant, setActiveDescendant] = React.useState(""); const [isScrolled, setIsScrolled] = React.useState(false); const [topOffset, setTopOffset] = React.useState(0); const refs = {}; const { isLargeMobile } = useMediaQuery(); const groupedOptions = React.useMemo(() => groupOptions(options, showAll, prevSelected), [options, prevSelected, showAll]); const [results, setResults] = React.useState(groupedOptions); const handleClose = selection => { if (!selection) { if (inputValue === "") { if (onOptionSelect) onOptionSelect(null); setSelectedOption(null); } else if (inputValue !== selectedOption?.title) { setInputValue(selectedOption?.title || ""); } setResults(groupedOptions); setActiveIdx(0); } if (onClose && isOpened) onClose(selection || (inputValue === "" ? null : selectedOption)); setIsOpened(false); }; const handleCloseClick = () => { handleClose(); }; useClickOutside(labelRef, handleCloseClick); const handleFocus = ev => { if (onFocus) onFocus(ev); setIsOpened(true); setResults(results || groupedOptions); }; const handleBlur = ev => { if (onBlur) onBlur(ev); }; const handleInputChange = ev => { const { value } = ev.currentTarget; if (onChange) onChange(ev); if (value.length === 0) { setResults(groupedOptions); } else { const filtered = options.filter(({ title }) => { return title.toLowerCase().includes(value.toLowerCase()); }); setResults({ groups: [], all: filtered, flattened: filtered }); } if (!isOpened) setIsOpened(true); setInputValue(value); setActiveIdx(0); }; const handleDropdownKey = ev => { if (!isOpened && (ev.keyCode === KEY_CODE.ENTER || ev.keyCode === KEY_CODE.ARROW_DOWN || ev.keyCode === KEY_CODE.ARROW_UP)) { setIsOpened(true); return; } if (isOpened && ev.keyCode === KEY_CODE.ESC) handleClose(); if (isOpened && ev.keyCode === KEY_CODE.ENTER) { ev.preventDefault(); if (results.all.length !== 0) { const option = results.flattened[activeIdx]; setSelectedOption(option); setInputValue(option.title); if (onOptionSelect) onOptionSelect(option); handleClose(option); } } if (ev.keyCode === KEY_CODE.ARROW_DOWN) { if (results.flattened.length - 1 > activeIdx) { const nextIdx = activeIdx + 1; setActiveIdx(nextIdx); setActiveDescendant(refs[nextIdx].current?.id); if (dropdownRef && dropdownRef.current) { dropdownRef.current.scrollTop = refs[nextIdx].current?.offsetTop; } } } if (ev.keyCode === KEY_CODE.ARROW_UP) { if (activeIdx > 0) { const prevIdx = activeIdx - 1; setActiveIdx(prevIdx); setActiveDescendant(refs[prevIdx].current?.id); if (dropdownRef && dropdownRef.current) { dropdownRef.current.scrollTop = refs[prevIdx].current?.offsetTop; } } } }; const input = /*#__PURE__*/React.createElement(InputField, _extends({ help: isLargeMobile && help, error: isLargeMobile && error, label: isLargeMobile && label, disabled: disabled, onFocus: handleFocus, onBlur: handleBlur, onChange: handleInputChange, id: inputId, placeholder: placeholder, autoFocus: !isLargeMobile, role: "combobox", value: inputValue, onKeyDown: ev => { if (onKeyDown) onKeyDown(ev); handleDropdownKey(ev); }, ariaHasPopup: isOpened, ariaExpanded: isOpened, ariaAutocomplete: "list", ariaActiveDescendant: activeDescendant, ariaControls: isOpened ? dropdownId : undefined, autoComplete: "off", ref: ref, prefix: selectedOption && selectedOption.prefix, suffix: String(inputValue).length > 1 && /*#__PURE__*/React.createElement(StyledCloseButton, { onClick: ev => { ev.preventDefault(); if (onOptionSelect) onOptionSelect(null); setInputValue(""); setResults(groupedOptions); setSelectedOption(null); setActiveIdx(0); }, $disabled: disabled }, /*#__PURE__*/React.createElement(CloseCircle, { color: "primary", ariaLabel: "Clear" })) }, props)); const renderOptions = () => { if (results.groups.length === 0) { return results.all.map((option, idx) => { const { title, description, prefix, value: optValue } = option; const optionId = randomId(title); const isSelected = optValue === selectedOption?.value; const optionRef = /*#__PURE__*/React.createRef(); refs[idx] = optionRef; return /*#__PURE__*/React.createElement(InputSelectOption, { key: optionId, id: optionId, active: activeIdx === idx, isSelected: isSelected, ref: optionRef, title: title, description: description, prefix: prefix, onClick: ev => { ev.preventDefault(); setActiveIdx(idx); setResults(groupedOptions); if (onOptionSelect) onOptionSelect(option); if (isLargeMobile) setIsOpened(false); if (!isSelected) { setInputValue(title); setSelectedOption(option); handleClose(option); } } }); }); } let idx = -1; return /*#__PURE__*/React.createElement(React.Fragment, null, results.groups.map((group, groupIdx) => { const prevSelectedOption = prevSelected && groupIdx === 0; const { group: groupTitle } = group[0]; const groupId = randomId(prevSelectedOption ? "prevSelected" : `${groupTitle}`); return /*#__PURE__*/React.createElement(React.Fragment, { key: groupId }, /*#__PURE__*/React.createElement(Box, { padding: "small" }, /*#__PURE__*/React.createElement(Text, { type: "secondary" }, prevSelectedOption ? prevSelectedLabel : groupTitle)), group.map(option => { idx += 1; const optionIdx = idx; const optionRef = /*#__PURE__*/React.createRef(); refs[optionIdx] = optionRef; const { title, description, prefix, value: optValue } = option; const optionId = randomId(title); const isSelected = optValue === selectedOption?.value; return /*#__PURE__*/React.createElement(InputSelectOption, { key: optionId, id: optionId, active: !!isLargeMobile && activeIdx === optionIdx, isSelected: isSelected, ref: optionRef, title: title, description: description, prefix: prefix, onClick: ev => { ev.preventDefault(); if (onOptionSelect) onOptionSelect(option); setActiveIdx(optionIdx); setResults(groupedOptions); if (isLargeMobile) setIsOpened(false); if (!isSelected) { setInputValue(title); setSelectedOption(option); handleClose(option); } } }); })); }), /*#__PURE__*/React.createElement(Box, { padding: "small" }, /*#__PURE__*/React.createElement(Text, { type: "secondary" }, showAllLabel)), results.all.map(option => { const { title, description, prefix, value: optValue, group } = option; if (group && !showAll) return null; idx += 1; const optionRef = /*#__PURE__*/React.createRef(); const optionIdx = idx; refs[optionIdx] = optionRef; const optionId = randomId(`all_${title}`); const isSelected = optValue === selectedOption?.value; return /*#__PURE__*/React.createElement(InputSelectOption, { key: optionId, id: optionId, active: activeIdx === optionIdx, isSelected: isSelected, ref: optionRef, title: title, description: description, prefix: prefix, onClick: ev => { ev.preventDefault(); if (onOptionSelect) onOptionSelect(option); setActiveIdx(optionIdx); setResults(groupedOptions); if (isLargeMobile) setIsOpened(false); if (!isSelected) { setInputValue(title); setSelectedOption(option); handleClose(option); } } }); })); }; const noResults = typeof emptyState === "string" ? /*#__PURE__*/React.createElement(Box, { padding: "medium" }, /*#__PURE__*/React.createElement(Text, null, emptyState)) : emptyState; const dropdown = isOpened && /*#__PURE__*/React.createElement(StyledDropdown, { role: "listbox", id: dropdownId, "aria-labelledby": inputId, $hasLabel: !!label, $maxHeight: maxHeight, $maxWidth: maxWidth, ref: dropdownRef }, results.all.length === 0 ? noResults : renderOptions()); return isLargeMobile ? /*#__PURE__*/React.createElement(StyledLabel, { htmlFor: inputId, ref: labelRef, spaceAfter: spaceAfter }, input, dropdown) : /*#__PURE__*/React.createElement(StyledLabel, { htmlFor: inputId, ref: labelRef, spaceAfter: spaceAfter }, /*#__PURE__*/React.createElement(InputField, { label: label, help: help, error: error, onFocus: () => setIsOpened(true), readOnly: true, role: "textbox", placeholder: placeholder, value: inputValue, prefix: selectedOption && selectedOption.prefix }), isOpened && /*#__PURE__*/React.createElement(StyledModalWrapper, { $maxHeight: maxHeight, isScrolled: isScrolled && topOffset > 50 }, /*#__PURE__*/React.createElement(Modal, { labelClose: labelClose, onClose: handleCloseClick, fixedFooter: true, onScroll: ev => { if (!isLargeMobile) { ev.preventDefault(); setIsScrolled(true); setTopOffset(ev.currentTarget.scrollTop); } }, mobileHeader: false, autoFocus: true }, /*#__PURE__*/React.createElement(StyledModalHeader, null, label && /*#__PURE__*/React.createElement(Stack, { align: "center", justify: "between" }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(Heading, { type: "title2" }, label)), /*#__PURE__*/React.createElement(ModalCloseButton, { onClick: handleCloseClick, title: labelClose })), input), /*#__PURE__*/React.createElement(StyledModalSection, null, dropdown), /*#__PURE__*/React.createElement(ModalFooter, { flex: "100%" }, /*#__PURE__*/React.createElement(Button, { type: "secondary", fullWidth: true, onClick: handleCloseClick }, labelClose))))); }); InputSelect.displayName = "InputSelect"; export default InputSelect;