@@ -133,10 +133,13 @@ const MARKER_HITBOX = { width: 50, height: 50 } as const;
133133
134134// Snap points for the sheet. Index 0 = 15% peek (mostly map). Index 1 =
135135// 62% mid — the Google-Maps-style "selected place" height where the hero
136- // image, facts, and action row are visible without forcing the sheet to
137- // fill the whole screen. Index 2 = 92% expanded (list fills the viewport
138- // up to the search pill — `topInset` caps it so the pill stays visible).
139- const SHEET_SNAP_POINTS : Array < string | number > = [ "15%" , "62%" , "92%" ] ;
136+ // image, facts, and action row are visible. Index 2 = 100% — the sheet
137+ // fills the screen all the way up behind the notch, sliding under the
138+ // floating search pill. At that snap the list content gains a top
139+ // padding equal to the pill height + safe area so rows never render
140+ // underneath the pill. Magnetic snapping pulls a flick past mid straight
141+ // to the top.
142+ const SHEET_SNAP_POINTS : Array < string | number > = [ "15%" , "62%" , "100%" ] ;
140143// Detail mode and list mode both use the mid snap so the camera padding
141144// stays consistent — the only difference is the rendered children.
142145const SHEET_INDEX_DETAIL = 1 ;
@@ -341,13 +344,15 @@ export default function MapScreen() {
341344 const [ userLocation , setUserLocation ] = useState < UserCoords | null > ( null ) ;
342345
343346 // Tapping the search field expands the sheet to its top snap so the
344- // filtered list fills the screen. On blur with an empty query we snap
345- // back to mid so the map is visible again. The search pill is always
346- // visible on top of the sheet; at the expanded snap the list gains a
347- // top padding via `listContentContainerStyle` so rows aren't hidden
348- // behind the pill .
347+ // filtered list fills the screen. We defer the snap one frame via
348+ // `requestAnimationFrame` — snapping synchronously inside the
349+ // `onFocus` handler fights with React Native's own focus bookkeeping
350+ // on Android and the TextInput loses focus a beat later. Queuing the
351+ // snap for the next frame lets focus settle first .
349352 const handleSearchFocus = useCallback ( ( ) => {
350- sheetRef . current ?. snapToIndex ( SHEET_INDEX_SEARCH ) ;
353+ requestAnimationFrame ( ( ) => {
354+ sheetRef . current ?. snapToIndex ( SHEET_INDEX_SEARCH ) ;
355+ } ) ;
351356 } , [ ] ) ;
352357
353358 const handleSearchBlur = useCallback ( ( ) => {
@@ -636,6 +641,20 @@ export default function MapScreen() {
636641 [ insets . top ] ,
637642 ) ;
638643
644+ // When the sheet reaches its fully-expanded snap, swap the pill's drop
645+ // shadow for a hairline border. A shadow against the sheet (which is now
646+ // flush with the pill) reads as grime; a border separates the pill from
647+ // the sheet cleanly.
648+ const pillSurfaceStyle = useMemo ( ( ) => {
649+ if ( sheetIndex >= SHEET_INDEX_SEARCH ) {
650+ return {
651+ borderWidth : StyleSheet . hairlineWidth ,
652+ borderColor : theme . colors . border ,
653+ } ;
654+ }
655+ return styles . floatingBar ;
656+ } , [ sheetIndex , theme . colors . border ] ) ;
657+
639658 const renderPlaceRow = useCallback (
640659 ( { item } : { item : { place : Place ; km : number | null } } ) => (
641660 < PlaceRow
@@ -702,12 +721,12 @@ export default function MapScreen() {
702721
703722 < View pointerEvents = "box-none" style = { searchPillBaseStyle } >
704723 < View
705- className = "flex-row items-center bg-background rounded-full pl-2 pr-3 h-14"
706- style = { styles . floatingBar }
724+ className = "flex-row items-center bg-surface rounded-full pl-2 pr-3 h-14"
725+ style = { pillSurfaceStyle }
707726 >
708727 < Pressable
709728 onPress = { ( ) => router . back ( ) }
710- className = "w-11 h-11 items-center justify-center rounded-full active:bg-surface "
729+ className = "w-11 h-11 items-center justify-center rounded-full active:opacity-70 "
711730 accessibilityRole = "button"
712731 accessibilityLabel = { t ( "common.back" ) }
713732 hitSlop = { 8 }
@@ -736,7 +755,7 @@ export default function MapScreen() {
736755 { query . length > 0 ? (
737756 < Pressable
738757 onPress = { ( ) => setQuery ( "" ) }
739- className = "w-8 h-8 items-center justify-center rounded-full active:bg-surface "
758+ className = "w-8 h-8 items-center justify-center rounded-full active:opacity-70 "
740759 accessibilityRole = "button"
741760 accessibilityLabel = { t ( "common.clear" ) }
742761 hitSlop = { 6 }
@@ -760,7 +779,7 @@ export default function MapScreen() {
760779 < View pointerEvents = "box-none" style = { styles . fabContainer } >
761780 < Pressable
762781 onPress = { handleLocateMe }
763- className = "w-12 h-12 rounded-full bg-background items-center justify-center active:opacity-80"
782+ className = "w-12 h-12 rounded-full bg-surface items-center justify-center active:opacity-80"
764783 style = { styles . fab }
765784 accessibilityRole = "button"
766785 accessibilityLabel = { t ( "map.locateMe.accessibility" ) }
@@ -778,6 +797,10 @@ export default function MapScreen() {
778797 snapPoints = { SHEET_SNAP_POINTS }
779798 index = { 1 }
780799 enablePanDownToClose = { false }
800+ enableDynamicSizing = { false }
801+ keyboardBehavior = "extend"
802+ keyboardBlurBehavior = "restore"
803+ android_keyboardInputMode = "adjustResize"
781804 onChange = { handleSheetChange }
782805 animatedIndex = { sheetAnimatedIndex }
783806 backgroundStyle = { sheetBackgroundStyle }
@@ -790,38 +813,39 @@ export default function MapScreen() {
790813 onClose = { handleCloseDetail }
791814 />
792815 ) : (
793- < View className = "flex-1" >
794- { /* Sticky top block (spacer grows with the drag, then title,
795- then filter chips). Renders outside the FlashList so it
796- stays pinned while the list scrolls beneath it. */ }
797- < Animated . View style = { headerSpacerStyle } />
798- < View className = "px-5 pt-4 pb-2" >
799- < Text className = "text-foreground text-lg font-semibold" >
800- { t ( "map.nearYou" ) }
801- </ Text >
802- < Text className = "text-muted-foreground text-xs mt-0.5" >
803- { filteredPlaces . length } { " " }
804- { filteredPlaces . length === 1
805- ? t ( "map.resultOne" )
806- : t ( "map.resultOther" ) }
807- </ Text >
808- </ View >
809- < CategoryFilterRow
810- value = { categoryFilter }
811- onChange = { handleCategoryFilter }
812- />
816+ < >
813817 < FlashList
814818 renderScrollComponent = { BottomSheetFlashListScrollable }
815819 data = { placesWithDistance }
816820 keyExtractor = { ( item ) => item . place . id }
817821 contentContainerStyle = { listContentContainerStyle }
822+ ListHeaderComponent = {
823+ < View >
824+ < Animated . View style = { headerSpacerStyle } />
825+ < View className = "px-5 pt-4 pb-2" >
826+ < Text className = "text-foreground text-lg font-semibold" >
827+ { t ( "map.nearYou" ) }
828+ </ Text >
829+ < Text className = "text-muted-foreground text-xs mt-0.5" >
830+ { filteredPlaces . length } { " " }
831+ { filteredPlaces . length === 1
832+ ? t ( "map.resultOne" )
833+ : t ( "map.resultOther" ) }
834+ </ Text >
835+ </ View >
836+ < CategoryFilterRow
837+ value = { categoryFilter }
838+ onChange = { handleCategoryFilter }
839+ />
840+ </ View >
841+ }
818842 ListEmptyComponent = {
819843 < EmptyState icon = "map-marker-off" title = { t ( "map.noResults" ) } />
820844 }
821845 renderItem = { renderPlaceRow }
822846 />
823847 < FloatingHandle />
824- </ View >
848+ </ >
825849 ) }
826850 </ BottomSheet >
827851
@@ -846,8 +870,8 @@ export default function MapScreen() {
846870
847871// ---------------------------------------------------------------------------
848872// CategoryFilterRow — horizontal scrollable chip row that filters the list
849- // by PlaceCategory. Rendered inside the FlashList header so it scrolls with
850- // the content .
873+ // by PlaceCategory. Rendered as a sibling of the FlashList (not inside it)
874+ // so it stays sticky at the top of the sheet while the list scrolls under .
851875// ---------------------------------------------------------------------------
852876
853877interface CategoryFilterRowProps {
0 commit comments