| 1 | import { |
| 2 | FilledIconButton, |
| 3 | FilledTonalIconButton, |
| 4 | Host, |
| 5 | IconButton, |
| 6 | } from "@expo/ui/jetpack-compose"; |
| 7 | import { size } from "@expo/ui/jetpack-compose/modifiers"; |
| 8 | import { View } from "react-native"; |
| 9 | |
| 10 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 11 | import { useAndroidControlSizing } from "./useAndroidControlSizing"; |
| 12 | import { SymbolView, type AppSymbolName } from "./AppSymbol"; |
| 13 | |
| 14 | export function MaterialIconButton(props: { |
| 15 | readonly accessibilityLabel: string; |
| 16 | readonly icon: AppSymbolName; |
| 17 | readonly onPress?: () => void; |
| 18 | readonly disabled?: boolean; |
| 19 | readonly selected?: boolean; |
| 20 | readonly variant?: "standard" | "primary" | "tonal" | "danger"; |
| 21 | readonly tintColorClassName?: string; |
| 22 | }) { |
| 23 | const { themeAppearance, themeVariables: colors } = useAppearancePreferences(); |
| 24 | const { iconSize, buttonSize } = useAndroidControlSizing(); |
| 25 | const variant = props.variant ?? "standard"; |
| 26 | const Component = |
| 27 | variant === "standard" |
| 28 | ? IconButton |
| 29 | : variant === "tonal" |
| 30 | ? FilledTonalIconButton |
| 31 | : FilledIconButton; |
| 32 | const containerColor = |
| 33 | variant === "primary" |
| 34 | ? colors["--color-primary"] |
| 35 | : variant === "danger" |
| 36 | ? colors["--color-danger"] |
| 37 | : colors["--color-secondary"]; |
| 38 | const iconTint = props.disabled |
| 39 | ? "accent-icon-subtle" |
| 40 | : variant === "primary" |
| 41 | ? "accent-primary-foreground" |
| 42 | : variant === "danger" |
| 43 | ? "accent-danger-foreground" |
| 44 | : variant === "tonal" |
| 45 | ? "accent-secondary-foreground" |
| 46 | : (props.tintColorClassName ?? "accent-foreground"); |
| 47 | return ( |
| 48 | <View |
| 49 | accessible |
| 50 | accessibilityLabel={props.accessibilityLabel} |
| 51 | accessibilityRole="button" |
| 52 | accessibilityState={{ disabled: Boolean(props.disabled), selected: props.selected }} |
| 53 | accessibilityActions={[{ name: "activate" }]} |
| 54 | onAccessibilityAction={() => { |
| 55 | if (!props.disabled) props.onPress?.(); |
| 56 | }} |
| 57 | style={{ width: buttonSize, height: buttonSize }} |
| 58 | > |
| 59 | <View importantForAccessibility="no-hide-descendants"> |
| 60 | <Host |
| 61 | colorScheme={themeAppearance} |
| 62 | ignoreSafeAreaKeyboardInsets |
| 63 | style={{ width: buttonSize, height: buttonSize }} |
| 64 | > |
| 65 | <Component |
| 66 | onClick={props.onPress} |
| 67 | enabled={!props.disabled} |
| 68 | modifiers={[size(buttonSize, buttonSize)]} |
| 69 | colors={ |
| 70 | variant === "standard" |
| 71 | ? undefined |
| 72 | : { containerColor, disabledContainerColor: colors["--color-subtle-strong"] } |
| 73 | } |
| 74 | > |
| 75 | {null} |
| 76 | </Component> |
| 77 | </Host> |
| 78 | </View> |
| 79 | {/* Keep RN SVG measurement outside Compose; the native button owns touch and ripple. */} |
| 80 | <View pointerEvents="none" className="absolute inset-0 items-center justify-center"> |
| 81 | <SymbolView |
| 82 | name={props.icon === "ellipsis" ? { ios: "ellipsis", android: "more_vert" } : props.icon} |
| 83 | size={iconSize} |
| 84 | tintColorClassName={iconTint} |
| 85 | type="monochrome" |
| 86 | /> |
| 87 | </View> |
| 88 | </View> |
| 89 | ); |
| 90 | } |