mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): stop the pointer hijacking quick switcher selection (#1955)
This commit is contained in:
1 parent
af5bee9149
commit
317b4e26c0
5 files changed
+270
-237
No files matched your search
+96
-80
@@ -12,6 +12,7 @@ import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
|||||||
import type {QuickSwitcherExecutableResult, QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes';
|
import type {QuickSwitcherExecutableResult, QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes';
|
||||||
import {
|
import {
|
||||||
createSections,
|
createSections,
|
||||||
|
dismissQuickSwitcher,
|
||||||
getQuickSwitcherResultAccessibilityMetadata,
|
getQuickSwitcherResultAccessibilityMetadata,
|
||||||
getQuickSwitcherTabs,
|
getQuickSwitcherTabs,
|
||||||
getResultKey,
|
getResultKey,
|
||||||
@@ -313,7 +314,8 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
QuickSwitcherCommands.hide();
|
event.stopPropagation();
|
||||||
|
dismissQuickSwitcher({isEscape: true});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -375,6 +377,7 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
() => results.filter((result) => result.type !== QuickSwitcherResultTypes.HEADER).length,
|
() => results.filter((result) => result.type !== QuickSwitcherResultTypes.HEADER).length,
|
||||||
[results],
|
[results],
|
||||||
);
|
);
|
||||||
|
const hasSearchIntent = query.trim().length > 0;
|
||||||
const activeDescendant =
|
const activeDescendant =
|
||||||
isSearchTab && keyboardFocusIndex >= 0 && results[keyboardFocusIndex]?.type !== QuickSwitcherResultTypes.HEADER
|
isSearchTab && keyboardFocusIndex >= 0 && results[keyboardFocusIndex]?.type !== QuickSwitcherResultTypes.HEADER
|
||||||
? getQuickSwitcherOptionId(listboxId, keyboardFocusIndex)
|
? getQuickSwitcherOptionId(listboxId, keyboardFocusIndex)
|
||||||
@@ -382,7 +385,9 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
const resultStatus = QuickSwitcher.isLoadingMemberResults
|
const resultStatus = QuickSwitcher.isLoadingMemberResults
|
||||||
? i18n._(SEARCHING_PEOPLE_DESCRIPTOR)
|
? i18n._(SEARCHING_PEOPLE_DESCRIPTOR)
|
||||||
: resultCount === 0
|
: resultCount === 0
|
||||||
? i18n._(NO_MATCHES_FOUND_DESCRIPTOR)
|
? hasSearchIntent
|
||||||
|
? i18n._(NO_MATCHES_FOUND_DESCRIPTOR)
|
||||||
|
: ''
|
||||||
: resultCount === 1
|
: resultCount === 1
|
||||||
? i18n._(MESSAGE_1_RESULT_AVAILABLE_DESCRIPTOR)
|
? i18n._(MESSAGE_1_RESULT_AVAILABLE_DESCRIPTOR)
|
||||||
: i18n._(RESULTS_AVAILABLE_DESCRIPTOR, {resultCount});
|
: i18n._(RESULTS_AVAILABLE_DESCRIPTOR, {resultCount});
|
||||||
@@ -392,7 +397,7 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
}
|
}
|
||||||
const result = results[keyboardFocusIndex];
|
const result = results[keyboardFocusIndex];
|
||||||
if (!result || result.type === QuickSwitcherResultTypes.HEADER) {
|
if (!result || result.type === QuickSwitcherResultTypes.HEADER) {
|
||||||
return resultCount === 0 ? i18n._(NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR) : '';
|
return resultCount === 0 && hasSearchIntent ? i18n._(NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR) : '';
|
||||||
}
|
}
|
||||||
const label = getQuickSwitcherResultAccessibilityMetadata(result as QuickSwitcherExecutableResult, i18n).label;
|
const label = getQuickSwitcherResultAccessibilityMetadata(result as QuickSwitcherExecutableResult, i18n).label;
|
||||||
const trimmedQuery = query.trim();
|
const trimmedQuery = query.trim();
|
||||||
@@ -404,7 +409,16 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
return selectedOptionPosition > 0
|
return selectedOptionPosition > 0
|
||||||
? i18n._(AUTOCOMPLETE_SUGGESTION_OF_DESCRIPTOR, {label, selectedOptionPosition, resultCount})
|
? i18n._(AUTOCOMPLETE_SUGGESTION_OF_DESCRIPTOR, {label, selectedOptionPosition, resultCount})
|
||||||
: i18n._(AUTOCOMPLETE_SUGGESTION_DESCRIPTOR, {label});
|
: i18n._(AUTOCOMPLETE_SUGGESTION_DESCRIPTOR, {label});
|
||||||
}, [i18n.locale, isSearchTab, keyboardFocusIndex, query, resultCount, results, selectedOptionPosition]);
|
}, [
|
||||||
|
hasSearchIntent,
|
||||||
|
i18n.locale,
|
||||||
|
isSearchTab,
|
||||||
|
keyboardFocusIndex,
|
||||||
|
query,
|
||||||
|
resultCount,
|
||||||
|
results,
|
||||||
|
selectedOptionPosition,
|
||||||
|
]);
|
||||||
const handleTabChange = useCallback((tab: 'search' | 'friends') => {
|
const handleTabChange = useCallback((tab: 'search' | 'friends') => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -526,85 +540,87 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
{activeSuggestionStatus}
|
{activeSuggestionStatus}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Scroller
|
{results.length === 0 && !hasSearchIntent ? null : (
|
||||||
ref={scrollerRef}
|
<Scroller
|
||||||
className={styles.scroller}
|
ref={scrollerRef}
|
||||||
key="quick_switcher-sheet-scroller"
|
className={styles.scroller}
|
||||||
data-flx="search.quick-switcher-bottom-sheet.scroller"
|
key="quick_switcher-sheet-scroller"
|
||||||
>
|
data-flx="search.quick-switcher-bottom-sheet.scroller"
|
||||||
<div className={styles.scrollContent} data-flx="search.quick-switcher-bottom-sheet.scroll-content">
|
>
|
||||||
{results.length === 0 ? (
|
<div className={styles.scrollContent} data-flx="search.quick-switcher-bottom-sheet.scroll-content">
|
||||||
<div className={styles.emptyState} data-flx="search.quick-switcher-bottom-sheet.empty-state">
|
{results.length === 0 ? (
|
||||||
<div
|
<div className={styles.emptyState} data-flx="search.quick-switcher-bottom-sheet.empty-state">
|
||||||
className={styles.emptyStateTitle}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.empty-state-title"
|
|
||||||
>
|
|
||||||
{i18n._(NO_MATCHES_FOUND_DESCRIPTOR)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={styles.emptyStateHint}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.empty-state-hint"
|
|
||||||
>
|
|
||||||
{i18n._(TRY_A_DIFFERENT_NAME_OR_USE_PREFIXES_TO_DESCRIPTOR)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
id={listboxId}
|
|
||||||
role="listbox"
|
|
||||||
aria-label={i18n._(QUICK_SWITCHER_RESULTS_DESCRIPTOR)}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.listbox"
|
|
||||||
>
|
|
||||||
{sections.map((section: QuickSwitcherSection, sidx: number) => (
|
|
||||||
<div
|
<div
|
||||||
key={`section-${sidx}`}
|
className={styles.emptyStateTitle}
|
||||||
className={styles.section}
|
data-flx="search.quick-switcher-bottom-sheet.empty-state-title"
|
||||||
role={section.header ? 'group' : 'presentation'}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.section"
|
|
||||||
{...(section.header ? {'aria-labelledby': `${listboxId}-section-${sidx}`} : {})}
|
|
||||||
>
|
>
|
||||||
{section.header && (
|
{i18n._(NO_MATCHES_FOUND_DESCRIPTOR)}
|
||||||
<div
|
|
||||||
id={`${listboxId}-section-${sidx}`}
|
|
||||||
className={styles.sectionHeader}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.section-header"
|
|
||||||
>
|
|
||||||
{section.header.title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={styles.sectionList}
|
|
||||||
role="presentation"
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.section-list"
|
|
||||||
>
|
|
||||||
{section.rows.map(
|
|
||||||
({result, index}: {result: QuickSwitcherExecutableResult; index: number}) => (
|
|
||||||
<ResultRow
|
|
||||||
key={getResultKey(result)}
|
|
||||||
result={result}
|
|
||||||
index={index}
|
|
||||||
isKeyboardSelected={index === keyboardFocusIndex}
|
|
||||||
isHovered={index === hoverIndexForRender}
|
|
||||||
onHover={handleHover}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
optionId={getQuickSwitcherOptionId(listboxId, index)}
|
|
||||||
positionInSet={selectableIndices.indexOf(index) + 1}
|
|
||||||
setSize={resultCount}
|
|
||||||
innerRef={(node) => {
|
|
||||||
rowRefs.current[index] = node;
|
|
||||||
}}
|
|
||||||
data-flx="search.quick-switcher-bottom-sheet.result-row"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div
|
||||||
</div>
|
className={styles.emptyStateHint}
|
||||||
)}
|
data-flx="search.quick-switcher-bottom-sheet.empty-state-hint"
|
||||||
</div>
|
>
|
||||||
</Scroller>
|
{i18n._(TRY_A_DIFFERENT_NAME_OR_USE_PREFIXES_TO_DESCRIPTOR)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
id={listboxId}
|
||||||
|
role="listbox"
|
||||||
|
aria-label={i18n._(QUICK_SWITCHER_RESULTS_DESCRIPTOR)}
|
||||||
|
data-flx="search.quick-switcher-bottom-sheet.listbox"
|
||||||
|
>
|
||||||
|
{sections.map((section: QuickSwitcherSection) => (
|
||||||
|
<div
|
||||||
|
key={section.key}
|
||||||
|
className={styles.section}
|
||||||
|
role={section.header ? 'group' : 'presentation'}
|
||||||
|
data-flx="search.quick-switcher-bottom-sheet.section"
|
||||||
|
{...(section.header ? {'aria-labelledby': `${listboxId}-${section.key}`} : {})}
|
||||||
|
>
|
||||||
|
{section.header && (
|
||||||
|
<div
|
||||||
|
id={`${listboxId}-${section.key}`}
|
||||||
|
className={styles.sectionHeader}
|
||||||
|
data-flx="search.quick-switcher-bottom-sheet.section-header"
|
||||||
|
>
|
||||||
|
{section.header.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={styles.sectionList}
|
||||||
|
role="presentation"
|
||||||
|
data-flx="search.quick-switcher-bottom-sheet.section-list"
|
||||||
|
>
|
||||||
|
{section.rows.map(
|
||||||
|
({result, index}: {result: QuickSwitcherExecutableResult; index: number}) => (
|
||||||
|
<ResultRow
|
||||||
|
key={getResultKey(result)}
|
||||||
|
result={result}
|
||||||
|
index={index}
|
||||||
|
isKeyboardSelected={index === keyboardFocusIndex}
|
||||||
|
isHovered={index === hoverIndexForRender}
|
||||||
|
onHover={handleHover}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
optionId={getQuickSwitcherOptionId(listboxId, index)}
|
||||||
|
positionInSet={selectableIndices.indexOf(index) + 1}
|
||||||
|
setSize={resultCount}
|
||||||
|
innerRef={(node) => {
|
||||||
|
rowRefs.current[index] = node;
|
||||||
|
}}
|
||||||
|
data-flx="search.quick-switcher-bottom-sheet.result-row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!directMessagesDisabled && (
|
{!directMessagesDisabled && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -14,18 +14,10 @@
|
|||||||
color: var(--qs-text-primary, var(--text-primary));
|
color: var(--qs-text-primary, var(--text-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.option:hover {
|
|
||||||
background-color: var(--qs-row-hover, var(--background-modifier-accent));
|
|
||||||
}
|
|
||||||
|
|
||||||
.optionActive {
|
.optionActive {
|
||||||
background-color: var(--qs-row-hover, var(--background-modifier-accent));
|
background-color: var(--qs-row-hover, var(--background-modifier-accent));
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-light) .option:hover {
|
|
||||||
background-color: var(--qs-row-hover-light, var(--background-modifier-hover));
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html.theme-light) .optionActive {
|
:global(html.theme-light) .optionActive {
|
||||||
background-color: var(--qs-row-hover-light, var(--background-modifier-hover));
|
background-color: var(--qs-row-hover-light, var(--background-modifier-hover));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||||
import {useListNavigation} from '@app/features/app/hooks/useListNavigation';
|
|
||||||
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
|
||||||
import {isIMEComposing} from '@app/features/messaging/utils/IMECompositionUtils';
|
import {isIMEComposing} from '@app/features/messaging/utils/IMECompositionUtils';
|
||||||
import * as QuickSwitcherCommands from '@app/features/search/commands/QuickSwitcherCommands';
|
import * as QuickSwitcherCommands from '@app/features/search/commands/QuickSwitcherCommands';
|
||||||
import {QuickSwitcherBottomSheet} from '@app/features/search/components/bottomsheets/QuickSwitcherBottomSheet';
|
import {QuickSwitcherBottomSheet} from '@app/features/search/components/bottomsheets/QuickSwitcherBottomSheet';
|
||||||
import quickStyles from '@app/features/search/components/quick_switcher/QuickSwitcherModal.module.css';
|
import quickStyles from '@app/features/search/components/quick_switcher/QuickSwitcherModal.module.css';
|
||||||
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
||||||
|
import {consumeEscapeIntent} from '@app/features/search/state/QuickSwitcherEscapeIntent';
|
||||||
import type {QuickSwitcherExecutableResult, QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes';
|
import type {QuickSwitcherExecutableResult, QuickSwitcherResult} from '@app/features/search/state/QuickSwitcherTypes';
|
||||||
import {
|
import {
|
||||||
createSections,
|
createSections,
|
||||||
|
dismissQuickSwitcher,
|
||||||
getQuickSwitcherResultAccessibilityMetadata,
|
getQuickSwitcherResultAccessibilityMetadata,
|
||||||
getResultKey,
|
getResultKey,
|
||||||
getViewContext,
|
getViewContext,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
PREFIX_HINTS,
|
PREFIX_HINTS,
|
||||||
type QuickSwitcherSection,
|
type QuickSwitcherSection,
|
||||||
renderIcon,
|
renderIcon,
|
||||||
|
useQuickSwitcherEscapeIntent,
|
||||||
useQuickSwitcherInputFocus,
|
useQuickSwitcherInputFocus,
|
||||||
useQuickSwitcherKeyboardHandling,
|
useQuickSwitcherKeyboardHandling,
|
||||||
} from '@app/features/search/utils/QuickSwitcherModalUtils';
|
} from '@app/features/search/utils/QuickSwitcherModalUtils';
|
||||||
@@ -34,7 +36,7 @@ import {Trans, useLingui} from '@lingui/react/macro';
|
|||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useCallback, useEffect, useId, useMemo, useRef} from 'react';
|
import {useCallback, useEffect, useId, useMemo, useRef, useState} from 'react';
|
||||||
|
|
||||||
const NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR = msg({
|
const NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR = msg({
|
||||||
message: 'No autocomplete suggestion',
|
message: 'No autocomplete suggestion',
|
||||||
@@ -99,6 +101,7 @@ const QUICK_SWITCHER_RESULTS_DESCRIPTOR = msg({
|
|||||||
message: 'Quick switcher results',
|
message: 'Quick switcher results',
|
||||||
comment: 'Accessible label for the desktop quick switcher result listbox.',
|
comment: 'Accessible label for the desktop quick switcher result listbox.',
|
||||||
});
|
});
|
||||||
|
const SELECTED_ROW_SCROLL_PADDING = 10;
|
||||||
const getQuickSwitcherOptionId = (baseId: string, index: number): string => `${baseId}-option-${index}`;
|
const getQuickSwitcherOptionId = (baseId: string, index: number): string => `${baseId}-option-${index}`;
|
||||||
type PrefixHint = (typeof PREFIX_HINTS)[number];
|
type PrefixHint = (typeof PREFIX_HINTS)[number];
|
||||||
const PrefixHintToken: React.FC<{
|
const PrefixHintToken: React.FC<{
|
||||||
@@ -121,9 +124,7 @@ const ResultRow = observer(
|
|||||||
result,
|
result,
|
||||||
index,
|
index,
|
||||||
isKeyboardSelected,
|
isKeyboardSelected,
|
||||||
isHovered,
|
|
||||||
onHover,
|
onHover,
|
||||||
onMouseLeave,
|
|
||||||
onConfirm,
|
onConfirm,
|
||||||
optionId,
|
optionId,
|
||||||
positionInSet,
|
positionInSet,
|
||||||
@@ -133,9 +134,7 @@ const ResultRow = observer(
|
|||||||
result: QuickSwitcherResult;
|
result: QuickSwitcherResult;
|
||||||
index: number;
|
index: number;
|
||||||
isKeyboardSelected: boolean;
|
isKeyboardSelected: boolean;
|
||||||
isHovered: boolean;
|
|
||||||
onHover: (index: number) => void;
|
onHover: (index: number) => void;
|
||||||
onMouseLeave: () => void;
|
|
||||||
onConfirm: (result: QuickSwitcherExecutableResult) => void;
|
onConfirm: (result: QuickSwitcherExecutableResult) => void;
|
||||||
optionId: string;
|
optionId: string;
|
||||||
positionInSet: number;
|
positionInSet: number;
|
||||||
@@ -158,7 +157,7 @@ const ResultRow = observer(
|
|||||||
const resultMetadata = getQuickSwitcherResultAccessibilityMetadata(executableResult, i18n);
|
const resultMetadata = getQuickSwitcherResultAccessibilityMetadata(executableResult, i18n);
|
||||||
const {guildName, isChannel, isUser, label: optionLabel, mentionCount, subtext, unreadCount} = resultMetadata;
|
const {guildName, isChannel, isUser, label: optionLabel, mentionCount, subtext, unreadCount} = resultMetadata;
|
||||||
const hasUnread = unreadCount > 0 || mentionCount > 0;
|
const hasUnread = unreadCount > 0 || mentionCount > 0;
|
||||||
const isActive = isKeyboardSelected || isHovered;
|
const isActive = isKeyboardSelected;
|
||||||
const isHighlight =
|
const isHighlight =
|
||||||
hasUnread &&
|
hasUnread &&
|
||||||
(executableResult.type === QuickSwitcherResultTypes.TEXT_CHANNEL ||
|
(executableResult.type === QuickSwitcherResultTypes.TEXT_CHANNEL ||
|
||||||
@@ -197,7 +196,6 @@ const ResultRow = observer(
|
|||||||
className={clsx(quickStyles.option, isActive && quickStyles.optionActive)}
|
className={clsx(quickStyles.option, isActive && quickStyles.optionActive)}
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
if (event.button === 0) event.preventDefault();
|
if (event.button === 0) event.preventDefault();
|
||||||
}}
|
}}
|
||||||
@@ -290,56 +288,20 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const scrollerRef = useRef<ScrollerHandle>(null);
|
const scrollerRef = useRef<ScrollerHandle>(null);
|
||||||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||||
const shouldScrollToSelection = useRef(false);
|
const previousQueryRef = useRef(query);
|
||||||
|
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(true);
|
||||||
const isMobile = MobileLayout.isMobileLayout();
|
const isMobile = MobileLayout.isMobileLayout();
|
||||||
const inputId = `${quickSwitcherId}-input`;
|
const inputId = `${quickSwitcherId}-input`;
|
||||||
const listboxId = `${quickSwitcherId}-results`;
|
const listboxId = `${quickSwitcherId}-results`;
|
||||||
const statusId = `${quickSwitcherId}-status`;
|
const statusId = `${quickSwitcherId}-status`;
|
||||||
const suggestionStatusId = `${quickSwitcherId}-suggestion-status`;
|
const suggestionStatusId = `${quickSwitcherId}-suggestion-status`;
|
||||||
const hintId = `${quickSwitcherId}-hint`;
|
const hintId = `${quickSwitcherId}-hint`;
|
||||||
const {
|
|
||||||
keyboardFocusIndex,
|
|
||||||
hoverIndexForRender,
|
|
||||||
handleMouseEnter: handleHoverIndex,
|
|
||||||
handleMouseLeave,
|
|
||||||
setSelectedIndex: setKeyboardIndex,
|
|
||||||
} = useListNavigation({
|
|
||||||
itemCount: results.length,
|
|
||||||
initialIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
|
||||||
loop: true,
|
|
||||||
});
|
|
||||||
if (rowRefs.current.length !== results.length) {
|
if (rowRefs.current.length !== results.length) {
|
||||||
rowRefs.current = Array(results.length).fill(null);
|
rowRefs.current = Array(results.length).fill(null);
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
|
||||||
if (results.length === 0) {
|
|
||||||
setKeyboardIndex(-1);
|
|
||||||
handleMouseLeave();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const clamped = selectedIndex >= 0 ? Math.min(selectedIndex, results.length - 1) : 0;
|
|
||||||
setKeyboardIndex(clamped);
|
|
||||||
}, [handleMouseLeave, results.length, selectedIndex, setKeyboardIndex]);
|
|
||||||
useEffect(() => {
|
|
||||||
handleMouseLeave();
|
|
||||||
}, [handleMouseLeave, results.length]);
|
|
||||||
useQuickSwitcherKeyboardHandling(isOpen, isMobile, inputRef, query);
|
useQuickSwitcherKeyboardHandling(isOpen, isMobile, inputRef, query);
|
||||||
useQuickSwitcherInputFocus(isOpen, isMobile, undefined, inputRef);
|
useQuickSwitcherInputFocus(isOpen, isMobile, undefined, inputRef);
|
||||||
useEffect(() => {
|
const escapeIntentRef = useQuickSwitcherEscapeIntent(isOpen && !isMobile);
|
||||||
if (!isOpen || isMobile) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
shouldScrollToSelection.current = true;
|
|
||||||
QuickSwitcherCommands.moveSelection(event.key === 'ArrowDown' ? 'down' : 'up');
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [isMobile, isOpen]);
|
|
||||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
QuickSwitcherCommands.search(event.target.value);
|
QuickSwitcherCommands.search(event.target.value);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -347,22 +309,18 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
if (isIMEComposing(event)) {
|
if (isIMEComposing(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setIsKeyboardNavigating(true);
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
shouldScrollToSelection.current = true;
|
|
||||||
QuickSwitcherCommands.moveSelection(event.key === 'ArrowDown' ? 'down' : 'up');
|
QuickSwitcherCommands.moveSelection(event.key === 'ArrowDown' ? 'down' : 'up');
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await QuickSwitcherCommands.confirmSelection();
|
await QuickSwitcherCommands.confirmSelection();
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
|
||||||
event.preventDefault();
|
|
||||||
QuickSwitcherCommands.hide();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -371,29 +329,41 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
if (LayerManager.hasType('contextmenu')) {
|
if (LayerManager.hasType('contextmenu')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QuickSwitcherCommands.hide();
|
dismissQuickSwitcher({isEscape: consumeEscapeIntent(escapeIntentRef)});
|
||||||
|
}, [escapeIntentRef]);
|
||||||
|
const handlePointerMove = useCallback(() => {
|
||||||
|
setIsKeyboardNavigating(false);
|
||||||
}, []);
|
}, []);
|
||||||
const handleHover = useCallback(
|
const handleHover = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
shouldScrollToSelection.current = false;
|
if (isKeyboardNavigating) {
|
||||||
handleHoverIndex(index);
|
return;
|
||||||
|
}
|
||||||
|
QuickSwitcherCommands.select(index);
|
||||||
},
|
},
|
||||||
[handleHoverIndex],
|
[isKeyboardNavigating],
|
||||||
);
|
);
|
||||||
const handleConfirm = useCallback((result: QuickSwitcherExecutableResult) => {
|
const handleConfirm = useCallback((result: QuickSwitcherExecutableResult) => {
|
||||||
void QuickSwitcherCommands.switchTo(result);
|
void QuickSwitcherCommands.switchTo(result);
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldScrollToSelection.current || keyboardFocusIndex < 0) {
|
const scroller = scrollerRef.current;
|
||||||
shouldScrollToSelection.current = false;
|
if (!scroller) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
shouldScrollToSelection.current = false;
|
if (previousQueryRef.current !== query) {
|
||||||
const node = rowRefs.current[keyboardFocusIndex];
|
previousQueryRef.current = query;
|
||||||
if (node) {
|
scroller.scrollTo({to: 0});
|
||||||
scrollerRef.current?.revealElement({node: node as HTMLElement, padding: 32});
|
return;
|
||||||
}
|
}
|
||||||
}, [keyboardFocusIndex]);
|
if (!isKeyboardNavigating || selectedIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = rowRefs.current[selectedIndex];
|
||||||
|
if (node) {
|
||||||
|
scroller.revealElement({node, padding: SELECTED_ROW_SCROLL_PADDING});
|
||||||
|
}
|
||||||
|
}, [isKeyboardNavigating, query, results, selectedIndex]);
|
||||||
const sections = useMemo(() => createSections(results), [results]);
|
const sections = useMemo(() => createSections(results), [results]);
|
||||||
const selectableIndices = useMemo(
|
const selectableIndices = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -405,19 +375,20 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
}, []),
|
}, []),
|
||||||
[results],
|
[results],
|
||||||
);
|
);
|
||||||
const selectedOptionPosition = keyboardFocusIndex >= 0 ? selectableIndices.indexOf(keyboardFocusIndex) + 1 : 0;
|
const selectedOptionPosition = selectedIndex >= 0 ? selectableIndices.indexOf(selectedIndex) + 1 : 0;
|
||||||
|
const hasSearchIntent = query.trim().length > 0;
|
||||||
const resultCount = useMemo(
|
const resultCount = useMemo(
|
||||||
() => results.filter((result) => result.type !== QuickSwitcherResultTypes.HEADER).length,
|
() => results.filter((result) => result.type !== QuickSwitcherResultTypes.HEADER).length,
|
||||||
[results],
|
[results],
|
||||||
);
|
);
|
||||||
const activeDescendant =
|
const activeDescendant =
|
||||||
keyboardFocusIndex >= 0 && results[keyboardFocusIndex]?.type !== QuickSwitcherResultTypes.HEADER
|
selectedIndex >= 0 && results[selectedIndex]?.type !== QuickSwitcherResultTypes.HEADER
|
||||||
? getQuickSwitcherOptionId(listboxId, keyboardFocusIndex)
|
? getQuickSwitcherOptionId(listboxId, selectedIndex)
|
||||||
: undefined;
|
: undefined;
|
||||||
const activeSuggestionStatus = useMemo(() => {
|
const activeSuggestionStatus = useMemo(() => {
|
||||||
const result = results[keyboardFocusIndex];
|
const result = results[selectedIndex];
|
||||||
if (!result || result.type === QuickSwitcherResultTypes.HEADER) {
|
if (!result || result.type === QuickSwitcherResultTypes.HEADER) {
|
||||||
return resultCount === 0 ? i18n._(NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR) : '';
|
return resultCount === 0 && hasSearchIntent ? i18n._(NO_AUTOCOMPLETE_SUGGESTION_DESCRIPTOR) : '';
|
||||||
}
|
}
|
||||||
const label = getQuickSwitcherResultAccessibilityMetadata(result as QuickSwitcherExecutableResult, i18n).label;
|
const label = getQuickSwitcherResultAccessibilityMetadata(result as QuickSwitcherExecutableResult, i18n).label;
|
||||||
const trimmedQuery = query.trim();
|
const trimmedQuery = query.trim();
|
||||||
@@ -429,11 +400,13 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
return selectedOptionPosition > 0
|
return selectedOptionPosition > 0
|
||||||
? i18n._(AUTOCOMPLETE_SUGGESTION_OF_DESCRIPTOR, {label, selectedOptionPosition, resultCount})
|
? i18n._(AUTOCOMPLETE_SUGGESTION_OF_DESCRIPTOR, {label, selectedOptionPosition, resultCount})
|
||||||
: i18n._(AUTOCOMPLETE_SUGGESTION_DESCRIPTOR, {label});
|
: i18n._(AUTOCOMPLETE_SUGGESTION_DESCRIPTOR, {label});
|
||||||
}, [i18n.locale, keyboardFocusIndex, query, resultCount, results, selectedOptionPosition]);
|
}, [hasSearchIntent, i18n.locale, query, resultCount, results, selectedIndex, selectedOptionPosition]);
|
||||||
const resultStatus = QuickSwitcher.isLoadingMemberResults
|
const resultStatus = QuickSwitcher.isLoadingMemberResults
|
||||||
? i18n._(SEARCHING_PEOPLE_DESCRIPTOR)
|
? i18n._(SEARCHING_PEOPLE_DESCRIPTOR)
|
||||||
: resultCount === 0
|
: resultCount === 0
|
||||||
? i18n._(NO_MATCHES_FOUND_DESCRIPTOR)
|
? hasSearchIntent
|
||||||
|
? i18n._(NO_MATCHES_FOUND_DESCRIPTOR)
|
||||||
|
: ''
|
||||||
: resultCount === 1
|
: resultCount === 1
|
||||||
? i18n._(MESSAGE_1_RESULT_AVAILABLE_DESCRIPTOR)
|
? i18n._(MESSAGE_1_RESULT_AVAILABLE_DESCRIPTOR)
|
||||||
: i18n._(RESULTS_AVAILABLE_DESCRIPTOR, {resultCount});
|
: i18n._(RESULTS_AVAILABLE_DESCRIPTOR, {resultCount});
|
||||||
@@ -455,7 +428,7 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
className={quickStyles.modalRoot}
|
className={quickStyles.modalRoot}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
initialFocusRef={inputRef}
|
initialFocusRef={inputRef}
|
||||||
transitionPreset="instant"
|
transitionPreset="quick"
|
||||||
data-quick-switcher-modal="true"
|
data-quick-switcher-modal="true"
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.modal-root"
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.modal-root"
|
||||||
>
|
>
|
||||||
@@ -463,8 +436,10 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
text={i18n._(QUICK_SWITCHER_DESCRIPTOR)}
|
text={i18n._(QUICK_SWITCHER_DESCRIPTOR)}
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.modal-screen-reader-label"
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.modal-screen-reader-label"
|
||||||
/>
|
/>
|
||||||
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: pointer arbitration surface for the result rows; it carries no affordance of its own. */}
|
||||||
<div
|
<div
|
||||||
className={quickStyles.container}
|
className={quickStyles.container}
|
||||||
|
onMouseMove={handlePointerMove}
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div"
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -522,86 +497,86 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
>
|
>
|
||||||
{activeSuggestionStatus}
|
{activeSuggestionStatus}
|
||||||
</div>
|
</div>
|
||||||
<Scroller
|
{results.length === 0 && !hasSearchIntent ? null : (
|
||||||
ref={scrollerRef}
|
<Scroller
|
||||||
className={quickStyles.scrollerContainer}
|
ref={scrollerRef}
|
||||||
overflow="scroll"
|
className={quickStyles.scrollerContainer}
|
||||||
key="quick_switcher-desktop-scroller"
|
overflow="scroll"
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.scroller"
|
key="quick_switcher-desktop-scroller"
|
||||||
>
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.scroller"
|
||||||
{results.length === 0 ? (
|
>
|
||||||
<div
|
{results.length === 0 ? (
|
||||||
className={quickStyles.emptyState}
|
|
||||||
role="status"
|
|
||||||
aria-live="polite"
|
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.status--3"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={quickStyles.emptyStateTitle}
|
className={quickStyles.emptyState}
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--5"
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.status--3"
|
||||||
>
|
>
|
||||||
{i18n._(NO_MATCHES_FOUND_DESCRIPTOR)}
|
<div
|
||||||
|
className={quickStyles.emptyStateTitle}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--5"
|
||||||
|
>
|
||||||
|
{i18n._(NO_MATCHES_FOUND_DESCRIPTOR)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={quickStyles.emptyStateHint}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--6"
|
||||||
|
>
|
||||||
|
{i18n._(TRY_A_DIFFERENT_NAME_OR_USE_PREFIXES_TO_DESCRIPTOR)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={quickStyles.emptyStateHint}
|
id={listboxId}
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--6"
|
role="listbox"
|
||||||
|
aria-label={i18n._(QUICK_SWITCHER_RESULTS_DESCRIPTOR)}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.listbox"
|
||||||
>
|
>
|
||||||
{i18n._(TRY_A_DIFFERENT_NAME_OR_USE_PREFIXES_TO_DESCRIPTOR)}
|
{sections.map((section: QuickSwitcherSection) => {
|
||||||
|
const sectionHeaderId = section.header ? `${listboxId}-${section.key}` : undefined;
|
||||||
|
const sectionAriaProps = sectionHeaderId
|
||||||
|
? ({role: 'group', 'aria-labelledby': sectionHeaderId} as const)
|
||||||
|
: ({role: 'presentation'} as const);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={section.key}
|
||||||
|
className={quickStyles.section}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--7"
|
||||||
|
{...sectionAriaProps}
|
||||||
|
>
|
||||||
|
{section.header && (
|
||||||
|
<div
|
||||||
|
id={sectionHeaderId}
|
||||||
|
className={quickStyles.sectionHeader}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--8"
|
||||||
|
>
|
||||||
|
{section.header.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{section.rows.map(({result, index}: {result: QuickSwitcherExecutableResult; index: number}) => (
|
||||||
|
<ResultRow
|
||||||
|
key={getResultKey(result)}
|
||||||
|
result={result}
|
||||||
|
index={index}
|
||||||
|
isKeyboardSelected={index === selectedIndex}
|
||||||
|
onHover={handleHover}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
optionId={getQuickSwitcherOptionId(listboxId, index)}
|
||||||
|
positionInSet={selectableIndices.indexOf(index) + 1}
|
||||||
|
setSize={resultCount}
|
||||||
|
innerRef={(node) => {
|
||||||
|
rowRefs.current[index] = node;
|
||||||
|
}}
|
||||||
|
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.result-row"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</Scroller>
|
||||||
<div
|
)}
|
||||||
id={listboxId}
|
|
||||||
role="listbox"
|
|
||||||
aria-label={i18n._(QUICK_SWITCHER_RESULTS_DESCRIPTOR)}
|
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.listbox"
|
|
||||||
>
|
|
||||||
{sections.map((section: QuickSwitcherSection, sidx: number) => {
|
|
||||||
const sectionHeaderId = section.header ? `${listboxId}-section-${sidx}` : undefined;
|
|
||||||
const sectionAriaProps = sectionHeaderId
|
|
||||||
? ({role: 'group', 'aria-labelledby': sectionHeaderId} as const)
|
|
||||||
: ({role: 'presentation'} as const);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`section-${sidx}`}
|
|
||||||
className={quickStyles.section}
|
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--7"
|
|
||||||
{...sectionAriaProps}
|
|
||||||
>
|
|
||||||
{section.header && (
|
|
||||||
<div
|
|
||||||
id={sectionHeaderId}
|
|
||||||
className={quickStyles.sectionHeader}
|
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.div--8"
|
|
||||||
>
|
|
||||||
{section.header.title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{section.rows.map(({result, index}: {result: QuickSwitcherExecutableResult; index: number}) => (
|
|
||||||
<ResultRow
|
|
||||||
key={getResultKey(result)}
|
|
||||||
result={result}
|
|
||||||
index={index}
|
|
||||||
isKeyboardSelected={index === keyboardFocusIndex}
|
|
||||||
isHovered={index === hoverIndexForRender}
|
|
||||||
onHover={handleHover}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
onConfirm={handleConfirm}
|
|
||||||
optionId={getQuickSwitcherOptionId(listboxId, index)}
|
|
||||||
positionInSet={selectableIndices.indexOf(index) + 1}
|
|
||||||
setSize={resultCount}
|
|
||||||
innerRef={(node) => {
|
|
||||||
rowRefs.current[index] = node;
|
|
||||||
}}
|
|
||||||
data-flx="search.quick-switcher.quick-switcher-modal.quick-switcher-modal-component.result-row"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Scroller>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={quickStyles.footer}
|
className={quickStyles.footer}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
export interface EscapeIntent {
|
||||||
|
current: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function trackEscapeIntentUntilNextTask(intent: EscapeIntent): () => void {
|
||||||
|
const recordEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Escape') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
intent.current = true;
|
||||||
|
window.setTimeout(() => {
|
||||||
|
intent.current = false;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', recordEscape, {capture: true});
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', recordEscape, {capture: true});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeEscapeIntent(intent: EscapeIntent): boolean {
|
||||||
|
const wasEscape = intent.current;
|
||||||
|
intent.current = false;
|
||||||
|
return wasEscape;
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import {shouldDisableAutofocusOnMobile} from '@app/features/platform/utils/Autof
|
|||||||
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
|
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
|
||||||
import ReadStates from '@app/features/read_state/state/ReadStates';
|
import ReadStates from '@app/features/read_state/state/ReadStates';
|
||||||
import * as QuickSwitcherCommands from '@app/features/search/commands/QuickSwitcherCommands';
|
import * as QuickSwitcherCommands from '@app/features/search/commands/QuickSwitcherCommands';
|
||||||
|
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
|
||||||
|
import {type EscapeIntent, trackEscapeIntentUntilNextTask} from '@app/features/search/state/QuickSwitcherEscapeIntent';
|
||||||
import type {
|
import type {
|
||||||
GroupDMResult,
|
GroupDMResult,
|
||||||
GuildResult,
|
GuildResult,
|
||||||
@@ -38,7 +40,7 @@ import {msg} from '@lingui/core/macro';
|
|||||||
import {ArrowRightIcon, HashIcon, HouseIcon, SpeakerHighIcon, StarIcon, UsersIcon} from '@phosphor-icons/react';
|
import {ArrowRightIcon, HashIcon, HouseIcon, SpeakerHighIcon, StarIcon, UsersIcon} from '@phosphor-icons/react';
|
||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import {useEffect, useLayoutEffect} from 'react';
|
import {useEffect, useLayoutEffect, useRef} from 'react';
|
||||||
|
|
||||||
const SEARCH_DESCRIPTOR = msg({
|
const SEARCH_DESCRIPTOR = msg({
|
||||||
message: 'Search',
|
message: 'Search',
|
||||||
@@ -75,6 +77,7 @@ const UNREAD_DESCRIPTOR = msg({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export interface QuickSwitcherSection {
|
export interface QuickSwitcherSection {
|
||||||
|
key: string;
|
||||||
header?: HeaderResult;
|
header?: HeaderResult;
|
||||||
rows: Array<{result: QuickSwitcherExecutableResult; index: number}>;
|
rows: Array<{result: QuickSwitcherExecutableResult; index: number}>;
|
||||||
}
|
}
|
||||||
@@ -415,12 +418,13 @@ export function createSections(results: Array<QuickSwitcherResult>): Array<Quick
|
|||||||
let current: QuickSwitcherSection | null = null;
|
let current: QuickSwitcherSection | null = null;
|
||||||
results.forEach((r, index) => {
|
results.forEach((r, index) => {
|
||||||
if (r.type === QuickSwitcherResultTypes.HEADER) {
|
if (r.type === QuickSwitcherResultTypes.HEADER) {
|
||||||
current = {header: r as HeaderResult, rows: []};
|
const header = r as HeaderResult;
|
||||||
|
current = {key: `header-${header.id}`, header, rows: []};
|
||||||
acc.push(current);
|
acc.push(current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!current) {
|
if (!current) {
|
||||||
current = {rows: []};
|
current = {key: 'leading', rows: []};
|
||||||
acc.push(current);
|
acc.push(current);
|
||||||
}
|
}
|
||||||
current.rows.push({result: r as QuickSwitcherExecutableResult, index});
|
current.rows.push({result: r as QuickSwitcherExecutableResult, index});
|
||||||
@@ -488,6 +492,25 @@ export function useQuickSwitcherKeyboardHandling(
|
|||||||
}, [isMobile, isOpen, query, inputRef]);
|
}, [isMobile, isOpen, query, inputRef]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dismissQuickSwitcher({isEscape}: {isEscape: boolean}): void {
|
||||||
|
if (isEscape && QuickSwitcher.query.length > 0) {
|
||||||
|
QuickSwitcherCommands.search('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QuickSwitcherCommands.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useQuickSwitcherEscapeIntent(isActive: boolean): EscapeIntent {
|
||||||
|
const escapeIntentRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return trackEscapeIntentUntilNextTask(escapeIntentRef);
|
||||||
|
}, [isActive]);
|
||||||
|
return escapeIntentRef;
|
||||||
|
}
|
||||||
|
|
||||||
export function useQuickSwitcherInputFocus(
|
export function useQuickSwitcherInputFocus(
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
@@ -500,7 +523,7 @@ export function useQuickSwitcherInputFocus(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = QuickSwitcherCommands.getModalKey();
|
const key = QuickSwitcherCommands.getModalKey();
|
||||||
LayerManager.addLayer('modal', key, () => QuickSwitcherCommands.hide());
|
LayerManager.addLayer('modal', key, () => dismissQuickSwitcher({isEscape: true}));
|
||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
inputRef?.current?.focus();
|
inputRef?.current?.focus();
|
||||||
inputRef?.current?.select();
|
inputRef?.current?.select();
|
||||||
|
|||||||
Reference in new issue
Block a user