|
| 1 | +import Select, { MultiValue as MultiValueT, SingleValue as SingleValueT } from "react-select"; |
| 2 | +import { useDefaultMessages } from ".."; |
| 3 | +import { FieldProps } from "../Field/FieldProps"; |
| 4 | +import { BentoConfigProvider, useBentoConfig } from "../BentoConfigContext"; |
| 5 | +import { AriaLabelingProps, DOMProps } from "@react-types/shared"; |
| 6 | +import * as selectComponents from "./components"; |
| 7 | +import { useEffect, useRef } from "react"; |
| 8 | +import { BaseMultiProps, BaseSelectProps, BaseSingleProps, SelectOption } from "./types"; |
| 9 | + |
| 10 | +type MultiProps<A> = BaseMultiProps & |
| 11 | + Pick<FieldProps<A[]>, "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value">; |
| 12 | + |
| 13 | +type SingleProps<A> = BaseSingleProps & |
| 14 | + Pick< |
| 15 | + FieldProps<A | undefined>, |
| 16 | + "autoFocus" | "disabled" | "name" | "onBlur" | "onChange" | "value" |
| 17 | + >; |
| 18 | + |
| 19 | +type Props<A> = BaseSelectProps<A> & { |
| 20 | + fieldProps: AriaLabelingProps & DOMProps; |
| 21 | + validationState: "valid" | "invalid"; |
| 22 | +} & (SingleProps<A> | MultiProps<A>); |
| 23 | + |
| 24 | +export function BaseSelect<A>(props: Props<A>) { |
| 25 | + const dropdownConfig = useBentoConfig().dropdown; |
| 26 | + const { defaultMessages } = useDefaultMessages(); |
| 27 | + |
| 28 | + const menuPortalTarget = useRef<HTMLDivElement>(); |
| 29 | + useEffect(() => { |
| 30 | + if (!menuPortalTarget.current) { |
| 31 | + menuPortalTarget.current = document.createElement("div"); |
| 32 | + } |
| 33 | + document.body.appendChild(menuPortalTarget.current); |
| 34 | + |
| 35 | + return () => { |
| 36 | + if (document.body.contains(menuPortalTarget.current!)) { |
| 37 | + document.body.removeChild(menuPortalTarget.current!); |
| 38 | + } |
| 39 | + }; |
| 40 | + }, [menuPortalTarget]); |
| 41 | + |
| 42 | + const { |
| 43 | + fieldProps, |
| 44 | + validationState, |
| 45 | + value, |
| 46 | + onChange, |
| 47 | + options, |
| 48 | + onBlur, |
| 49 | + name, |
| 50 | + placeholder, |
| 51 | + disabled, |
| 52 | + isReadOnly, |
| 53 | + isMulti, |
| 54 | + noOptionsMessage, |
| 55 | + autoFocus, |
| 56 | + menuSize = dropdownConfig.defaultMenuSize, |
| 57 | + searchable, |
| 58 | + } = props; |
| 59 | + |
| 60 | + return ( |
| 61 | + // NOTE(gabro): SelectField has its own config for List, so we override it here using BentoConfigProvider |
| 62 | + <BentoConfigProvider value={{ list: dropdownConfig.list }}> |
| 63 | + <Select |
| 64 | + id={fieldProps.id} |
| 65 | + name={name} |
| 66 | + aria-label={fieldProps["aria-label"]} |
| 67 | + aria-labelledby={fieldProps["aria-labelledby"]} |
| 68 | + isDisabled={disabled} |
| 69 | + isReadOnly={isReadOnly || false} |
| 70 | + autoFocus={autoFocus} |
| 71 | + value={ |
| 72 | + isMulti |
| 73 | + ? options.filter((o) => ((value ?? []) as readonly A[]).includes(o.value)) |
| 74 | + : options.find((o) => o.value === value) |
| 75 | + } |
| 76 | + onChange={(o) => { |
| 77 | + if (isMulti) { |
| 78 | + const multiValue = o as MultiValueT<SelectOption<A>>; |
| 79 | + onChange(multiValue.map((a) => a.value)); |
| 80 | + } else { |
| 81 | + const singleValue = o as SingleValueT<SelectOption<A>>; |
| 82 | + onChange(singleValue == null ? undefined : singleValue.value); |
| 83 | + } |
| 84 | + }} |
| 85 | + onBlur={onBlur} |
| 86 | + options={options |
| 87 | + .slice() // avoid mutating the original array |
| 88 | + .sort((a, b) => { |
| 89 | + // In case of multi-select, we display the selected options first |
| 90 | + if (isMulti) { |
| 91 | + const selectedValues = (value ?? []) as readonly A[]; |
| 92 | + const isSelected = (a: SelectOption<A>) => selectedValues.includes(a.value); |
| 93 | + if (isSelected(a) && !isSelected(b)) { |
| 94 | + return -1; |
| 95 | + } |
| 96 | + if (!isSelected(a) && isSelected(b)) { |
| 97 | + return 1; |
| 98 | + } |
| 99 | + } |
| 100 | + return 0; |
| 101 | + })} |
| 102 | + placeholder={placeholder} |
| 103 | + menuPortalTarget={menuPortalTarget.current} |
| 104 | + components={selectComponents} |
| 105 | + openMenuOnFocus |
| 106 | + styles={selectComponents.styles<SelectOption<A>>()} |
| 107 | + validationState={validationState} |
| 108 | + isMulti={isMulti} |
| 109 | + isClearable={false} |
| 110 | + noOptionsMessage={() => noOptionsMessage ?? defaultMessages.SelectField.noOptionsMessage} |
| 111 | + multiValueMessage={ |
| 112 | + props.isMulti && (!props.multiSelectMode || props.multiSelectMode === "summary") |
| 113 | + ? props.multiValueMessage ?? defaultMessages.SelectField.multiOptionsSelected |
| 114 | + : undefined |
| 115 | + } |
| 116 | + closeMenuOnSelect={!isMulti} |
| 117 | + hideSelectedOptions={false} |
| 118 | + menuSize={menuSize} |
| 119 | + menuIsOpen={isReadOnly ? false : undefined} |
| 120 | + isSearchable={isReadOnly ? false : searchable ?? true} |
| 121 | + showMultiSelectBulkActions={isMulti ? props.showMultiSelectBulkActions : false} |
| 122 | + clearAllButtonLabel={ |
| 123 | + isMulti |
| 124 | + ? props.clearAllButtonLabel ?? defaultMessages.SelectField.clearAllButtonLabel |
| 125 | + : undefined |
| 126 | + } |
| 127 | + selectAllButtonLabel={ |
| 128 | + isMulti |
| 129 | + ? props.selectAllButtonLabel ?? defaultMessages.SelectField.selectAllButtonLabel |
| 130 | + : undefined |
| 131 | + } |
| 132 | + multiSelectMode={isMulti ? props.multiSelectMode : undefined} |
| 133 | + /> |
| 134 | + </BentoConfigProvider> |
| 135 | + ); |
| 136 | +} |
0 commit comments