From 6f35fb5526d001017d4c3a70f301ef2f53f1f9f6 Mon Sep 17 00:00:00 2001 From: SATYA SB <107645008+satya07sb@users.noreply.github.com> Date: Mon, 19 Aug 2024 10:34:49 +0530 Subject: [PATCH] [accessibility-2819223]:Bug 2819223: [Keyboard navigation - Cosmos DB Query Copilot - Copilot]: The suggestions of 'Copilot search' edit field are not accessible with keyboard. (#1893) Co-authored-by: Satyapriya Bai --- .../QueryCopilot/QueryCopilotPromptbar.tsx | 68 +++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx index d2ecc7bf7..c86f1e955 100644 --- a/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx +++ b/src/Explorer/QueryCopilot/QueryCopilotPromptbar.tsx @@ -18,7 +18,7 @@ import { Text, TextField, } from "@fluentui/react"; -import { HttpStatusCodes } from "Common/Constants"; +import { HttpStatusCodes, NormalizedEventKey } from "Common/Constants"; import { handleError } from "Common/ErrorHandlingUtils"; import QueryError, { QueryErrorSeverity } from "Common/QueryError"; import { createUri } from "Common/UrlUtility"; @@ -35,7 +35,7 @@ import { SamplePrompts, SamplePromptsProps } from "Explorer/QueryCopilot/Shared/ import { Action } from "Shared/Telemetry/TelemetryConstants"; import { userContext } from "UserContext"; import { useQueryCopilot } from "hooks/useQueryCopilot"; -import React, { useRef, useState } from "react"; +import React, { useMemo, useRef, useState } from "react"; import HintIcon from "../../../images/Hint.svg"; import RecentIcon from "../../../images/Recent.svg"; import errorIcon from "../../../images/close-black.svg"; @@ -71,6 +71,8 @@ export const QueryCopilotPromptbar: React.FC = ({ }: QueryCopilotPromptProps): JSX.Element => { const [copilotTeachingBubbleVisible, setCopilotTeachingBubbleVisible] = useState(false); const inputEdited = useRef(false); + const itemRefs = useRef([]); + const searchInputRef = useRef(null); const { openFeedbackModal, hideFeedbackModalForLikedQueries, @@ -109,7 +111,7 @@ export const QueryCopilotPromptbar: React.FC = ({ setErrors, errors, } = useCopilotStore(); - + const [focusedIndex, setFocusedIndex] = useState(-1); const sampleProps: SamplePromptsProps = { isSamplePromptsOpen: isSamplePromptsOpen, setIsSamplePromptsOpen: setIsSamplePromptsOpen, @@ -142,6 +144,7 @@ export const QueryCopilotPromptbar: React.FC = ({ : getSuggestedPrompts(); const [filteredHistories, setFilteredHistories] = useState(histories); const [filteredSuggestedPrompts, setFilteredSuggestedPrompts] = useState(suggestedPrompts); + const { UpArrow, DownArrow, Enter } = NormalizedEventKey; const handleUserPromptChange = (event: React.ChangeEvent) => { inputEdited.current = true; @@ -307,7 +310,38 @@ export const QueryCopilotPromptbar: React.FC = ({ return "Content is updated"; } }; + const openSamplePrompts = () => { + inputEdited.current = true; + setShowSamplePrompts(true); + }; + const totalSuggestions = useMemo( + () => [...filteredSuggestedPrompts, ...filteredHistories], + [filteredSuggestedPrompts, filteredHistories], + ); + const handleKeyDownForInput = (event: React.KeyboardEvent) => { + if (event.key === DownArrow) { + setFocusedIndex(0); + itemRefs.current[0]?.current?.focus(); + } else if (event.key === Enter && userPrompt) { + inputEdited.current = true; + startGenerateQueryProcess(); + } + }; + + const handleKeyDownForItem = (event: React.KeyboardEvent) => { + if (event.key === UpArrow && focusedIndex > 0) { + itemRefs.current[focusedIndex - 1].current?.focus(); + setFocusedIndex((prevIndex) => prevIndex - 1); + } else if (event.key === DownArrow && focusedIndex < totalSuggestions.length - 1) { + itemRefs.current[focusedIndex + 1].current?.focus(); + setFocusedIndex((prevIndex) => prevIndex + 1); + } + }; + + React.useEffect(() => { + itemRefs.current = totalSuggestions.map(() => React.createRef()); + }, [totalSuggestions]); React.useEffect(() => { useTabs.getState().setIsQueryErrorThrown(false); }, []); @@ -337,23 +371,14 @@ export const QueryCopilotPromptbar: React.FC = ({ id="naturalLanguageInput" value={userPrompt} onChange={handleUserPromptChange} - onClick={() => { - inputEdited.current = true; - setShowSamplePrompts(true); - }} - onKeyDown={(e) => { - if (e.key === "Enter" && userPrompt) { - inputEdited.current = true; - startGenerateQueryProcess(); - } - }} + onClick={openSamplePrompts} + onFocus={() => setShowSamplePrompts(true)} + elementRef={searchInputRef} + onKeyDown={handleKeyDownForInput} style={{ lineHeight: 30 }} styles={{ root: { width: "100%" }, - suffix: { - background: "none", - padding: 0, - }, + suffix: { background: "none", padding: 0 }, fieldGroup: { borderRadius: 4, borderColor: "#D1D1D1", @@ -366,7 +391,8 @@ export const QueryCopilotPromptbar: React.FC = ({ }, }} disabled={isGeneratingQuery} - autoComplete="off" + autoComplete="list" + aria-expanded={showSamplePrompts} placeholder="Ask a question in natural language and we’ll generate the query for you." aria-labelledby="copilot-textfield-label" onRenderSuffix={() => { @@ -438,6 +464,8 @@ export const QueryCopilotPromptbar: React.FC = ({ setShowSamplePrompts(false); inputEdited.current = true; }} + elementRef={itemRefs.current[i]} + onKeyDown={handleKeyDownForItem} onRenderIcon={() => } styles={promptStyles} > @@ -460,14 +488,16 @@ export const QueryCopilotPromptbar: React.FC = ({ > Suggested Prompts - {filteredSuggestedPrompts.map((prompt) => ( + {filteredSuggestedPrompts.map((prompt, index) => ( { setUserPrompt(prompt.text); setShowSamplePrompts(false); inputEdited.current = true; }} + onKeyDown={handleKeyDownForItem} onRenderIcon={() => } styles={promptStyles} >