Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion apps/mobile/src/components/context-menu/context-menu.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,36 @@ import type { MenuAction } from "@expo/ui/community/menu"
import { Button, ContextMenu as SwiftUIContextMenu, Group, Host, RNHostView, Section } from "@expo/ui/swift-ui"
import type { ButtonProps } from "@expo/ui/swift-ui"
import { contentShape, shapes } from "@expo/ui/swift-ui/modifiers"
import { FillWidth } from "./fill-width"
import type { ContextMenuProps } from "./types"

export type { ContextMenuAction, ContextMenuProps } from "./types"

export function ContextMenu(props: ContextMenuProps) {
const { children, actions, mode = "longPress", title, previewBorderRadius, style, disabled = false, onPressAction } = props
const { children, actions, style, fillWidth = false, disabled = false } = props

if (disabled || actions.length === 0) {
return style ? <View style={style}>{children}</View> : <>{children}</>
}

if (fillWidth) {
return (
<FillWidth style={style}>
{(width) => (
<NativeMenu {...props} style={undefined}>
<View style={{ width }}>{children}</View>
</NativeMenu>
)}
</FillWidth>
)
}

return <NativeMenu {...props} />
}

function NativeMenu(props: ContextMenuProps) {
const { children, actions, mode = "longPress", title, previewBorderRadius, style, onPressAction } = props

if (mode === "tap") {
const menuActions: MenuAction[] = actions.map((action, index) => ({
id: String(index),
Expand Down
19 changes: 18 additions & 1 deletion apps/mobile/src/components/context-menu/context-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,35 @@ import React from "react"
import { View } from "react-native"
import { MenuView } from "@expo/ui/community/menu"
import type { MenuAction } from "@expo/ui/community/menu"
import { FillWidth } from "./fill-width"
import type { ContextMenuProps } from "./types"

export type { ContextMenuAction, ContextMenuProps } from "./types"

// Non-iOS: tap menus render a Material dropdown, long press is left to the caller.
export function ContextMenu(props: ContextMenuProps) {
const { children, actions, mode = "longPress", style, disabled = false, onPressAction } = props
const { children, actions, mode = "longPress", style, fillWidth = false, disabled = false } = props

if (disabled || mode !== "tap" || actions.length === 0) {
return style ? <View style={style}>{children}</View> : <>{children}</>
}

if (fillWidth) {
return (
<FillWidth style={style}>
{(width) => (
<NativeMenu {...props} style={undefined}>
<View style={{ width }}>{children}</View>
</NativeMenu>
)}
</FillWidth>
)
}

return <NativeMenu {...props} />
}

function NativeMenu({ children, actions, style, onPressAction }: ContextMenuProps) {
const menuActions: MenuAction[] = actions.map((action, index) => ({
id: String(index),
title: action.title,
Expand Down
27 changes: 27 additions & 0 deletions apps/mobile/src/components/context-menu/fill-width.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useLayoutEffect, useRef, useState } from "react"
import { View, type StyleProp, type ViewStyle } from "react-native"

interface FillWidthProps {
style?: StyleProp<ViewStyle>
children: (width: number | undefined) => React.ReactNode
}

// Since @expo/ui 57.0.15 a matchContents RNHostView lays its RN child out at the child's own width, with no
// parent width to stretch to, so auto-width content shrinks to its content and sits at the leading edge.
// We measure the container and hand the hosted child an explicit width, as upstream does for bottom-sheet.
// Drop this once RNHostView supports per-axis matchContents (Host already does).
export function FillWidth({ style, children }: FillWidthProps) {
const ref = useRef<View>(null)
const [width, setWidth] = useState<number>()

// Measured before paint, so the content doesn't flash at its narrow width
useLayoutEffect(() => {
setWidth(ref.current?.getBoundingClientRect().width || undefined)
}, [])

return (
<View ref={ref} style={style} onLayout={(event) => setWidth(event.nativeEvent.layout.width)}>
{children(width)}
</View>
)
}
2 changes: 2 additions & 0 deletions apps/mobile/src/components/context-menu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface ContextMenuProps {
/** Corner radius of the long-press preview */
previewBorderRadius?: number
style?: StyleProp<ViewStyle>
/** Stretches the content to the available width; native menu hosts otherwise size it to its content */
fillWidth?: boolean
disabled?: boolean
/** Runs before the selected action's `onPress` */
onPressAction?: () => void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function FavoriteRouteBox(props: FavoriteRouteBoxProps) {
]}
previewBorderRadius={12}
style={styles.contextMenu}
fillWidth
>
<TouchableScale
testID={`favorite-route-${id}`}
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/components/route-card/route-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export function RouteCard(props: RouteCardProps) {

return (
// The native menu host drops the card's own margins
<ContextMenu actions={generatedContextMenuActions} previewBorderRadius={12} style={style} disabled={IS_E2E}>
<ContextMenu actions={generatedContextMenuActions} previewBorderRadius={12} style={style} fillWidth disabled={IS_E2E}>
{cardContent}
</ContextMenu>
)
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/screens/route-list/fares-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function FaresScreen() {
{/* A native dropdown menu, so a Touchable child would swallow the tap — plain views only. */}
<ContextMenu
style={styles.profileMenu}
fillWidth
mode="tap"
title={passengerProfileLabel}
disabled={profileList.length === 0}
Expand Down
Loading