@boostdev/design-system-react-native 0.2.1 → 0.2.3
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/CHANGELOG.md +32 -0
- package/dist/index.cjs +225 -151
- package/dist/index.js +225 -151
- 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/src/components/interaction/form/SegmentedControl/SegmentedControl.mdx +12 -2
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.spec.tsx +49 -7
- package/src/components/interaction/form/SegmentedControl/SegmentedControl.tsx +46 -31
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
|
] });
|
|
@@ -2804,22 +2824,32 @@ function Combobox({
|
|
|
2804
2824
|
import { Children } from "react";
|
|
2805
2825
|
import { StyleSheet as StyleSheet34, View as View31, Pressable as Pressable19, Text as Text28 } from "react-native";
|
|
2806
2826
|
import { jsx as jsx38 } from "react/jsx-runtime";
|
|
2827
|
+
var TRACK_PADDING = spacing.xxxs;
|
|
2807
2828
|
var sizeTokens2 = {
|
|
2808
|
-
small: {
|
|
2809
|
-
medium: {
|
|
2810
|
-
large: {
|
|
2829
|
+
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
2830
|
+
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
2831
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s }
|
|
2832
|
+
};
|
|
2833
|
+
var thumbShadow = {
|
|
2834
|
+
shadowColor: "#000",
|
|
2835
|
+
shadowOffset: { width: 0, height: 1 },
|
|
2836
|
+
shadowOpacity: 0.12,
|
|
2837
|
+
shadowRadius: 2,
|
|
2838
|
+
elevation: 2
|
|
2811
2839
|
};
|
|
2812
2840
|
var styles34 = StyleSheet34.create({
|
|
2813
2841
|
track: {
|
|
2814
2842
|
flexDirection: "row",
|
|
2815
2843
|
borderRadius: border.radius.full,
|
|
2844
|
+
padding: TRACK_PADDING,
|
|
2816
2845
|
overflow: "hidden"
|
|
2817
2846
|
},
|
|
2818
|
-
|
|
2847
|
+
segment: {
|
|
2819
2848
|
flex: 1,
|
|
2820
2849
|
alignItems: "center",
|
|
2821
2850
|
justifyContent: "center",
|
|
2822
|
-
|
|
2851
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
2852
|
+
borderRadius: border.radius.full - TRACK_PADDING
|
|
2823
2853
|
},
|
|
2824
2854
|
label: {
|
|
2825
2855
|
fontFamily: font.family.body,
|
|
@@ -2838,21 +2868,26 @@ function SegmentedControl({
|
|
|
2838
2868
|
style
|
|
2839
2869
|
}) {
|
|
2840
2870
|
const { colors: colors2 } = useTheme();
|
|
2841
|
-
const {
|
|
2871
|
+
const { minHeight, fontSize, px, py } = sizeTokens2[size];
|
|
2842
2872
|
const items = Children.toArray(children);
|
|
2843
|
-
const
|
|
2873
|
+
const trackBg = colors2.colorBgSubtle;
|
|
2874
|
+
const thumbBg = variant === "outline" ? colors2.colorBg : colors2.colorInteractive;
|
|
2844
2875
|
return /* @__PURE__ */ jsx38(
|
|
2845
2876
|
View31,
|
|
2846
2877
|
{
|
|
2847
|
-
style: [styles34.track,
|
|
2878
|
+
style: [styles34.track, { backgroundColor: trackBg }, style],
|
|
2848
2879
|
accessibilityRole: "tablist",
|
|
2849
2880
|
accessibilityLabel,
|
|
2850
2881
|
children: items.map((child, index) => {
|
|
2851
2882
|
const isActive = index === selectedIndex;
|
|
2852
|
-
const
|
|
2853
|
-
const inactiveStyle = variant === "filled" ? {} : {};
|
|
2854
|
-
const textColor = variant === "filled" && isActive ? colors2.colorOnInteractive : isActive ? colors2.colorInteractive : colors2.colorOnBgSubtle;
|
|
2883
|
+
const labelColor = isActive ? variant === "outline" ? colors2.colorInteractiveOnBg : colors2.colorOnInteractive : colors2.colorOnBgSubtle;
|
|
2855
2884
|
const label = typeof child === "string" || typeof child === "number" ? String(child) : null;
|
|
2885
|
+
const segmentStyle = {
|
|
2886
|
+
minHeight,
|
|
2887
|
+
paddingHorizontal: px,
|
|
2888
|
+
paddingVertical: py,
|
|
2889
|
+
...isActive ? { backgroundColor: thumbBg, ...thumbShadow } : { backgroundColor: "transparent" }
|
|
2890
|
+
};
|
|
2856
2891
|
return /* @__PURE__ */ jsx38(
|
|
2857
2892
|
Pressable19,
|
|
2858
2893
|
{
|
|
@@ -2862,14 +2897,12 @@ function SegmentedControl({
|
|
|
2862
2897
|
accessibilityLabel: label ?? void 0,
|
|
2863
2898
|
accessibilityState: { selected: isActive, disabled },
|
|
2864
2899
|
style: ({ pressed }) => [
|
|
2865
|
-
styles34.
|
|
2866
|
-
|
|
2867
|
-
isActive ? activeStyle : inactiveStyle,
|
|
2868
|
-
variant === "filled" && isActive && { borderRadius: border.radius.full },
|
|
2900
|
+
styles34.segment,
|
|
2901
|
+
segmentStyle,
|
|
2869
2902
|
pressed && !disabled && { opacity: 0.7 },
|
|
2870
2903
|
disabled && { opacity: 0.4 }
|
|
2871
2904
|
],
|
|
2872
|
-
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color:
|
|
2905
|
+
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color: labelColor }], children: label }) : child
|
|
2873
2906
|
},
|
|
2874
2907
|
index
|
|
2875
2908
|
);
|
|
@@ -2886,6 +2919,7 @@ import {
|
|
|
2886
2919
|
TextInput as TextInput2
|
|
2887
2920
|
} from "react-native";
|
|
2888
2921
|
import { jsx as jsx39, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2922
|
+
var INPUT_HEIGHT2 = 44;
|
|
2889
2923
|
var keyboardTypeMap = {
|
|
2890
2924
|
text: "default",
|
|
2891
2925
|
email: "email-address",
|
|
@@ -2899,16 +2933,28 @@ var styles35 = StyleSheet35.create({
|
|
|
2899
2933
|
container: {
|
|
2900
2934
|
gap: spacing.xxs
|
|
2901
2935
|
},
|
|
2936
|
+
/**
|
|
2937
|
+
* The wrapper draws the pill border and provides the fixed 44pt touch-target height.
|
|
2938
|
+
* justifyContent: 'center' vertically centers the inner TextInput on both iOS and Android.
|
|
2939
|
+
*/
|
|
2940
|
+
inputWrapper: {
|
|
2941
|
+
height: INPUT_HEIGHT2,
|
|
2942
|
+
justifyContent: "center",
|
|
2943
|
+
borderWidth: 1,
|
|
2944
|
+
borderRadius: border.radius.full
|
|
2945
|
+
},
|
|
2946
|
+
/**
|
|
2947
|
+
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2948
|
+
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
2949
|
+
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
2950
|
+
*/
|
|
2902
2951
|
input: {
|
|
2903
2952
|
fontFamily: font.family.body,
|
|
2904
2953
|
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
2954
|
paddingHorizontal: spacing.s,
|
|
2908
|
-
paddingVertical:
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
minHeight: 44
|
|
2955
|
+
paddingVertical: 0,
|
|
2956
|
+
textAlignVertical: "center",
|
|
2957
|
+
includeFontPadding: false
|
|
2912
2958
|
}
|
|
2913
2959
|
});
|
|
2914
2960
|
function FormInput({
|
|
@@ -2932,38 +2978,46 @@ function FormInput({
|
|
|
2932
2978
|
return /* @__PURE__ */ jsxs24(View32, { style: [styles35.container, style], children: [
|
|
2933
2979
|
/* @__PURE__ */ jsx39(Label, { label }),
|
|
2934
2980
|
/* @__PURE__ */ jsx39(
|
|
2935
|
-
|
|
2981
|
+
View32,
|
|
2936
2982
|
{
|
|
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
2983
|
style: [
|
|
2959
|
-
styles35.
|
|
2984
|
+
styles35.inputWrapper,
|
|
2960
2985
|
{
|
|
2961
2986
|
borderColor,
|
|
2962
|
-
color: colors2.colorOnBg,
|
|
2963
2987
|
backgroundColor: colors2.colorBg
|
|
2964
2988
|
},
|
|
2965
2989
|
disabled && { opacity: 0.4 }
|
|
2966
|
-
]
|
|
2990
|
+
],
|
|
2991
|
+
children: /* @__PURE__ */ jsx39(
|
|
2992
|
+
TextInput2,
|
|
2993
|
+
{
|
|
2994
|
+
accessibilityLabel: typeof label === "string" ? label : name,
|
|
2995
|
+
accessibilityState: { disabled },
|
|
2996
|
+
accessibilityHint: hint,
|
|
2997
|
+
value,
|
|
2998
|
+
defaultValue,
|
|
2999
|
+
placeholder,
|
|
3000
|
+
placeholderTextColor: colors2.colorOnBgSubtle,
|
|
3001
|
+
keyboardType: keyboardTypeMap[type],
|
|
3002
|
+
secureTextEntry: type === "password",
|
|
3003
|
+
autoCapitalize: type === "email" || type === "url" ? "none" : "sentences",
|
|
3004
|
+
autoComplete: type === "email" ? "email" : type === "tel" ? "tel" : void 0,
|
|
3005
|
+
editable: !disabled,
|
|
3006
|
+
onChangeText: onChange,
|
|
3007
|
+
onFocus: () => {
|
|
3008
|
+
setFocused(true);
|
|
3009
|
+
onFocus?.();
|
|
3010
|
+
},
|
|
3011
|
+
onBlur: () => {
|
|
3012
|
+
setFocused(false);
|
|
3013
|
+
onBlur?.();
|
|
3014
|
+
},
|
|
3015
|
+
style: [
|
|
3016
|
+
styles35.input,
|
|
3017
|
+
{ color: colors2.colorOnBg }
|
|
3018
|
+
]
|
|
3019
|
+
}
|
|
3020
|
+
)
|
|
2967
3021
|
}
|
|
2968
3022
|
),
|
|
2969
3023
|
/* @__PURE__ */ jsx39(Message, { type: "error", message: error }),
|
|
@@ -3481,18 +3535,30 @@ var styles39 = StyleSheet39.create({
|
|
|
3481
3535
|
fontWeight: font.weight.semibold,
|
|
3482
3536
|
lineHeight: 24
|
|
3483
3537
|
},
|
|
3484
|
-
|
|
3538
|
+
/**
|
|
3539
|
+
* The inputWrapper holds the border and fixed height so justifyContent: 'center'
|
|
3540
|
+
* vertically centers the TextInput. flex: 1 lets it fill the space between steppers.
|
|
3541
|
+
*/
|
|
3542
|
+
inputWrapper: {
|
|
3485
3543
|
flex: 1,
|
|
3544
|
+
height: STEPPER_SIZE,
|
|
3545
|
+
justifyContent: "center",
|
|
3546
|
+
borderTopWidth: 1,
|
|
3547
|
+
borderBottomWidth: 1
|
|
3548
|
+
},
|
|
3549
|
+
/**
|
|
3550
|
+
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
3551
|
+
* textAlignVertical + includeFontPadding provide Android parity.
|
|
3552
|
+
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
3553
|
+
*/
|
|
3554
|
+
input: {
|
|
3486
3555
|
fontFamily: font.family.body,
|
|
3487
3556
|
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
3557
|
textAlign: "center",
|
|
3491
3558
|
paddingHorizontal: spacing.xs,
|
|
3492
|
-
paddingVertical:
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
minHeight: STEPPER_SIZE
|
|
3559
|
+
paddingVertical: 0,
|
|
3560
|
+
textAlignVertical: "center",
|
|
3561
|
+
includeFontPadding: false
|
|
3496
3562
|
}
|
|
3497
3563
|
});
|
|
3498
3564
|
function NumberInput({
|
|
@@ -3558,32 +3624,40 @@ function NumberInput({
|
|
|
3558
3624
|
}
|
|
3559
3625
|
),
|
|
3560
3626
|
/* @__PURE__ */ jsx43(
|
|
3561
|
-
|
|
3627
|
+
View36,
|
|
3562
3628
|
{
|
|
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
3629
|
style: [
|
|
3580
|
-
styles39.
|
|
3630
|
+
styles39.inputWrapper,
|
|
3581
3631
|
{
|
|
3582
3632
|
borderColor,
|
|
3583
|
-
color: colors2.colorOnBg,
|
|
3584
3633
|
backgroundColor: colors2.colorBg
|
|
3585
3634
|
}
|
|
3586
|
-
]
|
|
3635
|
+
],
|
|
3636
|
+
children: /* @__PURE__ */ jsx43(
|
|
3637
|
+
TextInput4,
|
|
3638
|
+
{
|
|
3639
|
+
accessibilityLabel: label,
|
|
3640
|
+
accessibilityState: { disabled },
|
|
3641
|
+
accessibilityValue: {
|
|
3642
|
+
min,
|
|
3643
|
+
max,
|
|
3644
|
+
now: currentValue,
|
|
3645
|
+
text: String(currentValue)
|
|
3646
|
+
},
|
|
3647
|
+
keyboardType: "numeric",
|
|
3648
|
+
value: String(currentValue),
|
|
3649
|
+
editable: !disabled,
|
|
3650
|
+
onChangeText: (text) => {
|
|
3651
|
+
const parsed = parseFloat(text);
|
|
3652
|
+
const safe = isNaN(parsed) ? 0 : parsed;
|
|
3653
|
+
update(safe);
|
|
3654
|
+
},
|
|
3655
|
+
style: [
|
|
3656
|
+
styles39.input,
|
|
3657
|
+
{ color: colors2.colorOnBg }
|
|
3658
|
+
]
|
|
3659
|
+
}
|
|
3660
|
+
)
|
|
3587
3661
|
}
|
|
3588
3662
|
),
|
|
3589
3663
|
/* @__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.3",
|
|
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
|
});
|