LibreChat/client/src/components/ui/InputWithDropDown.tsx

155 lines
5 KiB
TypeScript
Raw Normal View History

🧪 feat: Prompt Dropdown Variable; style: Add Markdown Support (#3681) * feat: Add extended inputs for promts library variables * feat: Add maxRows prop to VariableForm input field * 📩 feat: invite user (#3012) * feat: basic invite-user script * feat: add invite user functionality and registration validation middleware * fix: invite user fixes * refactor: consolidate direct model access to a central place of functions * style(Registration): add spinner to continue button * refactor: import ordrer * feat: improve invite user script and error handling * fix: merge conflict * refactor: remove `console.log` and use `logger` * fix: token operation and checkinvite issues * bring back comment and remove console log * fix: return invalid token when token is not found * fix: getInvite fix * refactor: Update Token.js to use async/await syntax for update and delete operations * feat: Refactor Token.js to use async/await syntax for createToken and findToken functions * refactor(inviteUser): define functions outside of module.exports * Update AuthService.js --------- Co-authored-by: Danny Avila <danny@librechat.ai> * style: improve OpenAI.tsx input field focus styling * refactor: update import statement in Input.tsx * refactor: remove multi-line * refactor: update placeholder text to use localization * style: new dropdown variable info and markdown styling for info * Add ReactMarkdown * chore: styling, import order * refactor: update ReactMarkdown usage in VariableForm * style: remove markdown class * refactor: update mobile styling and use code renderer * style(InputWithDropDown): update focus trigger style * style(OptionsPopover): update Save As Preset `focus` and `dark:bg` --------- Co-authored-by: Konstantin Meshcheryakov <kmeshcheryakov@klika-tech.com> Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com> Co-authored-by: bsu3338 <bsu3338@users.noreply.github.com>
2024-08-18 05:52:05 -04:00
import * as React from 'react';
import { Input } from '~/components/ui/Input';
import { cn } from '~/utils';
export type InputWithDropdownProps = React.InputHTMLAttributes<HTMLInputElement> & {
options: string[];
onSelect?: (value: string) => void;
};
const InputWithDropdown = React.forwardRef<HTMLInputElement, InputWithDropdownProps>(
({ className, options, onSelect, ...props }, ref) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState((props.value as string) || '');
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
const inputRef = React.useRef<HTMLInputElement>(null);
const handleSelect = (value: string) => {
setInputValue(value);
setIsOpen(false);
setHighlightedIndex(-1);
if (onSelect) {
onSelect(value);
}
if (props.onChange) {
props.onChange({ target: { value } } as React.ChangeEvent<HTMLInputElement>);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
if (props.onChange) {
props.onChange(e);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setHighlightedIndex((prevIndex) =>
prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex,
);
}
break;
case 'ArrowUp':
e.preventDefault();
setHighlightedIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : 0));
break;
case 'Enter':
e.preventDefault();
if (isOpen && highlightedIndex !== -1) {
handleSelect(options[highlightedIndex]);
}
setIsOpen(false);
break;
case 'Escape':
setIsOpen(false);
setHighlightedIndex(-1);
break;
}
};
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
setIsOpen(false);
setHighlightedIndex(-1);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative" ref={inputRef}>
<div className="relative">
<Input
{...props}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
aria-haspopup="listbox"
aria-controls="dropdown-list"
className={cn('bg-surface-secondary', className ?? '')}
ref={ref}
/>
<button
type="button"
className="text-tertiary hover:text-secondary absolute inset-y-0 right-0 flex items-center rounded-md px-2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring-primary"
onClick={() => setIsOpen(!isOpen)}
aria-label={isOpen ? 'Close dropdown' : 'Open dropdown'}
>
<svg
className="h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
{isOpen && (
<ul
id="dropdown-list"
role="listbox"
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-border-medium bg-surface-secondary shadow-lg focus:ring-1 focus:ring-inset focus:ring-ring-primary"
>
{options.map((option, index) => (
<li
key={index}
role="option"
aria-selected={index === highlightedIndex}
className={cn(
'cursor-pointer rounded-md px-3 py-2',
'focus:bg-surface-tertiary focus:outline-none focus:ring-1 focus:ring-inset focus:ring-ring-primary',
index === highlightedIndex
? 'text-primary bg-surface-active'
: 'text-secondary hover:bg-surface-tertiary',
)}
onClick={() => handleSelect(option)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleSelect(option);
}
}}
tabIndex={0}
>
{option}
</li>
))}
</ul>
)}
</div>
);
},
);
InputWithDropdown.displayName = 'InputWithDropdown';
export default InputWithDropdown;