Skip to content

Commit 4343e9b

Browse files
authored
🙌 a11y: Searchbar/Conversations List Focus (danny-avila#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
1 parent 0607ca2 commit 4343e9b

File tree

4 files changed

+18
-15
lines changed

4 files changed

+18
-15
lines changed

‎client/src/components/Chat/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default function Header() {
3636
return (
3737
<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">
3838
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
39-
<div className="mx-2 flex items-center gap-2">
39+
<div className="mx-1 flex items-center gap-2">
4040
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
4141
{!navVisible && <HeaderNewChat />}
4242
{<ModelSelector startupConfig={startupConfig} />}

‎client/src/components/Chat/Input/ChatForm.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
151151

152152
const textValue = useWatch({ control: methods.control, name: 'text' });
153153

154-
useEffect(() => {
155-
if (!search.isSearching && textAreaRef.current && !disableInputs) {
156-
textAreaRef.current.focus();
157-
}
158-
}, [search.isSearching, disableInputs]);
159-
160154
useEffect(() => {
161155
if (textAreaRef.current) {
162156
const style = window.getComputedStyle(textAreaRef.current);

‎client/src/components/Conversations/Conversations.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ const Conversations: FC<ConversationsProps> = ({
220220
role="list"
221221
aria-label="Conversations"
222222
onRowsRendered={handleRowsRendered}
223+
tabIndex={-1}
223224
/>
224225
)}
225226
</AutoSizer>

‎client/src/components/Nav/SearchBar.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { forwardRef, useState, useCallback, useMemo, useEffect, Ref } from 'react';
1+
import React, { forwardRef, useState, useCallback, useMemo, useEffect, useRef } from 'react';
22
import debounce from 'lodash/debounce';
3+
import { useRecoilState } from 'recoil';
34
import { Search, X } from 'lucide-react';
4-
import { useSetRecoilState, useRecoilValue } from 'recoil';
55
import { QueryKeys } from 'librechat-data-provider';
66
import { useQueryClient } from '@tanstack/react-query';
77
import { useLocation, useNavigate } from 'react-router-dom';
@@ -13,19 +13,19 @@ type SearchBarProps = {
1313
isSmallScreen?: boolean;
1414
};
1515

16-
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
16+
const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivElement>) => {
1717
const localize = useLocalize();
1818
const location = useLocation();
1919
const queryClient = useQueryClient();
2020
const navigate = useNavigate();
2121
const { isSmallScreen } = props;
2222

2323
const [text, setText] = useState('');
24+
const inputRef = useRef<HTMLInputElement>(null);
2425
const [showClearIcon, setShowClearIcon] = useState(false);
2526

2627
const { newConversation } = useNewConvo();
27-
const setSearchState = useSetRecoilState(store.search);
28-
const search = useRecoilValue(store.search);
28+
const [search, setSearchState] = useRecoilState(store.search);
2929

3030
const clearSearch = useCallback(() => {
3131
if (location.pathname.includes('/search')) {
@@ -44,6 +44,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
4444
isTyping: false,
4545
}));
4646
clearSearch();
47+
inputRef.current?.focus();
4748
}, [setSearchState, clearSearch]);
4849

4950
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -108,6 +109,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
108109
<Search className="absolute left-3 h-4 w-4 text-text-secondary group-focus-within:text-text-primary group-hover:text-text-primary" />
109110
<input
110111
type="text"
112+
ref={inputRef}
111113
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"
112114
value={text}
113115
onChange={onChange}
@@ -122,14 +124,20 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
122124
autoComplete="off"
123125
dir="auto"
124126
/>
125-
<X
127+
<button
128+
type="button"
129+
aria-label={`${localize('com_ui_clear')} ${localize('com_ui_search')}`}
126130
className={cn(
127-
'absolute right-[7px] h-5 w-5 cursor-pointer transition-opacity duration-200',
131+
'absolute right-[7px] flex h-5 w-5 items-center justify-center rounded-full border-none bg-transparent p-0 transition-opacity duration-200',
128132
showClearIcon ? 'opacity-100' : 'opacity-0',
129133
isSmallScreen === true ? 'right-[16px]' : '',
130134
)}
131135
onClick={clearText}
132-
/>
136+
tabIndex={showClearIcon ? 0 : -1}
137+
disabled={!showClearIcon}
138+
>
139+
<X className="h-5 w-5 cursor-pointer" />
140+
</button>
133141
</div>
134142
);
135143
});

0 commit comments

Comments
 (0)