ui-button.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. import { ActivityIndicator, Icon } from '@ant-design/react-native';
  2. import { clsx } from 'clsx';
  3. import { Link } from 'expo-router';
  4. import React, { useEffect, useState } from 'react';
  5. import { Pressable, Text, View } from 'react-native';
  6. import { Colors } from '@/constants/theme';
  7. import { isPromise } from '@/utils/tsutils';
  8. import type {IconNames} from '@ant-design/react-native/lib/icon';
  9. import type { Href} from 'expo-router';
  10. import type { ViewStyle } from 'react-native';
  11. interface UIButtonProps {
  12. children?: string | React.ReactNode;
  13. title?: string | React.ReactNode;
  14. onPress?: () => PromiseLike<unknown> | unknown;
  15. disabled?: boolean;
  16. className?: string;
  17. textClassName?: string;
  18. href?: Href;
  19. type?: 'primary' | 'second' | 'link';
  20. icon?: IconNames | React.ReactNode;
  21. style?: ViewStyle;
  22. loading?: boolean;
  23. }
  24. type ButtonType = UIButtonProps['type'];
  25. function getIconColor(type: ButtonType, disabled: boolean) {
  26. if (disabled) return Colors['on-surface']['variant'];
  27. if (type === 'primary') return '#fff';
  28. if (type === 'second') return Colors.secondary.DEFAULT;
  29. return Colors.primary.DEFAULT;
  30. }
  31. function ButtonLabel({
  32. type,
  33. disabled,
  34. textClassName,
  35. children,
  36. loading,
  37. }: {
  38. type: ButtonType;
  39. disabled: boolean;
  40. textClassName?: string;
  41. children: React.ReactNode;
  42. loading: boolean;
  43. }) {
  44. const labelClass = clsx(textClassName, 'text-sm font-semibold', {
  45. 'text-on-primary': !disabled && type === 'primary',
  46. 'text-primary': !disabled && type !== 'primary' && type !== 'second',
  47. 'text-secondary': !disabled && type === 'second',
  48. 'text-on-surface-variant/65 font-normal': disabled,
  49. });
  50. if (!loading) return <Text className={labelClass}>{children}</Text>;
  51. return (
  52. <View className="flex-row justify-center items-center">
  53. <ActivityIndicator color={disabled ? '#94a3b8' : Colors['on-primary']['DEFAULT']} />
  54. <Text className={labelClass}>{children}</Text>
  55. </View>
  56. );
  57. }
  58. export default function UIButton({
  59. onPress,
  60. href,
  61. type,
  62. icon,
  63. className,
  64. style,
  65. disabled,
  66. children,
  67. title,
  68. textClassName,
  69. loading,
  70. }: UIButtonProps) {
  71. const [busy, setBusy] = useState(loading);
  72. const isDisabled = Boolean(disabled || busy);
  73. useEffect(() => {
  74. setBusy(loading);
  75. }, [loading]);
  76. const label = children ?? title;
  77. const handlePress = () => {
  78. if (isDisabled) return;
  79. const res = onPress?.();
  80. if (isPromise(res)) {
  81. setBusy(true);
  82. (res as Promise<unknown>).finally(() => setBusy(false));
  83. }
  84. };
  85. const inner = (
  86. <Pressable
  87. className={clsx(
  88. 'flex-row justify-center items-center rounded-xl py-1.5 border-2 opacity-100 active:opacity-75 active:scale-[0.95]',
  89. className,
  90. {
  91. 'bg-primary border-primary': type === 'primary',
  92. 'bg-surface border-primary/25': !type,
  93. 'bg-surface border-on-surface/25': type === 'second',
  94. 'bg-transparent border-transparent': type === 'link',
  95. 'bg-primary-fixed-dim border-primary-fixed-dim': isDisabled && type === 'primary',
  96. 'bg-gray-300 border-gray-300': isDisabled && !type,
  97. 'bg-gray-400 border-gray-200': isDisabled && type === 'second',
  98. }
  99. )}
  100. disabled={isDisabled}
  101. onPress={handlePress}
  102. style={style}
  103. >
  104. {typeof icon === 'string' ? (
  105. <Icon
  106. name={icon as IconNames}
  107. size={20}
  108. style={{ marginRight: 4, color: getIconColor(type, isDisabled) }}
  109. />
  110. ) : (
  111. icon
  112. )}
  113. {typeof label === 'string' ? (
  114. <ButtonLabel type={type} disabled={isDisabled} loading={!!busy} textClassName={textClassName}>
  115. {label}
  116. </ButtonLabel>
  117. ) : (
  118. label
  119. )}
  120. </Pressable>
  121. );
  122. if (typeof href !== 'undefined') {
  123. return (
  124. <Link href={href} asChild>
  125. {inner}
  126. </Link>
  127. );
  128. }
  129. return inner;
  130. }