From 1a3ee37657fe76f0c23e835b27b445e603fc18c1 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 27 May 2026 15:24:05 +0200 Subject: [PATCH] fix(ios): repair PlatformDropdown sizing, tap-to-open, and option text on SDK 55 - Measure the trigger's intrinsic size in RN and pin it on the @expo/ui Host; SDK 55 Host fills available space by default and matchContents reports the native Menu's size, so neither sized the dropdown correctly. - Swap ContextMenu (long-press) for Menu (tap-to-open). - Render native Button/Picker items via the string `label` prop / SwiftUIText instead of RN children, which rendered invisibly inside SwiftUI. - Key/tag Picker options by index to avoid duplicate "[object Object]" keys from object-valued options (bitrate, media source). --- components/PlatformDropdown.tsx | 98 +++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/components/PlatformDropdown.tsx b/components/PlatformDropdown.tsx index 15b1cc17..b9c006a6 100644 --- a/components/PlatformDropdown.tsx +++ b/components/PlatformDropdown.tsx @@ -1,15 +1,21 @@ import { Button, - ContextMenu, Host, + Menu, Picker, Text as SwiftUIText, } from "@expo/ui/swift-ui"; import { disabled, tag } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; -import React, { useEffect } from "react"; -import { Platform, StyleSheet, TouchableOpacity, View } from "react-native"; +import React, { useEffect, useState } from "react"; +import { + type LayoutChangeEvent, + Platform, + StyleSheet, + TouchableOpacity, + View, +} from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Text } from "@/components/common/Text"; import { useGlobalModal } from "@/providers/GlobalModalProvider"; @@ -208,6 +214,24 @@ const PlatformDropdownComponent = ({ }: PlatformDropdownProps) => { const { showModal, hideModal, isVisible } = useGlobalModal(); + // @expo/ui's (SDK 55) fills its available space by default, and + // `matchContents` doesn't help here: it reports the native Menu's size via + // setStyleSize and overrides any explicit size. Instead we measure the + // trigger's intrinsic size in plain RN (off-layout) and pin it on the Host. + const [triggerSize, setTriggerSize] = useState<{ + width: number; + height: number; + } | null>(null); + + const handleMeasureTrigger = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + setTriggerSize((prev) => + prev && prev.width === width && prev.height === height + ? prev + : { width, height }, + ); + }; + // Handle controlled open state for Android useEffect(() => { if (Platform.OS === "android" && controlledOpen === true) { @@ -239,10 +263,24 @@ const PlatformDropdownComponent = ({ if (Platform.OS === "ios") { return ( - - - {trigger} - + + {/* Hidden measurer: lays the trigger out normally to capture its + intrinsic size, which we then pin onto the Host below. */} + + + {trigger} + + + + {groups.flatMap((group, groupIndex) => { // Check if this group has radio options const radioOptions = group.options.filter( @@ -261,27 +299,32 @@ const PlatformDropdownComponent = ({ // Otherwise render as individual buttons if (radioOptions.length > 0) { if (group.title) { - // Use Picker for grouped options - const selectedRadio = radioOptions.find( + // Use Picker for grouped options. + // Use the option index (a stable primitive) as the + // tag/selection value and React key. Option `value`s can be + // objects (e.g. bitrate / media source), which collapse to + // "[object Object]" as a key and never match the Picker's + // primitive selection. + const selectedRadioIndex = radioOptions.findIndex( (opt) => opt.selected, ); items.push( { - const selectedOption = radioOptions.find( - (opt) => opt.value === value, - ); + selection={ + selectedRadioIndex >= 0 ? selectedRadioIndex : undefined + } + onSelectionChange={(index) => { + const selectedOption = radioOptions[index as number]; selectedOption?.onPress(); onOptionSelect?.(selectedOption?.value); }} > - {radioOptions.map((opt) => ( + {radioOptions.map((opt, optionIndex) => ( {opt.label} @@ -294,6 +337,7 @@ const PlatformDropdownComponent = ({ items.push( , + />, ); }); } @@ -317,6 +359,7 @@ const PlatformDropdownComponent = ({ items.push( , + />, ); }); @@ -336,21 +377,20 @@ const PlatformDropdownComponent = ({ items.push( , + />, ); }); return items; })} - - - + + + ); }