@kaisarsofi/react-native-phone-input 1.0.0
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/LICENSE +20 -0
- package/README.md +467 -0
- package/lib/commonjs/CountryPicker.js +669 -0
- package/lib/commonjs/CountryPicker.js.map +1 -0
- package/lib/commonjs/Flag.js +23 -0
- package/lib/commonjs/Flag.js.map +1 -0
- package/lib/commonjs/PhoneInput.js +252 -0
- package/lib/commonjs/PhoneInput.js.map +1 -0
- package/lib/commonjs/countries.js +75 -0
- package/lib/commonjs/countries.js.map +1 -0
- package/lib/commonjs/data/countries.json +1472 -0
- package/lib/commonjs/index.js +71 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/safeArea.js +44 -0
- package/lib/commonjs/safeArea.js.map +1 -0
- package/lib/commonjs/theme.js +85 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/value.js +68 -0
- package/lib/commonjs/value.js.map +1 -0
- package/lib/module/CountryPicker.js +665 -0
- package/lib/module/CountryPicker.js.map +1 -0
- package/lib/module/Flag.js +19 -0
- package/lib/module/Flag.js.map +1 -0
- package/lib/module/PhoneInput.js +248 -0
- package/lib/module/PhoneInput.js.map +1 -0
- package/lib/module/countries.js +65 -0
- package/lib/module/countries.js.map +1 -0
- package/lib/module/data/countries.json +1472 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/safeArea.js +39 -0
- package/lib/module/safeArea.js.map +1 -0
- package/lib/module/theme.js +81 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/value.js +62 -0
- package/lib/module/value.js.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/CountryPicker.d.ts +37 -0
- package/lib/typescript/commonjs/src/CountryPicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Flag.d.ts +10 -0
- package/lib/typescript/commonjs/src/Flag.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/PhoneInput.d.ts +3 -0
- package/lib/typescript/commonjs/src/PhoneInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/countries.d.ts +28 -0
- package/lib/typescript/commonjs/src/countries.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +9 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/safeArea.d.ts +11 -0
- package/lib/typescript/commonjs/src/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/theme.d.ts +53 -0
- package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/types.d.ts +211 -0
- package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/value.d.ts +22 -0
- package/lib/typescript/commonjs/src/value.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/CountryPicker.d.ts +37 -0
- package/lib/typescript/module/src/CountryPicker.d.ts.map +1 -0
- package/lib/typescript/module/src/Flag.d.ts +10 -0
- package/lib/typescript/module/src/Flag.d.ts.map +1 -0
- package/lib/typescript/module/src/PhoneInput.d.ts +3 -0
- package/lib/typescript/module/src/PhoneInput.d.ts.map +1 -0
- package/lib/typescript/module/src/countries.d.ts +28 -0
- package/lib/typescript/module/src/countries.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +9 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/safeArea.d.ts +11 -0
- package/lib/typescript/module/src/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/src/theme.d.ts +53 -0
- package/lib/typescript/module/src/theme.d.ts.map +1 -0
- package/lib/typescript/module/src/types.d.ts +211 -0
- package/lib/typescript/module/src/types.d.ts.map +1 -0
- package/lib/typescript/module/src/value.d.ts +22 -0
- package/lib/typescript/module/src/value.d.ts.map +1 -0
- package/package.json +215 -0
- package/src/CountryPicker.tsx +805 -0
- package/src/Flag.tsx +17 -0
- package/src/PhoneInput.tsx +363 -0
- package/src/countries.ts +95 -0
- package/src/data/countries.json +1472 -0
- package/src/index.tsx +21 -0
- package/src/safeArea.ts +50 -0
- package/src/theme.ts +112 -0
- package/src/types.ts +229 -0
- package/src/value.ts +70 -0
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
|
|
5
|
+
import { groupCountriesByLetter } from "./countries.js";
|
|
6
|
+
import { Flag } from "./Flag.js";
|
|
7
|
+
import { useOptionalSafeAreaInsets } from "./safeArea.js";
|
|
8
|
+
import { usePhoneInputPalette } from "./theme.js";
|
|
9
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
11
|
+
const ROW_HEIGHT = 48;
|
|
12
|
+
const HEADER_HEIGHT = 28;
|
|
13
|
+
/** Width reserved on the right for the A-Z index, so list content and the
|
|
14
|
+
* search box stop at the same place instead of running under it. */
|
|
15
|
+
const SIDEBAR_GUTTER = 32;
|
|
16
|
+
export function CountryPicker({
|
|
17
|
+
visible,
|
|
18
|
+
countries,
|
|
19
|
+
selected,
|
|
20
|
+
onSelect,
|
|
21
|
+
onClose,
|
|
22
|
+
searchPlaceholder = 'Search country or code',
|
|
23
|
+
disableSearch,
|
|
24
|
+
renderCountryItem,
|
|
25
|
+
palette: providedPalette,
|
|
26
|
+
colorScheme,
|
|
27
|
+
safeAreaInsets,
|
|
28
|
+
presentationStyle,
|
|
29
|
+
groupAlphabetically = true,
|
|
30
|
+
showAlphabetIndex = true,
|
|
31
|
+
styles: styleOverrides
|
|
32
|
+
}) {
|
|
33
|
+
const fallbackPalette = usePhoneInputPalette(colorScheme);
|
|
34
|
+
const palette = providedPalette ?? fallbackPalette;
|
|
35
|
+
|
|
36
|
+
// An explicit prop is applied as given; auto-detected insets are adjusted,
|
|
37
|
+
// because the ambient value describes the *window*, not this sheet. Inside
|
|
38
|
+
// an iOS pageSheet/formSheet the system has already inset the top, so
|
|
39
|
+
// adding the window's top inset there would push the header down by the
|
|
40
|
+
// notch height for no reason.
|
|
41
|
+
const ambientInsets = useOptionalSafeAreaInsets();
|
|
42
|
+
const isIosSheet = Platform.OS === 'ios' && (presentationStyle ?? 'pageSheet') !== 'fullScreen';
|
|
43
|
+
|
|
44
|
+
// Read edge by edge rather than depending on the prop object, which callers
|
|
45
|
+
// naturally write inline.
|
|
46
|
+
const insetTop = safeAreaInsets?.top;
|
|
47
|
+
const insetBottom = safeAreaInsets?.bottom;
|
|
48
|
+
const insetLeft = safeAreaInsets?.left;
|
|
49
|
+
const insetRight = safeAreaInsets?.right;
|
|
50
|
+
const insets = useMemo(() => {
|
|
51
|
+
const auto = ambientInsets ? {
|
|
52
|
+
...ambientInsets,
|
|
53
|
+
top: isIosSheet ? 0 : ambientInsets.top
|
|
54
|
+
} : null;
|
|
55
|
+
if (insetTop === undefined && insetBottom === undefined && insetLeft === undefined && insetRight === undefined) {
|
|
56
|
+
return auto;
|
|
57
|
+
}
|
|
58
|
+
// Each edge given explicitly wins; the rest still fall back to the
|
|
59
|
+
// detected value, so `{ top: 0 }` overrides only the top.
|
|
60
|
+
return {
|
|
61
|
+
top: insetTop ?? auto?.top ?? 0,
|
|
62
|
+
bottom: insetBottom ?? auto?.bottom ?? 0,
|
|
63
|
+
left: insetLeft ?? auto?.left ?? 0,
|
|
64
|
+
right: insetRight ?? auto?.right ?? 0
|
|
65
|
+
};
|
|
66
|
+
}, [insetTop, insetBottom, insetLeft, insetRight, ambientInsets, isIosSheet]);
|
|
67
|
+
const [query, setQuery] = useState('');
|
|
68
|
+
const scrollRef = useRef(null);
|
|
69
|
+
const sidebarRef = useRef(null);
|
|
70
|
+
const [activeLetter, setActiveLetter] = useState(null);
|
|
71
|
+
const sidebarLayout = useRef({
|
|
72
|
+
y: 0,
|
|
73
|
+
height: 1
|
|
74
|
+
});
|
|
75
|
+
const hideActiveLetterTimer = useRef(null);
|
|
76
|
+
useEffect(() => () => {
|
|
77
|
+
if (hideActiveLetterTimer.current) {
|
|
78
|
+
clearTimeout(hideActiveLetterTimer.current);
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Reset the search when the sheet closes, so reopening it starts on the
|
|
83
|
+
// full list rather than on whatever was typed last time.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!visible) setQuery('');
|
|
86
|
+
}, [visible]);
|
|
87
|
+
const showActiveLetterBriefly = letter => {
|
|
88
|
+
setActiveLetter(letter);
|
|
89
|
+
if (hideActiveLetterTimer.current) {
|
|
90
|
+
clearTimeout(hideActiveLetterTimer.current);
|
|
91
|
+
}
|
|
92
|
+
hideActiveLetterTimer.current = setTimeout(() => {
|
|
93
|
+
setActiveLetter(null);
|
|
94
|
+
hideActiveLetterTimer.current = null;
|
|
95
|
+
}, 600);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Every color the picker draws lives here rather than in the StyleSheet
|
|
99
|
+
// below, which now holds layout only — the two hardcoded light values that
|
|
100
|
+
// used to sit in `row` and `search` were exactly what made dark mode
|
|
101
|
+
// unreadable (bright separator lines, a near-white search box).
|
|
102
|
+
const colors = useMemo(() => ({
|
|
103
|
+
container: {
|
|
104
|
+
backgroundColor: palette.background
|
|
105
|
+
},
|
|
106
|
+
headerSection: {
|
|
107
|
+
backgroundColor: palette.background,
|
|
108
|
+
borderBottomColor: palette.border
|
|
109
|
+
},
|
|
110
|
+
searchContainer: {
|
|
111
|
+
backgroundColor: palette.surface,
|
|
112
|
+
borderColor: palette.border
|
|
113
|
+
},
|
|
114
|
+
search: {
|
|
115
|
+
color: palette.text
|
|
116
|
+
},
|
|
117
|
+
searchIcon: {
|
|
118
|
+
borderColor: palette.placeholder
|
|
119
|
+
},
|
|
120
|
+
searchIconFill: {
|
|
121
|
+
backgroundColor: palette.placeholder
|
|
122
|
+
},
|
|
123
|
+
handle: {
|
|
124
|
+
backgroundColor: palette.handle
|
|
125
|
+
},
|
|
126
|
+
title: {
|
|
127
|
+
color: palette.text
|
|
128
|
+
},
|
|
129
|
+
closeButton: {
|
|
130
|
+
backgroundColor: palette.surface
|
|
131
|
+
},
|
|
132
|
+
closeButtonPressed: {
|
|
133
|
+
backgroundColor: palette.surfacePressed
|
|
134
|
+
},
|
|
135
|
+
closeButtonText: {
|
|
136
|
+
color: palette.textMuted
|
|
137
|
+
},
|
|
138
|
+
rowSeparator: {
|
|
139
|
+
borderBottomColor: palette.border
|
|
140
|
+
},
|
|
141
|
+
rowSelected: {
|
|
142
|
+
backgroundColor: palette.surface
|
|
143
|
+
},
|
|
144
|
+
name: {
|
|
145
|
+
color: palette.text
|
|
146
|
+
},
|
|
147
|
+
dialCode: {
|
|
148
|
+
color: palette.textMuted
|
|
149
|
+
},
|
|
150
|
+
empty: {
|
|
151
|
+
color: palette.textMuted
|
|
152
|
+
},
|
|
153
|
+
sectionHeader: {
|
|
154
|
+
backgroundColor: palette.background
|
|
155
|
+
},
|
|
156
|
+
sectionHeaderText: {
|
|
157
|
+
color: palette.textMuted
|
|
158
|
+
},
|
|
159
|
+
sidebarLetter: {
|
|
160
|
+
color: palette.accent
|
|
161
|
+
},
|
|
162
|
+
sidebarLetterDisabled: {
|
|
163
|
+
color: palette.disabled
|
|
164
|
+
},
|
|
165
|
+
sidebarLetterActive: {
|
|
166
|
+
color: palette.accentContrast,
|
|
167
|
+
backgroundColor: palette.accent
|
|
168
|
+
},
|
|
169
|
+
bubble: {
|
|
170
|
+
backgroundColor: palette.bubble
|
|
171
|
+
},
|
|
172
|
+
bubbleText: {
|
|
173
|
+
color: palette.bubbleText
|
|
174
|
+
}
|
|
175
|
+
}), [palette]);
|
|
176
|
+
const filtered = useMemo(() => {
|
|
177
|
+
const q = query.trim().toLowerCase();
|
|
178
|
+
if (!q) return countries;
|
|
179
|
+
return countries.filter(c => c.name.toLowerCase().includes(q) || c.dialCode.includes(q.replace(/^\+/, '')) || c.code.toLowerCase().includes(q));
|
|
180
|
+
}, [countries, query]);
|
|
181
|
+
const isSearching = !!query.trim();
|
|
182
|
+
|
|
183
|
+
// Countries are always grouped into A-Z chunks (unless searching) so the
|
|
184
|
+
// alphabet index has something to jump to even when section headers
|
|
185
|
+
// (groupAlphabetically) are turned off — the two are independent: headers
|
|
186
|
+
// control what's visible in the list, the index controls the sidebar.
|
|
187
|
+
// `filtered` is the `countries` array itself whenever the query is empty,
|
|
188
|
+
// so the unsearched sections and the sidebar's letters are the same grouping
|
|
189
|
+
// — computed once here instead of twice.
|
|
190
|
+
const allSections = useMemo(() => groupCountriesByLetter(countries), [countries]);
|
|
191
|
+
const sections = useMemo(() => isSearching ? [{
|
|
192
|
+
letter: '',
|
|
193
|
+
data: filtered
|
|
194
|
+
}] : allSections, [filtered, isSearching, allSections]);
|
|
195
|
+
const showHeaders = groupAlphabetically && !isSearching;
|
|
196
|
+
|
|
197
|
+
// Whether the A-Z gutter is reserved is deliberately independent of the
|
|
198
|
+
// query: the sidebar itself hides while searching, but the space stays
|
|
199
|
+
// claimed so the search box and the list don't jump wider mid-keystroke.
|
|
200
|
+
const reservesSidebar = showAlphabetIndex && allSections.length > 3;
|
|
201
|
+
const showSidebar = reservesSidebar && !isSearching;
|
|
202
|
+
const availableLetters = useMemo(() => new Set(sections.map(s => s.letter)), [sections]);
|
|
203
|
+
|
|
204
|
+
// Offset of each section header, computed from fixed row/header heights
|
|
205
|
+
// (styles.row / styles.sectionHeader below) rather than measured via
|
|
206
|
+
// onLayout — this content isn't virtualized, so the analytical offset is
|
|
207
|
+
// exact and avoids relying on layout events firing through the Modal.
|
|
208
|
+
const sectionOffsets = useMemo(() => {
|
|
209
|
+
const offsets = {};
|
|
210
|
+
let offset = 0;
|
|
211
|
+
for (const section of sections) {
|
|
212
|
+
offsets[section.letter] = offset;
|
|
213
|
+
if (showHeaders && section.letter) offset += HEADER_HEIGHT;
|
|
214
|
+
offset += section.data.length * ROW_HEIGHT;
|
|
215
|
+
}
|
|
216
|
+
return offsets;
|
|
217
|
+
}, [sections, showHeaders]);
|
|
218
|
+
const jumpToLetter = letter => {
|
|
219
|
+
let target = letter;
|
|
220
|
+
if (!availableLetters.has(target)) {
|
|
221
|
+
const forward = ALPHABET.find(l => l >= letter && availableLetters.has(l));
|
|
222
|
+
const fallback = forward ?? [...ALPHABET].reverse().find(l => availableLetters.has(l));
|
|
223
|
+
if (!fallback) return;
|
|
224
|
+
target = fallback;
|
|
225
|
+
}
|
|
226
|
+
showActiveLetterBriefly(target);
|
|
227
|
+
const offset = sectionOffsets[target];
|
|
228
|
+
if (offset === undefined) return;
|
|
229
|
+
scrollRef.current?.scrollTo({
|
|
230
|
+
y: offset,
|
|
231
|
+
animated: false
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
const jumpFromPageY = pageY => {
|
|
235
|
+
const {
|
|
236
|
+
height
|
|
237
|
+
} = sidebarLayout.current;
|
|
238
|
+
const y = pageY - sidebarLayout.current.y;
|
|
239
|
+
const idx = Math.min(ALPHABET.length - 1, Math.max(0, Math.floor(y / height * ALPHABET.length)));
|
|
240
|
+
const letter = ALPHABET[idx];
|
|
241
|
+
if (letter) jumpToLetter(letter);
|
|
242
|
+
};
|
|
243
|
+
const handleSidebarTouch = evt => {
|
|
244
|
+
jumpFromPageY(evt.nativeEvent.pageY);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// react-native-web doesn't synthesize touch events from mouse input, so
|
|
248
|
+
// onTouchStart/onTouchMove alone never fire for a mouse click/drag — the
|
|
249
|
+
// sidebar would be entirely inert on web without these. isMouseDown tracks
|
|
250
|
+
// whether the button is held, mirroring onTouchMove's "only while touching"
|
|
251
|
+
// behavior, since onMouseMove otherwise fires on hover too.
|
|
252
|
+
const isMouseDown = useRef(false);
|
|
253
|
+
const webMouseHandlers = Platform.OS === 'web' ? {
|
|
254
|
+
onMouseDown: e => {
|
|
255
|
+
isMouseDown.current = true;
|
|
256
|
+
jumpFromPageY(e.nativeEvent.pageY);
|
|
257
|
+
},
|
|
258
|
+
onMouseMove: e => {
|
|
259
|
+
if (isMouseDown.current) jumpFromPageY(e.nativeEvent.pageY);
|
|
260
|
+
},
|
|
261
|
+
onMouseUp: () => {
|
|
262
|
+
isMouseDown.current = false;
|
|
263
|
+
if (hideActiveLetterTimer.current) {
|
|
264
|
+
clearTimeout(hideActiveLetterTimer.current);
|
|
265
|
+
hideActiveLetterTimer.current = null;
|
|
266
|
+
}
|
|
267
|
+
setActiveLetter(null);
|
|
268
|
+
}
|
|
269
|
+
} : {};
|
|
270
|
+
const renderRow = useCallback(item => {
|
|
271
|
+
const isSelected = item.code === selected?.code;
|
|
272
|
+
const onPress = () => onSelect(item);
|
|
273
|
+
if (renderCountryItem) {
|
|
274
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
275
|
+
children: renderCountryItem({
|
|
276
|
+
item,
|
|
277
|
+
isSelected,
|
|
278
|
+
onPress
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
283
|
+
onPress: onPress,
|
|
284
|
+
style: ({
|
|
285
|
+
pressed
|
|
286
|
+
}) => [styles.row, styleOverrides?.row, isSelected && [styles.rowSelected, colors.rowSelected, styleOverrides?.rowSelected], pressed && styles.rowPressed],
|
|
287
|
+
accessibilityRole: "button",
|
|
288
|
+
accessibilityLabel: `${item.name}, +${item.dialCode}`,
|
|
289
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
290
|
+
style: [styles.rowInner, colors.rowSeparator],
|
|
291
|
+
children: [/*#__PURE__*/_jsx(Flag, {
|
|
292
|
+
country: item,
|
|
293
|
+
size: 22,
|
|
294
|
+
style: [styles.flag, styleOverrides?.flag]
|
|
295
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
296
|
+
style: [styles.name, colors.name, styleOverrides?.name],
|
|
297
|
+
numberOfLines: 1,
|
|
298
|
+
children: item.name
|
|
299
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
300
|
+
style: [styles.dialCode, colors.dialCode, styleOverrides?.dialCode],
|
|
301
|
+
children: ["+", item.dialCode]
|
|
302
|
+
})]
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
}, [selected, onSelect, renderCountryItem, styleOverrides, colors]);
|
|
306
|
+
|
|
307
|
+
// Memoized separately from the rest of the tree so sidebar drag-scrubbing
|
|
308
|
+
// (which updates `activeLetter` on every touch move) doesn't force React to
|
|
309
|
+
// re-render every one of the ~240 rows on each frame — only the sidebar and
|
|
310
|
+
// the letter bubble, which actually depend on `activeLetter`, do.
|
|
311
|
+
const listContent = useMemo(() => /*#__PURE__*/_jsxs(_Fragment, {
|
|
312
|
+
children: [filtered.length === 0 && /*#__PURE__*/_jsx(Text, {
|
|
313
|
+
style: [styles.empty, colors.empty],
|
|
314
|
+
children: "No countries found"
|
|
315
|
+
}), sections.map(section => /*#__PURE__*/_jsxs(View, {
|
|
316
|
+
children: [showHeaders && section.letter ? /*#__PURE__*/_jsx(View, {
|
|
317
|
+
style: [styles.sectionHeader, colors.sectionHeader, styleOverrides?.sectionHeader],
|
|
318
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
319
|
+
style: [styles.sectionHeaderText, colors.sectionHeaderText, styleOverrides?.sectionHeaderText],
|
|
320
|
+
children: section.letter
|
|
321
|
+
})
|
|
322
|
+
}) : null, section.data.map(item => /*#__PURE__*/_jsx(View, {
|
|
323
|
+
children: renderRow(item)
|
|
324
|
+
}, item.code))]
|
|
325
|
+
}, section.letter || 'all'))]
|
|
326
|
+
}), [filtered.length, sections, showHeaders, styleOverrides, renderRow, colors]);
|
|
327
|
+
return /*#__PURE__*/_jsx(Modal, {
|
|
328
|
+
visible: visible,
|
|
329
|
+
animationType: "slide",
|
|
330
|
+
onRequestClose: onClose,
|
|
331
|
+
presentationStyle: Platform.OS === 'ios' ? presentationStyle ?? 'pageSheet' : undefined,
|
|
332
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
333
|
+
style: [styles.container, colors.container, insets ? {
|
|
334
|
+
paddingTop: insets.top,
|
|
335
|
+
paddingBottom: insets.bottom,
|
|
336
|
+
paddingLeft: insets.left,
|
|
337
|
+
paddingRight: insets.right
|
|
338
|
+
} : null, styleOverrides?.container],
|
|
339
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
340
|
+
style: [styles.headerSection, colors.headerSection, styleOverrides?.headerSection],
|
|
341
|
+
children: [Platform.OS === 'ios' && (presentationStyle ?? 'pageSheet') === 'pageSheet' && /*#__PURE__*/_jsx(View, {
|
|
342
|
+
style: styles.dragHandleWrap,
|
|
343
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
344
|
+
style: [styles.dragHandle, colors.handle]
|
|
345
|
+
})
|
|
346
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
347
|
+
style: [styles.header, styleOverrides?.header],
|
|
348
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
349
|
+
style: [styles.title, colors.title, styleOverrides?.title],
|
|
350
|
+
children: "Select a country"
|
|
351
|
+
}), /*#__PURE__*/_jsx(Pressable, {
|
|
352
|
+
onPress: onClose,
|
|
353
|
+
hitSlop: 8,
|
|
354
|
+
style: ({
|
|
355
|
+
pressed
|
|
356
|
+
}) => [styles.closeButton, colors.closeButton, styleOverrides?.closeButton, pressed && colors.closeButtonPressed],
|
|
357
|
+
accessibilityRole: "button",
|
|
358
|
+
accessibilityLabel: "Close",
|
|
359
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
360
|
+
style: [styles.closeButtonText, colors.closeButtonText, styleOverrides?.closeButtonText],
|
|
361
|
+
children: "\u2715"
|
|
362
|
+
})
|
|
363
|
+
})]
|
|
364
|
+
}), !disableSearch && /*#__PURE__*/_jsxs(View, {
|
|
365
|
+
style: [styles.searchContainer, reservesSidebar ? styles.searchWithJump : null, colors.searchContainer, styleOverrides?.searchContainer],
|
|
366
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
367
|
+
style: styles.searchIcon,
|
|
368
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
369
|
+
style: [styles.searchIconLens, colors.searchIcon]
|
|
370
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
371
|
+
style: [styles.searchIconHandle, colors.searchIconFill]
|
|
372
|
+
})]
|
|
373
|
+
}), /*#__PURE__*/_jsx(TextInput, {
|
|
374
|
+
value: query,
|
|
375
|
+
onChangeText: setQuery,
|
|
376
|
+
placeholder: searchPlaceholder,
|
|
377
|
+
placeholderTextColor: palette.placeholder,
|
|
378
|
+
style: [styles.search, colors.search, styleOverrides?.search],
|
|
379
|
+
autoCorrect: false,
|
|
380
|
+
autoCapitalize: "none",
|
|
381
|
+
clearButtonMode: "while-editing"
|
|
382
|
+
})]
|
|
383
|
+
})]
|
|
384
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
385
|
+
style: styles.body,
|
|
386
|
+
children: [/*#__PURE__*/_jsx(ScrollView, {
|
|
387
|
+
ref: scrollRef,
|
|
388
|
+
keyboardShouldPersistTaps: "handled"
|
|
389
|
+
// Fallback for when no insets are available: UIKit adds the bottom
|
|
390
|
+
// inset itself, since the scroll view's frame overlaps the home
|
|
391
|
+
// indicator but not the notch. Turned off once we pad the
|
|
392
|
+
// container ourselves, so the two don't stack.
|
|
393
|
+
,
|
|
394
|
+
contentInsetAdjustmentBehavior: insets?.bottom ? 'never' : 'automatic',
|
|
395
|
+
contentContainerStyle: reservesSidebar ? styles.listContentWithJump : undefined,
|
|
396
|
+
children: listContent
|
|
397
|
+
}), showSidebar && /*#__PURE__*/_jsx(View, {
|
|
398
|
+
ref: sidebarRef,
|
|
399
|
+
style: styles.sidebar,
|
|
400
|
+
onLayout: () => {
|
|
401
|
+
// Measure the sidebar's absolute position in the same
|
|
402
|
+
// coordinate space as touch events' `pageY` (measure()'s
|
|
403
|
+
// pageY, not measureInWindow()'s window-relative y, which can
|
|
404
|
+
// differ by the status bar/safe-area inset) rather than
|
|
405
|
+
// deriving it from `pageY - locationY` on each touch — that
|
|
406
|
+
// calculation is unreliable once the touch target is a nested
|
|
407
|
+
// child (each letter below), which was causing the touch
|
|
408
|
+
// position -> letter mapping to drift, sometimes badly enough
|
|
409
|
+
// to land several letters off (e.g. tapping "M" landing on "P").
|
|
410
|
+
sidebarRef.current?.measure((_x, _y, _w, height, _px, pageY) => {
|
|
411
|
+
sidebarLayout.current = {
|
|
412
|
+
y: pageY,
|
|
413
|
+
height
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
onTouchStart: handleSidebarTouch,
|
|
418
|
+
onTouchMove: handleSidebarTouch,
|
|
419
|
+
onTouchEnd: () => {
|
|
420
|
+
if (hideActiveLetterTimer.current) {
|
|
421
|
+
clearTimeout(hideActiveLetterTimer.current);
|
|
422
|
+
hideActiveLetterTimer.current = null;
|
|
423
|
+
}
|
|
424
|
+
setActiveLetter(null);
|
|
425
|
+
},
|
|
426
|
+
...webMouseHandlers,
|
|
427
|
+
children: ALPHABET.map(letter => /*#__PURE__*/_jsx(View, {
|
|
428
|
+
style: styles.sidebarLetterTouchable,
|
|
429
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
430
|
+
style: [styles.sidebarLetter, colors.sidebarLetter, styleOverrides?.sidebarLetter, !availableLetters.has(letter) && colors.sidebarLetterDisabled, activeLetter === letter && [styles.sidebarLetterActive, colors.sidebarLetterActive, styleOverrides?.sidebarLetterActive]],
|
|
431
|
+
children: letter
|
|
432
|
+
})
|
|
433
|
+
}, letter))
|
|
434
|
+
}), activeLetter && /*#__PURE__*/_jsx(View, {
|
|
435
|
+
style: [styles.bubble, colors.bubble],
|
|
436
|
+
pointerEvents: "none",
|
|
437
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
438
|
+
style: [styles.bubbleText, colors.bubbleText],
|
|
439
|
+
children: activeLetter
|
|
440
|
+
})
|
|
441
|
+
})]
|
|
442
|
+
})]
|
|
443
|
+
})
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const styles = StyleSheet.create({
|
|
447
|
+
container: {
|
|
448
|
+
flex: 1
|
|
449
|
+
},
|
|
450
|
+
dragHandleWrap: {
|
|
451
|
+
alignItems: 'center',
|
|
452
|
+
paddingTop: 8
|
|
453
|
+
},
|
|
454
|
+
dragHandle: {
|
|
455
|
+
width: 36,
|
|
456
|
+
height: 5,
|
|
457
|
+
borderRadius: 3
|
|
458
|
+
},
|
|
459
|
+
// Title + search read as one block, ruled off from the list, instead of the
|
|
460
|
+
// search floating loose above the first section band.
|
|
461
|
+
headerSection: {
|
|
462
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
463
|
+
// Sits above the list so the shadow falls over the rows scrolling beneath
|
|
464
|
+
// it, which separates the two far better than a line on its own.
|
|
465
|
+
zIndex: 1,
|
|
466
|
+
...Platform.select({
|
|
467
|
+
ios: {
|
|
468
|
+
shadowColor: '#000',
|
|
469
|
+
shadowOpacity: 0.06,
|
|
470
|
+
shadowRadius: 8,
|
|
471
|
+
shadowOffset: {
|
|
472
|
+
width: 0,
|
|
473
|
+
height: 3
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
android: {
|
|
477
|
+
elevation: 3
|
|
478
|
+
},
|
|
479
|
+
default: {}
|
|
480
|
+
})
|
|
481
|
+
},
|
|
482
|
+
header: {
|
|
483
|
+
flexDirection: 'row',
|
|
484
|
+
alignItems: 'center',
|
|
485
|
+
justifyContent: 'space-between',
|
|
486
|
+
paddingHorizontal: 20,
|
|
487
|
+
paddingTop: 16,
|
|
488
|
+
paddingBottom: 16
|
|
489
|
+
},
|
|
490
|
+
title: {
|
|
491
|
+
fontSize: 20,
|
|
492
|
+
fontWeight: '700'
|
|
493
|
+
},
|
|
494
|
+
closeButton: {
|
|
495
|
+
width: 30,
|
|
496
|
+
height: 30,
|
|
497
|
+
borderRadius: 15,
|
|
498
|
+
alignItems: 'center',
|
|
499
|
+
justifyContent: 'center'
|
|
500
|
+
},
|
|
501
|
+
closeButtonText: {
|
|
502
|
+
fontSize: 14,
|
|
503
|
+
fontWeight: '700'
|
|
504
|
+
},
|
|
505
|
+
searchContainer: {
|
|
506
|
+
flexDirection: 'row',
|
|
507
|
+
alignItems: 'center',
|
|
508
|
+
gap: 8,
|
|
509
|
+
marginLeft: 20,
|
|
510
|
+
marginRight: 20,
|
|
511
|
+
marginBottom: 14,
|
|
512
|
+
paddingHorizontal: 12,
|
|
513
|
+
height: 48,
|
|
514
|
+
borderRadius: 12,
|
|
515
|
+
borderWidth: StyleSheet.hairlineWidth
|
|
516
|
+
},
|
|
517
|
+
searchIcon: {
|
|
518
|
+
width: 15,
|
|
519
|
+
height: 15,
|
|
520
|
+
alignItems: 'center',
|
|
521
|
+
justifyContent: 'center'
|
|
522
|
+
},
|
|
523
|
+
searchIconLens: {
|
|
524
|
+
width: 11,
|
|
525
|
+
height: 11,
|
|
526
|
+
borderRadius: 6,
|
|
527
|
+
borderWidth: 1.5,
|
|
528
|
+
position: 'absolute',
|
|
529
|
+
top: 0,
|
|
530
|
+
left: 0
|
|
531
|
+
},
|
|
532
|
+
searchIconHandle: {
|
|
533
|
+
position: 'absolute',
|
|
534
|
+
width: 1.5,
|
|
535
|
+
height: 6,
|
|
536
|
+
borderRadius: 1,
|
|
537
|
+
right: 1,
|
|
538
|
+
bottom: 0,
|
|
539
|
+
transform: [{
|
|
540
|
+
rotate: '-45deg'
|
|
541
|
+
}]
|
|
542
|
+
},
|
|
543
|
+
search: {
|
|
544
|
+
flex: 1,
|
|
545
|
+
minWidth: 0,
|
|
546
|
+
paddingVertical: 0,
|
|
547
|
+
fontSize: 16,
|
|
548
|
+
...(Platform.OS === 'web' ? {
|
|
549
|
+
outlineStyle: 'none'
|
|
550
|
+
} : null),
|
|
551
|
+
...(Platform.OS === 'android' ? {
|
|
552
|
+
textAlignVertical: 'center',
|
|
553
|
+
includeFontPadding: false
|
|
554
|
+
} : null)
|
|
555
|
+
},
|
|
556
|
+
searchWithJump: {
|
|
557
|
+
marginRight: 20 + SIDEBAR_GUTTER
|
|
558
|
+
},
|
|
559
|
+
body: {
|
|
560
|
+
flex: 1,
|
|
561
|
+
flexDirection: 'row'
|
|
562
|
+
},
|
|
563
|
+
listContentWithJump: {
|
|
564
|
+
paddingRight: SIDEBAR_GUTTER
|
|
565
|
+
},
|
|
566
|
+
row: {
|
|
567
|
+
height: ROW_HEIGHT,
|
|
568
|
+
flexDirection: 'row',
|
|
569
|
+
paddingLeft: 20
|
|
570
|
+
},
|
|
571
|
+
rowInner: {
|
|
572
|
+
flex: 1,
|
|
573
|
+
flexDirection: 'row',
|
|
574
|
+
alignItems: 'center',
|
|
575
|
+
paddingRight: 20,
|
|
576
|
+
gap: 12,
|
|
577
|
+
borderBottomWidth: StyleSheet.hairlineWidth
|
|
578
|
+
},
|
|
579
|
+
rowSelected: {},
|
|
580
|
+
rowPressed: {
|
|
581
|
+
opacity: 0.6
|
|
582
|
+
},
|
|
583
|
+
flag: {
|
|
584
|
+
marginRight: 0
|
|
585
|
+
},
|
|
586
|
+
name: {
|
|
587
|
+
flex: 1,
|
|
588
|
+
fontSize: 16
|
|
589
|
+
},
|
|
590
|
+
dialCode: {
|
|
591
|
+
fontSize: 16
|
|
592
|
+
},
|
|
593
|
+
empty: {
|
|
594
|
+
textAlign: 'center',
|
|
595
|
+
marginTop: 32
|
|
596
|
+
},
|
|
597
|
+
sectionHeader: {
|
|
598
|
+
height: HEADER_HEIGHT,
|
|
599
|
+
justifyContent: 'flex-end',
|
|
600
|
+
paddingHorizontal: 20,
|
|
601
|
+
paddingBottom: 4
|
|
602
|
+
},
|
|
603
|
+
sectionHeaderText: {
|
|
604
|
+
fontSize: 12,
|
|
605
|
+
fontWeight: '700',
|
|
606
|
+
letterSpacing: 0.6
|
|
607
|
+
},
|
|
608
|
+
sidebar: {
|
|
609
|
+
// Inset from top/bottom (rather than padding the content box) so the
|
|
610
|
+
// measured height used by handleSidebarTouch still matches exactly what
|
|
611
|
+
// the flex:1 letters occupy — padding would leave the touch math using a
|
|
612
|
+
// taller box than the letters actually fill, reintroducing the same
|
|
613
|
+
// mismatch fixed above. The larger bottom inset keeps "Z" clear of the
|
|
614
|
+
// safe-area edge/home indicator instead of sitting flush against it.
|
|
615
|
+
position: 'absolute',
|
|
616
|
+
// On web, a visible OS scrollbar can render on top of a sidebar pinned
|
|
617
|
+
// to the very edge (browsers reserve/overlay scrollbar width there),
|
|
618
|
+
// eating its clicks — inset it clear of that on web specifically.
|
|
619
|
+
right: Platform.OS === 'web' ? 14 : 0,
|
|
620
|
+
top: 8,
|
|
621
|
+
bottom: 28,
|
|
622
|
+
width: 28,
|
|
623
|
+
alignItems: 'stretch',
|
|
624
|
+
...(Platform.OS === 'web' ? {
|
|
625
|
+
cursor: 'pointer'
|
|
626
|
+
} : null)
|
|
627
|
+
},
|
|
628
|
+
// Each letter gets an equal flex share of the sidebar's full height, so
|
|
629
|
+
// its rendered slice matches exactly what `handleSidebarTouch` computes
|
|
630
|
+
// (`floor((y / height) * 26)`) — with the old center-packed layout, the 26
|
|
631
|
+
// letters occupied a short block in the middle of a much taller container,
|
|
632
|
+
// so a touch position mapped from the FULL height landed on the wrong
|
|
633
|
+
// letter (sometimes several letters off) almost everywhere except near the
|
|
634
|
+
// exact center.
|
|
635
|
+
sidebarLetterTouchable: {
|
|
636
|
+
flex: 1,
|
|
637
|
+
alignItems: 'center',
|
|
638
|
+
justifyContent: 'center'
|
|
639
|
+
},
|
|
640
|
+
sidebarLetter: {
|
|
641
|
+
fontSize: 12,
|
|
642
|
+
fontWeight: '600',
|
|
643
|
+
lineHeight: Platform.OS === 'ios' ? 15 : 17,
|
|
644
|
+
textAlign: 'center'
|
|
645
|
+
},
|
|
646
|
+
sidebarLetterActive: {
|
|
647
|
+
borderRadius: 6,
|
|
648
|
+
overflow: 'hidden'
|
|
649
|
+
},
|
|
650
|
+
bubble: {
|
|
651
|
+
position: 'absolute',
|
|
652
|
+
right: 40,
|
|
653
|
+
top: '45%',
|
|
654
|
+
width: 64,
|
|
655
|
+
height: 64,
|
|
656
|
+
borderRadius: 32,
|
|
657
|
+
alignItems: 'center',
|
|
658
|
+
justifyContent: 'center'
|
|
659
|
+
},
|
|
660
|
+
bubbleText: {
|
|
661
|
+
fontSize: 28,
|
|
662
|
+
fontWeight: '700'
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
//# sourceMappingURL=CountryPicker.js.map
|