zkit-ui 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/Button/index.d.ts.map +1 -1
- package/dist/ui/Button/index.js +164 -15
- package/dist/ui/Checkbox/index.d.ts.map +1 -1
- package/dist/ui/Checkbox/index.js +18 -4
- package/dist/ui/Radio/index.d.ts.map +1 -1
- package/dist/ui/Radio/index.js +17 -6
- package/dist/ui/Text/index.d.ts.map +1 -1
- package/dist/ui/Text/index.js +65 -17
- package/package.json +1 -1
- package/src/ui/Button/README.md +5 -1
- package/src/ui/Button/index.tsx +388 -54
- package/src/ui/Checkbox/README.md +4 -1
- package/src/ui/Checkbox/index.tsx +19 -4
- package/src/ui/Radio/README.md +4 -1
- package/src/ui/Radio/index.tsx +23 -10
- package/src/ui/Text/README.md +5 -2
- package/src/ui/Text/index.tsx +107 -32
package/src/ui/Button/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DimensionValue,
|
|
5
5
|
GestureResponderEvent,
|
|
6
6
|
Insets,
|
|
7
|
+
PressableStateCallbackType,
|
|
7
8
|
StyleProp,
|
|
8
9
|
TextStyle,
|
|
9
10
|
ViewStyle,
|
|
@@ -837,41 +838,71 @@ function resolveWebCursorStyle(disabled: boolean): ViewStyle | undefined {
|
|
|
837
838
|
return { cursor: disabled ? 'not-allowed' : 'pointer' } as ViewStyle;
|
|
838
839
|
}
|
|
839
840
|
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
841
|
+
function requiresAnimatedPressEffect(effect: ButtonPressEffect | undefined) {
|
|
842
|
+
return (
|
|
843
|
+
effect === 'scale' ||
|
|
844
|
+
effect === 'scale-opacity' ||
|
|
845
|
+
effect === 'highlight' ||
|
|
846
|
+
effect === 'scale-highlight'
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function shouldUseStaticButtonPath({
|
|
851
|
+
disabled,
|
|
852
|
+
loading,
|
|
853
|
+
pressEffect,
|
|
854
|
+
}: {
|
|
855
|
+
disabled?: boolean;
|
|
856
|
+
loading?: boolean;
|
|
857
|
+
pressEffect?: ButtonPressEffect;
|
|
858
|
+
}) {
|
|
859
|
+
if (loading) return false;
|
|
860
|
+
if (disabled) return true;
|
|
861
|
+
return !requiresAnimatedPressEffect(pressEffect);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function useButtonFrame({
|
|
865
|
+
variant = 'solid',
|
|
866
|
+
tone = 'primary',
|
|
867
|
+
size = 'md',
|
|
868
|
+
shape = 'rounded',
|
|
869
|
+
block = false,
|
|
870
|
+
disabled = false,
|
|
871
|
+
loading = false,
|
|
872
|
+
icon,
|
|
873
|
+
iconPlacement = 'start',
|
|
874
|
+
iconOnly = false,
|
|
875
|
+
color,
|
|
876
|
+
colors,
|
|
877
|
+
border,
|
|
878
|
+
layout,
|
|
879
|
+
gradient,
|
|
880
|
+
shadow = 'none',
|
|
881
|
+
children,
|
|
882
|
+
textStyle,
|
|
883
|
+
accessibilityLabel,
|
|
884
|
+
}: Pick<
|
|
885
|
+
ButtonProps,
|
|
886
|
+
| 'variant'
|
|
887
|
+
| 'tone'
|
|
888
|
+
| 'size'
|
|
889
|
+
| 'shape'
|
|
890
|
+
| 'block'
|
|
891
|
+
| 'disabled'
|
|
892
|
+
| 'loading'
|
|
893
|
+
| 'icon'
|
|
894
|
+
| 'iconPlacement'
|
|
895
|
+
| 'iconOnly'
|
|
896
|
+
| 'color'
|
|
897
|
+
| 'colors'
|
|
898
|
+
| 'border'
|
|
899
|
+
| 'layout'
|
|
900
|
+
| 'gradient'
|
|
901
|
+
| 'shadow'
|
|
902
|
+
| 'children'
|
|
903
|
+
| 'textStyle'
|
|
904
|
+
| 'accessibilityLabel'
|
|
905
|
+
>) {
|
|
875
906
|
const theme = useTheme();
|
|
876
907
|
const scheme = useColorScheme();
|
|
877
908
|
const { width: viewportWidth } = useWindowDimensions();
|
|
@@ -892,8 +923,6 @@ function ButtonImpl(
|
|
|
892
923
|
|
|
893
924
|
const interactionDisabled = disabled || loading;
|
|
894
925
|
const visualDisabled = disabled;
|
|
895
|
-
const resolvedLoadingMode = iconOnly ? 'overlay' : loadingMode;
|
|
896
|
-
const centeredLoading = resolvedLoadingMode !== 'inline';
|
|
897
926
|
|
|
898
927
|
const visualColors = React.useMemo(
|
|
899
928
|
() =>
|
|
@@ -1009,16 +1038,6 @@ function ButtonImpl(
|
|
|
1009
1038
|
const hasText = resolvedText != null;
|
|
1010
1039
|
const hasIcon = icon != null;
|
|
1011
1040
|
const iconSize = layout?.iconSize ?? metrics.iconSize;
|
|
1012
|
-
const loadingSize = React.useMemo(
|
|
1013
|
-
() =>
|
|
1014
|
-
resolveLoadingSize({
|
|
1015
|
-
iconOnly,
|
|
1016
|
-
layout,
|
|
1017
|
-
metrics,
|
|
1018
|
-
variant,
|
|
1019
|
-
}),
|
|
1020
|
-
[iconOnly, layout, metrics, variant]
|
|
1021
|
-
);
|
|
1022
1041
|
const contentGap = layout?.gap ?? metrics.gap;
|
|
1023
1042
|
const iconBaseBoxStyle = React.useMemo(() => resolveIconBoxStyle(hasIcon, iconSize), [hasIcon, iconSize]);
|
|
1024
1043
|
const iconPlacedBoxStyle = React.useMemo((): ViewStyle => {
|
|
@@ -1034,6 +1053,292 @@ function ButtonImpl(
|
|
|
1034
1053
|
iconOnly && isPrimitiveTextChild(children) ? String(children) : undefined;
|
|
1035
1054
|
const resolvedAccessibilityLabel = accessibilityLabel ?? inferredAccessibilityLabel;
|
|
1036
1055
|
|
|
1056
|
+
return {
|
|
1057
|
+
accentColor,
|
|
1058
|
+
contentGap,
|
|
1059
|
+
contentSizeStyle,
|
|
1060
|
+
defaultHitSlop,
|
|
1061
|
+
gradientCfg,
|
|
1062
|
+
hasGradientBackground,
|
|
1063
|
+
hasIcon,
|
|
1064
|
+
hasText,
|
|
1065
|
+
iconBaseBoxStyle,
|
|
1066
|
+
iconPlacedBoxStyle,
|
|
1067
|
+
iconSize,
|
|
1068
|
+
interactionDisabled,
|
|
1069
|
+
LinearGradientComponent,
|
|
1070
|
+
metrics,
|
|
1071
|
+
paddingStyle,
|
|
1072
|
+
radiusStyle,
|
|
1073
|
+
resolvedAccessibilityLabel,
|
|
1074
|
+
resolvedText,
|
|
1075
|
+
rootSizeStyle,
|
|
1076
|
+
shadowStyle,
|
|
1077
|
+
visualColors,
|
|
1078
|
+
visualDisabled,
|
|
1079
|
+
webCursorStyle,
|
|
1080
|
+
borderStyle,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function ButtonStaticImpl(
|
|
1085
|
+
{
|
|
1086
|
+
variant = 'solid',
|
|
1087
|
+
tone = 'primary',
|
|
1088
|
+
size = 'md',
|
|
1089
|
+
shape = 'rounded',
|
|
1090
|
+
block = false,
|
|
1091
|
+
disabled = false,
|
|
1092
|
+
loading = false,
|
|
1093
|
+
loadingMode: _loadingMode = 'inline',
|
|
1094
|
+
pressEffect = 'auto',
|
|
1095
|
+
icon,
|
|
1096
|
+
iconPlacement = 'start',
|
|
1097
|
+
iconOnly = false,
|
|
1098
|
+
color,
|
|
1099
|
+
colors,
|
|
1100
|
+
border,
|
|
1101
|
+
layout,
|
|
1102
|
+
gradient,
|
|
1103
|
+
shadow = 'none',
|
|
1104
|
+
children,
|
|
1105
|
+
style,
|
|
1106
|
+
contentStyle,
|
|
1107
|
+
textStyle,
|
|
1108
|
+
accessibilityLabel,
|
|
1109
|
+
accessibilityState,
|
|
1110
|
+
hitSlop,
|
|
1111
|
+
onPress,
|
|
1112
|
+
testID,
|
|
1113
|
+
...pressableProps
|
|
1114
|
+
}: ButtonProps,
|
|
1115
|
+
ref: React.ForwardedRef<ButtonRef>
|
|
1116
|
+
) {
|
|
1117
|
+
const frame = useButtonFrame({
|
|
1118
|
+
accessibilityLabel,
|
|
1119
|
+
block,
|
|
1120
|
+
border,
|
|
1121
|
+
children,
|
|
1122
|
+
color,
|
|
1123
|
+
colors,
|
|
1124
|
+
disabled,
|
|
1125
|
+
gradient,
|
|
1126
|
+
icon,
|
|
1127
|
+
iconOnly,
|
|
1128
|
+
iconPlacement,
|
|
1129
|
+
layout,
|
|
1130
|
+
loading,
|
|
1131
|
+
shadow,
|
|
1132
|
+
shape,
|
|
1133
|
+
size,
|
|
1134
|
+
textStyle,
|
|
1135
|
+
tone,
|
|
1136
|
+
variant,
|
|
1137
|
+
});
|
|
1138
|
+
const {
|
|
1139
|
+
borderStyle,
|
|
1140
|
+
contentSizeStyle,
|
|
1141
|
+
defaultHitSlop,
|
|
1142
|
+
gradientCfg,
|
|
1143
|
+
hasIcon,
|
|
1144
|
+
iconBaseBoxStyle,
|
|
1145
|
+
iconPlacedBoxStyle,
|
|
1146
|
+
interactionDisabled,
|
|
1147
|
+
LinearGradientComponent,
|
|
1148
|
+
paddingStyle,
|
|
1149
|
+
radiusStyle,
|
|
1150
|
+
resolvedAccessibilityLabel,
|
|
1151
|
+
resolvedText,
|
|
1152
|
+
rootSizeStyle,
|
|
1153
|
+
shadowStyle,
|
|
1154
|
+
visualColors,
|
|
1155
|
+
visualDisabled,
|
|
1156
|
+
webCursorStyle,
|
|
1157
|
+
} = frame;
|
|
1158
|
+
|
|
1159
|
+
const handlePress = React.useCallback(
|
|
1160
|
+
(event: GestureResponderEvent) => {
|
|
1161
|
+
if (interactionDisabled) return;
|
|
1162
|
+
onPress?.(event);
|
|
1163
|
+
},
|
|
1164
|
+
[interactionDisabled, onPress]
|
|
1165
|
+
);
|
|
1166
|
+
const pressOpacityEnabled = !interactionDisabled && (pressEffect === 'auto' || pressEffect === 'opacity');
|
|
1167
|
+
const rootStyle = React.useCallback(
|
|
1168
|
+
({ pressed }: PressableStateCallbackType) => [
|
|
1169
|
+
styles.root,
|
|
1170
|
+
rootSizeStyle,
|
|
1171
|
+
radiusStyle,
|
|
1172
|
+
shadowStyle,
|
|
1173
|
+
visualDisabled ? styles.disabledRoot : null,
|
|
1174
|
+
pressOpacityEnabled && pressed ? styles.pressedRoot : null,
|
|
1175
|
+
webCursorStyle,
|
|
1176
|
+
style,
|
|
1177
|
+
],
|
|
1178
|
+
[
|
|
1179
|
+
pressOpacityEnabled,
|
|
1180
|
+
radiusStyle,
|
|
1181
|
+
rootSizeStyle,
|
|
1182
|
+
shadowStyle,
|
|
1183
|
+
style,
|
|
1184
|
+
visualDisabled,
|
|
1185
|
+
webCursorStyle,
|
|
1186
|
+
]
|
|
1187
|
+
);
|
|
1188
|
+
|
|
1189
|
+
return (
|
|
1190
|
+
<Pressable
|
|
1191
|
+
{...pressableProps}
|
|
1192
|
+
ref={ref}
|
|
1193
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
1194
|
+
accessibilityRole="button"
|
|
1195
|
+
accessibilityState={{
|
|
1196
|
+
...accessibilityState,
|
|
1197
|
+
busy: Boolean(loading || accessibilityState?.busy),
|
|
1198
|
+
disabled: Boolean(interactionDisabled || accessibilityState?.disabled),
|
|
1199
|
+
}}
|
|
1200
|
+
disabled={interactionDisabled}
|
|
1201
|
+
hitSlop={hitSlop ?? defaultHitSlop}
|
|
1202
|
+
onPress={handlePress}
|
|
1203
|
+
style={rootStyle}
|
|
1204
|
+
testID={testID}
|
|
1205
|
+
>
|
|
1206
|
+
<View
|
|
1207
|
+
pointerEvents="none"
|
|
1208
|
+
style={[
|
|
1209
|
+
styles.content,
|
|
1210
|
+
contentSizeStyle,
|
|
1211
|
+
paddingStyle,
|
|
1212
|
+
radiusStyle,
|
|
1213
|
+
borderStyle,
|
|
1214
|
+
{ backgroundColor: visualColors.backgroundColor },
|
|
1215
|
+
contentStyle,
|
|
1216
|
+
]}
|
|
1217
|
+
>
|
|
1218
|
+
{LinearGradientComponent && gradientCfg ? (
|
|
1219
|
+
<LinearGradientComponent
|
|
1220
|
+
pointerEvents="none"
|
|
1221
|
+
colors={gradientCfg.colors}
|
|
1222
|
+
start={gradientCfg.start}
|
|
1223
|
+
end={gradientCfg.end}
|
|
1224
|
+
style={[StyleSheet.absoluteFillObject, radiusStyle]}
|
|
1225
|
+
/>
|
|
1226
|
+
) : null}
|
|
1227
|
+
|
|
1228
|
+
{iconOnly ? (
|
|
1229
|
+
(icon ?? resolvedText)
|
|
1230
|
+
) : (
|
|
1231
|
+
<>
|
|
1232
|
+
{icon && iconPlacement === 'start' ? (
|
|
1233
|
+
<View style={[iconBaseBoxStyle, iconPlacedBoxStyle]}>{icon}</View>
|
|
1234
|
+
) : null}
|
|
1235
|
+
{resolvedText}
|
|
1236
|
+
{icon && iconPlacement === 'end' ? (
|
|
1237
|
+
<View style={[iconBaseBoxStyle, iconPlacedBoxStyle]}>{icon}</View>
|
|
1238
|
+
) : null}
|
|
1239
|
+
</>
|
|
1240
|
+
)}
|
|
1241
|
+
</View>
|
|
1242
|
+
</Pressable>
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function ButtonAnimatedImpl(
|
|
1247
|
+
{
|
|
1248
|
+
variant = 'solid',
|
|
1249
|
+
tone = 'primary',
|
|
1250
|
+
size = 'md',
|
|
1251
|
+
shape = 'rounded',
|
|
1252
|
+
block = false,
|
|
1253
|
+
disabled = false,
|
|
1254
|
+
loading = false,
|
|
1255
|
+
loadingMode = 'inline',
|
|
1256
|
+
pressEffect = 'auto',
|
|
1257
|
+
icon,
|
|
1258
|
+
iconPlacement = 'start',
|
|
1259
|
+
iconOnly = false,
|
|
1260
|
+
color,
|
|
1261
|
+
colors,
|
|
1262
|
+
border,
|
|
1263
|
+
layout,
|
|
1264
|
+
gradient,
|
|
1265
|
+
shadow = 'none',
|
|
1266
|
+
children,
|
|
1267
|
+
style,
|
|
1268
|
+
contentStyle,
|
|
1269
|
+
textStyle,
|
|
1270
|
+
accessibilityLabel,
|
|
1271
|
+
accessibilityState,
|
|
1272
|
+
hitSlop,
|
|
1273
|
+
onPress,
|
|
1274
|
+
onPressIn,
|
|
1275
|
+
onPressOut,
|
|
1276
|
+
testID,
|
|
1277
|
+
...pressableProps
|
|
1278
|
+
}: ButtonProps,
|
|
1279
|
+
ref: React.ForwardedRef<ButtonRef>
|
|
1280
|
+
) {
|
|
1281
|
+
const resolvedLoadingMode = iconOnly ? 'overlay' : loadingMode;
|
|
1282
|
+
const centeredLoading = resolvedLoadingMode !== 'inline';
|
|
1283
|
+
const frame = useButtonFrame({
|
|
1284
|
+
accessibilityLabel,
|
|
1285
|
+
block,
|
|
1286
|
+
border,
|
|
1287
|
+
children,
|
|
1288
|
+
color,
|
|
1289
|
+
colors,
|
|
1290
|
+
disabled,
|
|
1291
|
+
gradient,
|
|
1292
|
+
icon,
|
|
1293
|
+
iconOnly,
|
|
1294
|
+
iconPlacement,
|
|
1295
|
+
layout,
|
|
1296
|
+
loading,
|
|
1297
|
+
shadow,
|
|
1298
|
+
shape,
|
|
1299
|
+
size,
|
|
1300
|
+
textStyle,
|
|
1301
|
+
tone,
|
|
1302
|
+
variant,
|
|
1303
|
+
});
|
|
1304
|
+
const {
|
|
1305
|
+
accentColor,
|
|
1306
|
+
borderStyle,
|
|
1307
|
+
contentGap,
|
|
1308
|
+
contentSizeStyle,
|
|
1309
|
+
defaultHitSlop,
|
|
1310
|
+
gradientCfg,
|
|
1311
|
+
hasGradientBackground,
|
|
1312
|
+
hasIcon,
|
|
1313
|
+
hasText,
|
|
1314
|
+
iconBaseBoxStyle,
|
|
1315
|
+
iconPlacedBoxStyle,
|
|
1316
|
+
iconSize,
|
|
1317
|
+
interactionDisabled,
|
|
1318
|
+
LinearGradientComponent,
|
|
1319
|
+
metrics,
|
|
1320
|
+
paddingStyle,
|
|
1321
|
+
radiusStyle,
|
|
1322
|
+
resolvedAccessibilityLabel,
|
|
1323
|
+
resolvedText,
|
|
1324
|
+
rootSizeStyle,
|
|
1325
|
+
shadowStyle,
|
|
1326
|
+
visualColors,
|
|
1327
|
+
visualDisabled,
|
|
1328
|
+
webCursorStyle,
|
|
1329
|
+
} = frame;
|
|
1330
|
+
|
|
1331
|
+
const loadingSize = React.useMemo(
|
|
1332
|
+
() =>
|
|
1333
|
+
resolveLoadingSize({
|
|
1334
|
+
iconOnly,
|
|
1335
|
+
layout,
|
|
1336
|
+
metrics,
|
|
1337
|
+
variant,
|
|
1338
|
+
}),
|
|
1339
|
+
[iconOnly, layout, metrics, variant]
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1037
1342
|
const pressSv = useSharedValue(0);
|
|
1038
1343
|
const loadingSv = useSharedValue(loading ? 1 : 0);
|
|
1039
1344
|
const [spinnerMounted, setSpinnerMounted] = React.useState(loading);
|
|
@@ -1075,23 +1380,32 @@ function ButtonImpl(
|
|
|
1075
1380
|
|
|
1076
1381
|
if (loading) {
|
|
1077
1382
|
setSpinnerMounted(true);
|
|
1383
|
+
loadingSv.value = withTiming(1, LOADING_TIMING);
|
|
1384
|
+
return () => {
|
|
1385
|
+
if (hideSpinnerTimerRef.current) {
|
|
1386
|
+
clearTimeout(hideSpinnerTimerRef.current);
|
|
1387
|
+
hideSpinnerTimerRef.current = null;
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1078
1390
|
}
|
|
1079
1391
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
hideSpinnerTimerRef.current = setTimeout(() => {
|
|
1084
|
-
setSpinnerMounted(false);
|
|
1085
|
-
}, LOADING_TIMING.duration);
|
|
1392
|
+
if (!spinnerMounted) {
|
|
1393
|
+
loadingSv.value = 0;
|
|
1394
|
+
return undefined;
|
|
1086
1395
|
}
|
|
1087
1396
|
|
|
1397
|
+
loadingSv.value = withTiming(0, LOADING_TIMING);
|
|
1398
|
+
hideSpinnerTimerRef.current = setTimeout(() => {
|
|
1399
|
+
setSpinnerMounted(false);
|
|
1400
|
+
}, LOADING_TIMING.duration);
|
|
1401
|
+
|
|
1088
1402
|
return () => {
|
|
1089
1403
|
if (hideSpinnerTimerRef.current) {
|
|
1090
1404
|
clearTimeout(hideSpinnerTimerRef.current);
|
|
1091
1405
|
hideSpinnerTimerRef.current = null;
|
|
1092
1406
|
}
|
|
1093
1407
|
};
|
|
1094
|
-
}, [loading, loadingSv]);
|
|
1408
|
+
}, [loading, loadingSv, spinnerMounted]);
|
|
1095
1409
|
|
|
1096
1410
|
const rootAnimatedStyle = useAnimatedStyle(() => {
|
|
1097
1411
|
const baseOpacity = visualDisabled ? DISABLED_OPACITY : 1;
|
|
@@ -1311,6 +1625,20 @@ function ButtonImpl(
|
|
|
1311
1625
|
);
|
|
1312
1626
|
}
|
|
1313
1627
|
|
|
1628
|
+
const ButtonStaticWithRef = React.forwardRef<ButtonRef, ButtonProps>(ButtonStaticImpl);
|
|
1629
|
+
ButtonStaticWithRef.displayName = 'ButtonStatic';
|
|
1630
|
+
|
|
1631
|
+
const ButtonAnimatedWithRef = React.forwardRef<ButtonRef, ButtonProps>(ButtonAnimatedImpl);
|
|
1632
|
+
ButtonAnimatedWithRef.displayName = 'ButtonAnimated';
|
|
1633
|
+
|
|
1634
|
+
function ButtonImpl(props: ButtonProps, ref: React.ForwardedRef<ButtonRef>) {
|
|
1635
|
+
if (shouldUseStaticButtonPath(props)) {
|
|
1636
|
+
return <ButtonStaticWithRef {...props} ref={ref} />;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
return <ButtonAnimatedWithRef {...props} ref={ref} />;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1314
1642
|
const ButtonWithRef = React.forwardRef<ButtonRef, ButtonProps>(ButtonImpl);
|
|
1315
1643
|
ButtonWithRef.displayName = 'Button';
|
|
1316
1644
|
|
|
@@ -1342,6 +1670,12 @@ const styles = StyleSheet.create({
|
|
|
1342
1670
|
textDecorationLine: 'underline',
|
|
1343
1671
|
},
|
|
1344
1672
|
root: {},
|
|
1673
|
+
disabledRoot: {
|
|
1674
|
+
opacity: DISABLED_OPACITY,
|
|
1675
|
+
},
|
|
1676
|
+
pressedRoot: {
|
|
1677
|
+
opacity: PRESSED_OPACITY,
|
|
1678
|
+
},
|
|
1345
1679
|
spinnerBox: {
|
|
1346
1680
|
alignItems: 'center',
|
|
1347
1681
|
justifyContent: 'center',
|
|
@@ -10,6 +10,7 @@ Checkbox 是三端一致的复选框组件,适合协议勾选、多选列表
|
|
|
10
10
|
- 非受控模式先在 UI 线程推进动画,再同步内部状态
|
|
11
11
|
- `CheckboxGroup` 使用 `value/defaultValue/onChange` 管理选中集合
|
|
12
12
|
- 默认触控热区至少补到 `wp(44)`,小尺寸也保持可点性
|
|
13
|
+
- 自定义 `layout.indicatorSize` 但未传 `layout.indicatorIconSize` 时,默认勾号会按最终指示器尺寸等比缩放并取整
|
|
13
14
|
|
|
14
15
|
## 基础用法
|
|
15
16
|
|
|
@@ -102,6 +103,8 @@ export function Demo() {
|
|
|
102
103
|
/>
|
|
103
104
|
```
|
|
104
105
|
|
|
106
|
+
只覆盖 `indicatorSize` 时,内置勾号尺寸会随最终指示器尺寸按比例取整;显式传入 `indicatorIconSize` 或 `indicatorRadius` 时仍以调用侧配置为准。`shape="circle"` 且未传 `indicatorRadius` 时,默认圆角会对 `indicatorSize / 2` 取整,减少 17、18 等小尺寸下的半像素圆角毛边。
|
|
107
|
+
|
|
105
108
|
## 自定义内容
|
|
106
109
|
|
|
107
110
|
`children` 支持 render-prop。根节点本身就是可点击区域,子节点通常只读取状态做视觉变化,不需要再绑定一次点击。
|
|
@@ -149,7 +152,7 @@ Checkbox 透传 React Native `Pressable` 的常用属性和事件;组件自身
|
|
|
149
152
|
- `shape?: 'rounded' | 'square' | 'circle'`:指示器形状,默认 `'rounded'`。
|
|
150
153
|
- `color?: string`:主色覆盖,支持主题 token、语义色或颜色字符串。
|
|
151
154
|
- `colors?: CheckboxColors`:结构化颜色覆盖。
|
|
152
|
-
- `layout?: CheckboxLayout
|
|
155
|
+
- `layout?: CheckboxLayout`:结构化尺寸覆盖;自定义 `indicatorSize` 且未显式传 `indicatorIconSize` 时,勾号默认随最终尺寸缩放。
|
|
153
156
|
- `duration?: number`:状态切换动画时长,默认 `170`。
|
|
154
157
|
|
|
155
158
|
### 内容
|
|
@@ -274,6 +274,7 @@ const SIZE_TOKENS = {
|
|
|
274
274
|
const DISABLED_OPACITY = 0.48;
|
|
275
275
|
const PRESSED_OPACITY = 0.86;
|
|
276
276
|
const PRESSED_SCALE = 0.965;
|
|
277
|
+
const CUSTOM_INDICATOR_ICON_SIZE_RATIO = 0.625;
|
|
277
278
|
const VALUE_TIMING_DURATION = 170;
|
|
278
279
|
const PRESS_IN_DURATION = 90;
|
|
279
280
|
const PRESS_OUT_DURATION = 140;
|
|
@@ -438,9 +439,19 @@ function resolveCheckboxMetrics(
|
|
|
438
439
|
layout: CheckboxLayout | undefined
|
|
439
440
|
): CheckboxMetrics {
|
|
440
441
|
const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
|
|
441
|
-
const
|
|
442
|
+
const tokenIndicatorSize = wp(token.indicatorSize);
|
|
443
|
+
const tokenIndicatorIconSize = wp(token.indicatorIconSize);
|
|
444
|
+
const hasCustomIndicatorSize = isPositiveNumber(layout?.indicatorSize);
|
|
445
|
+
const indicatorSize = resolvePositiveNumber(layout?.indicatorSize, tokenIndicatorSize);
|
|
446
|
+
const fallbackIconSize = hasCustomIndicatorSize
|
|
447
|
+
? Math.max(wp(1), Math.round(indicatorSize * CUSTOM_INDICATOR_ICON_SIZE_RATIO))
|
|
448
|
+
: tokenIndicatorIconSize;
|
|
442
449
|
const fallbackRadius =
|
|
443
|
-
shape === 'circle'
|
|
450
|
+
shape === 'circle'
|
|
451
|
+
? Math.round(indicatorSize / 2)
|
|
452
|
+
: shape === 'square'
|
|
453
|
+
? 0
|
|
454
|
+
: wp(token.indicatorRadius);
|
|
444
455
|
|
|
445
456
|
return {
|
|
446
457
|
indicatorSize,
|
|
@@ -449,7 +460,7 @@ function resolveCheckboxMetrics(
|
|
|
449
460
|
layout?.indicatorBorderWidth,
|
|
450
461
|
wp(token.indicatorBorderWidth)
|
|
451
462
|
),
|
|
452
|
-
indicatorIconSize: resolvePositiveNumber(layout?.indicatorIconSize,
|
|
463
|
+
indicatorIconSize: resolvePositiveNumber(layout?.indicatorIconSize, fallbackIconSize),
|
|
453
464
|
gap: resolveNonNegativeNumber(layout?.gap, wp(token.gap)),
|
|
454
465
|
minTouchTarget: resolvePositiveNumber(layout?.minTouchTarget, wp(44)),
|
|
455
466
|
focusRingWidth: resolvePositiveNumber(layout?.focusRingWidth, wp(2)),
|
|
@@ -457,8 +468,12 @@ function resolveCheckboxMetrics(
|
|
|
457
468
|
};
|
|
458
469
|
}
|
|
459
470
|
|
|
471
|
+
function isPositiveNumber(value: number | undefined): value is number {
|
|
472
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
460
475
|
function resolvePositiveNumber(value: number | undefined, fallback: number) {
|
|
461
|
-
return
|
|
476
|
+
return isPositiveNumber(value) ? value : fallback;
|
|
462
477
|
}
|
|
463
478
|
|
|
464
479
|
function resolveNonNegativeNumber(value: number | undefined, fallback: number) {
|
package/src/ui/Radio/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Radio 是三端一致的单选组件,适合互斥选项、设置项模式选
|
|
|
11
11
|
- 已选项默认再次点击不取消;筛选类场景需要清空时显式开启 `allowDeselect`
|
|
12
12
|
- `size/tone/variant` 提供语义化外观,`colors/layout` 作为集中 escape hatch
|
|
13
13
|
- 默认触控热区至少补到 `wp(44)`,小尺寸也保持可点性
|
|
14
|
+
- 自定义 `layout.indicatorSize` 但未传 `layout.indicatorDotSize` 时,默认内点会按最终指示器尺寸等比缩放并取整
|
|
14
15
|
|
|
15
16
|
## RadioGroup
|
|
16
17
|
|
|
@@ -82,6 +83,8 @@ Radio 默认遵循单选控件直觉:点击已选项不会取消。需要筛
|
|
|
82
83
|
/>
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
只覆盖 `indicatorSize` 时,内置圆点尺寸会随最终指示器尺寸按比例取整;显式传入 `indicatorDotSize` 时仍以调用侧配置为准。Radio 外圈默认圆角会对 `indicatorSize / 2` 取整,减少 17、18 等小尺寸下的半像素圆角毛边。
|
|
87
|
+
|
|
85
88
|
## 自定义内容
|
|
86
89
|
|
|
87
90
|
`children` 支持 render-prop。根节点本身就是可点击区域,子节点通常只读取状态做视觉变化,不需要再绑定一次点击。
|
|
@@ -148,7 +151,7 @@ Radio 透传 React Native `Pressable` 的常用属性和事件;组件自身固
|
|
|
148
151
|
- `variant?: 'outline' | 'soft' | 'solid'`:选中态视觉样式,默认 `'outline'`。
|
|
149
152
|
- `color?: string`:主色覆盖,支持主题 token、语义色或颜色字符串。
|
|
150
153
|
- `colors?: RadioColors`:结构化颜色覆盖。
|
|
151
|
-
- `layout?: RadioLayout
|
|
154
|
+
- `layout?: RadioLayout`:结构化尺寸覆盖;自定义 `indicatorSize` 且未显式传 `indicatorDotSize` 时,圆点默认随最终尺寸缩放。
|
|
152
155
|
- `duration?: number`:状态切换动画时长,默认 `170`。
|
|
153
156
|
|
|
154
157
|
### 内容
|
package/src/ui/Radio/index.tsx
CHANGED
|
@@ -72,6 +72,7 @@ export type RadioSlotProps = {
|
|
|
72
72
|
|
|
73
73
|
type RadioMetrics = {
|
|
74
74
|
indicatorSize: number;
|
|
75
|
+
indicatorRadius: number;
|
|
75
76
|
indicatorDotSize: number;
|
|
76
77
|
indicatorBorderWidth: number;
|
|
77
78
|
gap: number;
|
|
@@ -233,12 +234,16 @@ const SIZE_TOKENS = {
|
|
|
233
234
|
},
|
|
234
235
|
} as const satisfies Record<
|
|
235
236
|
RadioSize,
|
|
236
|
-
Omit<
|
|
237
|
+
Omit<
|
|
238
|
+
RadioMetrics,
|
|
239
|
+
'indicatorRadius' | 'minTouchTarget' | 'focusRingWidth' | 'focusRingOffset'
|
|
240
|
+
>
|
|
237
241
|
>;
|
|
238
242
|
|
|
239
243
|
const DISABLED_OPACITY = 0.48;
|
|
240
244
|
const PRESSED_OPACITY = 0.86;
|
|
241
245
|
const PRESSED_SCALE = 0.965;
|
|
246
|
+
const CUSTOM_INDICATOR_DOT_SIZE_RATIO = 0.45;
|
|
242
247
|
const VALUE_TIMING_DURATION = 170;
|
|
243
248
|
const PRESS_IN_DURATION = 90;
|
|
244
249
|
const PRESS_OUT_DURATION = 140;
|
|
@@ -377,14 +382,18 @@ function resolveVariantColors(
|
|
|
377
382
|
|
|
378
383
|
function resolveRadioMetrics(size: RadioSize, layout: RadioLayout | undefined): RadioMetrics {
|
|
379
384
|
const token = SIZE_TOKENS[size] ?? SIZE_TOKENS.md;
|
|
380
|
-
const
|
|
385
|
+
const tokenIndicatorSize = wp(token.indicatorSize);
|
|
386
|
+
const tokenIndicatorDotSize = wp(token.indicatorDotSize);
|
|
387
|
+
const hasCustomIndicatorSize = isPositiveNumber(layout?.indicatorSize);
|
|
388
|
+
const indicatorSize = resolvePositiveNumber(layout?.indicatorSize, tokenIndicatorSize);
|
|
389
|
+
const fallbackDotSize = hasCustomIndicatorSize
|
|
390
|
+
? Math.max(wp(1), Math.round(indicatorSize * CUSTOM_INDICATOR_DOT_SIZE_RATIO))
|
|
391
|
+
: tokenIndicatorDotSize;
|
|
381
392
|
|
|
382
393
|
return {
|
|
383
394
|
indicatorSize,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
wp(token.indicatorDotSize)
|
|
387
|
-
),
|
|
395
|
+
indicatorRadius: Math.round(indicatorSize / 2),
|
|
396
|
+
indicatorDotSize: resolvePositiveNumber(layout?.indicatorDotSize, fallbackDotSize),
|
|
388
397
|
indicatorBorderWidth: resolvePositiveNumber(
|
|
389
398
|
layout?.indicatorBorderWidth,
|
|
390
399
|
wp(token.indicatorBorderWidth)
|
|
@@ -396,8 +405,12 @@ function resolveRadioMetrics(size: RadioSize, layout: RadioLayout | undefined):
|
|
|
396
405
|
};
|
|
397
406
|
}
|
|
398
407
|
|
|
408
|
+
function isPositiveNumber(value: number | undefined): value is number {
|
|
409
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
399
412
|
function resolvePositiveNumber(value: number | undefined, fallback: number) {
|
|
400
|
-
return
|
|
413
|
+
return isPositiveNumber(value) ? value : fallback;
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
function resolveNonNegativeNumber(value: number | undefined, fallback: number) {
|
|
@@ -798,10 +811,10 @@ function RadioImpl(
|
|
|
798
811
|
() => ({
|
|
799
812
|
width: metrics.indicatorSize,
|
|
800
813
|
height: metrics.indicatorSize,
|
|
801
|
-
borderRadius: metrics.
|
|
814
|
+
borderRadius: metrics.indicatorRadius,
|
|
802
815
|
borderWidth: metrics.indicatorBorderWidth,
|
|
803
816
|
}),
|
|
804
|
-
[metrics.indicatorBorderWidth, metrics.indicatorSize]
|
|
817
|
+
[metrics.indicatorBorderWidth, metrics.indicatorRadius, metrics.indicatorSize]
|
|
805
818
|
);
|
|
806
819
|
|
|
807
820
|
const indicatorNode = showIndicator ? (
|
|
@@ -823,7 +836,7 @@ function RadioImpl(
|
|
|
823
836
|
bottom: -metrics.focusRingOffset,
|
|
824
837
|
left: -metrics.focusRingOffset,
|
|
825
838
|
right: -metrics.focusRingOffset,
|
|
826
|
-
borderRadius: metrics.
|
|
839
|
+
borderRadius: metrics.indicatorRadius + metrics.focusRingOffset,
|
|
827
840
|
borderWidth: metrics.focusRingWidth,
|
|
828
841
|
borderColor: resolvedColors.focusRing,
|
|
829
842
|
},
|