Files
orca/mobile/src/components/PickerModal.tsx
T
Jinjing 0971479866 Add shared workspace settings note and prevent filter modal expansion (#25300)
* fix(mobile): say that workspace sort, grouping, and filters are shared

The Manual sort option was subtitled 'Server order', but it orders by the
desktop's drag ranks. Sort, grouping, and filters on the phone all write the
host's shared view settings, so changing them also changes every other
device on that host, which the screen never said. Relabel Manual as 'Desktop
drag order' and add 'Shared with other devices on this host' under the Sort
By, Group By, and Filter titles. The note avoids naming a desktop sidebar
because headless hosts have none.

* fix(mobile): prevent filter modal heading expansion

Add flexShrink: 1 to allow the heading container to shrink when
space is constrained. Update comment to clarify why workspace view
is shared across devices.

* update wording
2026-10-04 14:35:16 -07:00

185 lines
4.6 KiB
TypeScript

import type { ReactNode } from 'react'
import { View, Text, Pressable, StyleSheet } from 'react-native'
import { Check } from 'lucide-react-native'
import { colors, spacing, typography } from '../theme/mobile-theme'
import { BottomDrawer } from './BottomDrawer'
export type PickerOption<T extends string = string> = {
value: T
label: string
subtitle?: string
disabled?: boolean
renderIcon?: (selected: boolean) => ReactNode
}
type Props<T extends string = string> = {
visible: boolean
title: string
subtitle?: string
options: PickerOption<T>[]
selected: T
onSelect: (value: T) => void
onLongSelect?: (value: T) => void
onClose: () => void
onAfterClose?: () => void
zIndex?: number
}
type PickerModalContentProps<T extends string = string> = Pick<
Props<T>,
'options' | 'selected' | 'onSelect' | 'onLongSelect' | 'onClose'
>
export function PickerModal<T extends string = string>({
visible,
title,
subtitle,
options,
selected,
onSelect,
onLongSelect,
onClose,
onAfterClose,
zIndex
}: Props<T>) {
return (
<BottomDrawer visible={visible} onClose={onClose} onAfterClose={onAfterClose} zIndex={zIndex}>
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
{subtitle ? <Text style={styles.subtitle}>{subtitle}</Text> : null}
</View>
<PickerModalContent
options={options}
selected={selected}
onSelect={onSelect}
onLongSelect={onLongSelect}
onClose={onClose}
/>
</BottomDrawer>
)
}
function PickerModalContent<T extends string = string>({
options,
selected,
onSelect,
onLongSelect,
onClose
}: PickerModalContentProps<T>) {
// Why: closed BottomDrawer instances return null, so keeping option rows in
// this child avoids rebuilding hidden picker contents on every parent render.
return (
<View style={styles.group}>
{options.map((opt, i) => {
const isSelected = opt.value === selected
return (
<View key={opt.value}>
{i > 0 && <View style={styles.separator} />}
<Pressable
accessible
accessibilityRole="button"
accessibilityState={{ disabled: Boolean(opt.disabled), selected: isSelected }}
disabled={opt.disabled}
style={({ pressed }) => [
styles.row,
pressed && !opt.disabled && styles.rowPressed,
opt.disabled && styles.rowDisabled
]}
onPress={() => {
if (opt.disabled) {
return
}
onSelect(opt.value)
onClose()
}}
onLongPress={
onLongSelect
? () => {
if (opt.disabled) {
return
}
onLongSelect(opt.value)
onClose()
}
: undefined
}
>
{opt.renderIcon ? (
<View style={styles.rowIcon}>{opt.renderIcon(isSelected)}</View>
) : null}
<View style={styles.rowContent}>
<Text style={[styles.rowLabel, isSelected && styles.rowLabelSelected]}>
{opt.label}
</Text>
{opt.subtitle ? <Text style={styles.rowSubtitle}>{opt.subtitle}</Text> : null}
</View>
{isSelected && <Check size={16} color={colors.textPrimary} />}
</Pressable>
</View>
)
})}
</View>
)
}
const styles = StyleSheet.create({
header: {
paddingHorizontal: spacing.xs,
paddingBottom: spacing.sm
},
title: {
fontSize: 13,
fontWeight: '500',
color: colors.textMuted
},
subtitle: {
fontSize: 11,
color: colors.textMuted,
marginTop: 2
},
group: {
backgroundColor: colors.bgPanel,
borderRadius: 12,
overflow: 'hidden'
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: colors.borderSubtle,
marginHorizontal: spacing.md
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.md + 2
},
rowPressed: {
backgroundColor: colors.bgRaised
},
rowDisabled: {
opacity: 0.45
},
rowContent: {
flex: 1,
minWidth: 0
},
rowIcon: {
width: 22,
alignItems: 'center',
marginRight: spacing.sm
},
rowLabel: {
fontSize: typography.bodySize,
color: colors.textPrimary
},
rowLabelSelected: {
fontWeight: '600'
},
rowSubtitle: {
fontSize: 11,
color: colors.textMuted,
marginTop: 1
}
})