132 lines
3.3 KiB
TypeScript
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>
|
|
));
|
|
}
|