File size: 5,157 Bytes
f0743f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | import { Search, X } from 'lucide-react';
import React, { useState, useMemo, useCallback, useRef } from 'react';
import { cn } from '~/utils';
/** This is a generic that can be added to Menu and Select components */
export default function MultiSearch({
value,
onChange,
placeholder,
className = '',
}: {
value: string | null;
onChange: (filter: string) => void;
placeholder?: string;
className?: string;
}) {
const inputRef = useRef<HTMLInputElement>(null);
const onChangeHandler: React.ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => onChange(e.target.value),
[onChange],
);
const clearSearch = () => {
onChange('');
setTimeout(() => {
inputRef.current?.focus();
}, 0);
};
return (
<div
className={cn(
'focus:to-surface-primary/50 group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-gradient-to-b from-surface-tertiary-alt from-65% to-transparent px-3 py-2 text-text-primary transition-colors duration-300 focus:bg-gradient-to-b focus:from-surface-primary',
className,
)}
>
<Search
className="h-4 w-4 text-text-secondary-alt transition-colors duration-300"
aria-hidden={'true'}
/>
<input
ref={inputRef}
type="text"
value={value ?? ''}
onChange={onChangeHandler}
placeholder={String(placeholder ?? 'Search...')}
aria-label="Search Model"
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-ring-primary"
/>
<button
className={cn(
'relative flex h-5 w-5 items-center justify-end rounded-md text-text-secondary-alt',
(value?.length ?? 0) ? 'cursor-pointer opacity-100' : 'hidden',
)}
aria-label={'Clear search'}
onClick={clearSearch}
tabIndex={0}
>
<X
aria-hidden={'true'}
className={cn(
'text-text-secondary-alt',
(value?.length ?? 0) ? 'cursor-pointer opacity-100' : 'opacity-0',
)}
/>
</button>
</div>
);
}
/**
* Helper function that will take a multiSearch input
* @param node
*/
function defaultGetStringKey(node: unknown): string {
if (typeof node === 'string') {
// BUGFIX: Detect psedeo separators and make sure they don't appear in the list when filtering items
// it makes sure (for the most part) that the model name starts and ends with dashes
// The long-term fix here would be to enable seperators (model groupings) but there's no
// feature mocks for such a thing yet
if (node.startsWith('---') && node.endsWith('---')) {
return '';
}
return node.toUpperCase();
}
// This should be a noop, but it's here for redundancy
return '';
}
/**
* Hook for conditionally making a multi-element list component into a sortable component
* Returns a RenderNode for search input when search functionality is available
* @param availableOptions
* @param placeholder
* @param getTextKeyOverride
* @param className - Additional classnames to add to the search container
* @param disabled - If the search should be disabled
* @returns
*/
export function useMultiSearch<OptionsType extends unknown[]>({
availableOptions = [] as unknown as OptionsType,
placeholder,
getTextKeyOverride,
className,
disabled = false,
}: {
availableOptions?: OptionsType;
placeholder?: string;
getTextKeyOverride?: (node: OptionsType[0]) => string;
className?: string;
disabled?: boolean;
}): [OptionsType, React.ReactNode] {
const [filterValue, setFilterValue] = useState<string | null>(null);
// We conditionally show the search when there's more than 10 elements in the menu
const shouldShowSearch = availableOptions.length > 10 && !disabled;
// Define the helper function used to enable search
// If this is invalidly described, we will assume developer error - tf. avoid rendering
const getTextKeyHelper = getTextKeyOverride || defaultGetStringKey;
// Iterate said options
const filteredOptions = useMemo(() => {
const currentFilter = filterValue ?? '';
if (!shouldShowSearch || !currentFilter || !availableOptions.length) {
// Don't render if available options aren't present, there's no filter active
return availableOptions;
}
// Filter through the values, using a simple text-based search
// nothing too fancy, but we can add a better search algo later if we need
const upperFilterValue = currentFilter.toUpperCase();
return availableOptions.filter((value) =>
getTextKeyHelper(value).includes(upperFilterValue),
) as OptionsType;
}, [availableOptions, getTextKeyHelper, filterValue, shouldShowSearch]);
const onSearchChange = useCallback(
(nextFilterValue: string) => setFilterValue(nextFilterValue),
[],
);
const searchRender = shouldShowSearch ? (
<MultiSearch
value={filterValue}
className={className}
onChange={onSearchChange}
placeholder={placeholder}
/>
) : null;
return [filteredOptions, searchRender];
}
|