🙌 a11y: Searchbar/Conversations List Focus (#7096)

* chore: remove redundancy of useSetRecoilState and useRecoilValue with useRecoilState in SearchBar

* refactor: remove unnecessary focus effect on text area in ChatForm

* refactor: improve searchbar and clear search button accessibility

* fix: add tabIndex to Conversations component for improved accessibility, moves focus directly conversation items

* style: adjust margin in Header component for improved layout symmetry with Nav

* chore: imports order
This commit is contained in:
Danny Avila 2025-04-27 15:13:19 -04:00 committed by GitHub
parent 550c7cc68a
commit 6826c0ed43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 18 additions and 15 deletions

View file

@ -36,7 +36,7 @@ export default function Header() {
return (
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
<div className="mx-2 flex items-center gap-2">
<div className="mx-1 flex items-center gap-2">
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
{!navVisible && <HeaderNewChat />}
{<ModelSelector startupConfig={startupConfig} />}

View file

@ -151,12 +151,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
const textValue = useWatch({ control: methods.control, name: 'text' });
useEffect(() => {
if (!search.isSearching && textAreaRef.current && !disableInputs) {
textAreaRef.current.focus();
}
}, [search.isSearching, disableInputs]);
useEffect(() => {
if (textAreaRef.current) {
const style = window.getComputedStyle(textAreaRef.current);

View file

@ -220,6 +220,7 @@ const Conversations: FC<ConversationsProps> = ({
role="list"
aria-label="Conversations"
onRowsRendered={handleRowsRendered}
tabIndex={-1}
/>
)}
</AutoSizer>

View file

@ -1,7 +1,7 @@
import { forwardRef, useState, useCallback, useMemo, useEffect, Ref } from 'react';
import React, { forwardRef, useState, useCallback, useMemo, useEffect, useRef } from 'react';
import debounce from 'lodash/debounce';
import { useRecoilState } from 'recoil';
import { Search, X } from 'lucide-react';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { useLocation, useNavigate } from 'react-router-dom';
@ -13,7 +13,7 @@ type SearchBarProps = {
isSmallScreen?: boolean;
};
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivElement>) => {
const localize = useLocalize();
const location = useLocation();
const queryClient = useQueryClient();
@ -21,11 +21,11 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
const { isSmallScreen } = props;
const [text, setText] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const [showClearIcon, setShowClearIcon] = useState(false);
const { newConversation } = useNewConvo();
const setSearchState = useSetRecoilState(store.search);
const search = useRecoilValue(store.search);
const [search, setSearchState] = useRecoilState(store.search);
const clearSearch = useCallback(() => {
if (location.pathname.includes('/search')) {
@ -44,6 +44,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
isTyping: false,
}));
clearSearch();
inputRef.current?.focus();
}, [setSearchState, clearSearch]);
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
@ -108,6 +109,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
<Search className="absolute left-3 h-4 w-4 text-text-secondary group-focus-within:text-text-primary group-hover:text-text-primary" />
<input
type="text"
ref={inputRef}
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight placeholder-text-secondary placeholder-opacity-100 focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
value={text}
onChange={onChange}
@ -122,14 +124,20 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
autoComplete="off"
dir="auto"
/>
<X
<button
type="button"
aria-label={`${localize('com_ui_clear')} ${localize('com_ui_search')}`}
className={cn(
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-200',
'absolute right-[7px] flex h-5 w-5 items-center justify-center rounded-full border-none bg-transparent p-0 transition-opacity duration-200',
showClearIcon ? 'opacity-100' : 'opacity-0',
isSmallScreen === true ? 'right-[16px]' : '',
)}
onClick={clearText}
/>
tabIndex={showClearIcon ? 0 : -1}
disabled={!showClearIcon}
>
<X className="h-5 w-5 cursor-pointer" />
</button>
</div>
);
});