@scripso-homepad/ui 0.3.5 → 0.3.7
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/README.md +232 -78
- package/dist/index.cjs +1128 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +322 -18
- package/dist/index.d.ts +322 -18
- package/dist/index.js +1107 -89
- package/dist/index.js.map +1 -1
- package/package.json +18 -6
- package/src/components/Button.stories.tsx +77 -57
- package/src/components/Button.tsx +146 -124
- package/src/components/CountryCodeSelector.stories.tsx +61 -0
- package/src/components/CountryCodeSelector.tsx +273 -0
- package/src/components/Input.stories.tsx +117 -0
- package/src/components/Input.tsx +177 -0
- package/src/components/Label.tsx +56 -0
- package/src/components/PhoneInput.stories.tsx +85 -0
- package/src/components/PhoneInput.tsx +172 -0
- package/src/data/countries.ts +98 -0
- package/src/icons/ArrowUpRightIcon.tsx +29 -0
- package/src/icons/ArrowUpRightIcon.web.tsx +35 -0
- package/src/icons/ChevronDownIcon.tsx +29 -0
- package/src/icons/ChevronDownIcon.web.tsx +35 -0
- package/src/icons/EyeIcon.tsx +48 -0
- package/src/icons/KeyIcon.tsx +36 -0
- package/src/icons/KeyIcon.web.tsx +42 -0
- package/src/icons/arrowUpRightPath.ts +2 -0
- package/src/icons/chevronDownPath.ts +1 -0
- package/src/icons/keyIconPaths.ts +5 -0
- package/src/index.ts +42 -0
- package/src/theme/input.ts +139 -0
- package/src/theme/tokens.ts +272 -0
- package/src/utils/countryDropdownScrollStyles.ts +52 -0
- package/src/utils/countryFlag.ts +9 -0
- package/src/utils/useApplyWebClassName.ts +3 -2
|
@@ -1,48 +1,111 @@
|
|
|
1
|
-
import { useRef, type ComponentRef } from "react";
|
|
1
|
+
import { useRef, type ComponentRef, type ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
StyleSheet,
|
|
4
4
|
Text,
|
|
5
5
|
TouchableOpacity,
|
|
6
|
+
View,
|
|
6
7
|
type GestureResponderEvent,
|
|
7
8
|
type StyleProp,
|
|
8
9
|
type TextStyle,
|
|
9
10
|
type ViewStyle,
|
|
10
11
|
} from "react-native";
|
|
12
|
+
import { ArrowUpRightIcon } from "../icons/ArrowUpRightIcon";
|
|
13
|
+
import { colors, buttonTypography } from "../theme/tokens";
|
|
11
14
|
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
12
15
|
|
|
13
|
-
export type ButtonVariant = "
|
|
14
|
-
export type ButtonSize = "
|
|
16
|
+
export type ButtonVariant = "white" | "primary" | "green" | "gray";
|
|
17
|
+
export type ButtonSize = "lg" | "sm";
|
|
15
18
|
|
|
16
19
|
export interface ButtonProps {
|
|
17
|
-
title
|
|
20
|
+
title?: string;
|
|
21
|
+
children?: ReactNode;
|
|
18
22
|
onPress: (event: GestureResponderEvent) => void;
|
|
19
23
|
disabled?: boolean;
|
|
20
|
-
/** Visual style preset. */
|
|
21
24
|
variant?: ButtonVariant;
|
|
22
|
-
/** Size preset. */
|
|
23
25
|
size?: ButtonSize;
|
|
24
|
-
|
|
26
|
+
showIcon?: boolean;
|
|
27
|
+
/** Custom icon inside the badge. Defaults to `ArrowUpRightIcon` when `showIcon` is true. */
|
|
28
|
+
icon?: ReactNode;
|
|
25
29
|
style?: StyleProp<ViewStyle>;
|
|
26
|
-
/** Additional label styles (works on web and native). */
|
|
27
30
|
textStyle?: StyleProp<TextStyle>;
|
|
28
|
-
/**
|
|
29
|
-
* CSS class names for the container (web: applied to the same DOM node as default styles).
|
|
30
|
-
* On native: ignored unless using NativeWind with cssInterop.
|
|
31
|
-
*/
|
|
32
31
|
className?: string;
|
|
33
|
-
/**
|
|
34
|
-
* CSS class names for the label (web).
|
|
35
|
-
* On native: ignored unless using NativeWind with cssInterop.
|
|
36
|
-
*/
|
|
37
32
|
textClassName?: string;
|
|
38
33
|
}
|
|
39
34
|
|
|
35
|
+
type VariantStyleSet = {
|
|
36
|
+
backgroundColor: string;
|
|
37
|
+
textColor: string;
|
|
38
|
+
iconBadgeBackgroundColor: string;
|
|
39
|
+
iconColor: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const variantConfig: Record<ButtonVariant, VariantStyleSet> = {
|
|
43
|
+
white: {
|
|
44
|
+
backgroundColor: colors.white,
|
|
45
|
+
textColor: colors.slateBlue,
|
|
46
|
+
iconBadgeBackgroundColor: colors.stormGray150,
|
|
47
|
+
iconColor: colors.navy,
|
|
48
|
+
},
|
|
49
|
+
primary: {
|
|
50
|
+
backgroundColor: colors.navy,
|
|
51
|
+
textColor: colors.stormGray0,
|
|
52
|
+
iconBadgeBackgroundColor: colors.white,
|
|
53
|
+
iconColor: colors.navy,
|
|
54
|
+
},
|
|
55
|
+
green: {
|
|
56
|
+
backgroundColor: colors.green,
|
|
57
|
+
textColor: colors.stormGray0,
|
|
58
|
+
iconBadgeBackgroundColor: colors.white,
|
|
59
|
+
iconColor: colors.green,
|
|
60
|
+
},
|
|
61
|
+
gray: {
|
|
62
|
+
backgroundColor: colors.stormGray50,
|
|
63
|
+
textColor: colors.slateBlue,
|
|
64
|
+
iconBadgeBackgroundColor: colors.stormGray150,
|
|
65
|
+
iconColor: colors.navy,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const sizeConfig = {
|
|
70
|
+
lg: {
|
|
71
|
+
borderRadius: 16,
|
|
72
|
+
paddingTop: 8,
|
|
73
|
+
paddingRight: 8,
|
|
74
|
+
paddingBottom: 8,
|
|
75
|
+
paddingLeft: 24,
|
|
76
|
+
paddingHorizontalCentered: 24,
|
|
77
|
+
minHeight: 52,
|
|
78
|
+
text: buttonTypography.lg,
|
|
79
|
+
iconContainerSize: 36,
|
|
80
|
+
iconContainerRadius: 12,
|
|
81
|
+
iconContainerPadding: 8,
|
|
82
|
+
iconSize: 20,
|
|
83
|
+
},
|
|
84
|
+
sm: {
|
|
85
|
+
borderRadius: 12,
|
|
86
|
+
paddingTop: 8,
|
|
87
|
+
paddingRight: 8,
|
|
88
|
+
paddingBottom: 8,
|
|
89
|
+
paddingLeft: 16,
|
|
90
|
+
paddingHorizontalCentered: 16,
|
|
91
|
+
minHeight: 48,
|
|
92
|
+
text: buttonTypography.sm,
|
|
93
|
+
iconContainerSize: 32,
|
|
94
|
+
iconContainerRadius: 8,
|
|
95
|
+
iconContainerPadding: 8,
|
|
96
|
+
iconSize: 16,
|
|
97
|
+
},
|
|
98
|
+
} as const;
|
|
99
|
+
|
|
40
100
|
export function Button({
|
|
41
101
|
title,
|
|
102
|
+
children,
|
|
42
103
|
onPress,
|
|
43
104
|
disabled = false,
|
|
44
105
|
variant = "primary",
|
|
45
|
-
size = "
|
|
106
|
+
size = "lg",
|
|
107
|
+
showIcon = false,
|
|
108
|
+
icon,
|
|
46
109
|
style,
|
|
47
110
|
textStyle,
|
|
48
111
|
className,
|
|
@@ -50,23 +113,40 @@ export function Button({
|
|
|
50
113
|
}: ButtonProps) {
|
|
51
114
|
const containerRef = useRef<ComponentRef<typeof TouchableOpacity>>(null);
|
|
52
115
|
const textRef = useRef<ComponentRef<typeof Text>>(null);
|
|
116
|
+
const preset = variantConfig[variant];
|
|
117
|
+
const metrics = sizeConfig[size];
|
|
53
118
|
|
|
54
119
|
useApplyWebClassName(containerRef, className);
|
|
55
120
|
useApplyWebClassName(textRef, textClassName);
|
|
56
121
|
|
|
122
|
+
const label = typeof children !== "undefined" ? children : title;
|
|
123
|
+
const hasCustomChildren = typeof children !== "undefined";
|
|
124
|
+
const hasIcon = showIcon || icon != null;
|
|
125
|
+
const iconNode =
|
|
126
|
+
icon ?? <ArrowUpRightIcon size={metrics.iconSize} color={preset.iconColor} />;
|
|
127
|
+
|
|
57
128
|
const containerStyle = [
|
|
58
129
|
styles.base,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
130
|
+
{
|
|
131
|
+
borderRadius: metrics.borderRadius,
|
|
132
|
+
backgroundColor: preset.backgroundColor,
|
|
133
|
+
minHeight: metrics.minHeight,
|
|
134
|
+
paddingTop: metrics.paddingTop,
|
|
135
|
+
paddingBottom: metrics.paddingBottom,
|
|
136
|
+
paddingLeft: hasIcon ? metrics.paddingLeft : metrics.paddingHorizontalCentered,
|
|
137
|
+
paddingRight: hasIcon ? metrics.paddingRight : metrics.paddingHorizontalCentered,
|
|
138
|
+
},
|
|
139
|
+
hasIcon ? styles.withIcon : styles.centered,
|
|
140
|
+
disabled && styles.disabled,
|
|
62
141
|
style,
|
|
63
142
|
];
|
|
64
143
|
|
|
65
144
|
const labelStyle = [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
145
|
+
styles.label,
|
|
146
|
+
metrics.text,
|
|
147
|
+
{ color: preset.textColor },
|
|
148
|
+
!hasIcon && styles.labelCentered,
|
|
149
|
+
hasIcon && styles.labelWithIcon,
|
|
70
150
|
textStyle,
|
|
71
151
|
];
|
|
72
152
|
|
|
@@ -80,119 +160,61 @@ export function Button({
|
|
|
80
160
|
accessibilityRole="button"
|
|
81
161
|
accessibilityState={{ disabled }}
|
|
82
162
|
>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
163
|
+
{hasCustomChildren ? (
|
|
164
|
+
children
|
|
165
|
+
) : (
|
|
166
|
+
<Text ref={textRef} style={labelStyle}>
|
|
167
|
+
{label}
|
|
168
|
+
</Text>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{hasIcon ? (
|
|
172
|
+
<View
|
|
173
|
+
style={[
|
|
174
|
+
styles.iconContainer,
|
|
175
|
+
{
|
|
176
|
+
width: metrics.iconContainerSize,
|
|
177
|
+
height: metrics.iconContainerSize,
|
|
178
|
+
borderRadius: metrics.iconContainerRadius,
|
|
179
|
+
padding: metrics.iconContainerPadding,
|
|
180
|
+
backgroundColor: preset.iconBadgeBackgroundColor,
|
|
181
|
+
},
|
|
182
|
+
]}
|
|
183
|
+
>
|
|
184
|
+
{iconNode}
|
|
185
|
+
</View>
|
|
186
|
+
) : null}
|
|
86
187
|
</TouchableOpacity>
|
|
87
188
|
);
|
|
88
189
|
}
|
|
89
190
|
|
|
90
191
|
const styles = StyleSheet.create({
|
|
91
192
|
base: {
|
|
193
|
+
flexDirection: "row",
|
|
92
194
|
alignItems: "center",
|
|
93
|
-
justifyContent: "center",
|
|
94
|
-
borderRadius: 8,
|
|
95
195
|
borderWidth: 0,
|
|
96
196
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const variantStyles = StyleSheet.create({
|
|
100
|
-
primary: {
|
|
101
|
-
backgroundColor: "#2563eb",
|
|
102
|
-
},
|
|
103
|
-
secondary: {
|
|
104
|
-
backgroundColor: "#4b5563",
|
|
105
|
-
},
|
|
106
|
-
outline: {
|
|
107
|
-
backgroundColor: "transparent",
|
|
108
|
-
borderWidth: 1,
|
|
109
|
-
borderColor: "#2563eb",
|
|
110
|
-
},
|
|
111
|
-
ghost: {
|
|
112
|
-
backgroundColor: "transparent",
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const sizeStyles = StyleSheet.create({
|
|
117
|
-
small: {
|
|
118
|
-
paddingVertical: 8,
|
|
119
|
-
paddingHorizontal: 16,
|
|
120
|
-
minWidth: 96,
|
|
121
|
-
},
|
|
122
|
-
medium: {
|
|
123
|
-
paddingVertical: 12,
|
|
124
|
-
paddingHorizontal: 24,
|
|
125
|
-
minWidth: 120,
|
|
126
|
-
},
|
|
127
|
-
large: {
|
|
128
|
-
paddingVertical: 16,
|
|
129
|
-
paddingHorizontal: 32,
|
|
130
|
-
minWidth: 160,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const disabledVariantStyles = StyleSheet.create({
|
|
135
|
-
primary: {
|
|
136
|
-
backgroundColor: "#93c5fd",
|
|
137
|
-
opacity: 0.7,
|
|
138
|
-
},
|
|
139
|
-
secondary: {
|
|
140
|
-
backgroundColor: "#9ca3af",
|
|
141
|
-
opacity: 0.7,
|
|
142
|
-
},
|
|
143
|
-
outline: {
|
|
144
|
-
borderColor: "#93c5fd",
|
|
145
|
-
opacity: 0.7,
|
|
146
|
-
},
|
|
147
|
-
ghost: {
|
|
148
|
-
opacity: 0.5,
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const textBaseStyles = StyleSheet.create({
|
|
153
|
-
base: {
|
|
154
|
-
fontWeight: "600",
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const textVariantStyles = StyleSheet.create({
|
|
159
|
-
primary: {
|
|
160
|
-
color: "#ffffff",
|
|
161
|
-
},
|
|
162
|
-
secondary: {
|
|
163
|
-
color: "#ffffff",
|
|
164
|
-
},
|
|
165
|
-
outline: {
|
|
166
|
-
color: "#2563eb",
|
|
197
|
+
centered: {
|
|
198
|
+
justifyContent: "center",
|
|
167
199
|
},
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
withIcon: {
|
|
201
|
+
justifyContent: "space-between",
|
|
170
202
|
},
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const textSizeStyles = StyleSheet.create({
|
|
174
|
-
small: {
|
|
175
|
-
fontSize: 14,
|
|
203
|
+
disabled: {
|
|
204
|
+
opacity: 0.6,
|
|
176
205
|
},
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
label: {},
|
|
207
|
+
labelCentered: {
|
|
208
|
+
textAlign: "center",
|
|
179
209
|
},
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
labelWithIcon: {
|
|
211
|
+
flex: 1,
|
|
212
|
+
flexShrink: 1,
|
|
213
|
+
marginRight: 8,
|
|
182
214
|
},
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
color: "#e5e7eb",
|
|
188
|
-
},
|
|
189
|
-
secondary: {
|
|
190
|
-
color: "#f3f4f6",
|
|
191
|
-
},
|
|
192
|
-
outline: {
|
|
193
|
-
color: "#93c5fd",
|
|
194
|
-
},
|
|
195
|
-
ghost: {
|
|
196
|
-
color: "#93c5fd",
|
|
215
|
+
iconContainer: {
|
|
216
|
+
alignItems: "center",
|
|
217
|
+
justifyContent: "center",
|
|
218
|
+
flexShrink: 0,
|
|
197
219
|
},
|
|
198
220
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { View, StyleSheet } from "react-native";
|
|
4
|
+
import { CountryCodeSelector } from "./CountryCodeSelector";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/CountryCodeSelector",
|
|
8
|
+
component: CountryCodeSelector,
|
|
9
|
+
} satisfies Meta<typeof CountryCodeSelector>;
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
export const Default: Story = {
|
|
15
|
+
render: () => (
|
|
16
|
+
<View style={styles.wrapper}>
|
|
17
|
+
<CountryCodeSelector />
|
|
18
|
+
</View>
|
|
19
|
+
),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Controlled: Story = {
|
|
23
|
+
render: function ControlledCountryStory() {
|
|
24
|
+
const [code, setCode] = useState("AM");
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.wrapper}>
|
|
27
|
+
<CountryCodeSelector value={code} onValueChange={setCode} />
|
|
28
|
+
</View>
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Disabled: Story = {
|
|
34
|
+
render: () => (
|
|
35
|
+
<View style={styles.wrapper}>
|
|
36
|
+
<CountryCodeSelector value="AM" disabled />
|
|
37
|
+
</View>
|
|
38
|
+
),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Opens upward when near the bottom of the viewport. */
|
|
42
|
+
export const OpenNearBottom: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<View style={styles.bottomWrapper}>
|
|
45
|
+
<CountryCodeSelector />
|
|
46
|
+
</View>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
wrapper: {
|
|
52
|
+
alignItems: "flex-start",
|
|
53
|
+
},
|
|
54
|
+
bottomWrapper: {
|
|
55
|
+
flex: 1,
|
|
56
|
+
minHeight: 500,
|
|
57
|
+
justifyContent: "flex-end",
|
|
58
|
+
alignItems: "flex-start",
|
|
59
|
+
paddingBottom: 16,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useRef, useState, useLayoutEffect, type ComponentRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dimensions,
|
|
4
|
+
Modal,
|
|
5
|
+
Platform,
|
|
6
|
+
Pressable,
|
|
7
|
+
ScrollView,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
View,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
type ViewStyle,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import {
|
|
15
|
+
countries,
|
|
16
|
+
defaultCountry,
|
|
17
|
+
findCountry,
|
|
18
|
+
type Country,
|
|
19
|
+
} from "../data/countries";
|
|
20
|
+
import { ChevronDownIcon } from "../icons/ChevronDownIcon";
|
|
21
|
+
import { colors, fontSize, fontWeight, radii, spacing, fonts } from "../theme/tokens";
|
|
22
|
+
import { countryCodeToFlagEmoji } from "../utils/countryFlag";
|
|
23
|
+
import {
|
|
24
|
+
COUNTRY_DROPDOWN_SCROLL_CLASS,
|
|
25
|
+
ensureCountryDropdownScrollStyles,
|
|
26
|
+
} from "../utils/countryDropdownScrollStyles";
|
|
27
|
+
import { useApplyWebClassName } from "../utils/useApplyWebClassName";
|
|
28
|
+
|
|
29
|
+
const DROPDOWN_GAP = 10;
|
|
30
|
+
const DROPDOWN_HEIGHT = 186;
|
|
31
|
+
const DROPDOWN_PADDING = 8;
|
|
32
|
+
const OPTION_GAP = 10;
|
|
33
|
+
const OPTION_HEIGHT = 35;
|
|
34
|
+
const TRIGGER_HEIGHT = 52;
|
|
35
|
+
const CHEVRON_SIZE = 16;
|
|
36
|
+
|
|
37
|
+
export interface CountryCodeSelectorProps {
|
|
38
|
+
value?: string;
|
|
39
|
+
onValueChange?: (countryCode: string) => void;
|
|
40
|
+
/** Country list (defaults to built-in list). */
|
|
41
|
+
options?: Country[];
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
style?: StyleProp<ViewStyle>;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type AnchorLayout = {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function resolveDropdownTop(anchor: AnchorLayout): number {
|
|
55
|
+
const windowHeight = Dimensions.get("window").height;
|
|
56
|
+
const topBelow = anchor.y + anchor.height + DROPDOWN_GAP;
|
|
57
|
+
const topAbove = anchor.y - DROPDOWN_GAP - DROPDOWN_HEIGHT;
|
|
58
|
+
const fitsBelow = topBelow + DROPDOWN_HEIGHT <= windowHeight;
|
|
59
|
+
|
|
60
|
+
if (fitsBelow) return topBelow;
|
|
61
|
+
if (topAbove >= 0) return topAbove;
|
|
62
|
+
|
|
63
|
+
return Math.max(0, Math.min(topBelow, windowHeight - DROPDOWN_HEIGHT));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function CountryCodeSelector({
|
|
67
|
+
value = defaultCountry.code,
|
|
68
|
+
onValueChange,
|
|
69
|
+
options = countries,
|
|
70
|
+
disabled = false,
|
|
71
|
+
style,
|
|
72
|
+
className,
|
|
73
|
+
}: CountryCodeSelectorProps) {
|
|
74
|
+
const wrapperRef = useRef<ComponentRef<typeof View>>(null);
|
|
75
|
+
const triggerRef = useRef<ComponentRef<typeof Pressable>>(null);
|
|
76
|
+
const scrollRef = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
77
|
+
const [open, setOpen] = useState(false);
|
|
78
|
+
const [anchor, setAnchor] = useState<AnchorLayout | null>(null);
|
|
79
|
+
|
|
80
|
+
useApplyWebClassName(wrapperRef, className);
|
|
81
|
+
useApplyWebClassName(
|
|
82
|
+
scrollRef,
|
|
83
|
+
COUNTRY_DROPDOWN_SCROLL_CLASS,
|
|
84
|
+
open && Boolean(anchor),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
useLayoutEffect(() => {
|
|
88
|
+
ensureCountryDropdownScrollStyles();
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const selected = findCountry(value);
|
|
92
|
+
const textColor = disabled ? colors.stormGray200 : colors.slateBlue;
|
|
93
|
+
|
|
94
|
+
function closeDropdown() {
|
|
95
|
+
setOpen(false);
|
|
96
|
+
setAnchor(null);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleOpen() {
|
|
100
|
+
if (disabled) return;
|
|
101
|
+
|
|
102
|
+
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
|
103
|
+
setAnchor({ x, y, width, height });
|
|
104
|
+
setOpen(true);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleSelect(code: string) {
|
|
109
|
+
onValueChange?.(code);
|
|
110
|
+
closeDropdown();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dropdownTop = anchor ? resolveDropdownTop(anchor) : 0;
|
|
114
|
+
|
|
115
|
+
const optionList = options.map((item, index) => {
|
|
116
|
+
const isSelected = item.code === selected.code;
|
|
117
|
+
const isLast = index === options.length - 1;
|
|
118
|
+
return (
|
|
119
|
+
<Pressable
|
|
120
|
+
key={item.code}
|
|
121
|
+
accessibilityRole="button"
|
|
122
|
+
onPress={() => handleSelect(item.code)}
|
|
123
|
+
style={[
|
|
124
|
+
styles.option,
|
|
125
|
+
isSelected && styles.optionSelected,
|
|
126
|
+
!isLast && styles.optionSpacing,
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
<Text style={styles.flag}>{countryCodeToFlagEmoji(item.code)}</Text>
|
|
130
|
+
<Text style={styles.optionDialCode}>{item.dialCode}</Text>
|
|
131
|
+
</Pressable>
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const dropdownList = (
|
|
136
|
+
<ScrollView
|
|
137
|
+
ref={scrollRef}
|
|
138
|
+
style={styles.dropdownScroll}
|
|
139
|
+
contentContainerStyle={styles.dropdownContent}
|
|
140
|
+
keyboardShouldPersistTaps="handled"
|
|
141
|
+
showsVerticalScrollIndicator
|
|
142
|
+
bounces={false}
|
|
143
|
+
nestedScrollEnabled
|
|
144
|
+
>
|
|
145
|
+
{optionList}
|
|
146
|
+
</ScrollView>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<View ref={wrapperRef} style={[styles.root, style]}>
|
|
151
|
+
<Pressable
|
|
152
|
+
ref={triggerRef}
|
|
153
|
+
accessibilityRole="button"
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
onPress={handleOpen}
|
|
156
|
+
style={[
|
|
157
|
+
styles.trigger,
|
|
158
|
+
disabled ? styles.triggerDisabled : styles.triggerEnabled,
|
|
159
|
+
Platform.OS === "web" ? styles.triggerWeb : null,
|
|
160
|
+
]}
|
|
161
|
+
>
|
|
162
|
+
<Text style={styles.flag}>{countryCodeToFlagEmoji(selected.code)}</Text>
|
|
163
|
+
<Text style={[styles.dialCode, { color: textColor }]}>{selected.dialCode}</Text>
|
|
164
|
+
<ChevronDownIcon size={CHEVRON_SIZE} color={colors.stormGray100} />
|
|
165
|
+
</Pressable>
|
|
166
|
+
|
|
167
|
+
<Modal
|
|
168
|
+
visible={open}
|
|
169
|
+
transparent
|
|
170
|
+
animationType="fade"
|
|
171
|
+
onRequestClose={closeDropdown}
|
|
172
|
+
>
|
|
173
|
+
<Pressable style={styles.overlay} onPress={closeDropdown}>
|
|
174
|
+
{anchor ? (
|
|
175
|
+
<View
|
|
176
|
+
style={[
|
|
177
|
+
styles.dropdown,
|
|
178
|
+
{
|
|
179
|
+
top: dropdownTop,
|
|
180
|
+
left: anchor.x,
|
|
181
|
+
width: anchor.width,
|
|
182
|
+
},
|
|
183
|
+
]}
|
|
184
|
+
>
|
|
185
|
+
{dropdownList}
|
|
186
|
+
</View>
|
|
187
|
+
) : null}
|
|
188
|
+
</Pressable>
|
|
189
|
+
</Modal>
|
|
190
|
+
</View>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const styles = StyleSheet.create({
|
|
195
|
+
root: {
|
|
196
|
+
alignSelf: "flex-start",
|
|
197
|
+
flexShrink: 0,
|
|
198
|
+
},
|
|
199
|
+
trigger: {
|
|
200
|
+
flexDirection: "row",
|
|
201
|
+
alignItems: "center",
|
|
202
|
+
alignSelf: "flex-start",
|
|
203
|
+
flexGrow: 0,
|
|
204
|
+
flexShrink: 0,
|
|
205
|
+
height: TRIGGER_HEIGHT,
|
|
206
|
+
borderRadius: radii.lg,
|
|
207
|
+
borderWidth: 1,
|
|
208
|
+
padding: spacing.lg,
|
|
209
|
+
gap: spacing.sm,
|
|
210
|
+
},
|
|
211
|
+
triggerEnabled: {
|
|
212
|
+
borderColor: colors.stormGray50,
|
|
213
|
+
backgroundColor: colors.white,
|
|
214
|
+
},
|
|
215
|
+
triggerDisabled: {
|
|
216
|
+
borderColor: colors.stormGray50,
|
|
217
|
+
backgroundColor: colors.stormGray50,
|
|
218
|
+
},
|
|
219
|
+
triggerWeb: {
|
|
220
|
+
width: "max-content" as ViewStyle["width"],
|
|
221
|
+
},
|
|
222
|
+
flag: {
|
|
223
|
+
fontSize: 18,
|
|
224
|
+
lineHeight: 22,
|
|
225
|
+
},
|
|
226
|
+
dialCode: {
|
|
227
|
+
fontSize: fontSize.md,
|
|
228
|
+
fontWeight: fontWeight.medium,
|
|
229
|
+
},
|
|
230
|
+
overlay: {
|
|
231
|
+
flex: 1,
|
|
232
|
+
backgroundColor: "transparent",
|
|
233
|
+
},
|
|
234
|
+
dropdown: {
|
|
235
|
+
position: "absolute",
|
|
236
|
+
height: DROPDOWN_HEIGHT,
|
|
237
|
+
borderRadius: radii.lg,
|
|
238
|
+
borderWidth: 1,
|
|
239
|
+
borderColor: colors.stormGray50,
|
|
240
|
+
backgroundColor: colors.white,
|
|
241
|
+
padding: DROPDOWN_PADDING,
|
|
242
|
+
overflow: "hidden",
|
|
243
|
+
},
|
|
244
|
+
dropdownScroll: {
|
|
245
|
+
flex: 1,
|
|
246
|
+
},
|
|
247
|
+
dropdownContent: {
|
|
248
|
+
flexGrow: 1,
|
|
249
|
+
},
|
|
250
|
+
option: {
|
|
251
|
+
flexDirection: "row",
|
|
252
|
+
alignItems: "center",
|
|
253
|
+
height: OPTION_HEIGHT,
|
|
254
|
+
borderRadius: radii.sm,
|
|
255
|
+
padding: spacing.sm,
|
|
256
|
+
gap: spacing.sm,
|
|
257
|
+
},
|
|
258
|
+
optionSpacing: {
|
|
259
|
+
marginBottom: OPTION_GAP,
|
|
260
|
+
},
|
|
261
|
+
optionSelected: {
|
|
262
|
+
backgroundColor: colors.countrySelectorSelectedBg,
|
|
263
|
+
},
|
|
264
|
+
optionDialCode: {
|
|
265
|
+
fontFamily: fonts.sans,
|
|
266
|
+
fontSize: fontSize.md,
|
|
267
|
+
fontWeight: fontWeight.regular,
|
|
268
|
+
lineHeight: fontSize.md,
|
|
269
|
+
letterSpacing: 0,
|
|
270
|
+
textAlign: "center",
|
|
271
|
+
color: colors.slateBlue,
|
|
272
|
+
},
|
|
273
|
+
});
|