fix(app): align compact menu sheet content
Route composer attachments through the shared compact menu sheet so headers and rows use the same alignment rails. Base the final-row padding on the device safe area to keep actions clear of system navigation.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getMenuSheetBottomPadding } from "./menu-sheet-layout";
|
||||
|
||||
describe("getMenuSheetBottomPadding", () => {
|
||||
it("keeps breathing room below the last row when there is no system inset", () => {
|
||||
expect(
|
||||
getMenuSheetBottomPadding({
|
||||
safeAreaBottom: 0,
|
||||
breathingRoom: 4,
|
||||
}),
|
||||
).toBe(4);
|
||||
});
|
||||
|
||||
it("adds breathing room after the system navigation inset", () => {
|
||||
expect(
|
||||
getMenuSheetBottomPadding({
|
||||
safeAreaBottom: 48,
|
||||
breathingRoom: 4,
|
||||
}),
|
||||
).toBe(52);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export function getMenuSheetBottomPadding(input: {
|
||||
safeAreaBottom: number;
|
||||
breathingRoom: number;
|
||||
}): number {
|
||||
return input.safeAreaBottom + input.breathingRoom;
|
||||
}
|
||||
@@ -14,16 +14,18 @@ import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { BottomSheetBackdrop, BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
||||
import { ChevronLeft } from "lucide-react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
IsolatedBottomSheetModal,
|
||||
useIsolatedBottomSheetVisibility,
|
||||
type ContextBridge,
|
||||
} from "@/components/ui/isolated-bottom-sheet-modal";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { SPACING, type Theme } from "@/styles/theme";
|
||||
import { useMenuContext, MenuContextProvider } from "./menu-context";
|
||||
import { MenuPage } from "./menu-item";
|
||||
import { currentPageId, isSubPageOpen } from "./menu-navigation";
|
||||
import { AnchoredSurface, MenuOverlay } from "./menu-overlay";
|
||||
import { getMenuSheetBottomPadding } from "./menu-sheet-layout";
|
||||
import type { Alignment, Placement } from "./menu-anchor";
|
||||
|
||||
const ThemedChevronLeft = withUnistyles(ChevronLeft);
|
||||
@@ -305,6 +307,17 @@ function MenuSheetSurface({
|
||||
}: MenuSurfaceProps): ReactElement | null {
|
||||
const menu = useMenuContext("MenuSurface");
|
||||
const { value: surfaceValue } = useSubAnchors();
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
|
||||
const sheetScrollContentStyle = useMemo(
|
||||
() => ({
|
||||
paddingBottom: getMenuSheetBottomPadding({
|
||||
safeAreaBottom: safeAreaInsets.bottom,
|
||||
breathingRoom: SPACING[1],
|
||||
}),
|
||||
}),
|
||||
[safeAreaInsets.bottom],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => menu.setOpen(false), [menu]);
|
||||
const { sheetRef, handleSheetChange, handleSheetDismiss } = useIsolatedBottomSheetVisibility({
|
||||
@@ -356,7 +369,7 @@ function MenuSheetSurface({
|
||||
keyboardBlurBehavior="restore"
|
||||
>
|
||||
<BottomSheetScrollView
|
||||
contentContainerStyle={styles.sheetScrollContent}
|
||||
contentContainerStyle={sheetScrollContentStyle}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID={testID ? `${testID}-content` : undefined}
|
||||
@@ -407,9 +420,6 @@ function MenuSheetHeader({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
sheetScrollContent: {
|
||||
paddingBottom: theme.spacing[6],
|
||||
},
|
||||
sheetHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
Pressable,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
NativeSyntheticEvent,
|
||||
TextInputContentSizeChangeEventData,
|
||||
@@ -48,8 +46,6 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||
import { useDismissKeyboardOnOpen } from "@/components/ui/keyboard-dismiss";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { useIosHardwareKeyboardSubmit } from "@/hooks/use-ios-hardware-keyboard-submit";
|
||||
import { formatShortcut, type ShortcutKey } from "@/utils/format-shortcut";
|
||||
@@ -164,8 +160,6 @@ const MIN_INPUT_HEIGHT_DESKTOP = 46;
|
||||
const DEFAULT_MAX_INPUT_HEIGHT = 160;
|
||||
const MAX_INPUT_VIEWPORT_RATIO = 0.5;
|
||||
const MIN_INPUT_HEIGHT = isWeb ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE;
|
||||
const ATTACHMENT_SHEET_SNAP_POINTS = ["34%", "45%"];
|
||||
|
||||
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
|
||||
TextInputKeyPressEventData & {
|
||||
metaKey?: boolean;
|
||||
@@ -225,55 +219,6 @@ function AttachmentMenuList({ items }: { items: AttachmentMenuItem[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentSheetItem({
|
||||
item,
|
||||
onSelect,
|
||||
}: {
|
||||
item: AttachmentMenuItem;
|
||||
onSelect: (item: AttachmentMenuItem) => void;
|
||||
}) {
|
||||
const handlePress = useCallback(() => {
|
||||
onSelect(item);
|
||||
}, [item, onSelect]);
|
||||
const pressableStyle = useCallback(
|
||||
({ pressed }: { pressed: boolean }) => [
|
||||
styles.attachmentSheetItem,
|
||||
pressed && styles.attachmentSheetItemPressed,
|
||||
item.disabled && styles.buttonDisabled,
|
||||
],
|
||||
[item.disabled],
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID={`message-input-attachment-menu-item-${item.id}`}
|
||||
accessibilityRole="button"
|
||||
disabled={item.disabled}
|
||||
onPress={handlePress}
|
||||
style={pressableStyle}
|
||||
>
|
||||
{item.icon ? <View style={styles.attachmentSheetItemIcon}>{item.icon}</View> : null}
|
||||
<Text style={styles.attachmentSheetItemText}>{item.label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentSheetList({
|
||||
items,
|
||||
onSelect,
|
||||
}: {
|
||||
items: AttachmentMenuItem[];
|
||||
onSelect: (item: AttachmentMenuItem) => void;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.attachmentSheetList}>
|
||||
{items.map((item) => (
|
||||
<AttachmentSheetItem key={item.id} item={item} onSelect={onSelect} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function AttachmentDropdown({
|
||||
isConnected,
|
||||
disabled,
|
||||
@@ -289,73 +234,9 @@ function AttachmentDropdown({
|
||||
attachmentMenuItems: AttachmentMenuItem[];
|
||||
addAttachmentLabel: string;
|
||||
}) {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||
useDismissKeyboardOnOpen(isSheetOpen, isCompact);
|
||||
|
||||
const isButtonDisabled = !isConnected || disabled;
|
||||
const attachmentSheetHeader = useMemo<SheetHeader>(
|
||||
() => ({ title: addAttachmentLabel }),
|
||||
[addAttachmentLabel],
|
||||
);
|
||||
const handleOpenSheet = useCallback(() => {
|
||||
if (isButtonDisabled) return;
|
||||
setIsSheetOpen(true);
|
||||
}, [isButtonDisabled]);
|
||||
const handleCloseSheet = useCallback(() => {
|
||||
setIsSheetOpen(false);
|
||||
}, []);
|
||||
const handleSheetItemSelect = useCallback((item: AttachmentMenuItem) => {
|
||||
if (item.disabled) return;
|
||||
setIsSheetOpen(false);
|
||||
if (Platform.OS === "ios") {
|
||||
setTimeout(item.onSelect, 250);
|
||||
return;
|
||||
}
|
||||
item.onSelect();
|
||||
}, []);
|
||||
const mobileAttachButtonStyle = useCallback(
|
||||
(state: { pressed: boolean; hovered?: boolean }) => {
|
||||
if (typeof attachButtonStyle === "function") {
|
||||
return attachButtonStyle({ ...state, hovered: Boolean(state.hovered), open: isSheetOpen });
|
||||
}
|
||||
return attachButtonStyle;
|
||||
},
|
||||
[attachButtonStyle, isSheetOpen],
|
||||
);
|
||||
const renderMobileAttachButtonIcon = useCallback(
|
||||
({ hovered }: { hovered?: boolean }) => renderAttachButtonIcon({ hovered }),
|
||||
[renderAttachButtonIcon],
|
||||
);
|
||||
|
||||
if (isCompact) {
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
disabled={isButtonDisabled}
|
||||
accessibilityLabel={addAttachmentLabel}
|
||||
accessibilityRole="button"
|
||||
testID="message-input-attach-button"
|
||||
onPress={handleOpenSheet}
|
||||
style={mobileAttachButtonStyle}
|
||||
>
|
||||
{renderMobileAttachButtonIcon}
|
||||
</Pressable>
|
||||
<AdaptiveModalSheet
|
||||
header={attachmentSheetHeader}
|
||||
visible={isSheetOpen}
|
||||
onClose={handleCloseSheet}
|
||||
snapPoints={ATTACHMENT_SHEET_SNAP_POINTS}
|
||||
testID="message-input-attachment-menu"
|
||||
>
|
||||
<AttachmentSheetList items={attachmentMenuItems} onSelect={handleSheetItemSelect} />
|
||||
</AdaptiveModalSheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu compactMode="sheet">
|
||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger
|
||||
@@ -378,6 +259,7 @@ function AttachmentDropdown({
|
||||
offset={8}
|
||||
minWidth={220}
|
||||
testID="message-input-attachment-menu"
|
||||
sheetTitle={addAttachmentLabel}
|
||||
>
|
||||
<AttachmentMenuList items={attachmentMenuItems} />
|
||||
</DropdownMenuContent>
|
||||
@@ -1941,31 +1823,6 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
attachmentSheetList: {
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
attachmentSheetItem: {
|
||||
minHeight: 44,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
},
|
||||
attachmentSheetItemPressed: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
attachmentSheetItemIcon: {
|
||||
width: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
attachmentSheetItemText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
overlayContainer: {
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
|
||||
@@ -1359,7 +1359,9 @@ function useNewWorkspaceFormStack(input: NewWorkspaceFormStackInput): ReactEleme
|
||||
style={badgePressableStyle}
|
||||
testID="host-picker-trigger"
|
||||
>
|
||||
<HostStatusDot serverId={host.selectedServerId} />
|
||||
<View style={styles.badgeIconBox}>
|
||||
<HostStatusDot serverId={host.selectedServerId} />
|
||||
</View>
|
||||
<Text style={styles.badgeText} numberOfLines={1}>
|
||||
{selectedHostLabel}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user