Core/src/Components/Select/OptionsSuggestions.tsx

132 lines
3.3 KiB
TypeScript

import React, {MutableRefObject, useCallback, useMemo, useRef, useState} from "react";
import {classes} from "../../Utils";
import {Check} from "@phosphor-icons/react";
/**
* Suggestions preselected options navigation configuration.
*/
export interface SuggestionsNavigation
{
/**
* Return true if the preselected options navigation is initialized.
*/
initialized(): boolean;
/**
* Preselect the next option in the suggestions.
*/
next(): void;
/**
* Preselect the previous option in the suggestions.
*/
previous(): void;
/**
* Select the currently preselected option.
*/
select(): void;
}
/**
* Hook to get the preselected options navigation reference.
*/
export function useSuggestionsNavigation(): MutableRefObject<SuggestionsNavigation>
{
return useRef({
initialized(): boolean
{
return false;
},
next(): void
{
},
previous(): void
{
},
select(): void
{
},
} as SuggestionsNavigation);
}
export function OptionsSuggestions<OptionKey extends keyof any, Option>({options, onSelected, selectedOptions, renderOption, navigator}: {
/**
* Options to suggest.
*/
options: Record<OptionKey, Option>;
/**
* Called when an option is selected.
* @param key
* @param option
*/
onSelected: (key: OptionKey, option: Option) => void;
/**
* Already selected options that will be shown differently.
*/
selectedOptions?: Record<OptionKey, Option>;
/**
* Render an option.
* @param option The option to render.
*/
renderOption?: (option: Option) => React.ReactNode;
/**
* A reference to a preselected suggestions options navigation object.
*/
navigator?: MutableRefObject<SuggestionsNavigation>;
}): React.ReactNode
{
// Initialize default option render function.
const defaultRenderOption = useCallback((option: Option) => (String(option)), []);
const optionsArray = useMemo(() => (Object.entries(options) as [OptionKey, Option][]), [options]);
const [preselectedOptionIndex, setPreselectedOptionIndex] = useState<number>(0);
const navigation = useMemo(() => ({
initialized(): boolean
{
return true;
},
next(): void
{
// Preselect the next option in the options array, or the first one if it's the last element.
setPreselectedOptionIndex((optionsArray.length == (preselectedOptionIndex + 1)) ? 0 : (preselectedOptionIndex + 1));
},
previous(): void
{
// Preselect the previous option in the options array, or the last one if it's the first element.
setPreselectedOptionIndex(((preselectedOptionIndex - 1) < 0) ? (optionsArray.length - 1) : (preselectedOptionIndex - 1));
},
select(): void
{
// Get the currently preselected option.
const [optionKey, option] = optionsArray[preselectedOptionIndex];
// Select the currently preselected option.
onSelected(optionKey, option);
},
}), [optionsArray, preselectedOptionIndex, setPreselectedOptionIndex]);
if (navigator)
// If navigator reference is set, assigning it.
navigator.current = navigation;
return optionsArray.map(([key, option], index) => (
<a key={String(key)} className={classes("suggestion", preselectedOptionIndex == index ? "preselected" : null, selectedOptions?.[key] ? "selected" : null)}
onClick={() => { onSelected(key, option); }}>
{(renderOption ?? defaultRenderOption)(option)}
<span className={"selected"}><Check weight={"bold"} /></span>
</a>
));
}