bazarr/frontend/src/components/inputs/Selector.tsx

142 lines
3.4 KiB
TypeScript
Raw Normal View History

import clsx from "clsx";
import { FocusEvent, useCallback, useMemo, useRef } from "react";
import Select, { GroupBase, OnChangeValue } from "react-select";
import { SelectComponents } from "react-select/dist/declarations/src/components";
export type SelectorOption<T> = {
label: string;
value: T;
};
export type SelectorComponents<T, M extends boolean> = SelectComponents<
SelectorOption<T>,
M,
GroupBase<SelectorOption<T>>
>;
export type SelectorValueType<T, M extends boolean> = M extends true
? ReadonlyArray<T>
: Nullable<T>;
2021-03-25 14:22:43 +00:00
export interface SelectorProps<T, M extends boolean> {
className?: string;
placeholder?: string;
options: readonly SelectorOption<T>[];
disabled?: boolean;
clearable?: boolean;
loading?: boolean;
multiple?: M;
onChange?: (k: SelectorValueType<T, M>) => void;
onFocus?: (e: FocusEvent<HTMLElement>) => void;
2021-03-25 14:22:43 +00:00
label?: (item: T) => string;
defaultValue?: SelectorValueType<T, M>;
value?: SelectorValueType<T, M>;
components?: Partial<
SelectComponents<SelectorOption<T>, M, GroupBase<SelectorOption<T>>>
>;
2021-03-25 14:22:43 +00:00
}
export function Selector<T = string, M extends boolean = false>(
props: SelectorProps<T, M>
) {
const {
className,
placeholder,
label,
disabled,
clearable,
loading,
options,
multiple,
onChange,
onFocus,
2021-03-25 14:22:43 +00:00
defaultValue,
components,
2021-03-25 14:22:43 +00:00
value,
} = props;
const labelRef = useRef(label);
const getName = useCallback(
2021-03-25 14:22:43 +00:00
(item: T) => {
if (labelRef.current) {
return labelRef.current(item);
}
return options.find((v) => v.value === item)?.label ?? "Unknown";
2021-03-25 14:22:43 +00:00
},
[options]
);
const wrapper = useCallback(
(
value: SelectorValueType<T, M> | undefined | null
):
| SelectorOption<T>
| ReadonlyArray<SelectorOption<T>>
| null
| undefined => {
if (value === null || value === undefined) {
return value as null | undefined;
} else {
if (multiple === true) {
2021-03-25 14:22:43 +00:00
return (value as SelectorValueType<T, true>).map((v) => ({
label: getName(v),
2021-03-25 14:22:43 +00:00
value: v,
}));
} else {
const v = value as T;
return {
label: getName(v),
2021-03-25 14:22:43 +00:00
value: v,
};
}
}
},
[multiple, getName]
2021-03-25 14:22:43 +00:00
);
const defaultWrapper = useMemo(
() => wrapper(defaultValue),
[defaultValue, wrapper]
);
2021-03-25 14:22:43 +00:00
const valueWrapper = useMemo(() => wrapper(value), [wrapper, value]);
return (
<Select
2021-03-25 14:22:43 +00:00
isLoading={loading}
placeholder={placeholder}
isSearchable={options.length >= 10}
isMulti={multiple}
closeMenuOnSelect={!multiple}
defaultValue={defaultWrapper}
value={valueWrapper}
isClearable={clearable}
isDisabled={disabled}
options={options}
components={components}
className={clsx("custom-selector w-100", className)}
2021-03-25 14:22:43 +00:00
classNamePrefix="selector"
onFocus={onFocus}
onChange={(newValue) => {
2021-03-25 14:22:43 +00:00
if (onChange) {
if (multiple === true) {
const values = (
newValue as OnChangeValue<SelectorOption<T>, true>
).map((v) => v.value) as ReadonlyArray<T>;
onChange(values as SelectorValueType<T, M>);
2021-03-25 14:22:43 +00:00
} else {
2022-04-27 02:40:35 +00:00
const value =
(newValue as OnChangeValue<SelectorOption<T>, false>)?.value ??
null;
onChange(value as SelectorValueType<T, M>);
2021-03-25 14:22:43 +00:00
}
}
}}
></Select>
2021-03-25 14:22:43 +00:00
);
}