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 };