Refactor SearchOverlay to use Context for state management (#58768)
This commit is contained in:
54
src/search/components/input/SearchContext.tsx
Normal file
54
src/search/components/input/SearchContext.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { createContext, useContext, RefObject, SetStateAction, MutableRefObject } from 'react'
|
||||
import type { AIReference } from '../types'
|
||||
import type { AutocompleteSearchHit, GeneralSearchHit } from '@/search/types'
|
||||
|
||||
export interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit {
|
||||
isUserQuery?: boolean
|
||||
}
|
||||
|
||||
export interface GeneralSearchHitWithOptions extends GeneralSearchHit {
|
||||
isViewAllResults?: boolean
|
||||
isNoResultsFound?: boolean
|
||||
isSearchDocsOption?: boolean
|
||||
}
|
||||
|
||||
export interface AskAIState {
|
||||
isAskAIState: boolean
|
||||
aiQuery: string
|
||||
debug: boolean
|
||||
currentVersion: string
|
||||
setAISearchError: (isError?: boolean) => void
|
||||
references: AIReference[]
|
||||
setReferences: (value: SetStateAction<AIReference[]>) => void
|
||||
referencesIndexOffset: number
|
||||
referenceOnSelect: (url: string) => void
|
||||
askAIEventGroupId: MutableRefObject<string>
|
||||
aiSearchError: boolean
|
||||
aiCouldNotAnswer: boolean
|
||||
setAICouldNotAnswer: (value: boolean) => void
|
||||
}
|
||||
|
||||
export interface SearchContextType {
|
||||
t: any
|
||||
generalSearchOptions: GeneralSearchHitWithOptions[]
|
||||
aiOptionsWithUserInput: AutocompleteSearchHitWithUserQuery[]
|
||||
generalSearchResultOnSelect: (selectedOption: GeneralSearchHit) => void
|
||||
aiAutocompleteOnSelect: (selectedOption: AutocompleteSearchHit) => void
|
||||
performGeneralSearch: () => void
|
||||
selectedIndex: number
|
||||
listElementsRef: RefObject<Array<HTMLLIElement | null>>
|
||||
askAIState: AskAIState
|
||||
showSpinner: boolean
|
||||
searchLoading: boolean
|
||||
previousSuggestionsListHeight: number | string
|
||||
}
|
||||
|
||||
export const SearchContext = createContext<SearchContextType | null>(null)
|
||||
|
||||
export const useSearchContext = () => {
|
||||
const context = useContext(SearchContext)
|
||||
if (!context) {
|
||||
throw new Error('useSearchContext must be used within a SearchContext.Provider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
251
src/search/components/input/SearchGroups.tsx
Normal file
251
src/search/components/input/SearchGroups.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import React from 'react'
|
||||
import { ActionList, Spinner } from '@primer/react'
|
||||
import {
|
||||
SearchIcon,
|
||||
FileIcon,
|
||||
ArrowRightIcon,
|
||||
CopilotIcon,
|
||||
CommentIcon,
|
||||
} from '@primer/octicons-react'
|
||||
|
||||
import { AskAIResults } from './AskAIResults'
|
||||
import { useSearchContext, AutocompleteSearchHitWithUserQuery } from './SearchContext'
|
||||
import styles from './SearchOverlay.module.scss'
|
||||
|
||||
export function SearchGroups() {
|
||||
const {
|
||||
t,
|
||||
generalSearchOptions,
|
||||
aiOptionsWithUserInput,
|
||||
generalSearchResultOnSelect,
|
||||
aiAutocompleteOnSelect,
|
||||
performGeneralSearch,
|
||||
selectedIndex,
|
||||
listElementsRef,
|
||||
askAIState,
|
||||
showSpinner,
|
||||
searchLoading,
|
||||
previousSuggestionsListHeight,
|
||||
} = useSearchContext()
|
||||
|
||||
const isInAskAIState = askAIState?.isAskAIState && !askAIState.aiSearchError
|
||||
const isInAskAIStateButNoAnswer = isInAskAIState && askAIState.aiCouldNotAnswer
|
||||
|
||||
// This spinner is for both the AI search and the general search results.
|
||||
// We already show a spinner when streaming AI response, so don't want to show 2 here
|
||||
if (showSpinner && !isInAskAIState) {
|
||||
return (
|
||||
<div
|
||||
key="loading"
|
||||
role="status"
|
||||
className={styles.loadingContainer}
|
||||
style={{
|
||||
height: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const groups = []
|
||||
|
||||
// We want to show general search suggestions above the AI Response section if the AI could not answer
|
||||
if (generalSearchOptions.length || isInAskAIStateButNoAnswer) {
|
||||
const items = []
|
||||
for (let index = 0; index < generalSearchOptions.length; index++) {
|
||||
const option = generalSearchOptions[index]
|
||||
if (option.isNoResultsFound) {
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
className={styles.noResultsFound}
|
||||
tabIndex={-1}
|
||||
aria-label={t('search.overlay.no_results_found')}
|
||||
disabled
|
||||
>
|
||||
{option.title}
|
||||
</ActionList.Item>,
|
||||
)
|
||||
// There should be no more items after the no results found item
|
||||
break
|
||||
// This is a special case where there is an error loading search results and we want to be able to search the docs using the user's query
|
||||
} else if (option.isSearchDocsOption) {
|
||||
const isActive = selectedIndex === index
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
tabIndex={-1}
|
||||
active={isActive}
|
||||
onSelect={() => performGeneralSearch()}
|
||||
aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[index] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionList.LeadingVisual aria-hidden>
|
||||
<SearchIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
{option.title}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>,
|
||||
)
|
||||
} else if (option.title) {
|
||||
const isActive = selectedIndex === index
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
aria-describedby="search-suggestions-list"
|
||||
onSelect={() =>
|
||||
option.isViewAllResults ? performGeneralSearch() : generalSearchResultOnSelect(option)
|
||||
}
|
||||
className={option.isViewAllResults ? styles.viewAllSearchResults : ''}
|
||||
active={isActive}
|
||||
tabIndex={-1}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[index] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!option.isNoResultsFound && (
|
||||
<ActionList.LeadingVisual
|
||||
aria-hidden
|
||||
className={
|
||||
option.isViewAllResults ? styles.leadingVisualHidden : styles.leadingVisualVisible
|
||||
}
|
||||
>
|
||||
<FileIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
)}
|
||||
{option.title}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(
|
||||
<ActionList.Group key="general" data-testid="general-autocomplete-suggestions">
|
||||
<ActionList.GroupHeading as="h3" tabIndex={-1}>
|
||||
{t('search.overlay.general_suggestions_list_heading')}
|
||||
</ActionList.GroupHeading>
|
||||
{searchLoading && isInAskAIState ? (
|
||||
<div
|
||||
role="status"
|
||||
className={styles.loadingContainer}
|
||||
style={{
|
||||
height: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
items
|
||||
)}
|
||||
</ActionList.Group>,
|
||||
)
|
||||
|
||||
if (isInAskAIState || isInAskAIStateButNoAnswer) {
|
||||
groups.push(<ActionList.Divider key="no-answer-divider" />)
|
||||
}
|
||||
|
||||
if (isInAskAIState) {
|
||||
groups.push(
|
||||
<ActionList.Group key="ai" data-testid="ask-ai">
|
||||
<li tabIndex={-1}>
|
||||
<AskAIResults
|
||||
query={askAIState.aiQuery}
|
||||
debug={askAIState.debug}
|
||||
version={askAIState.currentVersion}
|
||||
setAISearchError={askAIState.setAISearchError}
|
||||
references={askAIState.references}
|
||||
setReferences={askAIState.setReferences}
|
||||
referencesIndexOffset={askAIState.referencesIndexOffset}
|
||||
referenceOnSelect={askAIState.referenceOnSelect}
|
||||
selectedIndex={selectedIndex}
|
||||
askAIEventGroupId={askAIState.askAIEventGroupId}
|
||||
aiCouldNotAnswer={askAIState.aiCouldNotAnswer}
|
||||
setAICouldNotAnswer={askAIState.setAICouldNotAnswer}
|
||||
listElementsRef={listElementsRef}
|
||||
/>
|
||||
</li>
|
||||
</ActionList.Group>,
|
||||
)
|
||||
}
|
||||
|
||||
// Don't show the bottom divider if:
|
||||
// 1. We are in the AI could not answer state
|
||||
// 2. We are in the AI Search error state
|
||||
// 3. There are no AI suggestions to show in suggestions state
|
||||
if (
|
||||
!isInAskAIState &&
|
||||
!askAIState.aiSearchError &&
|
||||
generalSearchOptions.filter((option) => !option.isViewAllResults && !option.isNoResultsFound)
|
||||
.length &&
|
||||
aiOptionsWithUserInput.length
|
||||
) {
|
||||
groups.push(<ActionList.Divider key="bottom-divider" />)
|
||||
}
|
||||
}
|
||||
|
||||
if (aiOptionsWithUserInput.length && !isInAskAIState) {
|
||||
groups.push(
|
||||
<ActionList.Group key="ai-suggestions" data-testid="ai-autocomplete-suggestions">
|
||||
<ActionList.GroupHeading as="h3" id="copilot-suggestions" tabIndex={-1}>
|
||||
<CopilotIcon className="mr-1" />
|
||||
{t('search.overlay.ai_autocomplete_list_heading')}
|
||||
</ActionList.GroupHeading>
|
||||
{aiOptionsWithUserInput.map((option: AutocompleteSearchHitWithUserQuery, index: number) => {
|
||||
// Since general search comes first, we need to add an offset for AI suggestions
|
||||
const indexWithOffset = generalSearchOptions.length + index
|
||||
const isActive = selectedIndex === indexWithOffset
|
||||
const item = (
|
||||
<ActionList.Item
|
||||
key={`ai-${indexWithOffset}`}
|
||||
id={`search-option-ai-${indexWithOffset}`}
|
||||
aria-describedby="copilot-suggestions"
|
||||
onSelect={() => aiAutocompleteOnSelect(option)}
|
||||
active={isActive}
|
||||
tabIndex={-1}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[indexWithOffset] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionList.LeadingVisual aria-hidden>
|
||||
<CommentIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
{option.term}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>
|
||||
)
|
||||
return item
|
||||
})}
|
||||
</ActionList.Group>,
|
||||
)
|
||||
}
|
||||
|
||||
return <>{groups}</>
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import React, { useState, useRef, RefObject, useEffect, SetStateAction, useMemo } from 'react'
|
||||
import React, { useState, useRef, RefObject, useEffect, useMemo } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ActionList, IconButton, Overlay, Spinner, Stack, TextInput, Banner } from '@primer/react'
|
||||
import {
|
||||
SearchIcon,
|
||||
XCircleFillIcon,
|
||||
CommentIcon,
|
||||
CopilotIcon,
|
||||
FileIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowLeftIcon,
|
||||
} from '@primer/octicons-react'
|
||||
import { ActionList, IconButton, Overlay, Stack, TextInput, Banner } from '@primer/react'
|
||||
import { SearchIcon, XCircleFillIcon, CopilotIcon, ArrowLeftIcon } from '@primer/octicons-react'
|
||||
import { focusTrap } from '@primer/behaviors'
|
||||
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
@@ -21,7 +13,6 @@ import {
|
||||
GENERAL_SEARCH_CONTEXT,
|
||||
} from '../helpers/execute-search-actions'
|
||||
import { useCombinedSearchResults } from '@/search/components/hooks/useAISearchAutocomplete'
|
||||
import { AskAIResults } from './AskAIResults'
|
||||
import { sendEvent, uuidv4 } from '@/events/components/events'
|
||||
import { EventType } from '@/events/types'
|
||||
import { ASK_AI_EVENT_GROUP, SEARCH_OVERLAY_EVENT_GROUP } from '@/events/components/event-groups'
|
||||
@@ -32,6 +23,13 @@ import type { AutocompleteSearchHit, GeneralSearchHit } from '@/search/types'
|
||||
|
||||
import { sanitizeSearchQuery } from '@/search/lib/sanitize-search-query'
|
||||
|
||||
import {
|
||||
SearchContext,
|
||||
SearchContextType,
|
||||
AutocompleteSearchHitWithUserQuery,
|
||||
GeneralSearchHitWithOptions,
|
||||
} from './SearchContext'
|
||||
import { SearchGroups } from './SearchGroups'
|
||||
import styles from './SearchOverlay.module.scss'
|
||||
|
||||
type Props = {
|
||||
@@ -532,7 +530,6 @@ export function SearchOverlay({
|
||||
}
|
||||
|
||||
// We render the AI Result in the searchGroups call, so we pass the props down via an object
|
||||
// TODO: Move stateful logic to Context since we now have so many props:
|
||||
const askAIState = {
|
||||
isAskAIState,
|
||||
aiQuery,
|
||||
@@ -556,6 +553,21 @@ export function SearchOverlay({
|
||||
setAICouldNotAnswer,
|
||||
}
|
||||
|
||||
const searchContextValue: SearchContextType = {
|
||||
t,
|
||||
generalSearchOptions: generalOptionsWithViewStatus,
|
||||
aiOptionsWithUserInput,
|
||||
generalSearchResultOnSelect,
|
||||
aiAutocompleteOnSelect: aiSearchOptionOnSelect,
|
||||
performGeneralSearch,
|
||||
selectedIndex,
|
||||
listElementsRef: listElementsRef as RefObject<Array<HTMLLIElement | null>>,
|
||||
askAIState,
|
||||
showSpinner,
|
||||
searchLoading,
|
||||
previousSuggestionsListHeight,
|
||||
}
|
||||
|
||||
// We display different content in the overlay based:
|
||||
// 1. If either search (autocomplete results or ask AI) has an error
|
||||
// 2. The user has selected an AI query and we are showing the ask AI results
|
||||
@@ -566,7 +578,12 @@ export function SearchOverlay({
|
||||
const inErrorState = aiSearchError || (autoCompleteSearchError && !isAskAIState)
|
||||
if (inErrorState) {
|
||||
OverlayContents = (
|
||||
<>
|
||||
<SearchContext.Provider
|
||||
value={{
|
||||
...searchContextValue,
|
||||
aiOptionsWithUserInput: aiSearchError ? [] : aiOptionsWithUserInput,
|
||||
}}
|
||||
>
|
||||
<ActionList
|
||||
aria-label={t('search.overlay.suggestions_list_aria_label')}
|
||||
id="search-suggestions-list"
|
||||
@@ -613,50 +630,26 @@ export function SearchOverlay({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{renderSearchGroups(
|
||||
t,
|
||||
generalOptionsWithViewStatus,
|
||||
aiSearchError ? [] : aiOptionsWithUserInput,
|
||||
generalSearchResultOnSelect,
|
||||
aiSearchOptionOnSelect,
|
||||
performGeneralSearch,
|
||||
selectedIndex,
|
||||
listElementsRef,
|
||||
askAIState,
|
||||
showSpinner,
|
||||
searchLoading,
|
||||
previousSuggestionsListHeight,
|
||||
)}
|
||||
<SearchGroups />
|
||||
</ActionList>
|
||||
</>
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
} else {
|
||||
OverlayContents = (
|
||||
<ActionList
|
||||
id="search-suggestions-list"
|
||||
aria-label={t('search.overlay.suggestions_list_aria_label')}
|
||||
showDividers
|
||||
className={styles.suggestionsList}
|
||||
ref={suggestionsListHeightRef}
|
||||
style={{
|
||||
minHeight: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
{renderSearchGroups(
|
||||
t,
|
||||
generalOptionsWithViewStatus,
|
||||
aiOptionsWithUserInput,
|
||||
generalSearchResultOnSelect,
|
||||
aiSearchOptionOnSelect,
|
||||
performGeneralSearch,
|
||||
selectedIndex,
|
||||
listElementsRef,
|
||||
askAIState,
|
||||
showSpinner,
|
||||
searchLoading,
|
||||
previousSuggestionsListHeight,
|
||||
)}
|
||||
</ActionList>
|
||||
<SearchContext.Provider value={searchContextValue}>
|
||||
<ActionList
|
||||
id="search-suggestions-list"
|
||||
aria-label={t('search.overlay.suggestions_list_aria_label')}
|
||||
showDividers
|
||||
className={styles.suggestionsList}
|
||||
ref={suggestionsListHeightRef}
|
||||
style={{
|
||||
minHeight: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
<SearchGroups />
|
||||
</ActionList>
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -759,268 +752,6 @@ export function SearchOverlay({
|
||||
)
|
||||
}
|
||||
|
||||
interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit {
|
||||
isUserQuery?: boolean
|
||||
}
|
||||
|
||||
interface GeneralSearchHitWithOptions extends GeneralSearchHit {
|
||||
isViewAllResults?: boolean
|
||||
isNoResultsFound?: boolean
|
||||
isSearchDocsOption?: boolean
|
||||
}
|
||||
|
||||
// Render the autocomplete suggestions with AI suggestions first, headings, and a divider between the two
|
||||
function renderSearchGroups(
|
||||
t: any,
|
||||
generalSearchOptions: GeneralSearchHitWithOptions[],
|
||||
aiOptionsWithUserInput: AutocompleteSearchHitWithUserQuery[],
|
||||
generalSearchResultOnSelect: (selectedOption: GeneralSearchHit) => void,
|
||||
aiAutocompleteOnSelect: (selectedOption: AutocompleteSearchHit) => void,
|
||||
performGeneralSearch: () => void,
|
||||
selectedIndex: number,
|
||||
listElementsRef: RefObject<Array<HTMLLIElement | null>>,
|
||||
askAIState: {
|
||||
isAskAIState: boolean
|
||||
aiQuery: string
|
||||
debug: boolean
|
||||
currentVersion: string
|
||||
setAISearchError: () => void
|
||||
references: AIReference[]
|
||||
setReferences: (value: SetStateAction<AIReference[]>) => void
|
||||
referencesIndexOffset: number
|
||||
referenceOnSelect: (url: string) => void
|
||||
askAIEventGroupId: React.MutableRefObject<string>
|
||||
aiSearchError: boolean
|
||||
aiCouldNotAnswer: boolean
|
||||
setAICouldNotAnswer: (value: boolean) => void
|
||||
},
|
||||
showSpinner: boolean,
|
||||
searchLoading: boolean,
|
||||
previousSuggestionsListHeight: number | string,
|
||||
) {
|
||||
const groups = []
|
||||
|
||||
const isInAskAIState = askAIState?.isAskAIState && !askAIState.aiSearchError
|
||||
const isInAskAIStateButNoAnswer = isInAskAIState && askAIState.aiCouldNotAnswer
|
||||
|
||||
// This spinner is for both the AI search and the general search results.
|
||||
// We already show a spinner when streaming AI response, so don't want to show 2 here
|
||||
if (showSpinner && !isInAskAIState) {
|
||||
groups.push(
|
||||
<div
|
||||
key="loading"
|
||||
role="status"
|
||||
className={styles.loadingContainer}
|
||||
style={{
|
||||
height: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</div>,
|
||||
)
|
||||
return groups
|
||||
}
|
||||
|
||||
// We want to show general search suggestions above the AI Response section if the AI could not answer
|
||||
if (generalSearchOptions.length || isInAskAIStateButNoAnswer) {
|
||||
const items = []
|
||||
for (let index = 0; index < generalSearchOptions.length; index++) {
|
||||
const option = generalSearchOptions[index]
|
||||
if (option.isNoResultsFound) {
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
className={styles.noResultsFound}
|
||||
tabIndex={-1}
|
||||
aria-label={t('search.overlay.no_results_found')}
|
||||
disabled
|
||||
>
|
||||
{option.title}
|
||||
</ActionList.Item>,
|
||||
)
|
||||
// There should be no more items after the no results found item
|
||||
break
|
||||
// This is a special case where there is an error loading search results and we want to be able to search the docs using the user's query
|
||||
} else if (option.isSearchDocsOption) {
|
||||
const isActive = selectedIndex === index
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
tabIndex={-1}
|
||||
active={isActive}
|
||||
onSelect={() => performGeneralSearch()}
|
||||
aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[index] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionList.LeadingVisual aria-hidden>
|
||||
<SearchIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
{option.title}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>,
|
||||
)
|
||||
} else if (option.title) {
|
||||
const isActive = selectedIndex === index
|
||||
items.push(
|
||||
<ActionList.Item
|
||||
key={`general-${index}`}
|
||||
id={`search-option-general-${index}`}
|
||||
aria-describedby="search-suggestions-list"
|
||||
onSelect={() =>
|
||||
option.isViewAllResults ? performGeneralSearch() : generalSearchResultOnSelect(option)
|
||||
}
|
||||
className={option.isViewAllResults ? styles.viewAllSearchResults : ''}
|
||||
active={isActive}
|
||||
tabIndex={-1}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[index] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!option.isNoResultsFound && (
|
||||
<ActionList.LeadingVisual
|
||||
aria-hidden
|
||||
className={
|
||||
option.isViewAllResults ? styles.leadingVisualHidden : styles.leadingVisualVisible
|
||||
}
|
||||
>
|
||||
<FileIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
)}
|
||||
{option.title}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
groups.push(
|
||||
<ActionList.Group key="general" data-testid="general-autocomplete-suggestions">
|
||||
<ActionList.GroupHeading as="h3" tabIndex={-1}>
|
||||
{t('search.overlay.general_suggestions_list_heading')}
|
||||
</ActionList.GroupHeading>
|
||||
{searchLoading && isInAskAIState ? (
|
||||
<div
|
||||
role="status"
|
||||
className={styles.loadingContainer}
|
||||
style={{
|
||||
height: `${previousSuggestionsListHeight}px`,
|
||||
}}
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
items
|
||||
)}
|
||||
</ActionList.Group>,
|
||||
)
|
||||
|
||||
if (isInAskAIState || isInAskAIStateButNoAnswer) {
|
||||
groups.push(<ActionList.Divider key="no-answer-divider" />)
|
||||
}
|
||||
|
||||
if (isInAskAIState) {
|
||||
groups.push(
|
||||
<ActionList.Group key="ai" data-testid="ask-ai">
|
||||
<li tabIndex={-1}>
|
||||
<AskAIResults
|
||||
query={askAIState.aiQuery}
|
||||
debug={askAIState.debug}
|
||||
version={askAIState.currentVersion}
|
||||
setAISearchError={askAIState.setAISearchError}
|
||||
references={askAIState.references}
|
||||
setReferences={askAIState.setReferences}
|
||||
referencesIndexOffset={askAIState.referencesIndexOffset}
|
||||
referenceOnSelect={askAIState.referenceOnSelect}
|
||||
selectedIndex={selectedIndex}
|
||||
askAIEventGroupId={askAIState.askAIEventGroupId}
|
||||
aiCouldNotAnswer={askAIState.aiCouldNotAnswer}
|
||||
setAICouldNotAnswer={askAIState.setAICouldNotAnswer}
|
||||
listElementsRef={listElementsRef}
|
||||
/>
|
||||
</li>
|
||||
</ActionList.Group>,
|
||||
)
|
||||
}
|
||||
|
||||
// Don't show the bottom divider if:
|
||||
// 1. We are in the AI could not answer state
|
||||
// 2. We are in the AI Search error state
|
||||
// 3. There are no AI suggestions to show in suggestions state
|
||||
if (
|
||||
!isInAskAIState &&
|
||||
!askAIState.aiSearchError &&
|
||||
generalSearchOptions.filter((option) => !option.isViewAllResults && !option.isNoResultsFound)
|
||||
.length &&
|
||||
aiOptionsWithUserInput.length
|
||||
) {
|
||||
groups.push(<ActionList.Divider key="bottom-divider" />)
|
||||
}
|
||||
}
|
||||
|
||||
if (aiOptionsWithUserInput.length && !isInAskAIState) {
|
||||
groups.push(
|
||||
<ActionList.Group key="ai-suggestions" data-testid="ai-autocomplete-suggestions">
|
||||
<ActionList.GroupHeading as="h3" id="copilot-suggestions" tabIndex={-1}>
|
||||
<CopilotIcon className="mr-1" />
|
||||
{t('search.overlay.ai_autocomplete_list_heading')}
|
||||
</ActionList.GroupHeading>
|
||||
{aiOptionsWithUserInput.map((option: AutocompleteSearchHitWithUserQuery, index: number) => {
|
||||
// Since general search comes first, we need to add an offset for AI suggestions
|
||||
const indexWithOffset = generalSearchOptions.length + index
|
||||
const isActive = selectedIndex === indexWithOffset
|
||||
const item = (
|
||||
<ActionList.Item
|
||||
key={`ai-${indexWithOffset}`}
|
||||
id={`search-option-ai-${indexWithOffset}`}
|
||||
aria-describedby="copilot-suggestions"
|
||||
onSelect={() => aiAutocompleteOnSelect(option)}
|
||||
active={isActive}
|
||||
tabIndex={-1}
|
||||
ref={(element: HTMLLIElement | null) => {
|
||||
if (listElementsRef.current) {
|
||||
listElementsRef.current[index] = element
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ActionList.LeadingVisual aria-hidden>
|
||||
<CommentIcon />
|
||||
</ActionList.LeadingVisual>
|
||||
{option.term}
|
||||
<ActionList.TrailingVisual
|
||||
aria-hidden
|
||||
className={isActive ? styles.trailingVisualActive : styles.trailingVisualHidden}
|
||||
>
|
||||
<ArrowRightIcon />
|
||||
</ActionList.TrailingVisual>
|
||||
</ActionList.Item>
|
||||
)
|
||||
return item
|
||||
})}
|
||||
</ActionList.Group>,
|
||||
)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
function sendKeyboardEvent(
|
||||
pressedKey: string,
|
||||
pressedOn: string,
|
||||
|
||||
Reference in New Issue
Block a user