mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-24 20:27:13 -05:00
166 lines
4.0 KiB
TypeScript
166 lines
4.0 KiB
TypeScript
import * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import styles from "./SearchSelect.module.css";
|
|
import { SendouSelect, SendouSelectItem } from "./Select";
|
|
import type { EntitySearch } from "./useEntitySearch";
|
|
|
|
const PLACEHOLDER_TEXTS = {
|
|
organizationSearch: {
|
|
placeholder: "common:forms.organizationSearch.placeholder",
|
|
noResults: "common:forms.organizationSearch.noResults",
|
|
},
|
|
teamSearch: {
|
|
placeholder: "common:forms.teamSearch.placeholder",
|
|
noResults: "common:forms.teamSearch.noResults",
|
|
},
|
|
tournamentSearch: {
|
|
placeholder: "common:forms.tournamentSearch.placeholder",
|
|
noResults: "common:forms.tournamentSearch.noResults",
|
|
},
|
|
userSearch: {
|
|
placeholder: "common:forms.userSearch.placeholder",
|
|
noResults: "common:forms.userSearch.noResults",
|
|
},
|
|
} as const;
|
|
|
|
/** Field props the entity search components (`UserSearch`, `TeamSearch`, ...) pass straight through. */
|
|
export interface SearchSelectFieldProps {
|
|
name?: string;
|
|
label?: string;
|
|
bottomText?: string;
|
|
errorText?: string;
|
|
isRequired?: boolean;
|
|
isDisabled?: boolean;
|
|
className?: string;
|
|
onBlur?: () => void;
|
|
"data-testid"?: string;
|
|
}
|
|
|
|
interface SearchSelectProps<TItem extends { id: number; name: string }>
|
|
extends SearchSelectFieldProps {
|
|
ariaLabel: string;
|
|
inputTestId: string;
|
|
inputClassName?: string;
|
|
i18nKey: keyof typeof PLACEHOLDER_TEXTS;
|
|
search: EntitySearch<TItem>;
|
|
buttonRef?: React.Ref<HTMLButtonElement>;
|
|
renderItem: (item: TItem) => React.ReactElement;
|
|
}
|
|
|
|
/** Presentational autocomplete select for the entity searches (`UserSearch` etc.); `search` comes from `useEntitySearch`. */
|
|
export function SearchSelect<TItem extends { id: number; name: string }>({
|
|
name,
|
|
label,
|
|
bottomText,
|
|
errorText,
|
|
ariaLabel,
|
|
inputTestId,
|
|
inputClassName,
|
|
i18nKey,
|
|
search,
|
|
buttonRef,
|
|
renderItem,
|
|
isRequired,
|
|
isDisabled,
|
|
className,
|
|
onBlur,
|
|
"data-testid": testId,
|
|
}: SearchSelectProps<TItem>) {
|
|
return (
|
|
<SendouSelect
|
|
name={name}
|
|
label={label}
|
|
labelRequired={isRequired}
|
|
isRequired={isRequired}
|
|
isDisabled={isDisabled}
|
|
className={className}
|
|
onBlur={onBlur}
|
|
data-testid={testId}
|
|
selectedKey={search.selectedKey}
|
|
onSelectionChange={(key) => {
|
|
if (key != null) {
|
|
search.onSelectionChange(Number(key));
|
|
}
|
|
}}
|
|
aria-label={ariaLabel}
|
|
searchInputValue={search.filterText}
|
|
onSearchInputChange={search.setFilterText}
|
|
search={{ testId: inputTestId, inputClassName: inputClassName ?? "" }}
|
|
bottomText={bottomText}
|
|
errorText={errorText}
|
|
triggerRef={buttonRef}
|
|
popoverClassName={styles.popover}
|
|
>
|
|
{search.items.map((item) =>
|
|
typeof item.id === "string" ? (
|
|
<PlaceholderItem key={item.id} id={item.id} i18nKey={i18nKey} />
|
|
) : (
|
|
React.cloneElement(renderItem(item as TItem), { key: item.id })
|
|
),
|
|
)}
|
|
</SendouSelect>
|
|
);
|
|
}
|
|
|
|
function PlaceholderItem({
|
|
id,
|
|
i18nKey,
|
|
}: {
|
|
id: "PLACEHOLDER" | "NO_RESULTS";
|
|
i18nKey: keyof typeof PLACEHOLDER_TEXTS;
|
|
}) {
|
|
const { t } = useTranslation(["common"]);
|
|
|
|
return (
|
|
<SendouSelectItem
|
|
id={id}
|
|
textValue="PLACEHOLDER"
|
|
isDisabled
|
|
className={styles.placeholder}
|
|
>
|
|
{id === "PLACEHOLDER"
|
|
? t(PLACEHOLDER_TEXTS[i18nKey].placeholder)
|
|
: t(PLACEHOLDER_TEXTS[i18nKey].noResults)}
|
|
</SendouSelectItem>
|
|
);
|
|
}
|
|
|
|
/** One `SearchSelect` result; `SearchSelectItemAdditionalText` is the muted second line, hidden in the trigger. */
|
|
export function SearchSelectItem({
|
|
id,
|
|
textValue,
|
|
testId,
|
|
leading,
|
|
children,
|
|
}: {
|
|
id: number;
|
|
textValue: string;
|
|
testId: string;
|
|
leading?: React.ReactNode;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<SendouSelectItem
|
|
id={id}
|
|
textValue={textValue}
|
|
className={styles.item}
|
|
data-testid={testId}
|
|
>
|
|
{leading}
|
|
<div className={styles.itemTextsContainer}>{children}</div>
|
|
</SendouSelectItem>
|
|
);
|
|
}
|
|
|
|
export function SearchSelectItemLogo({ src }: { src: string }) {
|
|
return <img src={src} alt="" className={styles.logo} />;
|
|
}
|
|
|
|
export function SearchSelectItemAdditionalText({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
return <div className={styles.itemAdditionalText}>{children}</div>;
|
|
}
|