mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 01:10:14 +01:00
🙌 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:
parent
550c7cc68a
commit
6826c0ed43
4 changed files with 18 additions and 15 deletions
|
|
@ -36,7 +36,7 @@ export default function Header() {
|
||||||
return (
|
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="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="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 && <OpenSidebar setNavVisible={setNavVisible} />}
|
||||||
{!navVisible && <HeaderNewChat />}
|
{!navVisible && <HeaderNewChat />}
|
||||||
{<ModelSelector startupConfig={startupConfig} />}
|
{<ModelSelector startupConfig={startupConfig} />}
|
||||||
|
|
|
||||||
|
|
@ -151,12 +151,6 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => {
|
||||||
|
|
||||||
const textValue = useWatch({ control: methods.control, name: 'text' });
|
const textValue = useWatch({ control: methods.control, name: 'text' });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!search.isSearching && textAreaRef.current && !disableInputs) {
|
|
||||||
textAreaRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [search.isSearching, disableInputs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textAreaRef.current) {
|
if (textAreaRef.current) {
|
||||||
const style = window.getComputedStyle(textAreaRef.current);
|
const style = window.getComputedStyle(textAreaRef.current);
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||||
role="list"
|
role="list"
|
||||||
aria-label="Conversations"
|
aria-label="Conversations"
|
||||||
onRowsRendered={handleRowsRendered}
|
onRowsRendered={handleRowsRendered}
|
||||||
|
tabIndex={-1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
|
|
||||||
|
|
@ -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 debounce from 'lodash/debounce';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
|
||||||
import { QueryKeys } from 'librechat-data-provider';
|
import { QueryKeys } from 'librechat-data-provider';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
@ -13,7 +13,7 @@ type SearchBarProps = {
|
||||||
isSmallScreen?: boolean;
|
isSmallScreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
|
const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivElement>) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -21,11 +21,11 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
const { isSmallScreen } = props;
|
const { isSmallScreen } = props;
|
||||||
|
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||||
|
|
||||||
const { newConversation } = useNewConvo();
|
const { newConversation } = useNewConvo();
|
||||||
const setSearchState = useSetRecoilState(store.search);
|
const [search, setSearchState] = useRecoilState(store.search);
|
||||||
const search = useRecoilValue(store.search);
|
|
||||||
|
|
||||||
const clearSearch = useCallback(() => {
|
const clearSearch = useCallback(() => {
|
||||||
if (location.pathname.includes('/search')) {
|
if (location.pathname.includes('/search')) {
|
||||||
|
|
@ -44,6 +44,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
isTyping: false,
|
isTyping: false,
|
||||||
}));
|
}));
|
||||||
clearSearch();
|
clearSearch();
|
||||||
|
inputRef.current?.focus();
|
||||||
}, [setSearchState, clearSearch]);
|
}, [setSearchState, clearSearch]);
|
||||||
|
|
||||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
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" />
|
<Search className="absolute left-3 h-4 w-4 text-text-secondary group-focus-within:text-text-primary group-hover:text-text-primary" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
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"
|
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}
|
value={text}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
@ -122,14 +124,20 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
<X
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`${localize('com_ui_clear')} ${localize('com_ui_search')}`}
|
||||||
className={cn(
|
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',
|
showClearIcon ? 'opacity-100' : 'opacity-0',
|
||||||
isSmallScreen === true ? 'right-[16px]' : '',
|
isSmallScreen === true ? 'right-[16px]' : '',
|
||||||
)}
|
)}
|
||||||
onClick={clearText}
|
onClick={clearText}
|
||||||
/>
|
tabIndex={showClearIcon ? 0 : -1}
|
||||||
|
disabled={!showClearIcon}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5 cursor-pointer" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue