Skip to content

Commit

Permalink
📱 fix: Resolve Android Device and Accessibility Issues of Sidebar Com…
Browse files Browse the repository at this point in the history
…bobox (#3689)

* chore: Update @ariakit/react dependency to version 0.4.8

* refactor: Fix Combobox Android issue with radix-ui

* fix: Improve scrolling behavior by setting abort scroll state to false after scrolling to end

* wip: first pass switcher rewrite

* feat: Add button width calculation for ComboboxComponent

* refactor: Update ComboboxComponent styling for improved layout and appearance

* refactor: Update AssistantSwitcher component to handle null values for assistant names and avatar URLs

* refactor: Update ModelSwitcher component to use SimpleCombobox for improved functionality and styling

* refactor: Update Switcher Separator styling for improved layout and appearance

* refactor: Improve accessibility by adding aria-label to ComboboxComponent select items

* refactor: rename SimpleCombobox -> ControlCombobox
  • Loading branch information
danny-avila authored Aug 18, 2024
1 parent b22f1c1 commit 87d95a9
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 37 deletions.
2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
},
"homepage": "https://librechat.ai",
"dependencies": {
"@ariakit/react": "^0.4.5",
"@ariakit/react": "^0.4.8",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^2.1.2",
Expand Down
14 changes: 7 additions & 7 deletions client/src/components/SidePanel/AssistantSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useEffect, useMemo } from 'react';
import { Combobox } from '~/components/ui';
import { isAssistantsEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import type { AssistantsEndpoint } from 'librechat-data-provider';
import type { SwitcherProps, AssistantListItem } from '~/common';
import { useSetIndexOptions, useSelectAssistant, useLocalize, useAssistantListMap } from '~/hooks';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import ControlCombobox from '~/components/ui/ControlCombobox';
import Icon from '~/components/Endpoints/Icon';

export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
Expand All @@ -31,7 +31,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
localStorage.getItem(`${LocalStorageKeys.ASST_ID_PREFIX}${index}${endpoint}`) ??
assistants[0]?.id ??
'';
const assistant = assistantMap?.[endpoint ?? '']?.[assistant_id];
const assistant = assistantMap[endpoint ?? ''][assistant_id];

if (!assistant) {
return;
Expand All @@ -51,22 +51,22 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
const assistantOptions = useMemo(() => {
return assistants.map((assistant) => {
return {
label: assistant.name ?? '',
label: (assistant.name as string | null) ?? '',
value: assistant.id,
icon: (
<Icon
isCreatedByUser={false}
endpoint={endpoint}
assistantName={assistant.name ?? ''}
iconURL={(assistant.metadata?.avatar as string) ?? ''}
assistantName={(assistant.name as string | null) ?? ''}
iconURL={assistant.metadata?.avatar ?? ''}
/>
),
};
});
}, [assistants, endpoint]);

return (
<Combobox
<ControlCombobox
selectedValue={currentAssistant?.id ?? ''}
displayValue={
assistants.find((assistant) => assistant.id === selectedAssistant)?.name ??
Expand All @@ -83,7 +83,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
isCreatedByUser={false}
endpoint={endpoint}
assistantName={currentAssistant?.name ?? ''}
iconURL={(currentAssistant?.metadata?.avatar as string) ?? ''}
iconURL={currentAssistant?.metadata?.avatar ?? ''}
/>
}
/>
Expand Down
12 changes: 8 additions & 4 deletions client/src/components/SidePanel/ModelSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useMemo, useRef, useCallback } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { SwitcherProps } from '~/common';
import ControlCombobox from '~/components/ui/ControlCombobox';
import MinimalIcon from '~/components/Endpoints/MinimalIcon';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import type { SwitcherProps } from '~/common';
import { useChatContext } from '~/Providers';
import { Combobox } from '~/components/ui';
import { mainTextareaId } from '~/common';

export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {
Expand All @@ -16,7 +16,10 @@ export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {

const { endpoint, model = null } = conversation ?? {};
const models = useMemo(() => {
return modelsQuery?.data?.[endpoint ?? ''] ?? [];
return (modelsQuery.data?.[endpoint ?? ''] ?? []).map((model) => ({
label: model,
value: model,
}));
}, [modelsQuery, endpoint]);

const setModel = useCallback(
Expand All @@ -34,7 +37,8 @@ export default function ModelSwitcher({ isCollapsed }: SwitcherProps) {
);

return (
<Combobox
<ControlCombobox
displayValue={model ?? ''}
selectPlaceholder={localize('com_ui_select_model')}
searchPlaceholder={localize('com_ui_select_search_model')}
isCollapsed={isCollapsed}
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/SidePanel/Switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Switcher(props: SwitcherProps) {
return (
<>
<AssistantSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
} else if (isAssistantsEndpoint(props.endpoint)) {
Expand All @@ -19,7 +19,7 @@ export default function Switcher(props: SwitcherProps) {
return (
<>
<ModelSwitcher {...props} />
<Separator className="bg-gray-100/50 dark:bg-gray-600" />
<Separator className="max-w-[98%] bg-surface-tertiary" />
</>
);
}
18 changes: 16 additions & 2 deletions client/src/components/ui/Combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { startTransition, useMemo } from 'react';
import { startTransition } from 'react';
import { Search as SearchIcon } from 'lucide-react';
import * as RadixSelect from '@radix-ui/react-select';
import { CheckIcon, ChevronDownIcon } from '@radix-ui/react-icons';
Expand Down Expand Up @@ -52,7 +52,16 @@ export default function ComboboxComponent({
value={selectedValue}
onValueChange={setValue}
open={open}
onOpenChange={setOpen}
/** Hacky fix for radix-ui Android issue: https://github.com/radix-ui/primitives/issues/1658 */
onOpenChange={() => {
if (open === true) {
setOpen(false);
return;
}
setTimeout(() => {
setOpen(!open);
}, 75);
}}
>
<ComboboxProvider
open={open}
Expand Down Expand Up @@ -134,6 +143,11 @@ export default function ComboboxComponent({
'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',
)}
/** Hacky fix for radix-ui Android issue: https://github.com/radix-ui/primitives/issues/1658 */
onTouchEnd={() => {
setValue(`${value ?? ''}`);
setOpen(false);
}}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<RadixSelect.ItemIndicator>
Expand Down
123 changes: 123 additions & 0 deletions client/src/components/ui/ControlCombobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as Ariakit from '@ariakit/react';
import { matchSorter } from 'match-sorter';
import { startTransition, useMemo, useState, useEffect, useRef } from 'react';
import { cn } from '~/utils';
import type { OptionWithIcon } from '~/common';
import { Search } from 'lucide-react';

interface ControlComboboxProps {
selectedValue: string;
displayValue?: string;
items: OptionWithIcon[];
setValue: (value: string) => void;
ariaLabel: string;
searchPlaceholder?: string;
selectPlaceholder?: string;
isCollapsed: boolean;
SelectIcon?: React.ReactNode;
}

export default function ControlCombobox({
selectedValue,
displayValue,
items,
setValue,
ariaLabel,
searchPlaceholder,
selectPlaceholder,
isCollapsed,
SelectIcon,
}: ControlComboboxProps) {
const [searchValue, setSearchValue] = useState('');
const buttonRef = useRef<HTMLButtonElement>(null);
const [buttonWidth, setButtonWidth] = useState<number | null>(null);

const matches = useMemo(() => {
return matchSorter(items, searchValue, {
keys: ['value', 'label'],
baseSort: (a, b) => (a.index < b.index ? -1 : 1),
});
}, [searchValue, items]);

useEffect(() => {
if (buttonRef.current && !isCollapsed) {
setButtonWidth(buttonRef.current.offsetWidth);
}
}, [isCollapsed]);

return (
<div className="flex w-full items-center justify-center px-1">
<Ariakit.ComboboxProvider
resetValueOnHide
setValue={(value) => {
startTransition(() => {
setSearchValue(value);
});
}}
>
<Ariakit.SelectProvider value={selectedValue} setValue={setValue}>
<Ariakit.SelectLabel className="sr-only">{ariaLabel}</Ariakit.SelectLabel>
<Ariakit.Select
ref={buttonRef}
className={cn(
'flex items-center justify-center gap-2 rounded-full bg-surface-secondary',
'text-text-primary hover:bg-surface-tertiary',
'border border-border-light',
isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm',
)}
>
{SelectIcon != null && (
<div className="assistant-item flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
{SelectIcon}
</div>
)}
{!isCollapsed && (
<span className="flex-grow truncate text-left">
{displayValue ?? selectPlaceholder}
</span>
)}
</Ariakit.Select>
<Ariakit.SelectPopover
gutter={4}
portal
className="z-50 overflow-hidden rounded-md border border-border-light bg-surface-secondary shadow-lg"
style={{ width: isCollapsed ? '300px' : buttonWidth ?? '300px' }}
>
<div className="p-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
<Ariakit.Combobox
autoSelect
placeholder={searchPlaceholder}
className="w-full rounded-md border border-border-light bg-surface-tertiary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
/>
</div>
</div>
<Ariakit.ComboboxList className="max-h-[50vh] overflow-auto">
{matches.map((item) => (
<Ariakit.SelectItem
key={item.value}
value={`${item.value ?? ''}`}
aria-label={`${item.label ?? item.value ?? ''}`}
className={cn(
'flex cursor-pointer items-center px-3 py-2 text-sm',
'text-text-primary hover:bg-surface-tertiary',
'data-[active-item]:bg-surface-tertiary',
)}
render={<Ariakit.ComboboxItem />}
>
{item.icon != null && (
<div className="assistant-item mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
{item.icon}
</div>
)}
<span className="flex-grow truncate text-left">{item.label}</span>
</Ariakit.SelectItem>
))}
</Ariakit.ComboboxList>
</Ariakit.SelectPopover>
</Ariakit.SelectProvider>
</Ariakit.ComboboxProvider>
</div>
);
}
12 changes: 8 additions & 4 deletions client/src/hooks/SSE/useEventHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { v4 } from 'uuid';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import {
QueryKeys,
Constants,
Expand Down Expand Up @@ -29,6 +30,7 @@ import useContentHandler from '~/hooks/SSE/useContentHandler';
import type { TGenTitleMutation } from '~/data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
import { useLiveAnnouncer } from '~/Providers';
import store from '~/store';

type TSyncData = {
sync: boolean;
Expand Down Expand Up @@ -65,6 +67,7 @@ export default function useEventHandlers({
resetLatestMessage,
}: EventHandlerParams) {
const queryClient = useQueryClient();
const setAbortScroll = useSetRecoilState(store.abortScroll);
const { announcePolite, announceAssertive } = useLiveAnnouncer();

const { conversationId: paramId } = useParams();
Expand Down Expand Up @@ -306,15 +309,16 @@ export default function useEventHandlers({
resetLatestMessage();
}

scrollToEnd();
scrollToEnd(() => setAbortScroll(false));
},
[
setMessages,
setConversation,
queryClient,
setAbortScroll,
isAddedRequest,
resetLatestMessage,
setConversation,
announceAssertive,
resetLatestMessage,
],
);

Expand Down
5 changes: 4 additions & 1 deletion client/src/utils/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,12 @@ export const getTextKey = (message?: TMessage | null, convoId?: string | null) =
}${message.conversationId ?? convoId}`;
};

export const scrollToEnd = () => {
export const scrollToEnd = (callback?: () => void) => {
const messagesEndElement = document.getElementById('messages-end');
if (messagesEndElement) {
messagesEndElement.scrollIntoView({ behavior: 'instant' });
if (callback) {
callback();
}
}
};
Loading

0 comments on commit 87d95a9

Please sign in to comment.