@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.
Files changed (91) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +467 -0
  3. package/lib/commonjs/CountryPicker.js +669 -0
  4. package/lib/commonjs/CountryPicker.js.map +1 -0
  5. package/lib/commonjs/Flag.js +23 -0
  6. package/lib/commonjs/Flag.js.map +1 -0
  7. package/lib/commonjs/PhoneInput.js +252 -0
  8. package/lib/commonjs/PhoneInput.js.map +1 -0
  9. package/lib/commonjs/countries.js +75 -0
  10. package/lib/commonjs/countries.js.map +1 -0
  11. package/lib/commonjs/data/countries.json +1472 -0
  12. package/lib/commonjs/index.js +71 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/package.json +1 -0
  15. package/lib/commonjs/safeArea.js +44 -0
  16. package/lib/commonjs/safeArea.js.map +1 -0
  17. package/lib/commonjs/theme.js +85 -0
  18. package/lib/commonjs/theme.js.map +1 -0
  19. package/lib/commonjs/types.js +6 -0
  20. package/lib/commonjs/types.js.map +1 -0
  21. package/lib/commonjs/value.js +68 -0
  22. package/lib/commonjs/value.js.map +1 -0
  23. package/lib/module/CountryPicker.js +665 -0
  24. package/lib/module/CountryPicker.js.map +1 -0
  25. package/lib/module/Flag.js +19 -0
  26. package/lib/module/Flag.js.map +1 -0
  27. package/lib/module/PhoneInput.js +248 -0
  28. package/lib/module/PhoneInput.js.map +1 -0
  29. package/lib/module/countries.js +65 -0
  30. package/lib/module/countries.js.map +1 -0
  31. package/lib/module/data/countries.json +1472 -0
  32. package/lib/module/index.js +8 -0
  33. package/lib/module/index.js.map +1 -0
  34. package/lib/module/package.json +1 -0
  35. package/lib/module/safeArea.js +39 -0
  36. package/lib/module/safeArea.js.map +1 -0
  37. package/lib/module/theme.js +81 -0
  38. package/lib/module/theme.js.map +1 -0
  39. package/lib/module/types.js +4 -0
  40. package/lib/module/types.js.map +1 -0
  41. package/lib/module/value.js +62 -0
  42. package/lib/module/value.js.map +1 -0
  43. package/lib/typescript/commonjs/package.json +1 -0
  44. package/lib/typescript/commonjs/src/CountryPicker.d.ts +37 -0
  45. package/lib/typescript/commonjs/src/CountryPicker.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/src/Flag.d.ts +10 -0
  47. package/lib/typescript/commonjs/src/Flag.d.ts.map +1 -0
  48. package/lib/typescript/commonjs/src/PhoneInput.d.ts +3 -0
  49. package/lib/typescript/commonjs/src/PhoneInput.d.ts.map +1 -0
  50. package/lib/typescript/commonjs/src/countries.d.ts +28 -0
  51. package/lib/typescript/commonjs/src/countries.d.ts.map +1 -0
  52. package/lib/typescript/commonjs/src/index.d.ts +9 -0
  53. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/src/safeArea.d.ts +11 -0
  55. package/lib/typescript/commonjs/src/safeArea.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/theme.d.ts +53 -0
  57. package/lib/typescript/commonjs/src/theme.d.ts.map +1 -0
  58. package/lib/typescript/commonjs/src/types.d.ts +211 -0
  59. package/lib/typescript/commonjs/src/types.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/src/value.d.ts +22 -0
  61. package/lib/typescript/commonjs/src/value.d.ts.map +1 -0
  62. package/lib/typescript/module/package.json +1 -0
  63. package/lib/typescript/module/src/CountryPicker.d.ts +37 -0
  64. package/lib/typescript/module/src/CountryPicker.d.ts.map +1 -0
  65. package/lib/typescript/module/src/Flag.d.ts +10 -0
  66. package/lib/typescript/module/src/Flag.d.ts.map +1 -0
  67. package/lib/typescript/module/src/PhoneInput.d.ts +3 -0
  68. package/lib/typescript/module/src/PhoneInput.d.ts.map +1 -0
  69. package/lib/typescript/module/src/countries.d.ts +28 -0
  70. package/lib/typescript/module/src/countries.d.ts.map +1 -0
  71. package/lib/typescript/module/src/index.d.ts +9 -0
  72. package/lib/typescript/module/src/index.d.ts.map +1 -0
  73. package/lib/typescript/module/src/safeArea.d.ts +11 -0
  74. package/lib/typescript/module/src/safeArea.d.ts.map +1 -0
  75. package/lib/typescript/module/src/theme.d.ts +53 -0
  76. package/lib/typescript/module/src/theme.d.ts.map +1 -0
  77. package/lib/typescript/module/src/types.d.ts +211 -0
  78. package/lib/typescript/module/src/types.d.ts.map +1 -0
  79. package/lib/typescript/module/src/value.d.ts +22 -0
  80. package/lib/typescript/module/src/value.d.ts.map +1 -0
  81. package/package.json +215 -0
  82. package/src/CountryPicker.tsx +805 -0
  83. package/src/Flag.tsx +17 -0
  84. package/src/PhoneInput.tsx +363 -0
  85. package/src/countries.ts +95 -0
  86. package/src/data/countries.json +1472 -0
  87. package/src/index.tsx +21 -0
  88. package/src/safeArea.ts +50 -0
  89. package/src/theme.ts +112 -0
  90. package/src/types.ts +229 -0
  91. 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