Something went wrong. Try again.
Utilities and UI components for cross-platform React Native apps
Something went wrong. Try again.
5.5 kB · 205 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206// Copyright (c) 2025 Joseph Hale//// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.
/* * KNOWN BUGS: * `mode="elevated" disabled`: The text is highlighted lighter than the rest of the button */
import { Pressable, StyleSheet, View, type ViewStyle } from 'react-native';
import type { Optional } from '../utils/typing';import Text from './Text';import type { MaterialTheme } from '../theme/material/types';import { useMaterialTheme } from '../theme/material/useMaterialTheme';
type ButtonMode = 'text' | 'contained' | 'outlined' | 'tonal' | 'elevated';type ButtonIntent = 'normal' | 'danger';
export interface ButtonProps { children: string; mode: ButtonMode; intent: ButtonIntent; disabled: boolean; onPress: () => void; before?: React.ReactNode; after?: React.ReactNode;}
type PropsWithDefaults = 'mode' | 'intent' | 'disabled' | 'onPress';
const propDefaults: Pick<ButtonProps, PropsWithDefaults> = { mode: 'text', intent: 'normal', disabled: false, onPress: () => { /* no-op */ },};
export default function Button(args: Optional<ButtonProps, PropsWithDefaults>) { const { theme } = useMaterialTheme(); const props: ButtonProps = { ...propDefaults, ...args }; const modeColors = buttonModeColors(theme)[props.mode]; const dangerColors = buttonDangerColors(theme)[props.mode]; const disabledColors = buttonDisabledColors(theme)[props.mode]; return ( <Pressable onPress={props.onPress} disabled={props.disabled} style={[ styles.container, modeColors, props.intent === 'danger' && dangerColors, props.disabled && disabledColors, ]} > <View style={styles.contents}> {props.before} <Text style={[ { color: modeColors.textColor }, props.intent === 'danger' && { color: dangerColors.textColor }, props.disabled && { color: disabledColors.textColor }, ]} > {props.children} </Text> {props.after} </View> </Pressable> );}
interface ButtonColors extends ViewStyle { backgroundColor: string; textColor: string; borderColor: string; borderWidth: number;}
function buttonModeColors( theme: MaterialTheme): Record<ButtonMode, ButtonColors> { return { text: { backgroundColor: 'transparent', textColor: theme.colors.primary, borderColor: 'transparent', borderWidth: 0, }, contained: { backgroundColor: theme.colors.primary, textColor: theme.colors.onPrimary, borderColor: 'transparent', borderWidth: 0, }, outlined: { backgroundColor: 'transparent', textColor: theme.colors.primary, borderColor: theme.colors.outline, borderWidth: 1, }, tonal: { backgroundColor: theme.colors.secondaryContainer, textColor: theme.colors.onSecondaryContainer, borderColor: 'transparent', borderWidth: 0, }, elevated: { backgroundColor: theme.colors.elevation.level2, textColor: theme.colors.primary, borderColor: 'transparent', borderWidth: 0, elevation: 2, }, };}
function buttonDangerColors( theme: MaterialTheme): Record<ButtonMode, ButtonColors> { return { text: { backgroundColor: 'transparent', textColor: theme.colors.error, borderColor: 'transparent', borderWidth: 0, }, contained: { backgroundColor: theme.colors.error, textColor: theme.colors.onError, borderColor: 'transparent', borderWidth: 0, }, outlined: { backgroundColor: 'transparent', textColor: theme.colors.error, borderColor: theme.colors.error, borderWidth: 1, }, tonal: { backgroundColor: theme.colors.errorContainer, textColor: theme.colors.onErrorContainer, borderColor: 'transparent', borderWidth: 0, }, elevated: { backgroundColor: theme.colors.errorContainer, textColor: theme.colors.onErrorContainer, borderColor: 'transparent', borderWidth: 0, elevation: 2, }, };}
function buttonDisabledColors( theme: MaterialTheme): Record<ButtonMode, ButtonColors> { return { text: { backgroundColor: 'transparent', textColor: theme.colors.onSurfaceDisabled, borderColor: 'transparent', borderWidth: 0, }, contained: { backgroundColor: theme.colors.surfaceDisabled, textColor: theme.colors.onSurfaceDisabled, borderColor: 'transparent', borderWidth: 0, }, outlined: { backgroundColor: 'transparent', textColor: theme.colors.onSurfaceDisabled, borderColor: theme.colors.surfaceDisabled, borderWidth: 1, }, tonal: { backgroundColor: theme.colors.surfaceDisabled, textColor: theme.colors.onSurfaceDisabled, borderColor: 'transparent', borderWidth: 0, }, elevated: { backgroundColor: theme.colors.surfaceDisabled, textColor: theme.colors.onSurfaceDisabled, borderColor: 'transparent', borderWidth: 0, }, };}
const styles = StyleSheet.create({ container: { margin: 2, padding: 8, borderRadius: 8 }, contents: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-evenly', gap: 8, },});