import React, { Fragment, useEffect, useId, useRef, useState } from 'react'; import styled from 'styled-components'; import type { JSX } from 'react'; import type { SelectOption, SelectProps } from '@redocly/theme/core/types'; import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon'; import { SelectInput } from '@redocly/theme/components/Select/SelectInput'; import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown'; import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu'; import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem'; import { useOutsideClick, useThemeHooks } from '@redocly/theme/core/hooks'; export function Select(props: SelectProps): JSX.Element { const { className, value, options, dataAttributes, withArrow = true, triggerEvent = 'click', placement, alignment, icon, onlyIcon, disabled, placeholder, hideCheckmarkIcon, checkmarkIconPosition, dataTestId = 'select', multiple, searchable, clearable, footer, onChange, onSearch, renderInput, renderDivider, } = props; const { useTranslate } = useThemeHooks(); const { translate } = useTranslate(); const getSelectedOptionsFromPropsValue = () => { const values = Array.isArray(value) ? value : [value]; return values .map((value) => { const selectedOption = options.find((option) => option.value === value); return ( selectedOption || (typeof value === 'string' ? ({ value } as SelectOption) : (value as SelectOption)) ); }) .filter((option) => !!option); }; const [selectedOptions, setSelectedOptions] = useState[]>( getSelectedOptionsFromPropsValue(), ); const selectRef = useRef(null); const selectInputRef = useRef(null); const [searchValue, setSearchValue] = useState(null); const [dropdownActive, setDropdownActive] = useState(false); const [filteredOptions, setFilteredOptions] = useState[]>(options); const [stickyInputValue, setStickyInputValue] = useState(placeholder || ''); const inputId = useId(); useOutsideClick(selectRef, () => { setDropdownActive(false); }); useEffect(() => { setFilteredOptions(options); }, [options]); useEffect(() => { setSelectedOptions(getSelectedOptionsFromPropsValue()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [multiple, value]); useEffect(() => { if (onSearch) { onSearch?.(searchValue as T); } else { if (typeof searchValue === 'string') { const filteredOptions = options.filter((option) => { const valueForSearch = String(option.label ?? option.value ?? ''); return ( !valueForSearch || valueForSearch.toLowerCase().indexOf(searchValue.toLowerCase()) > -1 ); }); setFilteredOptions(filteredOptions); } else { setFilteredOptions(options); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchValue]); const selectHandler = (selectedOption: SelectOption) => { const newSelectedOptions = multiple ? isSelected(selectedOption) ? selectedOptions.filter((option) => option.value !== selectedOption.value) : [...selectedOptions, selectedOption] : [selectedOption]; const newSelectedValues = newSelectedOptions.length ? multiple ? newSelectedOptions.map((o) => o.value) : newSelectedOptions[0].value : multiple ? [] : ('' as T); setSelectedOptions(newSelectedOptions); onChange?.(newSelectedValues); setSearchValue(null); setDropdownActive(false); if (!multiple) { setStickyInputValue(''); } selectInputRef.current?.focus(); }; const searchHandler = (e: React.ChangeEvent) => { const targetValue = e.target.value; setSearchValue(targetValue); setDropdownActive(true); }; const clearHandler = (option?: SelectOption) => { if (option) { selectHandler(option); } else { if (!multiple) { setStickyInputValue(''); } setSelectedOptions([]); onChange?.(multiple ? [] : ('' as T)); } }; const inputBlurHandler = (e: React.FocusEvent) => { const relatedTarget = e.relatedTarget as HTMLElement; const isDropdownItem = relatedTarget?.attributes.getNamedItem('data-component-name')?.value === 'Dropdown/DropdownMenuItem'; if (!isDropdownItem) { if (!e.relatedTarget || e.relatedTarget?.id !== inputId) { setDropdownActive(false); } setSearchValue(null); setStickyInputValue(''); } }; const inputFocusHandler = () => { if (!multiple && selectedOptions.length) { setStickyInputValue(selectedOptions[0].label || (selectedOptions[0].value as string)); } }; const inputClickHandler = () => { setDropdownActive(!dropdownActive); }; const isSelected = (option: SelectOption) => { return !!selectedOptions.find( (selectOption) => selectOption.value === option.value || selectOption.value === option.label, ); }; const renderDefaultInput = (inputRef: React.ForwardedRef) => { return ( ); }; return ( { if (e.key === 'Enter' && document.activeElement === selectRef.current) { e.preventDefault(); setDropdownActive(!dropdownActive); } }} tabIndex={0} aria-haspopup="listbox" aria-expanded={dropdownActive} aria-label="Select option" onFocus={(e) => { if (e.target === selectRef.current) { selectInputRef.current?.focus(); } }} onMouseDown={(e) => { e.preventDefault(); }} > {filteredOptions.length === 0 ? ( {translate('select.noResults', 'No results')} ) : ( filteredOptions.map((option, index) => { return ( selectHandler(option)} prefix={ !hideCheckmarkIcon && (checkmarkIconPosition ? checkmarkIconPosition === 'start' : false) && isSelected(option) && } suffix={ !hideCheckmarkIcon && (checkmarkIconPosition ? checkmarkIconPosition === 'end' : true) && isSelected(option) && } > {typeof option.element === 'string' ? (
{option.element}
) : ( option.element )}
{renderDivider && index !== options.length - 1 ? renderDivider() : null}
); }) || translate('select.noResults', 'No results') )}
); } export const SelectWrapper = styled.div<{ disabled?: boolean }>` display: flex; position: relative; font-size: var(--select-font-size); font-weight: var(--select-font-weight); line-height: var(--select-line-height); border-radius: var(--select-border-radius); border: var(--select-border); color: var(--select-text-color); min-width: 0; &:focus-visible { outline: 1px solid var(--button-border-color-focused); } ${({ disabled }) => disabled && ` opacity: 0.59; pointer-events: none; `} `; const SelectDropdown = styled(Dropdown)` width: 100%; > * { min-width: 100%; } `; const SelectDropdownMenu = styled(DropdownMenu)` width: 100%; margin-top: var(--spacing-unit); `;