@boostdev/design-system-react-native 0.2.1 → 0.2.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/index.cjs +196 -135
- package/dist/index.js +196 -135
- package/package.json +1 -1
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +28 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +31 -13
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +25 -8
- package/src/components/interaction/form/FormInput/FormInput.tsx +52 -31
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +24 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +45 -27
package/dist/index.cjs
CHANGED
|
@@ -2682,24 +2682,36 @@ function FileInput({
|
|
|
2682
2682
|
var import_react9 = require("react");
|
|
2683
2683
|
var import_react_native37 = require("react-native");
|
|
2684
2684
|
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
2685
|
+
var INPUT_HEIGHT = 44;
|
|
2685
2686
|
var MAX_DROPDOWN_HEIGHT = 200;
|
|
2686
2687
|
var styles33 = import_react_native37.StyleSheet.create({
|
|
2687
2688
|
container: {
|
|
2688
2689
|
gap: import_native.spacing.xxs
|
|
2689
2690
|
},
|
|
2691
|
+
/**
|
|
2692
|
+
* Draws the pill border and provides the fixed 44pt touch-target height.
|
|
2693
|
+
* justifyContent: 'center' vertically centers the inner TextInput.
|
|
2694
|
+
* position: 'relative' is required for the dropdown's absolute positioning.
|
|
2695
|
+
*/
|
|
2690
2696
|
inputWrapper: {
|
|
2691
|
-
position: "relative"
|
|
2697
|
+
position: "relative",
|
|
2698
|
+
height: INPUT_HEIGHT,
|
|
2699
|
+
justifyContent: "center",
|
|
2700
|
+
borderWidth: 1,
|
|
2701
|
+
borderRadius: import_native.border.radius.full
|
|
2692
2702
|
},
|
|
2703
|
+
/**
|
|
2704
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2705
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
2706
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
2707
|
+
*/
|
|
2693
2708
|
input: {
|
|
2694
2709
|
fontFamily: import_native.font.family.body,
|
|
2695
2710
|
fontSize: import_native.font.size.body,
|
|
2696
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
2697
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
2698
|
-
paddingVertical: import_native.spacing.xs,
|
|
2699
2711
|
paddingHorizontal: import_native.spacing.s,
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2712
|
+
paddingVertical: 0,
|
|
2713
|
+
textAlignVertical: "center",
|
|
2714
|
+
includeFontPadding: false
|
|
2703
2715
|
},
|
|
2704
2716
|
dropdown: {
|
|
2705
2717
|
position: "absolute",
|
|
@@ -2768,80 +2780,88 @@ function Combobox({
|
|
|
2768
2780
|
const borderColor = error ? colors2.colorError : colors2.colorOnBg;
|
|
2769
2781
|
return /* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(import_react_native37.View, { style: [styles33.container, style], children: [
|
|
2770
2782
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Label, { label }),
|
|
2771
|
-
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
style: [
|
|
2786
|
-
styles33.input,
|
|
2787
|
-
{
|
|
2788
|
-
borderColor,
|
|
2789
|
-
color: colors2.colorOnBg,
|
|
2790
|
-
backgroundColor: colors2.colorBg
|
|
2791
|
-
},
|
|
2792
|
-
disabled && { opacity: 0.4 }
|
|
2793
|
-
]
|
|
2794
|
-
}
|
|
2795
|
-
),
|
|
2796
|
-
isOpen && filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2797
|
-
import_react_native37.ScrollView,
|
|
2798
|
-
{
|
|
2799
|
-
style: [
|
|
2800
|
-
styles33.dropdown,
|
|
2783
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsxs)(
|
|
2784
|
+
import_react_native37.View,
|
|
2785
|
+
{
|
|
2786
|
+
style: [
|
|
2787
|
+
styles33.inputWrapper,
|
|
2788
|
+
{
|
|
2789
|
+
borderColor,
|
|
2790
|
+
backgroundColor: colors2.colorBg
|
|
2791
|
+
},
|
|
2792
|
+
disabled && { opacity: 0.4 }
|
|
2793
|
+
],
|
|
2794
|
+
children: [
|
|
2795
|
+
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2796
|
+
import_react_native37.TextInput,
|
|
2801
2797
|
{
|
|
2802
|
-
|
|
2803
|
-
|
|
2798
|
+
value: inputValue,
|
|
2799
|
+
onChangeText: handleChangeText,
|
|
2800
|
+
onFocus: handleFocus,
|
|
2801
|
+
onBlur: handleBlur,
|
|
2802
|
+
placeholder,
|
|
2803
|
+
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
2804
|
+
editable: !disabled,
|
|
2805
|
+
accessibilityRole: "combobox",
|
|
2806
|
+
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2807
|
+
accessibilityState: { disabled, expanded: isOpen },
|
|
2808
|
+
style: [
|
|
2809
|
+
styles33.input,
|
|
2810
|
+
{ color: colors2.colorOnBg }
|
|
2811
|
+
]
|
|
2804
2812
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
isDisabled && { opacity: 0.4 }
|
|
2824
|
-
],
|
|
2825
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2826
|
-
import_react_native37.Text,
|
|
2813
|
+
),
|
|
2814
|
+
isOpen && filtered.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2815
|
+
import_react_native37.ScrollView,
|
|
2816
|
+
{
|
|
2817
|
+
style: [
|
|
2818
|
+
styles33.dropdown,
|
|
2819
|
+
{
|
|
2820
|
+
borderColor: colors2.colorBgSubtle,
|
|
2821
|
+
backgroundColor: colors2.colorBg
|
|
2822
|
+
}
|
|
2823
|
+
],
|
|
2824
|
+
keyboardShouldPersistTaps: "handled",
|
|
2825
|
+
nestedScrollEnabled: true,
|
|
2826
|
+
children: filtered.map((option) => {
|
|
2827
|
+
const isSelected = option.value === value;
|
|
2828
|
+
const isDisabled = option.disabled;
|
|
2829
|
+
return /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2830
|
+
import_react_native37.Pressable,
|
|
2827
2831
|
{
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2832
|
+
onPress: () => !isDisabled && selectOption(option),
|
|
2833
|
+
disabled: isDisabled,
|
|
2834
|
+
accessibilityRole: "menuitem",
|
|
2835
|
+
accessibilityLabel: option.label,
|
|
2836
|
+
accessibilityState: { selected: isSelected, disabled: isDisabled },
|
|
2837
|
+
style: ({ pressed }) => [
|
|
2838
|
+
styles33.option,
|
|
2839
|
+
isSelected && { backgroundColor: colors2.colorBlueSubtle },
|
|
2840
|
+
pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
|
|
2841
|
+
isDisabled && { opacity: 0.4 }
|
|
2834
2842
|
],
|
|
2835
|
-
children:
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2843
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime37.jsx)(
|
|
2844
|
+
import_react_native37.Text,
|
|
2845
|
+
{
|
|
2846
|
+
style: [
|
|
2847
|
+
styles33.optionText,
|
|
2848
|
+
{
|
|
2849
|
+
color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
|
|
2850
|
+
},
|
|
2851
|
+
isSelected && { fontWeight: import_native.font.weight.semibold }
|
|
2852
|
+
],
|
|
2853
|
+
children: option.label
|
|
2854
|
+
}
|
|
2855
|
+
)
|
|
2856
|
+
},
|
|
2857
|
+
option.value
|
|
2858
|
+
);
|
|
2859
|
+
})
|
|
2860
|
+
}
|
|
2861
|
+
)
|
|
2862
|
+
]
|
|
2863
|
+
}
|
|
2864
|
+
),
|
|
2845
2865
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Message, { type: "error", message: error }),
|
|
2846
2866
|
/* @__PURE__ */ (0, import_jsx_runtime37.jsx)(Message, { type: "hint", message: hint })
|
|
2847
2867
|
] });
|
|
@@ -2929,6 +2949,7 @@ function SegmentedControl({
|
|
|
2929
2949
|
var import_react11 = require("react");
|
|
2930
2950
|
var import_react_native39 = require("react-native");
|
|
2931
2951
|
var import_jsx_runtime39 = require("react/jsx-runtime");
|
|
2952
|
+
var INPUT_HEIGHT2 = 44;
|
|
2932
2953
|
var keyboardTypeMap = {
|
|
2933
2954
|
text: "default",
|
|
2934
2955
|
email: "email-address",
|
|
@@ -2942,16 +2963,28 @@ var styles35 = import_react_native39.StyleSheet.create({
|
|
|
2942
2963
|
container: {
|
|
2943
2964
|
gap: import_native.spacing.xxs
|
|
2944
2965
|
},
|
|
2966
|
+
/**
|
|
2967
|
+
* The wrapper draws the pill border and provides the fixed 44pt touch-target height.
|
|
2968
|
+
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
2969
|
+
*/
|
|
2970
|
+
inputWrapper: {
|
|
2971
|
+
height: INPUT_HEIGHT2,
|
|
2972
|
+
justifyContent: "center",
|
|
2973
|
+
borderWidth: 1,
|
|
2974
|
+
borderRadius: import_native.border.radius.full
|
|
2975
|
+
},
|
|
2976
|
+
/**
|
|
2977
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2978
|
+
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
2979
|
+
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
2980
|
+
*/
|
|
2945
2981
|
input: {
|
|
2946
2982
|
fontFamily: import_native.font.family.body,
|
|
2947
2983
|
fontSize: import_native.font.size.body,
|
|
2948
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
2949
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
2950
2984
|
paddingHorizontal: import_native.spacing.s,
|
|
2951
|
-
paddingVertical:
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
minHeight: 44
|
|
2985
|
+
paddingVertical: 0,
|
|
2986
|
+
textAlignVertical: "center",
|
|
2987
|
+
includeFontPadding: false
|
|
2955
2988
|
}
|
|
2956
2989
|
});
|
|
2957
2990
|
function FormInput({
|
|
@@ -2975,38 +3008,46 @@ function FormInput({
|
|
|
2975
3008
|
return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(import_react_native39.View, { style: [styles35.container, style], children: [
|
|
2976
3009
|
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Label, { label }),
|
|
2977
3010
|
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
2978
|
-
import_react_native39.
|
|
3011
|
+
import_react_native39.View,
|
|
2979
3012
|
{
|
|
2980
|
-
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2981
|
-
accessibilityState: { disabled },
|
|
2982
|
-
accessibilityHint: hint,
|
|
2983
|
-
value,
|
|
2984
|
-
defaultValue,
|
|
2985
|
-
placeholder,
|
|
2986
|
-
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
2987
|
-
keyboardType: keyboardTypeMap[type],
|
|
2988
|
-
secureTextEntry: type === "password",
|
|
2989
|
-
autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
|
|
2990
|
-
autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
|
|
2991
|
-
editable: !disabled,
|
|
2992
|
-
onChangeText: onChange,
|
|
2993
|
-
onFocus: () => {
|
|
2994
|
-
setFocused(true);
|
|
2995
|
-
onFocus?.();
|
|
2996
|
-
},
|
|
2997
|
-
onBlur: () => {
|
|
2998
|
-
setFocused(false);
|
|
2999
|
-
onBlur?.();
|
|
3000
|
-
},
|
|
3001
3013
|
style: [
|
|
3002
|
-
styles35.
|
|
3014
|
+
styles35.inputWrapper,
|
|
3003
3015
|
{
|
|
3004
3016
|
borderColor,
|
|
3005
|
-
color: colors2.colorOnBg,
|
|
3006
3017
|
backgroundColor: colors2.colorBg
|
|
3007
3018
|
},
|
|
3008
3019
|
disabled && { opacity: 0.4 }
|
|
3009
|
-
]
|
|
3020
|
+
],
|
|
3021
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
|
|
3022
|
+
import_react_native39.TextInput,
|
|
3023
|
+
{
|
|
3024
|
+
accessibilityLabel: typeof label === "string" ? label : name,
|
|
3025
|
+
accessibilityState: { disabled },
|
|
3026
|
+
accessibilityHint: hint,
|
|
3027
|
+
value,
|
|
3028
|
+
defaultValue,
|
|
3029
|
+
placeholder,
|
|
3030
|
+
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
3031
|
+
keyboardType: keyboardTypeMap[type],
|
|
3032
|
+
secureTextEntry: type === "password",
|
|
3033
|
+
autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
|
|
3034
|
+
autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
|
|
3035
|
+
editable: !disabled,
|
|
3036
|
+
onChangeText: onChange,
|
|
3037
|
+
onFocus: () => {
|
|
3038
|
+
setFocused(true);
|
|
3039
|
+
onFocus?.();
|
|
3040
|
+
},
|
|
3041
|
+
onBlur: () => {
|
|
3042
|
+
setFocused(false);
|
|
3043
|
+
onBlur?.();
|
|
3044
|
+
},
|
|
3045
|
+
style: [
|
|
3046
|
+
styles35.input,
|
|
3047
|
+
{ color: colors2.colorOnBg }
|
|
3048
|
+
]
|
|
3049
|
+
}
|
|
3050
|
+
)
|
|
3010
3051
|
}
|
|
3011
3052
|
),
|
|
3012
3053
|
/* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Message, { type: "error", message: error }),
|
|
@@ -3502,18 +3543,30 @@ var styles39 = import_react_native43.StyleSheet.create({
|
|
|
3502
3543
|
fontWeight: import_native.font.weight.semibold,
|
|
3503
3544
|
lineHeight: 24
|
|
3504
3545
|
},
|
|
3505
|
-
|
|
3546
|
+
/**
|
|
3547
|
+
* The inputWrapper holds the border and fixed height so justifyContent: 'center'
|
|
3548
|
+
* vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
|
|
3549
|
+
*/
|
|
3550
|
+
inputWrapper: {
|
|
3506
3551
|
flex: 1,
|
|
3552
|
+
height: STEPPER_SIZE,
|
|
3553
|
+
justifyContent: "center",
|
|
3554
|
+
borderTopWidth: 1,
|
|
3555
|
+
borderBottomWidth: 1
|
|
3556
|
+
},
|
|
3557
|
+
/**
|
|
3558
|
+
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
3559
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
3560
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
3561
|
+
*/
|
|
3562
|
+
input: {
|
|
3507
3563
|
fontFamily: import_native.font.family.body,
|
|
3508
3564
|
fontSize: import_native.font.size.body,
|
|
3509
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
3510
|
-
// lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
|
|
3511
3565
|
textAlign: "center",
|
|
3512
3566
|
paddingHorizontal: import_native.spacing.xs,
|
|
3513
|
-
paddingVertical:
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
minHeight: STEPPER_SIZE
|
|
3567
|
+
paddingVertical: 0,
|
|
3568
|
+
textAlignVertical: "center",
|
|
3569
|
+
includeFontPadding: false
|
|
3517
3570
|
}
|
|
3518
3571
|
});
|
|
3519
3572
|
function NumberInput({
|
|
@@ -3579,32 +3632,40 @@ function NumberInput({
|
|
|
3579
3632
|
}
|
|
3580
3633
|
),
|
|
3581
3634
|
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
|
3582
|
-
import_react_native43.
|
|
3635
|
+
import_react_native43.View,
|
|
3583
3636
|
{
|
|
3584
|
-
accessibilityLabel: label,
|
|
3585
|
-
accessibilityState: { disabled },
|
|
3586
|
-
accessibilityValue: {
|
|
3587
|
-
min,
|
|
3588
|
-
max,
|
|
3589
|
-
now: currentValue,
|
|
3590
|
-
text: String(currentValue)
|
|
3591
|
-
},
|
|
3592
|
-
keyboardType: "numeric",
|
|
3593
|
-
value: String(currentValue),
|
|
3594
|
-
editable: !disabled,
|
|
3595
|
-
onChangeText: (text) => {
|
|
3596
|
-
const parsed = parseFloat(text);
|
|
3597
|
-
const safe = isNaN(parsed) ? 0 : parsed;
|
|
3598
|
-
update(safe);
|
|
3599
|
-
},
|
|
3600
3637
|
style: [
|
|
3601
|
-
styles39.
|
|
3638
|
+
styles39.inputWrapper,
|
|
3602
3639
|
{
|
|
3603
3640
|
borderColor,
|
|
3604
|
-
color: colors2.colorOnBg,
|
|
3605
3641
|
backgroundColor: colors2.colorBg
|
|
3606
3642
|
}
|
|
3607
|
-
]
|
|
3643
|
+
],
|
|
3644
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
|
3645
|
+
import_react_native43.TextInput,
|
|
3646
|
+
{
|
|
3647
|
+
accessibilityLabel: label,
|
|
3648
|
+
accessibilityState: { disabled },
|
|
3649
|
+
accessibilityValue: {
|
|
3650
|
+
min,
|
|
3651
|
+
max,
|
|
3652
|
+
now: currentValue,
|
|
3653
|
+
text: String(currentValue)
|
|
3654
|
+
},
|
|
3655
|
+
keyboardType: "numeric",
|
|
3656
|
+
value: String(currentValue),
|
|
3657
|
+
editable: !disabled,
|
|
3658
|
+
onChangeText: (text) => {
|
|
3659
|
+
const parsed = parseFloat(text);
|
|
3660
|
+
const safe = isNaN(parsed) ? 0 : parsed;
|
|
3661
|
+
update(safe);
|
|
3662
|
+
},
|
|
3663
|
+
style: [
|
|
3664
|
+
styles39.input,
|
|
3665
|
+
{ color: colors2.colorOnBg }
|
|
3666
|
+
]
|
|
3667
|
+
}
|
|
3668
|
+
)
|
|
3608
3669
|
}
|
|
3609
3670
|
),
|
|
3610
3671
|
/* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
|
package/dist/index.js
CHANGED
|
@@ -2635,24 +2635,36 @@ import {
|
|
|
2635
2635
|
ScrollView as ScrollView4
|
|
2636
2636
|
} from "react-native";
|
|
2637
2637
|
import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2638
|
+
var INPUT_HEIGHT = 44;
|
|
2638
2639
|
var MAX_DROPDOWN_HEIGHT = 200;
|
|
2639
2640
|
var styles33 = StyleSheet33.create({
|
|
2640
2641
|
container: {
|
|
2641
2642
|
gap: spacing.xxs
|
|
2642
2643
|
},
|
|
2644
|
+
/**
|
|
2645
|
+
* Draws the pill border and provides the fixed 44pt touch-target height.
|
|
2646
|
+
* justifyContent: 'center' vertically centers the inner TextInput.
|
|
2647
|
+
* position: 'relative' is required for the dropdown's absolute positioning.
|
|
2648
|
+
*/
|
|
2643
2649
|
inputWrapper: {
|
|
2644
|
-
position: "relative"
|
|
2650
|
+
position: "relative",
|
|
2651
|
+
height: INPUT_HEIGHT,
|
|
2652
|
+
justifyContent: "center",
|
|
2653
|
+
borderWidth: 1,
|
|
2654
|
+
borderRadius: border.radius.full
|
|
2645
2655
|
},
|
|
2656
|
+
/**
|
|
2657
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2658
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
2659
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
2660
|
+
*/
|
|
2646
2661
|
input: {
|
|
2647
2662
|
fontFamily: font.family.body,
|
|
2648
2663
|
fontSize: font.size.body,
|
|
2649
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
2650
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
2651
|
-
paddingVertical: spacing.xs,
|
|
2652
2664
|
paddingHorizontal: spacing.s,
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2665
|
+
paddingVertical: 0,
|
|
2666
|
+
textAlignVertical: "center",
|
|
2667
|
+
includeFontPadding: false
|
|
2656
2668
|
},
|
|
2657
2669
|
dropdown: {
|
|
2658
2670
|
position: "absolute",
|
|
@@ -2721,80 +2733,88 @@ function Combobox({
|
|
|
2721
2733
|
const borderColor = error ? colors2.colorError : colors2.colorOnBg;
|
|
2722
2734
|
return /* @__PURE__ */ jsxs23(View30, { style: [styles33.container, style], children: [
|
|
2723
2735
|
/* @__PURE__ */ jsx37(Label, { label }),
|
|
2724
|
-
/* @__PURE__ */ jsxs23(
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
style: [
|
|
2739
|
-
styles33.input,
|
|
2740
|
-
{
|
|
2741
|
-
borderColor,
|
|
2742
|
-
color: colors2.colorOnBg,
|
|
2743
|
-
backgroundColor: colors2.colorBg
|
|
2744
|
-
},
|
|
2745
|
-
disabled && { opacity: 0.4 }
|
|
2746
|
-
]
|
|
2747
|
-
}
|
|
2748
|
-
),
|
|
2749
|
-
isOpen && filtered.length > 0 && /* @__PURE__ */ jsx37(
|
|
2750
|
-
ScrollView4,
|
|
2751
|
-
{
|
|
2752
|
-
style: [
|
|
2753
|
-
styles33.dropdown,
|
|
2736
|
+
/* @__PURE__ */ jsxs23(
|
|
2737
|
+
View30,
|
|
2738
|
+
{
|
|
2739
|
+
style: [
|
|
2740
|
+
styles33.inputWrapper,
|
|
2741
|
+
{
|
|
2742
|
+
borderColor,
|
|
2743
|
+
backgroundColor: colors2.colorBg
|
|
2744
|
+
},
|
|
2745
|
+
disabled && { opacity: 0.4 }
|
|
2746
|
+
],
|
|
2747
|
+
children: [
|
|
2748
|
+
/* @__PURE__ */ jsx37(
|
|
2749
|
+
TextInput,
|
|
2754
2750
|
{
|
|
2755
|
-
|
|
2756
|
-
|
|
2751
|
+
value: inputValue,
|
|
2752
|
+
onChangeText: handleChangeText,
|
|
2753
|
+
onFocus: handleFocus,
|
|
2754
|
+
onBlur: handleBlur,
|
|
2755
|
+
placeholder,
|
|
2756
|
+
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
2757
|
+
editable: !disabled,
|
|
2758
|
+
accessibilityRole: "combobox",
|
|
2759
|
+
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2760
|
+
accessibilityState: { disabled, expanded: isOpen },
|
|
2761
|
+
style: [
|
|
2762
|
+
styles33.input,
|
|
2763
|
+
{ color: colors2.colorOnBg }
|
|
2764
|
+
]
|
|
2757
2765
|
}
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
isDisabled && { opacity: 0.4 }
|
|
2777
|
-
],
|
|
2778
|
-
children: /* @__PURE__ */ jsx37(
|
|
2779
|
-
Text27,
|
|
2766
|
+
),
|
|
2767
|
+
isOpen && filtered.length > 0 && /* @__PURE__ */ jsx37(
|
|
2768
|
+
ScrollView4,
|
|
2769
|
+
{
|
|
2770
|
+
style: [
|
|
2771
|
+
styles33.dropdown,
|
|
2772
|
+
{
|
|
2773
|
+
borderColor: colors2.colorBgSubtle,
|
|
2774
|
+
backgroundColor: colors2.colorBg
|
|
2775
|
+
}
|
|
2776
|
+
],
|
|
2777
|
+
keyboardShouldPersistTaps: "handled",
|
|
2778
|
+
nestedScrollEnabled: true,
|
|
2779
|
+
children: filtered.map((option) => {
|
|
2780
|
+
const isSelected = option.value === value;
|
|
2781
|
+
const isDisabled = option.disabled;
|
|
2782
|
+
return /* @__PURE__ */ jsx37(
|
|
2783
|
+
Pressable18,
|
|
2780
2784
|
{
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2785
|
+
onPress: () => !isDisabled && selectOption(option),
|
|
2786
|
+
disabled: isDisabled,
|
|
2787
|
+
accessibilityRole: "menuitem",
|
|
2788
|
+
accessibilityLabel: option.label,
|
|
2789
|
+
accessibilityState: { selected: isSelected, disabled: isDisabled },
|
|
2790
|
+
style: ({ pressed }) => [
|
|
2791
|
+
styles33.option,
|
|
2792
|
+
isSelected && { backgroundColor: colors2.colorBlueSubtle },
|
|
2793
|
+
pressed && !isDisabled && { backgroundColor: colors2.colorBgSubtle },
|
|
2794
|
+
isDisabled && { opacity: 0.4 }
|
|
2787
2795
|
],
|
|
2788
|
-
children:
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2796
|
+
children: /* @__PURE__ */ jsx37(
|
|
2797
|
+
Text27,
|
|
2798
|
+
{
|
|
2799
|
+
style: [
|
|
2800
|
+
styles33.optionText,
|
|
2801
|
+
{
|
|
2802
|
+
color: isSelected ? colors2.colorInteractive : colors2.colorOnBg
|
|
2803
|
+
},
|
|
2804
|
+
isSelected && { fontWeight: font.weight.semibold }
|
|
2805
|
+
],
|
|
2806
|
+
children: option.label
|
|
2807
|
+
}
|
|
2808
|
+
)
|
|
2809
|
+
},
|
|
2810
|
+
option.value
|
|
2811
|
+
);
|
|
2812
|
+
})
|
|
2813
|
+
}
|
|
2814
|
+
)
|
|
2815
|
+
]
|
|
2816
|
+
}
|
|
2817
|
+
),
|
|
2798
2818
|
/* @__PURE__ */ jsx37(Message, { type: "error", message: error }),
|
|
2799
2819
|
/* @__PURE__ */ jsx37(Message, { type: "hint", message: hint })
|
|
2800
2820
|
] });
|
|
@@ -2886,6 +2906,7 @@ import {
|
|
|
2886
2906
|
TextInput as TextInput2
|
|
2887
2907
|
} from "react-native";
|
|
2888
2908
|
import { jsx as jsx39, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2909
|
+
var INPUT_HEIGHT2 = 44;
|
|
2889
2910
|
var keyboardTypeMap = {
|
|
2890
2911
|
text: "default",
|
|
2891
2912
|
email: "email-address",
|
|
@@ -2899,16 +2920,28 @@ var styles35 = StyleSheet35.create({
|
|
|
2899
2920
|
container: {
|
|
2900
2921
|
gap: spacing.xxs
|
|
2901
2922
|
},
|
|
2923
|
+
/**
|
|
2924
|
+
* The wrapper draws the pill border and provides the fixed 44pt touch-target height.
|
|
2925
|
+
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
2926
|
+
*/
|
|
2927
|
+
inputWrapper: {
|
|
2928
|
+
height: INPUT_HEIGHT2,
|
|
2929
|
+
justifyContent: "center",
|
|
2930
|
+
borderWidth: 1,
|
|
2931
|
+
borderRadius: border.radius.full
|
|
2932
|
+
},
|
|
2933
|
+
/**
|
|
2934
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2935
|
+
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
2936
|
+
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
2937
|
+
*/
|
|
2902
2938
|
input: {
|
|
2903
2939
|
fontFamily: font.family.body,
|
|
2904
2940
|
fontSize: font.size.body,
|
|
2905
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
2906
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
2907
2941
|
paddingHorizontal: spacing.s,
|
|
2908
|
-
paddingVertical:
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
minHeight: 44
|
|
2942
|
+
paddingVertical: 0,
|
|
2943
|
+
textAlignVertical: "center",
|
|
2944
|
+
includeFontPadding: false
|
|
2912
2945
|
}
|
|
2913
2946
|
});
|
|
2914
2947
|
function FormInput({
|
|
@@ -2932,38 +2965,46 @@ function FormInput({
|
|
|
2932
2965
|
return /* @__PURE__ */ jsxs24(View32, { style: [styles35.container, style], children: [
|
|
2933
2966
|
/* @__PURE__ */ jsx39(Label, { label }),
|
|
2934
2967
|
/* @__PURE__ */ jsx39(
|
|
2935
|
-
|
|
2968
|
+
View32,
|
|
2936
2969
|
{
|
|
2937
|
-
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2938
|
-
accessibilityState: { disabled },
|
|
2939
|
-
accessibilityHint: hint,
|
|
2940
|
-
value,
|
|
2941
|
-
defaultValue,
|
|
2942
|
-
placeholder,
|
|
2943
|
-
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
2944
|
-
keyboardType: keyboardTypeMap[type],
|
|
2945
|
-
secureTextEntry: type === "password",
|
|
2946
|
-
autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
|
|
2947
|
-
autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
|
|
2948
|
-
editable: !disabled,
|
|
2949
|
-
onChangeText: onChange,
|
|
2950
|
-
onFocus: () => {
|
|
2951
|
-
setFocused(true);
|
|
2952
|
-
onFocus?.();
|
|
2953
|
-
},
|
|
2954
|
-
onBlur: () => {
|
|
2955
|
-
setFocused(false);
|
|
2956
|
-
onBlur?.();
|
|
2957
|
-
},
|
|
2958
2970
|
style: [
|
|
2959
|
-
styles35.
|
|
2971
|
+
styles35.inputWrapper,
|
|
2960
2972
|
{
|
|
2961
2973
|
borderColor,
|
|
2962
|
-
color: colors2.colorOnBg,
|
|
2963
2974
|
backgroundColor: colors2.colorBg
|
|
2964
2975
|
},
|
|
2965
2976
|
disabled && { opacity: 0.4 }
|
|
2966
|
-
]
|
|
2977
|
+
],
|
|
2978
|
+
children: /* @__PURE__ */ jsx39(
|
|
2979
|
+
TextInput2,
|
|
2980
|
+
{
|
|
2981
|
+
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2982
|
+
accessibilityState: { disabled },
|
|
2983
|
+
accessibilityHint: hint,
|
|
2984
|
+
value,
|
|
2985
|
+
defaultValue,
|
|
2986
|
+
placeholder,
|
|
2987
|
+
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
2988
|
+
keyboardType: keyboardTypeMap[type],
|
|
2989
|
+
secureTextEntry: type === "password",
|
|
2990
|
+
autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
|
|
2991
|
+
autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
|
|
2992
|
+
editable: !disabled,
|
|
2993
|
+
onChangeText: onChange,
|
|
2994
|
+
onFocus: () => {
|
|
2995
|
+
setFocused(true);
|
|
2996
|
+
onFocus?.();
|
|
2997
|
+
},
|
|
2998
|
+
onBlur: () => {
|
|
2999
|
+
setFocused(false);
|
|
3000
|
+
onBlur?.();
|
|
3001
|
+
},
|
|
3002
|
+
style: [
|
|
3003
|
+
styles35.input,
|
|
3004
|
+
{ color: colors2.colorOnBg }
|
|
3005
|
+
]
|
|
3006
|
+
}
|
|
3007
|
+
)
|
|
2967
3008
|
}
|
|
2968
3009
|
),
|
|
2969
3010
|
/* @__PURE__ */ jsx39(Message, { type: "error", message: error }),
|
|
@@ -3481,18 +3522,30 @@ var styles39 = StyleSheet39.create({
|
|
|
3481
3522
|
fontWeight: font.weight.semibold,
|
|
3482
3523
|
lineHeight: 24
|
|
3483
3524
|
},
|
|
3484
|
-
|
|
3525
|
+
/**
|
|
3526
|
+
* The inputWrapper holds the border and fixed height so justifyContent: 'center'
|
|
3527
|
+
* vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
|
|
3528
|
+
*/
|
|
3529
|
+
inputWrapper: {
|
|
3485
3530
|
flex: 1,
|
|
3531
|
+
height: STEPPER_SIZE,
|
|
3532
|
+
justifyContent: "center",
|
|
3533
|
+
borderTopWidth: 1,
|
|
3534
|
+
borderBottomWidth: 1
|
|
3535
|
+
},
|
|
3536
|
+
/**
|
|
3537
|
+
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
3538
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
3539
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
3540
|
+
*/
|
|
3541
|
+
input: {
|
|
3486
3542
|
fontFamily: font.family.body,
|
|
3487
3543
|
fontSize: font.size.body,
|
|
3488
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
3489
|
-
// lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
|
|
3490
3544
|
textAlign: "center",
|
|
3491
3545
|
paddingHorizontal: spacing.xs,
|
|
3492
|
-
paddingVertical:
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
minHeight: STEPPER_SIZE
|
|
3546
|
+
paddingVertical: 0,
|
|
3547
|
+
textAlignVertical: "center",
|
|
3548
|
+
includeFontPadding: false
|
|
3496
3549
|
}
|
|
3497
3550
|
});
|
|
3498
3551
|
function NumberInput({
|
|
@@ -3558,32 +3611,40 @@ function NumberInput({
|
|
|
3558
3611
|
}
|
|
3559
3612
|
),
|
|
3560
3613
|
/* @__PURE__ */ jsx43(
|
|
3561
|
-
|
|
3614
|
+
View36,
|
|
3562
3615
|
{
|
|
3563
|
-
accessibilityLabel: label,
|
|
3564
|
-
accessibilityState: { disabled },
|
|
3565
|
-
accessibilityValue: {
|
|
3566
|
-
min,
|
|
3567
|
-
max,
|
|
3568
|
-
now: currentValue,
|
|
3569
|
-
text: String(currentValue)
|
|
3570
|
-
},
|
|
3571
|
-
keyboardType: "numeric",
|
|
3572
|
-
value: String(currentValue),
|
|
3573
|
-
editable: !disabled,
|
|
3574
|
-
onChangeText: (text) => {
|
|
3575
|
-
const parsed = parseFloat(text);
|
|
3576
|
-
const safe = isNaN(parsed) ? 0 : parsed;
|
|
3577
|
-
update(safe);
|
|
3578
|
-
},
|
|
3579
3616
|
style: [
|
|
3580
|
-
styles39.
|
|
3617
|
+
styles39.inputWrapper,
|
|
3581
3618
|
{
|
|
3582
3619
|
borderColor,
|
|
3583
|
-
color: colors2.colorOnBg,
|
|
3584
3620
|
backgroundColor: colors2.colorBg
|
|
3585
3621
|
}
|
|
3586
|
-
]
|
|
3622
|
+
],
|
|
3623
|
+
children: /* @__PURE__ */ jsx43(
|
|
3624
|
+
TextInput4,
|
|
3625
|
+
{
|
|
3626
|
+
accessibilityLabel: label,
|
|
3627
|
+
accessibilityState: { disabled },
|
|
3628
|
+
accessibilityValue: {
|
|
3629
|
+
min,
|
|
3630
|
+
max,
|
|
3631
|
+
now: currentValue,
|
|
3632
|
+
text: String(currentValue)
|
|
3633
|
+
},
|
|
3634
|
+
keyboardType: "numeric",
|
|
3635
|
+
value: String(currentValue),
|
|
3636
|
+
editable: !disabled,
|
|
3637
|
+
onChangeText: (text) => {
|
|
3638
|
+
const parsed = parseFloat(text);
|
|
3639
|
+
const safe = isNaN(parsed) ? 0 : parsed;
|
|
3640
|
+
update(safe);
|
|
3641
|
+
},
|
|
3642
|
+
style: [
|
|
3643
|
+
styles39.input,
|
|
3644
|
+
{ color: colors2.colorOnBg }
|
|
3645
|
+
]
|
|
3646
|
+
}
|
|
3647
|
+
)
|
|
3587
3648
|
}
|
|
3588
3649
|
),
|
|
3589
3650
|
/* @__PURE__ */ jsx43(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-react-native",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "BoostDev React Native component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -57,4 +57,32 @@ describe('Combobox (native)', () => {
|
|
|
57
57
|
);
|
|
58
58
|
expect(screen.getByText('Type to filter.')).toBeInTheDocument();
|
|
59
59
|
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Vertical-centering invariant: the TextInput must have no vertical padding so that
|
|
63
|
+
* iOS centers text automatically within the wrapper height.
|
|
64
|
+
*/
|
|
65
|
+
it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
|
|
66
|
+
renderWithTheme(
|
|
67
|
+
<Combobox label="Fruit" name="fruit" options={fruits} />,
|
|
68
|
+
);
|
|
69
|
+
const input = screen.getByRole('combobox');
|
|
70
|
+
const style = getComputedStyle(input);
|
|
71
|
+
expect(parseFloat(style.paddingTop)).toBe(0);
|
|
72
|
+
expect(parseFloat(style.paddingBottom)).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Touch-target invariant: the inputWrapper View that draws the pill border must
|
|
77
|
+
* provide the 44pt touch-target height.
|
|
78
|
+
*/
|
|
79
|
+
it('has height of 44pt on the pill wrapper (touch target)', () => {
|
|
80
|
+
renderWithTheme(
|
|
81
|
+
<Combobox label="Fruit" name="fruit" options={fruits} />,
|
|
82
|
+
);
|
|
83
|
+
const input = screen.getByRole('combobox');
|
|
84
|
+
const wrapper = input.parentElement as HTMLElement;
|
|
85
|
+
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
86
|
+
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
87
|
+
});
|
|
60
88
|
});
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
ViewStyle,
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { spacing, font, border, lineHeight } from '../../../../native/tokens'; // lineHeight used for optionText (Text, not TextInput)
|
|
13
|
+
|
|
14
|
+
/** Fixed height for single-line control fields (satisfies 44pt touch target). */
|
|
15
|
+
const INPUT_HEIGHT = 44;
|
|
13
16
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
14
17
|
import { Label } from '../atoms/Label';
|
|
15
18
|
import { Message } from '../atoms/Message';
|
|
@@ -39,19 +42,30 @@ const styles = StyleSheet.create({
|
|
|
39
42
|
container: {
|
|
40
43
|
gap: spacing.xxs,
|
|
41
44
|
},
|
|
45
|
+
/**
|
|
46
|
+
* Draws the pill border and provides the fixed 44pt touch-target height.
|
|
47
|
+
* justifyContent: 'center' vertically centers the inner TextInput.
|
|
48
|
+
* position: 'relative' is required for the dropdown's absolute positioning.
|
|
49
|
+
*/
|
|
42
50
|
inputWrapper: {
|
|
43
51
|
position: 'relative',
|
|
52
|
+
height: INPUT_HEIGHT,
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
borderWidth: 1,
|
|
55
|
+
borderRadius: border.radius.full,
|
|
44
56
|
},
|
|
57
|
+
/**
|
|
58
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
59
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
60
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
61
|
+
*/
|
|
45
62
|
input: {
|
|
46
63
|
fontFamily: font.family.body,
|
|
47
64
|
fontSize: font.size.body,
|
|
48
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
49
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
50
|
-
paddingVertical: spacing.xs,
|
|
51
65
|
paddingHorizontal: spacing.s,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
paddingVertical: 0,
|
|
67
|
+
textAlignVertical: 'center',
|
|
68
|
+
includeFontPadding: false,
|
|
55
69
|
},
|
|
56
70
|
dropdown: {
|
|
57
71
|
position: 'absolute',
|
|
@@ -133,7 +147,16 @@ export function Combobox({
|
|
|
133
147
|
return (
|
|
134
148
|
<View style={[styles.container, style]}>
|
|
135
149
|
<Label label={label} />
|
|
136
|
-
<View
|
|
150
|
+
<View
|
|
151
|
+
style={[
|
|
152
|
+
styles.inputWrapper,
|
|
153
|
+
{
|
|
154
|
+
borderColor,
|
|
155
|
+
backgroundColor: colors.colorBg,
|
|
156
|
+
},
|
|
157
|
+
disabled && { opacity: 0.4 },
|
|
158
|
+
]}
|
|
159
|
+
>
|
|
137
160
|
<TextInput
|
|
138
161
|
value={inputValue}
|
|
139
162
|
onChangeText={handleChangeText}
|
|
@@ -147,12 +170,7 @@ export function Combobox({
|
|
|
147
170
|
accessibilityState={{ disabled, expanded: isOpen }}
|
|
148
171
|
style={[
|
|
149
172
|
styles.input,
|
|
150
|
-
{
|
|
151
|
-
borderColor,
|
|
152
|
-
color: colors.colorOnBg,
|
|
153
|
-
backgroundColor: colors.colorBg,
|
|
154
|
-
},
|
|
155
|
-
disabled && { opacity: 0.4 },
|
|
173
|
+
{ color: colors.colorOnBg },
|
|
156
174
|
]}
|
|
157
175
|
/>
|
|
158
176
|
{isOpen && filtered.length > 0 && (
|
|
@@ -54,8 +54,8 @@ describe('FormInput (native)', () => {
|
|
|
54
54
|
*
|
|
55
55
|
* On iOS, an explicit lineHeight on a single-line TextInput causes the text to
|
|
56
56
|
* render vertically clipped — descenders and ascenders are cut off at the line box
|
|
57
|
-
* boundary. The
|
|
58
|
-
* needing lineHeight. This test pins that invariant so it can't silently regress.
|
|
57
|
+
* boundary. The wrapper height + justifyContent: 'center' satisfies vertical centering
|
|
58
|
+
* without needing lineHeight. This test pins that invariant so it can't silently regress.
|
|
59
59
|
*
|
|
60
60
|
* react-native-web applies token-based styles as atomic CSS classes. getComputedStyle
|
|
61
61
|
* resolves those classes in jsdom and returns "normal" when lineHeight is absent.
|
|
@@ -68,14 +68,31 @@ describe('FormInput (native)', () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* Vertical-centering invariant: the TextInput must have no vertical padding so that
|
|
72
|
+
* iOS centers text automatically within the wrapper height. A non-zero paddingVertical
|
|
73
|
+
* shifts text off-center because iOS adds its own default internal top padding on top.
|
|
73
74
|
*/
|
|
74
|
-
it('has
|
|
75
|
+
it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
|
|
75
76
|
renderWithTheme(<FormInput label="Email" name="email" />);
|
|
76
77
|
const input = screen.getByRole('textbox');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
78
|
+
const style = getComputedStyle(input);
|
|
79
|
+
expect(parseFloat(style.paddingTop)).toBe(0);
|
|
80
|
+
expect(parseFloat(style.paddingBottom)).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Touch-target invariant: the wrapper View that draws the pill border must provide
|
|
85
|
+
* the 44pt touch-target height (replacing the minHeight that was previously on the
|
|
86
|
+
* TextInput itself).
|
|
87
|
+
*/
|
|
88
|
+
it('has height of 44pt on the pill wrapper (touch target)', () => {
|
|
89
|
+
const { container } = renderWithTheme(<FormInput label="Email" name="email" />);
|
|
90
|
+
// The wrapper is the first div that contains the input in react-native-web's output.
|
|
91
|
+
const input = screen.getByRole('textbox');
|
|
92
|
+
const wrapper = input.parentElement as HTMLElement;
|
|
93
|
+
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
94
|
+
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
95
|
+
// Suppress unused-variable warning from destructuring
|
|
96
|
+
void container;
|
|
80
97
|
});
|
|
81
98
|
});
|
|
@@ -12,6 +12,9 @@ import { useTheme } from '../../../../native/ThemeContext';
|
|
|
12
12
|
import { Label } from '../atoms/Label';
|
|
13
13
|
import { Message } from '../atoms/Message';
|
|
14
14
|
|
|
15
|
+
/** Fixed height for single-line control fields (satisfies 44pt touch target). */
|
|
16
|
+
const INPUT_HEIGHT = 44;
|
|
17
|
+
|
|
15
18
|
type InputType = 'text' | 'email' | 'password' | 'tel' | 'url' | 'number' | 'search';
|
|
16
19
|
|
|
17
20
|
interface FormInputProps {
|
|
@@ -44,16 +47,28 @@ const styles = StyleSheet.create({
|
|
|
44
47
|
container: {
|
|
45
48
|
gap: spacing.xxs,
|
|
46
49
|
},
|
|
50
|
+
/**
|
|
51
|
+
* The wrapper draws the pill border and provides the fixed 44pt touch-target height.
|
|
52
|
+
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
53
|
+
*/
|
|
54
|
+
inputWrapper: {
|
|
55
|
+
height: INPUT_HEIGHT,
|
|
56
|
+
justifyContent: 'center',
|
|
57
|
+
borderWidth: 1,
|
|
58
|
+
borderRadius: border.radius.full,
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
62
|
+
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
63
|
+
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
64
|
+
*/
|
|
47
65
|
input: {
|
|
48
66
|
fontFamily: font.family.body,
|
|
49
67
|
fontSize: font.size.body,
|
|
50
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
51
|
-
// lineHeight is set explicitly. minHeight: 44 satisfies the touch-target requirement.
|
|
52
68
|
paddingHorizontal: spacing.s,
|
|
53
|
-
paddingVertical:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
minHeight: 44,
|
|
69
|
+
paddingVertical: 0,
|
|
70
|
+
textAlignVertical: 'center',
|
|
71
|
+
includeFontPadding: false,
|
|
57
72
|
},
|
|
58
73
|
});
|
|
59
74
|
|
|
@@ -84,38 +99,44 @@ export function FormInput({
|
|
|
84
99
|
return (
|
|
85
100
|
<View style={[styles.container, style]}>
|
|
86
101
|
<Label label={label} />
|
|
87
|
-
<
|
|
88
|
-
accessibilityLabel={typeof label === 'string' ? label : name}
|
|
89
|
-
accessibilityState={{ disabled }}
|
|
90
|
-
accessibilityHint={hint}
|
|
91
|
-
value={value}
|
|
92
|
-
defaultValue={defaultValue}
|
|
93
|
-
placeholder={placeholder}
|
|
94
|
-
placeholderTextColor={colors.colorOnBgSubtle}
|
|
95
|
-
keyboardType={keyboardTypeMap[type]}
|
|
96
|
-
secureTextEntry={type === 'password'}
|
|
97
|
-
autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
|
|
98
|
-
autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
|
|
99
|
-
editable={!disabled}
|
|
100
|
-
onChangeText={onChange}
|
|
101
|
-
onFocus={() => {
|
|
102
|
-
setFocused(true);
|
|
103
|
-
onFocus?.();
|
|
104
|
-
}}
|
|
105
|
-
onBlur={() => {
|
|
106
|
-
setFocused(false);
|
|
107
|
-
onBlur?.();
|
|
108
|
-
}}
|
|
102
|
+
<View
|
|
109
103
|
style={[
|
|
110
|
-
styles.
|
|
104
|
+
styles.inputWrapper,
|
|
111
105
|
{
|
|
112
106
|
borderColor,
|
|
113
|
-
color: colors.colorOnBg,
|
|
114
107
|
backgroundColor: colors.colorBg,
|
|
115
108
|
},
|
|
116
109
|
disabled && { opacity: 0.4 },
|
|
117
110
|
]}
|
|
118
|
-
|
|
111
|
+
>
|
|
112
|
+
<TextInput
|
|
113
|
+
accessibilityLabel={typeof label === 'string' ? label : name}
|
|
114
|
+
accessibilityState={{ disabled }}
|
|
115
|
+
accessibilityHint={hint}
|
|
116
|
+
value={value}
|
|
117
|
+
defaultValue={defaultValue}
|
|
118
|
+
placeholder={placeholder}
|
|
119
|
+
placeholderTextColor={colors.colorOnBgSubtle}
|
|
120
|
+
keyboardType={keyboardTypeMap[type]}
|
|
121
|
+
secureTextEntry={type === 'password'}
|
|
122
|
+
autoCapitalize={type === 'email' || type === 'url' ? 'none' : 'sentences'}
|
|
123
|
+
autoComplete={type === 'email' ? 'email' : type === 'tel' ? 'tel' : undefined}
|
|
124
|
+
editable={!disabled}
|
|
125
|
+
onChangeText={onChange}
|
|
126
|
+
onFocus={() => {
|
|
127
|
+
setFocused(true);
|
|
128
|
+
onFocus?.();
|
|
129
|
+
}}
|
|
130
|
+
onBlur={() => {
|
|
131
|
+
setFocused(false);
|
|
132
|
+
onBlur?.();
|
|
133
|
+
}}
|
|
134
|
+
style={[
|
|
135
|
+
styles.input,
|
|
136
|
+
{ color: colors.colorOnBg },
|
|
137
|
+
]}
|
|
138
|
+
/>
|
|
139
|
+
</View>
|
|
119
140
|
<Message type="error" message={error} />
|
|
120
141
|
<Message type="hint" message={hint} />
|
|
121
142
|
</View>
|
|
@@ -60,4 +60,28 @@ describe('NumberInput (native)', () => {
|
|
|
60
60
|
expect(screen.getByRole('button', { name: 'Decrease' })).toBeDisabled();
|
|
61
61
|
expect(screen.getByRole('button', { name: 'Increase' })).toBeDisabled();
|
|
62
62
|
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Vertical-centering invariant: the TextInput must have no vertical padding so that
|
|
66
|
+
* iOS centers text automatically within the wrapper height.
|
|
67
|
+
*/
|
|
68
|
+
it('has paddingVertical: 0 on the TextInput (vertical centering)', () => {
|
|
69
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
70
|
+
const input = screen.getByDisplayValue('3');
|
|
71
|
+
const style = getComputedStyle(input);
|
|
72
|
+
expect(parseFloat(style.paddingTop)).toBe(0);
|
|
73
|
+
expect(parseFloat(style.paddingBottom)).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Touch-target invariant: the wrapper View around the TextInput must provide
|
|
78
|
+
* the 44pt height (stepper height), replacing minHeight on the TextInput itself.
|
|
79
|
+
*/
|
|
80
|
+
it('has height of 44pt on the TextInput wrapper (touch target)', () => {
|
|
81
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
82
|
+
const input = screen.getByDisplayValue('3');
|
|
83
|
+
const wrapper = input.parentElement as HTMLElement;
|
|
84
|
+
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
85
|
+
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
86
|
+
});
|
|
63
87
|
});
|
|
@@ -60,18 +60,30 @@ const styles = StyleSheet.create({
|
|
|
60
60
|
fontWeight: font.weight.semibold,
|
|
61
61
|
lineHeight: 24,
|
|
62
62
|
},
|
|
63
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The inputWrapper holds the border and fixed height so justifyContent: 'center'
|
|
65
|
+
* vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
|
|
66
|
+
*/
|
|
67
|
+
inputWrapper: {
|
|
64
68
|
flex: 1,
|
|
69
|
+
height: STEPPER_SIZE,
|
|
70
|
+
justifyContent: 'center',
|
|
71
|
+
borderTopWidth: 1,
|
|
72
|
+
borderBottomWidth: 1,
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
76
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
77
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
78
|
+
*/
|
|
79
|
+
input: {
|
|
65
80
|
fontFamily: font.family.body,
|
|
66
81
|
fontSize: font.size.body,
|
|
67
|
-
// No lineHeight on single-line TextInput: iOS clips descenders/ascenders when
|
|
68
|
-
// lineHeight is set explicitly. minHeight: STEPPER_SIZE (44) covers the touch target.
|
|
69
82
|
textAlign: 'center',
|
|
70
83
|
paddingHorizontal: spacing.xs,
|
|
71
|
-
paddingVertical:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
minHeight: STEPPER_SIZE,
|
|
84
|
+
paddingVertical: 0,
|
|
85
|
+
textAlignVertical: 'center',
|
|
86
|
+
includeFontPadding: false,
|
|
75
87
|
},
|
|
76
88
|
});
|
|
77
89
|
|
|
@@ -140,32 +152,38 @@ export function NumberInput({
|
|
|
140
152
|
</Text>
|
|
141
153
|
</Pressable>
|
|
142
154
|
|
|
143
|
-
<
|
|
144
|
-
accessibilityLabel={label}
|
|
145
|
-
accessibilityState={{ disabled }}
|
|
146
|
-
accessibilityValue={{
|
|
147
|
-
min,
|
|
148
|
-
max,
|
|
149
|
-
now: currentValue,
|
|
150
|
-
text: String(currentValue),
|
|
151
|
-
}}
|
|
152
|
-
keyboardType="numeric"
|
|
153
|
-
value={String(currentValue)}
|
|
154
|
-
editable={!disabled}
|
|
155
|
-
onChangeText={(text) => {
|
|
156
|
-
const parsed = parseFloat(text);
|
|
157
|
-
const safe = isNaN(parsed) ? 0 : parsed;
|
|
158
|
-
update(safe);
|
|
159
|
-
}}
|
|
155
|
+
<View
|
|
160
156
|
style={[
|
|
161
|
-
styles.
|
|
157
|
+
styles.inputWrapper,
|
|
162
158
|
{
|
|
163
159
|
borderColor,
|
|
164
|
-
color: colors.colorOnBg,
|
|
165
160
|
backgroundColor: colors.colorBg,
|
|
166
161
|
},
|
|
167
162
|
]}
|
|
168
|
-
|
|
163
|
+
>
|
|
164
|
+
<TextInput
|
|
165
|
+
accessibilityLabel={label}
|
|
166
|
+
accessibilityState={{ disabled }}
|
|
167
|
+
accessibilityValue={{
|
|
168
|
+
min,
|
|
169
|
+
max,
|
|
170
|
+
now: currentValue,
|
|
171
|
+
text: String(currentValue),
|
|
172
|
+
}}
|
|
173
|
+
keyboardType="numeric"
|
|
174
|
+
value={String(currentValue)}
|
|
175
|
+
editable={!disabled}
|
|
176
|
+
onChangeText={(text) => {
|
|
177
|
+
const parsed = parseFloat(text);
|
|
178
|
+
const safe = isNaN(parsed) ? 0 : parsed;
|
|
179
|
+
update(safe);
|
|
180
|
+
}}
|
|
181
|
+
style={[
|
|
182
|
+
styles.input,
|
|
183
|
+
{ color: colors.colorOnBg },
|
|
184
|
+
]}
|
|
185
|
+
/>
|
|
186
|
+
</View>
|
|
169
187
|
|
|
170
188
|
<Pressable
|
|
171
189
|
accessibilityRole="button"
|