mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 02:46:29 +00:00
fix(app): keep command suggestions visible as the composer grows (#7910)
Long command descriptions exhausted the available space above a tall draft, leaving no complete selectable row. Bound the preview and reserve list space. Let the popover own its composer anchor so layout changes refresh its position while suggestions remain open.
This commit is contained in:
@@ -5,9 +5,9 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
type ReactElement,
|
||||
type RefObject,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import { View, useWindowDimensions, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { Portal } from "@gorhom/portal";
|
||||
import Animated, { useAnimatedReaction, useAnimatedStyle } from "react-native-reanimated";
|
||||
import { scheduleOnRN } from "react-native-worklets";
|
||||
@@ -32,7 +32,8 @@ interface RelativeAnchorRect {
|
||||
|
||||
interface AutocompletePopoverProps {
|
||||
visible: boolean;
|
||||
anchorRef: RefObject<View | null>;
|
||||
children: ReactNode;
|
||||
anchorStyle?: StyleProp<ViewStyle>;
|
||||
options: readonly AutocompleteOption[];
|
||||
selectedIndex: number;
|
||||
onSelect: (option: AutocompleteOption) => void;
|
||||
@@ -44,7 +45,8 @@ interface AutocompletePopoverProps {
|
||||
|
||||
export function AutocompletePopover({
|
||||
visible,
|
||||
anchorRef,
|
||||
children,
|
||||
anchorStyle,
|
||||
options,
|
||||
selectedIndex,
|
||||
onSelect,
|
||||
@@ -55,6 +57,7 @@ export function AutocompletePopover({
|
||||
}: AutocompletePopoverProps): ReactElement | null {
|
||||
"use no memo";
|
||||
// React Compiler memoizes effect captures by reading SharedValue.value during render.
|
||||
const anchorRef = useRef<View>(null);
|
||||
const [relativeAnchorRect, setRelativeAnchorRect] = useState<RelativeAnchorRect | null>(null);
|
||||
const windowDimensions = useWindowDimensions();
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
@@ -137,25 +140,29 @@ export function AutocompletePopover({
|
||||
[anchorY, reservedLift, safeAreaInsets.top],
|
||||
);
|
||||
|
||||
if (!visible || !relativeAnchorRect || !baseStyle) return null;
|
||||
if (options.length > 0 && selectedIndex < 0) return null;
|
||||
const showPopover = canMeasure && relativeAnchorRect && baseStyle;
|
||||
|
||||
// The translate view is the popover's own frame, so it lifts only the popover.
|
||||
return (
|
||||
<Portal hostName={portalHostName}>
|
||||
<KeyboardTranslateView {...anchor.translation} style={baseStyle}>
|
||||
<Animated.View testID="composer-autocomplete-popover" style={heightStyle}>
|
||||
<Autocomplete
|
||||
options={options}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelect={onSelect}
|
||||
isLoading={isLoading}
|
||||
errorMessage={errorMessage}
|
||||
loadingText={loadingText}
|
||||
emptyText={emptyText}
|
||||
/>
|
||||
</Animated.View>
|
||||
</KeyboardTranslateView>
|
||||
</Portal>
|
||||
<View ref={anchorRef} onLayout={remeasure} style={anchorStyle} collapsable={false}>
|
||||
{children}
|
||||
{showPopover ? (
|
||||
<Portal hostName={portalHostName}>
|
||||
<KeyboardTranslateView {...anchor.translation} style={baseStyle}>
|
||||
<Animated.View testID="composer-autocomplete-popover" style={heightStyle}>
|
||||
<Autocomplete
|
||||
options={options}
|
||||
selectedIndex={selectedIndex}
|
||||
onSelect={onSelect}
|
||||
isLoading={isLoading}
|
||||
errorMessage={errorMessage}
|
||||
loadingText={loadingText}
|
||||
emptyText={emptyText}
|
||||
/>
|
||||
</Animated.View>
|
||||
</KeyboardTranslateView>
|
||||
</Portal>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,14 +247,16 @@ export function Autocomplete({
|
||||
<View style={styles.outerWrapper}>
|
||||
{selectedOption?.kind === "command" && selectedOption.description ? (
|
||||
<View style={styles.detailCard}>
|
||||
<Text style={styles.detailLabel}>
|
||||
<Text style={styles.detailLabel} numberOfLines={1}>
|
||||
{removeBoltGlyphs(selectedOption.label) ?? selectedOption.label}
|
||||
</Text>
|
||||
<Text style={styles.detailDescription}>
|
||||
<Text style={styles.detailDescription} numberOfLines={2} ellipsizeMode="tail">
|
||||
{removeBoltGlyphs(selectedOption.description)}
|
||||
</Text>
|
||||
{selectedOption.detail ? (
|
||||
<Text style={styles.detailHint}>{removeBoltGlyphs(selectedOption.detail)}</Text>
|
||||
<Text style={styles.detailHint} numberOfLines={1}>
|
||||
{removeBoltGlyphs(selectedOption.detail)}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
@@ -293,6 +295,9 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
detailCard: {
|
||||
flexShrink: 1,
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.colors.surface1,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.borderAccent,
|
||||
@@ -318,7 +323,8 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
||||
},
|
||||
container: {
|
||||
flexShrink: 1,
|
||||
minHeight: 0,
|
||||
// Keep a complete selectable row (36), list padding (8), and borders (2).
|
||||
minHeight: 46,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.borderAccent,
|
||||
|
||||
@@ -1003,7 +1003,8 @@ function ComposerAutocompleteBinding({
|
||||
recall,
|
||||
configuration,
|
||||
inputRef,
|
||||
anchorRef,
|
||||
children,
|
||||
anchorStyle,
|
||||
show,
|
||||
ref,
|
||||
}: {
|
||||
@@ -1015,7 +1016,8 @@ function ComposerAutocompleteBinding({
|
||||
"userInput" | "cursorIndex" | "onAutocompleteApplied"
|
||||
>;
|
||||
inputRef: React.RefObject<MessageInputRef | null>;
|
||||
anchorRef: React.RefObject<View | null>;
|
||||
children: React.ReactNode;
|
||||
anchorStyle: React.ComponentProps<typeof View>["style"];
|
||||
show: boolean;
|
||||
ref: React.Ref<ComposerAutocompleteHandle>;
|
||||
}) {
|
||||
@@ -1042,7 +1044,7 @@ function ComposerAutocompleteBinding({
|
||||
return (
|
||||
<ComposerAutocomplete
|
||||
visible={autocomplete.isVisible && isShown}
|
||||
anchorRef={anchorRef}
|
||||
anchorStyle={anchorStyle}
|
||||
options={autocomplete.options}
|
||||
selectedIndex={autocomplete.selectedIndex}
|
||||
onSelect={onSelect}
|
||||
@@ -1050,7 +1052,9 @@ function ComposerAutocompleteBinding({
|
||||
errorMessage={autocomplete.errorMessage}
|
||||
loadingText={autocomplete.loadingText}
|
||||
emptyText={autocomplete.emptyText}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</ComposerAutocomplete>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2379,7 +2383,6 @@ function ComposerContentImpl({
|
||||
pluginClientSlashCommands,
|
||||
],
|
||||
);
|
||||
const messageInputContainerRef = useRef<View>(null);
|
||||
|
||||
const isSubmitLoadingVisible =
|
||||
isProcessing || isSubmitLoading || isUploadingFile || pendingNativeImagePastes > 0;
|
||||
@@ -2438,17 +2441,16 @@ function ComposerContentImpl({
|
||||
{isFullscreen ? null : queueList}
|
||||
{!isFullscreen && sendErrorNode}
|
||||
|
||||
<View ref={messageInputContainerRef} style={messageInputContainerStyle}>
|
||||
<ComposerAutocompleteBinding
|
||||
text={textSource}
|
||||
cursor={cursor}
|
||||
recall={recall}
|
||||
inputRef={messageInputRef}
|
||||
anchorRef={messageInputContainerRef}
|
||||
show={mode.showAutocomplete && !isFullscreen}
|
||||
ref={autocompleteRef}
|
||||
configuration={autocompleteConfiguration}
|
||||
/>
|
||||
<ComposerAutocompleteBinding
|
||||
text={textSource}
|
||||
cursor={cursor}
|
||||
recall={recall}
|
||||
inputRef={messageInputRef}
|
||||
anchorStyle={messageInputContainerStyle}
|
||||
show={mode.showAutocomplete && !isFullscreen}
|
||||
ref={autocompleteRef}
|
||||
configuration={autocompleteConfiguration}
|
||||
>
|
||||
<ComposerForgeBinding
|
||||
text={textSource}
|
||||
configuration={forgeConfiguration}
|
||||
@@ -2528,7 +2530,7 @@ function ComposerContentImpl({
|
||||
renderOption={renderGithubPickerOption}
|
||||
/>
|
||||
{pluginAttachments.picker}
|
||||
</View>
|
||||
</ComposerAutocompleteBinding>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user