feat(mobile): autocomplete redesign for touch surfaces
Command, file/agent, skill, and snippet autocompletes now stop at the top of the chat area (Capacitor) or the visible viewport edge (mobile browsers, which pan the page for the keyboard) and may grow that far, measured live across keyboard settles and viewport changes. On mobile the keyboard-hint footer and description lines are gone, rows center their icons, and list overscroll no longer bounces the page behind. Selecting a command no longer dismisses the keyboard (its rows now block the tap's focus steal like the composer buttons), and the dead dismissKeyboard option is removed.
This commit is contained in:
@@ -9,6 +9,7 @@ import { Icon } from "@/components/icon/Icon";
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { useMobileAutocompleteMaxHeight } from './useMobileAutocompleteMaxHeight';
|
||||
|
||||
type CommandSource = 'openchamber' | 'opencode' | 'skill';
|
||||
|
||||
@@ -49,7 +50,7 @@ const NEUTRAL_BADGE_CLASS = cn(
|
||||
|
||||
interface CommandAutocompleteProps {
|
||||
searchQuery: string;
|
||||
onCommandSelect: (command: CommandInfo, options?: { dismissKeyboard?: boolean }) => void;
|
||||
onCommandSelect: (command: CommandInfo) => void;
|
||||
onClose: () => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
@@ -81,6 +82,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
const keyboardNavigationRef = React.useRef(false);
|
||||
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
||||
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const mobileMaxHeight = useMobileAutocompleteMaxHeight(containerRef, isMobile);
|
||||
const ignoreClickRef = React.useRef(false);
|
||||
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
||||
const pointerMovedRef = React.useRef(false);
|
||||
@@ -346,9 +348,9 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
||||
style={style}
|
||||
style={mobileMaxHeight !== undefined ? { ...style, maxHeight: mobileMaxHeight } : style}
|
||||
>
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="px-0 pb-2">
|
||||
<ScrollableOverlay preventOverscroll outerClassName="flex-1 min-h-0" className="px-0 pb-2">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Icon name="refresh" className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
@@ -363,9 +365,15 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
key={command.id}
|
||||
ref={(el) => { itemRefs.current[index] = el; }}
|
||||
className={cn(
|
||||
"flex items-start gap-2 px-3 py-2 cursor-pointer rounded-lg",
|
||||
"flex gap-2 px-3 py-2 cursor-pointer rounded-lg",
|
||||
isMobile ? "items-center" : "items-start",
|
||||
index === selectedIndex && "bg-interactive-selection"
|
||||
)}
|
||||
// Block the focus transfer the tap would perform: the textarea
|
||||
// must stay focused so selecting a command doesn't dismiss the
|
||||
// soft keyboard (the blur raced the keyboard-hide trigger and
|
||||
// won against the deferred refocus).
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onPointerDown={(event) => {
|
||||
if (event.pointerType !== 'touch') {
|
||||
return;
|
||||
@@ -396,7 +404,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
ignoreClickRef.current = true;
|
||||
onCommandSelect(command, { dismissKeyboard: true });
|
||||
onCommandSelect(command);
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
pointerStartRef.current = null;
|
||||
@@ -414,7 +422,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
setSelectedIndex(index);
|
||||
}}
|
||||
>
|
||||
<div className="mt-0.5">
|
||||
<div className={cn(!isMobile && "mt-0.5")}>
|
||||
{getCommandIcon(command)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -448,7 +456,7 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{command.description && (
|
||||
{command.description && !isMobile && (
|
||||
<div className="typography-meta text-muted-foreground mt-0.5 truncate">
|
||||
{command.description}
|
||||
</div>
|
||||
@@ -465,9 +473,11 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
</div>
|
||||
)}
|
||||
</ScrollableOverlay>
|
||||
<div className="px-3 pt-1 pb-1.5 border-t typography-meta text-muted-foreground">
|
||||
{t('chat.autocomplete.keyboardHint')}
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="px-3 pt-1 pb-1.5 border-t typography-meta text-muted-foreground">
|
||||
{t('chat.autocomplete.keyboardHint')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user