import React from "react"; import type { StyleProp, ViewStyle } from "react-native"; import { TouchableHighlight, View } from "react-native"; import type { IconColorNames, IconNames } from "@jobber/design"; import type { XOR } from "ts-xor"; import { useStyles } from "./Button.style"; import { InternalButtonLoading } from "./components/InternalButtonLoading"; import type { ButtonSize, ButtonType, ButtonVariation } from "./types"; import type { ActionLabelVariation } from "../ActionLabel"; import { ActionLabel } from "../ActionLabel"; import { Icon } from "../Icon"; import { useAtlantisTheme } from "../AtlantisThemeContext"; interface CommonButtonProps { /** * Press handler */ readonly onPress?: () => void; /** * Themes the button to the type of action it performs */ readonly variation?: ButtonVariation; /** * Sets the visual hierarchy */ readonly type?: ButtonType; /** * Defines the size of the button * * @default "base" */ readonly size?: ButtonSize; /** * Will make the button scale to take up all the available height */ readonly fullHeight?: boolean; /** * Will make the button scale to take up all of the available width */ readonly fullWidth?: boolean; /** * Makes the button un-clickable */ readonly disabled?: boolean; /** * Accessibility hint to help users understand what will happen when they press the button */ readonly accessibilityHint?: string; /** * Changes the button interface to imply loading and prevents the press callback * * @default false */ readonly loading?: boolean; /** * Adds an leading icon beside the label. */ readonly icon?: IconNames; /** * Accessibility label for the component. This is required for components that * have an `icon` but not a `label`. * * If the string is the same as the `label` prop, you don't need to add an * `accessibilityLabel`. **Don't use this for testing purposes.** */ readonly accessibilityLabel?: string; /** * Used to locate this view in end-to-end tests. */ readonly testID?: string; /** * **Use at your own risk:** Custom style for specific elements. This should only be used as a * **last resort**. Using this may result in unexpected side effects. * More information in the [Customizing components Guide](https://atlantis.getjobber.com/guides/customizing-components). */ readonly UNSAFE_style?: ButtonUnsafeStyle; } export interface ButtonUnsafeStyle { container?: StyleProp; contentContainer?: StyleProp; iconContainer?: StyleProp; actionLabelContainer?: StyleProp; } interface LabelButton extends CommonButtonProps { /** * Text to be displayed on the button */ readonly label: string; } interface IconButton extends CommonButtonProps { readonly icon: IconNames; readonly accessibilityLabel: string; } export type ButtonProps = XOR; export function Button({ label, onPress, variation = "work", type = "primary", fullHeight = false, fullWidth = true, disabled = false, loading = false, size = "base", accessibilityLabel, accessibilityHint, icon, testID, UNSAFE_style, }: ButtonProps) { const { tokens } = useAtlantisTheme(); const styles = useStyles(); const buttonStyle = [ styles.button, styles[size], styles[variation], styles[type], type === "secondary" && variation === "cancel" && styles.cancelSecondary, disabled && styles.disabled, fullWidth && styles.reducedPaddingForFullWidth, fullHeight && styles.fullHeight, ]; // attempts to use Pressable caused problems. When a ScrollView contained // an InputText that was focused, it required two presses to activate the // Pressable. Using a TouchableHighlight made things register correctly // in a single press return ( {loading && } {icon && ( )} {label && ( {label} )} ); } function getActionLabelVariation( variation: string, type: string, ): ActionLabelVariation { if (type === "primary" && variation !== "cancel") { return "onPrimary"; } switch (variation) { case "learning": return "interactiveSubtle"; case "destructive": return "destructive"; case "cancel": return "interactiveSubtle"; default: return "interactive"; } } function getIconColorVariation( variation: ButtonVariation, type: string, disabled: boolean, ): IconColorNames { if (disabled) { return "disabled"; } if (type === "primary" && variation !== "cancel") { return "surface"; // This should be a text color token instead, but the action label also uses this } switch (variation) { case "learning": return "interactiveSubtle"; case "destructive": return "destructive"; case "cancel": return "interactiveSubtle"; default: return "interactive"; } } function getContentStyles( label: string | undefined, icon: IconNames | undefined, styles: ReturnType, ) { if (label && !icon) { return undefined; } return [ styles.content, icon && !!label && styles.iconPaddingOffset, !!label && styles.contentWithLabel, ]; } function buildTestID( testID: string | undefined, label: string | undefined, accessibilityLabel: string | undefined, ): string | undefined { if (testID) { return `ATL-${testID}-Button`; } return accessibilityLabel || label; }