import React, { HTMLAttributes, ReactNode } from "react"; import classNames from "classnames"; import { useSelect } from "downshift"; import { SingleSelectContext } from "./single/SingleSelectContext"; import { SelectOption, SelectOptionsByCategory, SingleSelectedOption, } from "./shared/types"; import { Box } from "../Box"; import { SingleSelectOption } from "./single/SingleSelectOption"; import { SelectOptionCategory } from "./shared/SelectOptionCategory"; import { useSelectLayout } from "./hooks/useSelectLayout"; import { SelectStatus } from "./shared/SelectStatus"; import { STATUS_VARIANT } from "../../types"; import { useSelectCreatable } from "./hooks/useSelectCreatable"; import { useTransformSelectOptions } from "./hooks/useTransformSelectOptions"; import { useDownshiftConfig } from "./hooks/useDownshiftConfig"; import { SingleSelectSelected } from "./single/SingleSelectSelected"; import { SelectControl } from "./shared/SelectControl"; import { SelectMenu } from "./shared/SelectMenu"; interface SingleSelectBaseProps extends Pick, "className" | "style"> { /** * The currently selected item (`T`) or `null`. * * Generic `T` extends the base options shape: `{ value: string; label: string;}` */ selected: SingleSelectedOption; /** * Called whenever the `selected` item should change */ onChange: (newSelected: SingleSelectedOption) => void; /** * `children` is what gets displayed in the dropdown when opened. Since `children` * are rendered as a direct descendent of a `ul` element, every child must render * an `li` as its outer-most element. The `SingleSelect.Option` and * `SingleSelect.Status` components do this automatically. You'll need to make sure * any other elements you pass to `children` do as well. * * **SUPER IMPORTANT NOTE**: `children` need to be rendered in the exact * same order as `options`. If you need to do any sorting, make sure you are using * the sorted array for both `options` and `children`. */ children: ReactNode; /** * The current value of the search input */ inputValue?: string; /** * If `true`, an 'X' button will appear within the component when an item is selected * that allows the user to clear the selection, setting `selected` to `null`. */ isClearable?: boolean; /** * If `true`, the component gets disabled styles and cannot be interacted with. */ isDisabled?: boolean; /** * If `true`, a loading indicator will be displayed within the component. This is useful * when fetching results or when saving a user's selection. */ isLoading?: boolean; /** * **IMPORTANT: This prop is technically optional but should always be used unless this component * is being used within a `FormGroup`!** * * Providing a `label` will make this component accessible to those using a * screen reader. However, when this component is used inside a `FormGroup` with a `Label`, that * label will be used automatically. See the "FormGroup" demo for details. */ label?: string; /** * If a function is provided to `onCreateOption` and the search field is enabled (by * providing a function to `onInputChange`), users will be able to create new options * based on their search query. This function will be called when the new option is * created. */ onCreateOption?: (newOption: string) => void; /** * When a function is provided to `onInputChange`, it will enable the search field in * the dropdown. * * **NOTE**: This component does not handle any actual filtering. It's up to you to * filter options based on the query, provide them to the `options` prop, and use * them to render `children`. */ onInputChange?: (newValue: string) => void; /** * If a string/element is provided to `placeholder`, it will be displayed when `selected` * is `null`. */ placeholder?: ReactNode; /** * If provided, this function will replace the default render function that displays the * currently selected item. * * **NOTE**: The default render function accounts for labels that are too long. You should * probably do the same when using this function (perhaps using Arrow's `Truncate` component) * unless you are sure none of your selected items will exceed the allotted space. */ renderSelected?: ({ selectedItem, }: { selectedItem: SingleSelectedOption; }) => ReactNode; /** * Useful for indicating success, warning, or failure status */ variant?: STATUS_VARIANT; } export interface SingleSelectBasicProps extends SingleSelectBaseProps { /** * This prop will define the shape of the `options` passed in to the component. * The default, `basic`, will be used the vast majority of the time and will only * need to be changed if the display of options within the dropdown should be * anything other than a flat list. */ optionsDisplay?: "basic"; /** * When `optionsDisplay` is `basic` (default), the `options` prop should be a flat * array of options. * * Generic `T` extends the base options shape: `{ value: string; label: string;}` */ options: T[]; } export interface SingleSelectCategoriesProps extends SingleSelectBaseProps { /** * Setting `optionsDisplay` to `categories` makes it so options can be displayed * under various category headers within the dropdown. */ optionsDisplay: "categories"; /** * When `optionsDisplay` is `categories`, the `options` prop should be an array * of categories with `title` and `options` properties. The `title` is just a * string and the `options` should be an array of `T`. */ options: SelectOptionsByCategory[]; } export type SingleSelectProps = | SingleSelectBasicProps | SingleSelectCategoriesProps; function SingleSelect({ selected, onChange, label, renderSelected, children, className, inputValue, onInputChange, isLoading, placeholder, isClearable, onCreateOption, isDisabled, variant: variantProp, ...props }: SingleSelectProps) { const { isCreatable, creatableOption, inputHasFocus, setInputHasFocus, } = useSelectCreatable({ onCreateOption, inputValue }); const transformedOptions = useTransformSelectOptions({ ...props, isCreatable, creatableOption, }); const downshiftConfig = useDownshiftConfig({ transformedOptions, selected, onChange, onInputChange, inputHasFocus, }); const { isOpen, selectedItem, selectItem, getToggleButtonProps, getMenuProps, highlightedIndex, getItemProps, getLabelProps, closeMenu, setHighlightedIndex, } = useSelect>(downshiftConfig); const { setReferenceElement, popperRef, controlClasses, wrapperClasses, callbackElementProps, inputRef, hasContextLabel, optionIndexRef, setOptionIndex, menuProps, createOption, } = useSelectLayout({ isOpen, hasSearch: !!onInputChange, isDisabled, variantProp, getLabelProps, onCreateOption, inputValue, closeMenu, getMenuProps, transformedOptions, highlightedIndex, onInputChange, inputHasFocus, isCreatable, setHighlightedIndex, }); // Swallow props we don't want to pass to the outer element const { options, optionsDisplay, ...rest } = props; return ( selectItem(null)} hasSelection={!!selectedItem} className={controlClasses} > {children} ); } SingleSelect.Option = SingleSelectOption; SingleSelect.Category = SelectOptionCategory; SingleSelect.Status = SelectStatus; SingleSelect.defaultProps = { optionsDisplay: "basic", variant: STATUS_VARIANT.DEFAULT, }; export { SingleSelect };