@boostdev/design-system-react-native 0.2.2 → 0.2.4
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 +48 -0
- package/dist/index.cjs +43 -19
- package/dist/index.js +43 -19
- package/package.json +1 -1
- package/src/components/interaction/form/Combobox/Combobox.spec.tsx +15 -0
- package/src/components/interaction/form/Combobox/Combobox.tsx +3 -0
- package/src/components/interaction/form/FormInput/FormInput.spec.tsx +24 -0
- package/src/components/interaction/form/FormInput/FormInput.tsx +3 -0
- package/src/components/interaction/form/NumberInput/NumberInput.spec.tsx +13 -0
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +3 -0
- 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/src/components/interaction/form/atoms/inputHelpers.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to `@boostdev/design-system-react-native` are documented here.
|
|
4
|
+
## [0.2.4] - 2026-06-11
|
|
5
|
+
### Fixed
|
|
6
|
+
- Fix(form): optical vertical centering for single-line input text
|
|
7
|
+
|
|
8
|
+
Arial's asymmetric metrics (ascent 0.905em, descent 0.212em) place ink
|
|
9
|
+
~0.08em below the layout-box centre on iOS. Add OPTICAL_TEXT_SHIFT = -1.5
|
|
10
|
+
constant to form/atoms/inputHelpers.ts (single source of truth) and apply
|
|
11
|
+
transform: [{translateY: OPTICAL_TEXT_SHIFT}] to the single-line TextInput
|
|
12
|
+
in FormInput, NumberInput, and Combobox. Verified on-simulator: shifts ink
|
|
13
|
+
from +1.33pt to -0.33pt at fontSize 16.
|
|
14
|
+
|
|
15
|
+
Textarea (multiline) is unchanged.
|
|
16
|
+
|
|
17
|
+
Spec invariants extended: pin translateY shift alongside existing
|
|
18
|
+
no-lineHeight, paddingVertical-0, and wrapper-height-≥44 assertions. ([`a3e08e5`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/a3e08e5a6f4c53ef12ba810ef96d395e4ee116e7))
|
|
19
|
+
|
|
20
|
+
## [0.2.3] - 2026-06-11
|
|
21
|
+
### Fixed
|
|
22
|
+
- Fix(segmented-control): proper track + thumb rendering
|
|
23
|
+
|
|
24
|
+
Replace per-segment outline/border approach with a single rounded track
|
|
25
|
+
(colorBgSubtle background, 2 px inner padding) containing equal-width
|
|
26
|
+
segments. Selected segment renders a contrasting thumb: colorBg + shadow
|
|
27
|
+
for the outline variant (white elevated surface), colorInteractive for
|
|
28
|
+
the filled variant. No per-segment borders; no stray focus rectangle.
|
|
29
|
+
|
|
30
|
+
Mirrors the web DS visual contract (track bg, thumb bg, thumb radius =
|
|
31
|
+
track radius minus padding). Dark mode via useTheme throughout.
|
|
32
|
+
All accessibilityRole/accessibilityState props preserved.
|
|
33
|
+
Spec updated to pin track-has-one-background and thumb structure. ([`2a83f88`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/2a83f8804553e390528fc85dd79c1541bf92343e))
|
|
34
|
+
|
|
35
|
+
## [0.2.2] - 2026-06-11
|
|
36
|
+
### Fixed
|
|
37
|
+
- Fix(form): vertically center text in single-line inputs
|
|
38
|
+
|
|
39
|
+
Replace minHeight + paddingVertical on TextInput with a wrapper View
|
|
40
|
+
(height: 44, justifyContent: center) + paddingVertical: 0 on the inner
|
|
41
|
+
TextInput. iOS auto-centers text within the wrapper height; paddingVertical
|
|
42
|
+
on the TextInput itself caused text to sit off-center after the 0.2.1
|
|
43
|
+
lineHeight removal. Added textAlignVertical: center and includeFontPadding:
|
|
44
|
+
false for Android parity.
|
|
45
|
+
|
|
46
|
+
Applied consistently to FormInput, NumberInput, and Combobox.
|
|
47
|
+
Textarea (multiline) is unaffected.
|
|
48
|
+
|
|
49
|
+
Spec invariants updated: paddingVertical === 0 on TextInput, wrapper
|
|
50
|
+
height >= 44 replace the previous minHeight >= 44 assertion. ([`437b16d`](https://gitlab.com/b00stdev/boostdev-design-system-react-native/-/commit/437b16dccb60b3b3948c84519990c18688e51f71))
|
|
51
|
+
|
|
4
52
|
## [0.2.1] - 2026-06-11
|
|
5
53
|
### Fixed
|
|
6
54
|
- Fix(form): prevent iOS text clipping in single-line inputs
|
package/dist/index.cjs
CHANGED
|
@@ -2681,6 +2681,11 @@ function FileInput({
|
|
|
2681
2681
|
// src/components/interaction/form/Combobox/Combobox.tsx
|
|
2682
2682
|
var import_react9 = require("react");
|
|
2683
2683
|
var import_react_native37 = require("react-native");
|
|
2684
|
+
|
|
2685
|
+
// src/components/interaction/form/atoms/inputHelpers.ts
|
|
2686
|
+
var OPTICAL_TEXT_SHIFT = -1.5;
|
|
2687
|
+
|
|
2688
|
+
// src/components/interaction/form/Combobox/Combobox.tsx
|
|
2684
2689
|
var import_jsx_runtime37 = require("react/jsx-runtime");
|
|
2685
2690
|
var INPUT_HEIGHT = 44;
|
|
2686
2691
|
var MAX_DROPDOWN_HEIGHT = 200;
|
|
@@ -2704,6 +2709,7 @@ var styles33 = import_react_native37.StyleSheet.create({
|
|
|
2704
2709
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2705
2710
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
2706
2711
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
2712
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
2707
2713
|
*/
|
|
2708
2714
|
input: {
|
|
2709
2715
|
fontFamily: import_native.font.family.body,
|
|
@@ -2711,7 +2717,8 @@ var styles33 = import_react_native37.StyleSheet.create({
|
|
|
2711
2717
|
paddingHorizontal: import_native.spacing.s,
|
|
2712
2718
|
paddingVertical: 0,
|
|
2713
2719
|
textAlignVertical: "center",
|
|
2714
|
-
includeFontPadding: false
|
|
2720
|
+
includeFontPadding: false,
|
|
2721
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
2715
2722
|
},
|
|
2716
2723
|
dropdown: {
|
|
2717
2724
|
position: "absolute",
|
|
@@ -2871,22 +2878,32 @@ function Combobox({
|
|
|
2871
2878
|
var import_react10 = require("react");
|
|
2872
2879
|
var import_react_native38 = require("react-native");
|
|
2873
2880
|
var import_jsx_runtime38 = require("react/jsx-runtime");
|
|
2881
|
+
var TRACK_PADDING = import_native.spacing.xxxs;
|
|
2874
2882
|
var sizeTokens2 = {
|
|
2875
|
-
small: {
|
|
2876
|
-
medium: {
|
|
2877
|
-
large: {
|
|
2883
|
+
small: { minHeight: 32, fontSize: import_native.font.size.bodyS, px: import_native.spacing.s, py: import_native.spacing.xxs },
|
|
2884
|
+
medium: { minHeight: 44, fontSize: import_native.font.size.body, px: import_native.spacing.m, py: import_native.spacing.xs },
|
|
2885
|
+
large: { minHeight: 48, fontSize: import_native.font.size.body, px: import_native.spacing.m, py: import_native.spacing.s }
|
|
2886
|
+
};
|
|
2887
|
+
var thumbShadow = {
|
|
2888
|
+
shadowColor: "#000",
|
|
2889
|
+
shadowOffset: { width: 0, height: 1 },
|
|
2890
|
+
shadowOpacity: 0.12,
|
|
2891
|
+
shadowRadius: 2,
|
|
2892
|
+
elevation: 2
|
|
2878
2893
|
};
|
|
2879
2894
|
var styles34 = import_react_native38.StyleSheet.create({
|
|
2880
2895
|
track: {
|
|
2881
2896
|
flexDirection: "row",
|
|
2882
2897
|
borderRadius: import_native.border.radius.full,
|
|
2898
|
+
padding: TRACK_PADDING,
|
|
2883
2899
|
overflow: "hidden"
|
|
2884
2900
|
},
|
|
2885
|
-
|
|
2901
|
+
segment: {
|
|
2886
2902
|
flex: 1,
|
|
2887
2903
|
alignItems: "center",
|
|
2888
2904
|
justifyContent: "center",
|
|
2889
|
-
|
|
2905
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
2906
|
+
borderRadius: import_native.border.radius.full - TRACK_PADDING
|
|
2890
2907
|
},
|
|
2891
2908
|
label: {
|
|
2892
2909
|
fontFamily: import_native.font.family.body,
|
|
@@ -2905,21 +2922,26 @@ function SegmentedControl({
|
|
|
2905
2922
|
style
|
|
2906
2923
|
}) {
|
|
2907
2924
|
const { colors: colors2 } = useTheme();
|
|
2908
|
-
const {
|
|
2925
|
+
const { minHeight, fontSize, px, py } = sizeTokens2[size];
|
|
2909
2926
|
const items = import_react10.Children.toArray(children);
|
|
2910
|
-
const
|
|
2927
|
+
const trackBg = colors2.colorBgSubtle;
|
|
2928
|
+
const thumbBg = variant === "outline" ? colors2.colorBg : colors2.colorInteractive;
|
|
2911
2929
|
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
2912
2930
|
import_react_native38.View,
|
|
2913
2931
|
{
|
|
2914
|
-
style: [styles34.track,
|
|
2932
|
+
style: [styles34.track, { backgroundColor: trackBg }, style],
|
|
2915
2933
|
accessibilityRole: "tablist",
|
|
2916
2934
|
accessibilityLabel,
|
|
2917
2935
|
children: items.map((child, index) => {
|
|
2918
2936
|
const isActive = index === selectedIndex;
|
|
2919
|
-
const
|
|
2920
|
-
const inactiveStyle = variant === "filled" ? {} : {};
|
|
2921
|
-
const textColor = variant === "filled" && isActive ? colors2.colorOnInteractive : isActive ? colors2.colorInteractive : colors2.colorOnBgSubtle;
|
|
2937
|
+
const labelColor = isActive ? variant === "outline" ? colors2.colorInteractiveOnBg : colors2.colorOnInteractive : colors2.colorOnBgSubtle;
|
|
2922
2938
|
const label = typeof child === "string" || typeof child === "number" ? String(child) : null;
|
|
2939
|
+
const segmentStyle = {
|
|
2940
|
+
minHeight,
|
|
2941
|
+
paddingHorizontal: px,
|
|
2942
|
+
paddingVertical: py,
|
|
2943
|
+
...isActive ? { backgroundColor: thumbBg, ...thumbShadow } : { backgroundColor: "transparent" }
|
|
2944
|
+
};
|
|
2923
2945
|
return /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
|
|
2924
2946
|
import_react_native38.Pressable,
|
|
2925
2947
|
{
|
|
@@ -2929,14 +2951,12 @@ function SegmentedControl({
|
|
|
2929
2951
|
accessibilityLabel: label ?? void 0,
|
|
2930
2952
|
accessibilityState: { selected: isActive, disabled },
|
|
2931
2953
|
style: ({ pressed }) => [
|
|
2932
|
-
styles34.
|
|
2933
|
-
|
|
2934
|
-
isActive ? activeStyle : inactiveStyle,
|
|
2935
|
-
variant === "filled" && isActive && { borderRadius: import_native.border.radius.full },
|
|
2954
|
+
styles34.segment,
|
|
2955
|
+
segmentStyle,
|
|
2936
2956
|
pressed && !disabled && { opacity: 0.7 },
|
|
2937
2957
|
disabled && { opacity: 0.4 }
|
|
2938
2958
|
],
|
|
2939
|
-
children: label !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_react_native38.Text, { style: [styles34.label, { fontSize, color:
|
|
2959
|
+
children: label !== null ? /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_react_native38.Text, { style: [styles34.label, { fontSize, color: labelColor }], children: label }) : child
|
|
2940
2960
|
},
|
|
2941
2961
|
index
|
|
2942
2962
|
);
|
|
@@ -2977,6 +2997,7 @@ var styles35 = import_react_native39.StyleSheet.create({
|
|
|
2977
2997
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2978
2998
|
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
2979
2999
|
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
3000
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
2980
3001
|
*/
|
|
2981
3002
|
input: {
|
|
2982
3003
|
fontFamily: import_native.font.family.body,
|
|
@@ -2984,7 +3005,8 @@ var styles35 = import_react_native39.StyleSheet.create({
|
|
|
2984
3005
|
paddingHorizontal: import_native.spacing.s,
|
|
2985
3006
|
paddingVertical: 0,
|
|
2986
3007
|
textAlignVertical: "center",
|
|
2987
|
-
includeFontPadding: false
|
|
3008
|
+
includeFontPadding: false,
|
|
3009
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
2988
3010
|
}
|
|
2989
3011
|
});
|
|
2990
3012
|
function FormInput({
|
|
@@ -3558,6 +3580,7 @@ var styles39 = import_react_native43.StyleSheet.create({
|
|
|
3558
3580
|
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
3559
3581
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
3560
3582
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
3583
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
3561
3584
|
*/
|
|
3562
3585
|
input: {
|
|
3563
3586
|
fontFamily: import_native.font.family.body,
|
|
@@ -3566,7 +3589,8 @@ var styles39 = import_react_native43.StyleSheet.create({
|
|
|
3566
3589
|
paddingHorizontal: import_native.spacing.xs,
|
|
3567
3590
|
paddingVertical: 0,
|
|
3568
3591
|
textAlignVertical: "center",
|
|
3569
|
-
includeFontPadding: false
|
|
3592
|
+
includeFontPadding: false,
|
|
3593
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
3570
3594
|
}
|
|
3571
3595
|
});
|
|
3572
3596
|
function NumberInput({
|
package/dist/index.js
CHANGED
|
@@ -2634,6 +2634,11 @@ import {
|
|
|
2634
2634
|
Text as Text27,
|
|
2635
2635
|
ScrollView as ScrollView4
|
|
2636
2636
|
} from "react-native";
|
|
2637
|
+
|
|
2638
|
+
// src/components/interaction/form/atoms/inputHelpers.ts
|
|
2639
|
+
var OPTICAL_TEXT_SHIFT = -1.5;
|
|
2640
|
+
|
|
2641
|
+
// src/components/interaction/form/Combobox/Combobox.tsx
|
|
2637
2642
|
import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2638
2643
|
var INPUT_HEIGHT = 44;
|
|
2639
2644
|
var MAX_DROPDOWN_HEIGHT = 200;
|
|
@@ -2657,6 +2662,7 @@ var styles33 = StyleSheet33.create({
|
|
|
2657
2662
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2658
2663
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
2659
2664
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
2665
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
2660
2666
|
*/
|
|
2661
2667
|
input: {
|
|
2662
2668
|
fontFamily: font.family.body,
|
|
@@ -2664,7 +2670,8 @@ var styles33 = StyleSheet33.create({
|
|
|
2664
2670
|
paddingHorizontal: spacing.s,
|
|
2665
2671
|
paddingVertical: 0,
|
|
2666
2672
|
textAlignVertical: "center",
|
|
2667
|
-
includeFontPadding: false
|
|
2673
|
+
includeFontPadding: false,
|
|
2674
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
2668
2675
|
},
|
|
2669
2676
|
dropdown: {
|
|
2670
2677
|
position: "absolute",
|
|
@@ -2824,22 +2831,32 @@ function Combobox({
|
|
|
2824
2831
|
import { Children } from "react";
|
|
2825
2832
|
import { StyleSheet as StyleSheet34, View as View31, Pressable as Pressable19, Text as Text28 } from "react-native";
|
|
2826
2833
|
import { jsx as jsx38 } from "react/jsx-runtime";
|
|
2834
|
+
var TRACK_PADDING = spacing.xxxs;
|
|
2827
2835
|
var sizeTokens2 = {
|
|
2828
|
-
small: {
|
|
2829
|
-
medium: {
|
|
2830
|
-
large: {
|
|
2836
|
+
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
2837
|
+
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
2838
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s }
|
|
2839
|
+
};
|
|
2840
|
+
var thumbShadow = {
|
|
2841
|
+
shadowColor: "#000",
|
|
2842
|
+
shadowOffset: { width: 0, height: 1 },
|
|
2843
|
+
shadowOpacity: 0.12,
|
|
2844
|
+
shadowRadius: 2,
|
|
2845
|
+
elevation: 2
|
|
2831
2846
|
};
|
|
2832
2847
|
var styles34 = StyleSheet34.create({
|
|
2833
2848
|
track: {
|
|
2834
2849
|
flexDirection: "row",
|
|
2835
2850
|
borderRadius: border.radius.full,
|
|
2851
|
+
padding: TRACK_PADDING,
|
|
2836
2852
|
overflow: "hidden"
|
|
2837
2853
|
},
|
|
2838
|
-
|
|
2854
|
+
segment: {
|
|
2839
2855
|
flex: 1,
|
|
2840
2856
|
alignItems: "center",
|
|
2841
2857
|
justifyContent: "center",
|
|
2842
|
-
|
|
2858
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
2859
|
+
borderRadius: border.radius.full - TRACK_PADDING
|
|
2843
2860
|
},
|
|
2844
2861
|
label: {
|
|
2845
2862
|
fontFamily: font.family.body,
|
|
@@ -2858,21 +2875,26 @@ function SegmentedControl({
|
|
|
2858
2875
|
style
|
|
2859
2876
|
}) {
|
|
2860
2877
|
const { colors: colors2 } = useTheme();
|
|
2861
|
-
const {
|
|
2878
|
+
const { minHeight, fontSize, px, py } = sizeTokens2[size];
|
|
2862
2879
|
const items = Children.toArray(children);
|
|
2863
|
-
const
|
|
2880
|
+
const trackBg = colors2.colorBgSubtle;
|
|
2881
|
+
const thumbBg = variant === "outline" ? colors2.colorBg : colors2.colorInteractive;
|
|
2864
2882
|
return /* @__PURE__ */ jsx38(
|
|
2865
2883
|
View31,
|
|
2866
2884
|
{
|
|
2867
|
-
style: [styles34.track,
|
|
2885
|
+
style: [styles34.track, { backgroundColor: trackBg }, style],
|
|
2868
2886
|
accessibilityRole: "tablist",
|
|
2869
2887
|
accessibilityLabel,
|
|
2870
2888
|
children: items.map((child, index) => {
|
|
2871
2889
|
const isActive = index === selectedIndex;
|
|
2872
|
-
const
|
|
2873
|
-
const inactiveStyle = variant === "filled" ? {} : {};
|
|
2874
|
-
const textColor = variant === "filled" && isActive ? colors2.colorOnInteractive : isActive ? colors2.colorInteractive : colors2.colorOnBgSubtle;
|
|
2890
|
+
const labelColor = isActive ? variant === "outline" ? colors2.colorInteractiveOnBg : colors2.colorOnInteractive : colors2.colorOnBgSubtle;
|
|
2875
2891
|
const label = typeof child === "string" || typeof child === "number" ? String(child) : null;
|
|
2892
|
+
const segmentStyle = {
|
|
2893
|
+
minHeight,
|
|
2894
|
+
paddingHorizontal: px,
|
|
2895
|
+
paddingVertical: py,
|
|
2896
|
+
...isActive ? { backgroundColor: thumbBg, ...thumbShadow } : { backgroundColor: "transparent" }
|
|
2897
|
+
};
|
|
2876
2898
|
return /* @__PURE__ */ jsx38(
|
|
2877
2899
|
Pressable19,
|
|
2878
2900
|
{
|
|
@@ -2882,14 +2904,12 @@ function SegmentedControl({
|
|
|
2882
2904
|
accessibilityLabel: label ?? void 0,
|
|
2883
2905
|
accessibilityState: { selected: isActive, disabled },
|
|
2884
2906
|
style: ({ pressed }) => [
|
|
2885
|
-
styles34.
|
|
2886
|
-
|
|
2887
|
-
isActive ? activeStyle : inactiveStyle,
|
|
2888
|
-
variant === "filled" && isActive && { borderRadius: border.radius.full },
|
|
2907
|
+
styles34.segment,
|
|
2908
|
+
segmentStyle,
|
|
2889
2909
|
pressed && !disabled && { opacity: 0.7 },
|
|
2890
2910
|
disabled && { opacity: 0.4 }
|
|
2891
2911
|
],
|
|
2892
|
-
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color:
|
|
2912
|
+
children: label !== null ? /* @__PURE__ */ jsx38(Text28, { style: [styles34.label, { fontSize, color: labelColor }], children: label }) : child
|
|
2893
2913
|
},
|
|
2894
2914
|
index
|
|
2895
2915
|
);
|
|
@@ -2934,6 +2954,7 @@ var styles35 = StyleSheet35.create({
|
|
|
2934
2954
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
2935
2955
|
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
2936
2956
|
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
2957
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
2937
2958
|
*/
|
|
2938
2959
|
input: {
|
|
2939
2960
|
fontFamily: font.family.body,
|
|
@@ -2941,7 +2962,8 @@ var styles35 = StyleSheet35.create({
|
|
|
2941
2962
|
paddingHorizontal: spacing.s,
|
|
2942
2963
|
paddingVertical: 0,
|
|
2943
2964
|
textAlignVertical: "center",
|
|
2944
|
-
includeFontPadding: false
|
|
2965
|
+
includeFontPadding: false,
|
|
2966
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
2945
2967
|
}
|
|
2946
2968
|
});
|
|
2947
2969
|
function FormInput({
|
|
@@ -3537,6 +3559,7 @@ var styles39 = StyleSheet39.create({
|
|
|
3537
3559
|
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
3538
3560
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
3539
3561
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
3562
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
3540
3563
|
*/
|
|
3541
3564
|
input: {
|
|
3542
3565
|
fontFamily: font.family.body,
|
|
@@ -3545,7 +3568,8 @@ var styles39 = StyleSheet39.create({
|
|
|
3545
3568
|
paddingHorizontal: spacing.xs,
|
|
3546
3569
|
paddingVertical: 0,
|
|
3547
3570
|
textAlignVertical: "center",
|
|
3548
|
-
includeFontPadding: false
|
|
3571
|
+
includeFontPadding: false,
|
|
3572
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }]
|
|
3549
3573
|
}
|
|
3550
3574
|
});
|
|
3551
3575
|
function NumberInput({
|
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.4",
|
|
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",
|
|
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
|
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { Combobox } from './Combobox';
|
|
4
4
|
import type { ComboboxOption } from './Combobox';
|
|
5
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
5
6
|
|
|
6
7
|
function renderWithTheme(ui: React.ReactElement) {
|
|
7
8
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -85,4 +86,18 @@ describe('Combobox (native)', () => {
|
|
|
85
86
|
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
86
87
|
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
87
88
|
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optical-centering invariant: the TextInput must have a translateY transform equal
|
|
92
|
+
* to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
|
|
93
|
+
*/
|
|
94
|
+
it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
|
|
95
|
+
renderWithTheme(
|
|
96
|
+
<Combobox label="Fruit" name="fruit" options={fruits} />,
|
|
97
|
+
);
|
|
98
|
+
const input = screen.getByRole('combobox');
|
|
99
|
+
const transform = getComputedStyle(input).transform;
|
|
100
|
+
expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
|
|
101
|
+
expect(transform).toMatch(/(-1\.5|-1\.50)/);
|
|
102
|
+
});
|
|
88
103
|
});
|
|
@@ -10,6 +10,7 @@ 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
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
13
14
|
|
|
14
15
|
/** Fixed height for single-line control fields (satisfies 44pt touch target). */
|
|
15
16
|
const INPUT_HEIGHT = 44;
|
|
@@ -58,6 +59,7 @@ const styles = StyleSheet.create({
|
|
|
58
59
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
59
60
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
60
61
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
62
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
61
63
|
*/
|
|
62
64
|
input: {
|
|
63
65
|
fontFamily: font.family.body,
|
|
@@ -66,6 +68,7 @@ const styles = StyleSheet.create({
|
|
|
66
68
|
paddingVertical: 0,
|
|
67
69
|
textAlignVertical: 'center',
|
|
68
70
|
includeFontPadding: false,
|
|
71
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
|
|
69
72
|
},
|
|
70
73
|
dropdown: {
|
|
71
74
|
position: 'absolute',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { FormInput } from './FormInput';
|
|
4
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
4
5
|
|
|
5
6
|
function renderWithTheme(ui: React.ReactElement) {
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -95,4 +96,27 @@ describe('FormInput (native)', () => {
|
|
|
95
96
|
// Suppress unused-variable warning from destructuring
|
|
96
97
|
void container;
|
|
97
98
|
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Optical-centering invariant: the TextInput must have a translateY transform equal
|
|
102
|
+
* to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
|
|
103
|
+
*
|
|
104
|
+
* Arial's ascent (0.905em) vs descent (0.212em) means iOS places ink ~1.3pt below
|
|
105
|
+
* the geometric centre of the wrapper. The translateY shift was measured on-simulator
|
|
106
|
+
* to bring ink from +1.33pt to -0.33pt, which is visually centred.
|
|
107
|
+
*
|
|
108
|
+
* react-native-web serialises transform: [{translateY: n}] as a CSS transform string,
|
|
109
|
+
* so we check that the computed style contains the expected value.
|
|
110
|
+
*/
|
|
111
|
+
it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
|
|
112
|
+
renderWithTheme(<FormInput label="Email" name="email" value="bjorn@boonen.eu" />);
|
|
113
|
+
const input = screen.getByDisplayValue('bjorn@boonen.eu');
|
|
114
|
+
const transform = getComputedStyle(input).transform;
|
|
115
|
+
// react-native-web renders translateY as a matrix or translate() — check the value
|
|
116
|
+
// is present and matches the constant.
|
|
117
|
+
expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
|
|
118
|
+
// The transform string must encode a non-zero Y translation.
|
|
119
|
+
// matrix(1, 0, 0, 1, 0, -1.5) or translateY(-1.5px) — both contain the value.
|
|
120
|
+
expect(transform).toMatch(/(-1\.5|-1\.50)/);
|
|
121
|
+
});
|
|
98
122
|
});
|
|
@@ -11,6 +11,7 @@ import { spacing, font, border } from '../../../../native/tokens';
|
|
|
11
11
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
12
12
|
import { Label } from '../atoms/Label';
|
|
13
13
|
import { Message } from '../atoms/Message';
|
|
14
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
14
15
|
|
|
15
16
|
/** Fixed height for single-line control fields (satisfies 44pt touch target). */
|
|
16
17
|
const INPUT_HEIGHT = 44;
|
|
@@ -61,6 +62,7 @@ const styles = StyleSheet.create({
|
|
|
61
62
|
* No vertical padding: iOS centers text automatically inside the wrapper height.
|
|
62
63
|
* textAlignVertical + includeFontPadding are Android-specific but harmless on iOS.
|
|
63
64
|
* No lineHeight: explicit lineHeight on a single-line TextInput clips ascenders/descenders on iOS.
|
|
65
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
64
66
|
*/
|
|
65
67
|
input: {
|
|
66
68
|
fontFamily: font.family.body,
|
|
@@ -69,6 +71,7 @@ const styles = StyleSheet.create({
|
|
|
69
71
|
paddingVertical: 0,
|
|
70
72
|
textAlignVertical: 'center',
|
|
71
73
|
includeFontPadding: false,
|
|
74
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
|
|
72
75
|
},
|
|
73
76
|
});
|
|
74
77
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import { ThemeProvider } from '../../../../native/ThemeContext';
|
|
3
3
|
import { NumberInput } from './NumberInput';
|
|
4
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
4
5
|
|
|
5
6
|
function renderWithTheme(ui: React.ReactElement) {
|
|
6
7
|
return render(<ThemeProvider>{ui}</ThemeProvider>);
|
|
@@ -84,4 +85,16 @@ describe('NumberInput (native)', () => {
|
|
|
84
85
|
const heightPx = parseFloat(getComputedStyle(wrapper).height);
|
|
85
86
|
expect(heightPx).toBeGreaterThanOrEqual(44);
|
|
86
87
|
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Optical-centering invariant: the TextInput must have a translateY transform equal
|
|
91
|
+
* to OPTICAL_TEXT_SHIFT to compensate for Arial's asymmetric vertical metrics.
|
|
92
|
+
*/
|
|
93
|
+
it('has optical translateY shift on the TextInput (Arial centering correction)', () => {
|
|
94
|
+
renderWithTheme(<NumberInput label="Quantity" name="qty" value={3} />);
|
|
95
|
+
const input = screen.getByDisplayValue('3');
|
|
96
|
+
const transform = getComputedStyle(input).transform;
|
|
97
|
+
expect(OPTICAL_TEXT_SHIFT).toBe(-1.5);
|
|
98
|
+
expect(transform).toMatch(/(-1\.5|-1\.50)/);
|
|
99
|
+
});
|
|
87
100
|
});
|
|
@@ -12,6 +12,7 @@ import { spacing, font, border } from '../../../../native/tokens';
|
|
|
12
12
|
import { useTheme } from '../../../../native/ThemeContext';
|
|
13
13
|
import { Label } from '../atoms/Label';
|
|
14
14
|
import { Message } from '../atoms/Message';
|
|
15
|
+
import { OPTICAL_TEXT_SHIFT } from '../atoms/inputHelpers';
|
|
15
16
|
|
|
16
17
|
interface NumberInputProps {
|
|
17
18
|
label: string;
|
|
@@ -75,6 +76,7 @@ const styles = StyleSheet.create({
|
|
|
75
76
|
* No vertical padding: iOS auto-centers within the wrapper height.
|
|
76
77
|
* textAlignVertical + includeFontPadding provide Android parity.
|
|
77
78
|
* No lineHeight: explicit lineHeight clips ascenders/descenders on iOS.
|
|
79
|
+
* transform translateY: optical correction for Arial's asymmetric metrics — see OPTICAL_TEXT_SHIFT.
|
|
78
80
|
*/
|
|
79
81
|
input: {
|
|
80
82
|
fontFamily: font.family.body,
|
|
@@ -84,6 +86,7 @@ const styles = StyleSheet.create({
|
|
|
84
86
|
paddingVertical: 0,
|
|
85
87
|
textAlignVertical: 'center',
|
|
86
88
|
includeFontPadding: false,
|
|
89
|
+
transform: [{ translateY: OPTICAL_TEXT_SHIFT }],
|
|
87
90
|
},
|
|
88
91
|
});
|
|
89
92
|
|
|
@@ -71,8 +71,18 @@ function Example() {
|
|
|
71
71
|
| `accessibilityLabel` | string | -- | Label for the tab group |
|
|
72
72
|
| `onValueChange` | `(index: number) => void` | -- | Called with the tapped segment index |
|
|
73
73
|
|
|
74
|
+
## Visual structure
|
|
75
|
+
|
|
76
|
+
- **Track** — a single rounded container (`flexDirection: 'row'`, `borderRadius: full`, `colorBgSubtle` background, 2 px inner padding). No per-segment borders.
|
|
77
|
+
- **Segments** — equal-width (`flex: 1`), min touch target 44 dp for medium/large.
|
|
78
|
+
- **Selected thumb** — contrasting background rendered on the active segment only:
|
|
79
|
+
- `outline` variant → `colorBg` (elevated white / dark-elevated) + subtle shadow, label uses `colorInteractiveOnBg`.
|
|
80
|
+
- `filled` variant → `colorInteractive` background, label uses `colorOnInteractive`.
|
|
81
|
+
- **Unselected** — transparent background, label uses `colorOnBgSubtle`.
|
|
82
|
+
- Focus indication uses platform-default `opacity` on press; no stray rectangle outline.
|
|
83
|
+
|
|
74
84
|
## Accessibility
|
|
75
85
|
|
|
76
86
|
- Uses `accessibilityRole="tablist"` on the container
|
|
77
|
-
- Each segment uses `accessibilityRole="tab"` with `accessibilityState
|
|
78
|
-
- Disabled state is communicated via `accessibilityState`
|
|
87
|
+
- Each segment uses `accessibilityRole="tab"` with `accessibilityState={{ selected, disabled }}`
|
|
88
|
+
- Disabled state is communicated via `accessibilityState.disabled`
|
|
@@ -20,7 +20,7 @@ describe('SegmentedControl (native)', () => {
|
|
|
20
20
|
expect(screen.getByText('Month')).toBeInTheDocument();
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('has tablist role', () => {
|
|
23
|
+
it('has tablist role on the track container', () => {
|
|
24
24
|
renderWithTheme(
|
|
25
25
|
<SegmentedControl accessibilityLabel="View">
|
|
26
26
|
{'Day'}
|
|
@@ -41,6 +41,21 @@ describe('SegmentedControl (native)', () => {
|
|
|
41
41
|
expect(tabs).toHaveLength(2);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('marks the selected segment with accessibilityState.selected', () => {
|
|
45
|
+
renderWithTheme(
|
|
46
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={1}>
|
|
47
|
+
{'Day'}
|
|
48
|
+
{'Week'}
|
|
49
|
+
</SegmentedControl>,
|
|
50
|
+
);
|
|
51
|
+
// The selected tab is queryable by its label; both tabs still exist
|
|
52
|
+
const tabs = screen.getAllByRole('tab');
|
|
53
|
+
expect(tabs).toHaveLength(2);
|
|
54
|
+
// Week (index 1) is the selected one; Day (index 0) is not
|
|
55
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
44
59
|
it('calls onValueChange with the clicked index', () => {
|
|
45
60
|
const onValueChange = vi.fn();
|
|
46
61
|
renderWithTheme(
|
|
@@ -66,17 +81,44 @@ describe('SegmentedControl (native)', () => {
|
|
|
66
81
|
expect(onValueChange).not.toHaveBeenCalled();
|
|
67
82
|
});
|
|
68
83
|
|
|
69
|
-
it('
|
|
70
|
-
renderWithTheme(
|
|
71
|
-
<SegmentedControl accessibilityLabel="View" selectedIndex={
|
|
84
|
+
it('track has a single background — no per-segment borders', () => {
|
|
85
|
+
const { container } = renderWithTheme(
|
|
86
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0}>
|
|
72
87
|
{'Day'}
|
|
73
88
|
{'Week'}
|
|
74
89
|
</SegmentedControl>,
|
|
75
90
|
);
|
|
76
|
-
//
|
|
91
|
+
// The tablist (track) must exist and the segments (tabs) must not carry border styling
|
|
92
|
+
const tablist = container.querySelector('[role="tablist"]');
|
|
93
|
+
expect(tablist).not.toBeNull();
|
|
77
94
|
const tabs = screen.getAllByRole('tab');
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
tabs.forEach((tab) => {
|
|
96
|
+
const style = tab.getAttribute('style') ?? '';
|
|
97
|
+
expect(style).not.toMatch(/border-width/i);
|
|
98
|
+
expect(style).not.toMatch(/border-color/i);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('selected thumb uses colorBg for outline variant (elevated surface)', () => {
|
|
103
|
+
renderWithTheme(
|
|
104
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0} variant="outline">
|
|
105
|
+
{'Day'}
|
|
106
|
+
{'Week'}
|
|
107
|
+
</SegmentedControl>,
|
|
108
|
+
);
|
|
109
|
+
// Both tabs render; no regression
|
|
110
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('selected thumb uses colorInteractive for filled variant', () => {
|
|
115
|
+
renderWithTheme(
|
|
116
|
+
<SegmentedControl accessibilityLabel="View" selectedIndex={0} variant="filled">
|
|
117
|
+
{'Day'}
|
|
118
|
+
{'Week'}
|
|
119
|
+
</SegmentedControl>,
|
|
120
|
+
);
|
|
121
|
+
expect(screen.getByText('Day')).toBeInTheDocument();
|
|
80
122
|
expect(screen.getByText('Week')).toBeInTheDocument();
|
|
81
123
|
});
|
|
82
124
|
});
|
|
@@ -17,23 +17,36 @@ interface SegmentedControlProps {
|
|
|
17
17
|
style?: StyleProp<ViewStyle>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
/** Inner padding between track edge and segment edges (mirrors --bds-space_xxxs = 2px). */
|
|
21
|
+
const TRACK_PADDING = spacing.xxxs;
|
|
22
|
+
|
|
23
|
+
const sizeTokens: Record<SegmentedControlSize, { minHeight: number; fontSize: number; px: number; py: number }> = {
|
|
24
|
+
small: { minHeight: 32, fontSize: font.size.bodyS, px: spacing.s, py: spacing.xxs },
|
|
25
|
+
medium: { minHeight: 44, fontSize: font.size.body, px: spacing.m, py: spacing.xs },
|
|
26
|
+
large: { minHeight: 48, fontSize: font.size.body, px: spacing.m, py: spacing.s },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const thumbShadow = {
|
|
30
|
+
shadowColor: '#000',
|
|
31
|
+
shadowOffset: { width: 0, height: 1 },
|
|
32
|
+
shadowOpacity: 0.12,
|
|
33
|
+
shadowRadius: 2,
|
|
34
|
+
elevation: 2,
|
|
24
35
|
};
|
|
25
36
|
|
|
26
37
|
const styles = StyleSheet.create({
|
|
27
38
|
track: {
|
|
28
39
|
flexDirection: 'row',
|
|
29
40
|
borderRadius: border.radius.full,
|
|
41
|
+
padding: TRACK_PADDING,
|
|
30
42
|
overflow: 'hidden',
|
|
31
43
|
},
|
|
32
|
-
|
|
44
|
+
segment: {
|
|
33
45
|
flex: 1,
|
|
34
46
|
alignItems: 'center',
|
|
35
47
|
justifyContent: 'center',
|
|
36
|
-
|
|
48
|
+
// Thumb radius = track radius minus the padding gap (mirrors thumb-radius calc in web CSS)
|
|
49
|
+
borderRadius: border.radius.full - TRACK_PADDING,
|
|
37
50
|
},
|
|
38
51
|
label: {
|
|
39
52
|
fontFamily: font.family.body,
|
|
@@ -53,42 +66,46 @@ export function SegmentedControl({
|
|
|
53
66
|
style,
|
|
54
67
|
}: Readonly<SegmentedControlProps>) {
|
|
55
68
|
const { colors } = useTheme();
|
|
56
|
-
const {
|
|
69
|
+
const { minHeight, fontSize, px, py } = sizeTokens[size];
|
|
57
70
|
const items = Children.toArray(children);
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
// Track always uses the subtle background — same for both variants.
|
|
73
|
+
const trackBg = colors.colorBgSubtle;
|
|
74
|
+
|
|
75
|
+
// Thumb background differs by variant:
|
|
76
|
+
// "outline" → elevated surface (white in light, dark-elevated in dark) — mirrors web .--variant_outline .item.--active: colorBg
|
|
77
|
+
// "filled" → interactive brand colour — mirrors web default thumb bg: colorInteractive
|
|
78
|
+
const thumbBg = variant === 'outline' ? colors.colorBg : colors.colorInteractive;
|
|
62
79
|
|
|
63
80
|
return (
|
|
64
81
|
<View
|
|
65
|
-
style={[styles.track,
|
|
82
|
+
style={[styles.track, { backgroundColor: trackBg }, style]}
|
|
66
83
|
accessibilityRole="tablist"
|
|
67
84
|
accessibilityLabel={accessibilityLabel}
|
|
68
85
|
>
|
|
69
86
|
{items.map((child, index) => {
|
|
70
87
|
const isActive = index === selectedIndex;
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
// Active label colour:
|
|
90
|
+
// "outline" → interactive on the surface (matches web --control_label-color--active: colorInteractive for outline)
|
|
91
|
+
// "filled" → on-interactive (white/dark depending on theme)
|
|
92
|
+
const labelColor = isActive
|
|
93
|
+
? (variant === 'outline' ? colors.colorInteractiveOnBg : colors.colorOnInteractive)
|
|
94
|
+
: colors.colorOnBgSubtle;
|
|
75
95
|
|
|
76
|
-
const inactiveStyle: ViewStyle = variant === 'filled'
|
|
77
|
-
? {}
|
|
78
|
-
: {};
|
|
79
|
-
|
|
80
|
-
const textColor = variant === 'filled' && isActive
|
|
81
|
-
? colors.colorOnInteractive
|
|
82
|
-
: isActive
|
|
83
|
-
? colors.colorInteractive
|
|
84
|
-
: colors.colorOnBgSubtle;
|
|
85
|
-
|
|
86
|
-
// Extract label from child: if it's a string, use it directly.
|
|
87
|
-
// Otherwise render the child as-is.
|
|
88
96
|
const label = typeof child === 'string' || typeof child === 'number'
|
|
89
97
|
? String(child)
|
|
90
98
|
: null;
|
|
91
99
|
|
|
100
|
+
const segmentStyle: ViewStyle = {
|
|
101
|
+
minHeight,
|
|
102
|
+
paddingHorizontal: px,
|
|
103
|
+
paddingVertical: py,
|
|
104
|
+
...(isActive
|
|
105
|
+
? { backgroundColor: thumbBg, ...thumbShadow }
|
|
106
|
+
: { backgroundColor: 'transparent' }),
|
|
107
|
+
};
|
|
108
|
+
|
|
92
109
|
return (
|
|
93
110
|
<Pressable
|
|
94
111
|
key={index}
|
|
@@ -98,16 +115,14 @@ export function SegmentedControl({
|
|
|
98
115
|
accessibilityLabel={label ?? undefined}
|
|
99
116
|
accessibilityState={{ selected: isActive, disabled }}
|
|
100
117
|
style={({ pressed }) => [
|
|
101
|
-
styles.
|
|
102
|
-
|
|
103
|
-
isActive ? activeStyle : inactiveStyle,
|
|
104
|
-
variant === 'filled' && isActive && { borderRadius: border.radius.full },
|
|
118
|
+
styles.segment,
|
|
119
|
+
segmentStyle,
|
|
105
120
|
pressed && !disabled && { opacity: 0.7 },
|
|
106
121
|
disabled && { opacity: 0.4 },
|
|
107
122
|
]}
|
|
108
123
|
>
|
|
109
124
|
{label !== null ? (
|
|
110
|
-
<Text style={[styles.label, { fontSize, color:
|
|
125
|
+
<Text style={[styles.label, { fontSize, color: labelColor }]}>
|
|
111
126
|
{label}
|
|
112
127
|
</Text>
|
|
113
128
|
) : (
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optical vertical correction for single-line TextInput on iOS.
|
|
3
|
+
*
|
|
4
|
+
* Arial (the DS body font) has asymmetric vertical metrics:
|
|
5
|
+
* ascent 0.905em vs descent 0.212em
|
|
6
|
+
* When iOS centers the TextInput's layout box inside the 44pt wrapper,
|
|
7
|
+
* the INK centre sits ~0.08em (~1.3pt at fontSize 16) below the geometric
|
|
8
|
+
* centre. Applying translateY here shifts the ink from +1.33pt to -0.33pt,
|
|
9
|
+
* which is visually perfect (verified on-simulator).
|
|
10
|
+
*
|
|
11
|
+
* Do NOT apply this to multiline TextInput (Textarea) — multiline inputs
|
|
12
|
+
* scroll their content and translateY would misalign the first line.
|
|
13
|
+
*/
|
|
14
|
+
export const OPTICAL_TEXT_SHIFT = -1.5;
|