import { getContrastColor } from '@rific/auto-paper' import { TouchableRipple } from '@rific/feedback-press' import { ScrollView, StyleSheet, View } from 'react-native' import { Icon, Text } from 'react-native-paper' import { PopoverBody } from './PopoverBody' import { AlignResult, useAutoAlign } from './useAutoAlign' import { PopoverHost } from './usePopoverHost' const TRIGGER_HEIGHT = 28 const POPOVER_WIDTH = 180 const ROW_HEIGHT = 36 const LIST_PADDING = 8 // Exported so a caller that needs to independently compute this popover's own placement - to feed // alignOverride with, say, a @tastic/split-screen-zone-aware hook's result - can match its real // content dimensions exactly, rather than hardcoding or re-deriving this component's own internal // sizing (which alignOverride itself deliberately doesn't take a dependency on any particular // zone-awareness package to compute). export const LABELED_DROPDOWN_POPOVER_WIDTH = POPOVER_WIDTH export function getLabeledDropdownContentHeight(optionCount: number): number { return LIST_PADDING * 2 + optionCount * ROW_HEIGHT } export interface LabeledDropdownOption { value: T label: string icon?: string } interface Props { id: string host: PopoverHost options: LabeledDropdownOption[] value: T onChange: (value: T) => void color: string dark: boolean // Forces a specific horizontal alignment instead of letting whichever placement result applies // (see alignOverride below) decide. Matches PopoverBody's own align prop meaning. align?: 'left' | 'right' | 'center' // Substitutes this package's own plain useAutoAlign-based placement wholesale (align, // verticalAlign, maxHeight, measured, triggerRef) with a caller-supplied one - e.g. a popover // living inside a @tastic/split-screen zone, where the plain window-relative decision can pick a // direction that overflows into the shared row instead of the actual zone boundary. Omit for the // ordinary case (a popover with nothing but the screen edge to avoid). alignOverride?: AlignResult } // A name-trigger + selection popover in the same visual style as @tastic/profile's own // ProfilePicker trigger - an uppercase text label + chevron, opening a list whose selected row // fills entirely with the seat's own accent color - rather than this package's own // SectionedDropdown, whose trigger is a fixed icon with a gauge ring around it. For a choice that // reads more like "who/what you're playing" than "a game setting" (a CPU seat's own difficulty, // occupying the same slot a human seat's ProfilePicker would), the always-visible text label // carries more of the meaning than an icon would on its own. export function LabeledDropdown({ id, host, options, value, onChange, color, dark, align: forcedAlign, alignOverride }: Props) { const menuBg = dark ? '#000000' : '#FFFFFF' const fg = dark ? '#FFFFFF' : '#000000' const open = host.openId === id const selected = options.find((o) => o.value === value) ?? null const contentHeight = LIST_PADDING * 2 + options.length * ROW_HEIGHT // Always called, even when alignOverride is supplied and this result goes unused - hooks can't be // called conditionally. A caller passing alignOverride (e.g. this package's own // useZoneClampedAlign, which already wraps this same hook) does end up measuring the trigger // twice; an acceptable tradeoff for keeping this component's own hook usage unconditional and // override-shaped rather than needing a second, hook-free code path. const auto = useAutoAlign(open, POPOVER_WIDTH, contentHeight) const placement = alignOverride ?? auto const { maxHeight, measured, triggerRef, verticalAlign } = placement const align = forcedAlign ?? placement.align const handleSelect = (option: LabeledDropdownOption) => { onChange(option.value) host.close() } return ( host.toggle(id)} style={styles.trigger}> {(selected?.label ?? '').toUpperCase()} {/* Gated on `measured`, not just `open` - see InlineColorPicker's identical fix. */} {options.map((option) => { const isSelected = option.value === value const onColor = getContrastColor(color) return ( handleSelect(option)} style={[styles.row, isSelected && { backgroundColor: color }]}> {option.icon && } {option.label} ) })} ) } const styles = StyleSheet.create({ anchor: { position: 'relative' }, // React Native Web gives every position:'relative' view its own stacking context, so the // elevation has to live on the anchor itself - see PopoverBody's own stacking-context notes. anchorOpen: { zIndex: 100 }, menu: { borderRadius: 12, borderWidth: 2, elevation: 8, shadowColor: '#000000', shadowOffset: { height: 2, width: 0 }, shadowOpacity: 0.3, shadowRadius: 8 }, menuContent: { padding: LIST_PADDING }, row: { borderRadius: 8, height: ROW_HEIGHT }, rowInner: { alignItems: 'center', flexDirection: 'row', flex: 1, gap: 8, paddingHorizontal: 12 }, rowLabel: { flex: 1, fontSize: 14 }, trigger: { alignSelf: 'flex-start' }, triggerInner: { alignItems: 'center', flexDirection: 'row', gap: 2, height: TRIGGER_HEIGHT }, triggerLabel: { fontSize: 13, fontWeight: '700', letterSpacing: 1 } })