2024-04-10 14:27:22 -04:00
|
|
|
import { startTransition, useMemo } from 'react';
|
|
|
|
|
import { Search as SearchIcon } from 'lucide-react';
|
2024-04-11 02:12:48 -04:00
|
|
|
import * as RadixSelect from '@radix-ui/react-select';
|
2024-04-10 14:27:22 -04:00
|
|
|
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
|
|
|
|
|
import {
|
|
|
|
|
Combobox,
|
|
|
|
|
ComboboxItem,
|
|
|
|
|
ComboboxList,
|
|
|
|
|
ComboboxProvider,
|
|
|
|
|
ComboboxCancel,
|
|
|
|
|
} from '@ariakit/react';
|
|
|
|
|
import type { OptionWithIcon } from '~/common';
|
2024-04-11 02:12:48 -04:00
|
|
|
import { SelectTrigger, SelectValue, SelectScrollDownButton } from './Select';
|
2024-04-10 14:27:22 -04:00
|
|
|
import useCombobox from '~/hooks/Input/useCombobox';
|
|
|
|
|
import { cn } from '~/utils';
|
|
|
|
|
|
|
|
|
|
export default function ComboboxComponent({
|
|
|
|
|
selectedValue,
|
|
|
|
|
displayValue,
|
|
|
|
|
items,
|
|
|
|
|
setValue,
|
|
|
|
|
ariaLabel,
|
|
|
|
|
searchPlaceholder,
|
|
|
|
|
selectPlaceholder,
|
|
|
|
|
isCollapsed,
|
|
|
|
|
SelectIcon,
|
|
|
|
|
}: {
|
|
|
|
|
ariaLabel: string;
|
|
|
|
|
displayValue?: string;
|
|
|
|
|
selectedValue: string;
|
|
|
|
|
searchPlaceholder?: string;
|
|
|
|
|
selectPlaceholder?: string;
|
|
|
|
|
items: OptionWithIcon[] | string[];
|
|
|
|
|
setValue: (value: string) => void;
|
|
|
|
|
isCollapsed: boolean;
|
|
|
|
|
SelectIcon?: React.ReactNode;
|
|
|
|
|
}) {
|
2024-05-29 09:15:05 -04:00
|
|
|
const options: OptionWithIcon[] = (items ?? []).map((option: string | OptionWithIcon) => {
|
|
|
|
|
if (typeof option === 'string') {
|
|
|
|
|
return { label: option, value: option };
|
2024-04-10 14:27:22 -04:00
|
|
|
}
|
2024-05-29 09:15:05 -04:00
|
|
|
return option;
|
|
|
|
|
});
|
2024-04-10 14:27:22 -04:00
|
|
|
|
|
|
|
|
const { open, setOpen, setSearchValue, matches } = useCombobox({
|
|
|
|
|
value: selectedValue,
|
|
|
|
|
options,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<RadixSelect.Root
|
|
|
|
|
value={selectedValue}
|
|
|
|
|
onValueChange={setValue}
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={setOpen}
|
|
|
|
|
>
|
|
|
|
|
<ComboboxProvider
|
|
|
|
|
open={open}
|
|
|
|
|
setOpen={setOpen}
|
|
|
|
|
resetValueOnHide
|
|
|
|
|
includesBaseElement={false}
|
|
|
|
|
setValue={(value) => {
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
setSearchValue(value);
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
aria-label={ariaLabel}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0',
|
|
|
|
|
isCollapsed
|
|
|
|
|
? 'flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden'
|
|
|
|
|
: '',
|
🤲 feat(a11y): Initial a11y improvements, added linters, tests; fix: close sidebars in mobile view (#3536)
* chore: playwright setup update
* refactor: update ChatRoute component with accessible loading spinner with live region
* chore(Message): typing
* ci: first pass, a11y testing
* refactor: update lang attribute in index.html to "en-US"
* ci: jsx-a11y dev eslint plugin
* ci: jsx plugin
* fix: Exclude 'vite.config.ts' from TypeScript compilation for testing
* fix(a11y): Remove tabIndex from non-interactive element in MessagesView component
* fix(a11y):
- Visible, non-interactive elements with click handlers must have at least one keyboard listener.eslintjsx-a11y/click-events-have-key-events
- Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.eslintjsx-a11y/no-static-element-interactions
chore: remove unused bookmarks panel
- fix some "Unexpected nullable boolean value in conditional" warnings
* fix(NewChat): a11y, nested button issue, add aria-label, remove implicit role
* fix(a11y):
- partially address #3515 with `main` landmark
other:
- eslint@typescript-eslint/strict-boolean-expressions
* chore(MenuButton): Use button element instead of div for accessibility
* chore: Update TitleButton to use button element for accessibility
* chore: Update TitleButton to use button element for accessibility
* refactor(ChatMenuItem): Improve focus accessibility and code readability
* chore(MenuButton): Update aria-label to dynamically include primaryText
* fix(a11y): SearchBar
- If a form control does not have a properly associated text label, the function or purpose of that form control may not be presented to screen reader users. Visible form labels also provide visible descriptions and larger clickable targets for form controls which placeholders do not.
* chore: remove duplicate SearchBar twcss
* fix(a11y):
- The edit and copy buttons that are visually hidden are exposed to Assistive technology and are announced to screen reader users.
* fix(a11y): visible focus outline
* fix(a11y): The button to select the LLM Model has the aria-haspopup and aria- expanded attributes which makes its role ambuguous and unclear. It functions like a combobox but doesn't fully support that interaction and also fucntions like a dialog but doesn't completely support that interaction either.
* fix(a11y): fix visible focus outline
* fix(a11y): Scroll to bottom button missing accessible name #3474
* fix(a11y): The page lacks any heading structure. There should be at least one H1 and other headings to help users understand the orgainzation of the page and the contents.
Note: h1 won't be correct here so made it h2
* fix(a11y): LLM controls aria-labels
* fix(a11y): There is no visible focus outline to the 'send message' button
* fix(a11y): fix visible focus outline for Fork button
* refactor(MessageRender): add focus ring to message cards, consolidate complex conditions, add logger for setting latest message, add tabindex for card
* fix: focus border color and fix set latest message card condition
* fix(a11y): Adequate contrast for MessageAudio buttton
* feat: Add GitHub Actions workflow for accessibility linting
* chore: Update GitHub Actions workflow for accessibility linting to include client/src/** path
* fix(Nav): navmask and accessibility
* fix: Update Nav component to handle potential undefined type in SearchContext
* fix(a11y): add focus visibility to attach files button #3475
* fix(a11y): discernible text for NewChat button
* fix(a11y): accessible landmark names, all page content in landmarks, ensures landmarks are unique #3514 #3515
* fix(Prompts): update isChatRoute prop to be required in List component
* fix(a11y): buttons must have discernible text
2024-08-04 20:39:52 -04:00
|
|
|
'bg-white text-black hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-gray-500 dark:bg-gray-850 dark:text-white ',
|
2024-04-10 14:27:22 -04:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<SelectValue placeholder={selectPlaceholder}>
|
|
|
|
|
<div className="assistant-item flex items-center justify-center overflow-hidden rounded-full">
|
|
|
|
|
{SelectIcon ? SelectIcon : <ChevronDownIcon />}
|
|
|
|
|
</div>
|
|
|
|
|
<span
|
|
|
|
|
className={cn('ml-2', isCollapsed ? 'hidden' : '')}
|
|
|
|
|
style={{ userSelect: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
{selectedValue
|
|
|
|
|
? displayValue ?? selectedValue
|
|
|
|
|
: selectPlaceholder && selectPlaceholder}
|
|
|
|
|
</span>
|
|
|
|
|
</SelectValue>
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<RadixSelect.Portal>
|
|
|
|
|
<RadixSelect.Content
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-label={ariaLabel + 's'}
|
|
|
|
|
position="popper"
|
|
|
|
|
className={cn(
|
2024-04-11 02:12:48 -04:00
|
|
|
'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
|
2024-04-10 14:27:22 -04:00
|
|
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
|
|
|
|
'bg-white dark:bg-gray-700',
|
|
|
|
|
)}
|
|
|
|
|
>
|
2024-04-11 02:12:48 -04:00
|
|
|
<RadixSelect.Viewport className="mb-5 h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]">
|
|
|
|
|
<div className="group sticky left-0 top-0 z-10 flex h-12 items-center gap-2 bg-white px-2 px-3 py-2 text-black duration-300 dark:bg-gray-700 dark:text-white">
|
|
|
|
|
<SearchIcon className="h-4 w-4 text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300" />
|
|
|
|
|
<Combobox
|
|
|
|
|
autoSelect
|
|
|
|
|
placeholder={searchPlaceholder}
|
|
|
|
|
className="flex-1 rounded-md border-none bg-transparent px-2.5 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-gray-700/10 dark:focus:ring-gray-200/10"
|
|
|
|
|
// Ariakit's Combobox manually triggers a blur event on virtually
|
|
|
|
|
// blurred items, making them work as if they had actual DOM
|
|
|
|
|
// focus. These blur events might happen after the corresponding
|
|
|
|
|
// focus events in the capture phase, leading Radix Select to
|
|
|
|
|
// close the popover. This happens because Radix Select relies on
|
|
|
|
|
// the order of these captured events to discern if the focus was
|
|
|
|
|
// outside the element. Since we don't have access to the
|
|
|
|
|
// onInteractOutside prop in the Radix SelectContent component to
|
|
|
|
|
// stop this behavior, we can turn off Ariakit's behavior here.
|
|
|
|
|
onBlurCapture={(event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<ComboboxCancel
|
|
|
|
|
hideWhenEmpty={true}
|
|
|
|
|
className="relative flex h-5 w-5 items-center justify-end text-gray-500 transition-colors duration-300 dark:group-focus-within:text-gray-300 dark:group-hover:text-gray-300"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<ComboboxList className="overflow-y-auto p-1 py-2">
|
|
|
|
|
{matches.map(({ label, value, icon }) => (
|
|
|
|
|
<RadixSelect.Item key={value} value={`${value ?? ''}`} asChild>
|
|
|
|
|
<ComboboxItem
|
|
|
|
|
className={cn(
|
|
|
|
|
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
|
|
|
'rounded-lg hover:bg-gray-100/50 hover:bg-gray-50 dark:text-white dark:hover:bg-gray-600',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
|
|
|
<RadixSelect.ItemIndicator>
|
|
|
|
|
<CheckIcon className="h-4 w-4" />
|
|
|
|
|
</RadixSelect.ItemIndicator>
|
|
|
|
|
</span>
|
|
|
|
|
<RadixSelect.ItemText>
|
|
|
|
|
<div className="[&_svg]:text-foreground flex items-center justify-center gap-3 dark:text-white [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0">
|
|
|
|
|
<div className="assistant-item overflow-hidden rounded-full ">
|
|
|
|
|
{icon && icon}
|
|
|
|
|
</div>
|
|
|
|
|
{label}
|
2024-04-10 14:27:22 -04:00
|
|
|
</div>
|
2024-04-11 02:12:48 -04:00
|
|
|
</RadixSelect.ItemText>
|
|
|
|
|
</ComboboxItem>
|
|
|
|
|
</RadixSelect.Item>
|
|
|
|
|
))}
|
|
|
|
|
</ComboboxList>
|
|
|
|
|
</RadixSelect.Viewport>
|
|
|
|
|
<SelectScrollDownButton className="absolute bottom-0 left-0 right-0" />
|
2024-04-10 14:27:22 -04:00
|
|
|
</RadixSelect.Content>
|
|
|
|
|
</RadixSelect.Portal>
|
|
|
|
|
</ComboboxProvider>
|
|
|
|
|
</RadixSelect.Root>
|
|
|
|
|
);
|
|
|
|
|
}
|