@blocklet/payment-react 1.18.55 → 1.19.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 (220) hide show
  1. package/es/checkout/donate.d.ts +1 -15
  2. package/es/checkout/donate.js +301 -189
  3. package/es/checkout/form.d.ts +1 -15
  4. package/es/checkout/form.js +5 -13
  5. package/es/checkout/table.js +3 -3
  6. package/es/components/blockchain/gas.d.ts +1 -5
  7. package/es/components/blockchain/gas.js +10 -2
  8. package/es/components/blockchain/tx.d.ts +1 -8
  9. package/es/components/blockchain/tx.js +28 -7
  10. package/es/components/confirm.d.ts +1 -10
  11. package/es/components/confirm.js +4 -10
  12. package/es/components/country-select.d.ts +3 -2
  13. package/es/components/country-select.js +375 -352
  14. package/es/components/input.d.ts +11 -20
  15. package/es/components/input.js +46 -43
  16. package/es/components/lazy-loader.js +1 -2
  17. package/es/components/link.d.ts +2 -9
  18. package/es/components/link.js +9 -6
  19. package/es/components/livemode.d.ts +2 -8
  20. package/es/components/livemode.js +1 -5
  21. package/es/components/loading-button.d.ts +6 -1
  22. package/es/components/loading-button.js +56 -66
  23. package/es/components/over-due-invoice-payment.d.ts +0 -18
  24. package/es/components/over-due-invoice-payment.js +138 -95
  25. package/es/components/payment-beneficiaries.d.ts +2 -7
  26. package/es/components/payment-beneficiaries.js +86 -40
  27. package/es/components/pricing-item.d.ts +0 -5
  28. package/es/components/pricing-item.js +1 -4
  29. package/es/components/pricing-table.d.ts +2 -10
  30. package/es/components/pricing-table.js +8 -7
  31. package/es/components/resume-subscription.d.ts +0 -10
  32. package/es/components/resume-subscription.js +42 -21
  33. package/es/components/truncated-text.d.ts +2 -9
  34. package/es/components/truncated-text.js +0 -5
  35. package/es/contexts/donate.d.ts +0 -7
  36. package/es/contexts/donate.js +10 -8
  37. package/es/contexts/payment.d.ts +1 -4
  38. package/es/contexts/payment.js +7 -2
  39. package/es/history/invoice/list.d.ts +2 -18
  40. package/es/history/invoice/list.js +151 -73
  41. package/es/history/payment/list.js +115 -38
  42. package/es/hooks/keyboard.d.ts +1 -1
  43. package/es/hooks/keyboard.js +2 -4
  44. package/es/libs/cached-request.js +2 -4
  45. package/es/libs/phone-validator.js +1 -2
  46. package/es/libs/util.js +2 -4
  47. package/es/libs/validator.js +2 -4
  48. package/es/payment/amount.d.ts +2 -7
  49. package/es/payment/amount.js +1 -5
  50. package/es/payment/donation-form.d.ts +2 -10
  51. package/es/payment/donation-form.js +196 -160
  52. package/es/payment/error.d.ts +2 -8
  53. package/es/payment/error.js +40 -20
  54. package/es/payment/footer.d.ts +2 -3
  55. package/es/payment/footer.js +19 -6
  56. package/es/payment/form/addon.js +14 -4
  57. package/es/payment/form/address.d.ts +2 -9
  58. package/es/payment/form/address.js +3 -6
  59. package/es/payment/form/currency.js +45 -25
  60. package/es/payment/form/index.d.ts +2 -8
  61. package/es/payment/form/index.js +107 -65
  62. package/es/payment/form/phone.js +2 -4
  63. package/es/payment/form/stripe/form.d.ts +2 -8
  64. package/es/payment/form/stripe/form.js +1 -3
  65. package/es/payment/header.js +38 -16
  66. package/es/payment/index.d.ts +2 -9
  67. package/es/payment/index.js +5 -14
  68. package/es/payment/product-card.d.ts +2 -11
  69. package/es/payment/product-card.js +84 -50
  70. package/es/payment/product-donation.js +175 -114
  71. package/es/payment/product-item.d.ts +2 -9
  72. package/es/payment/product-item.js +185 -142
  73. package/es/payment/product-skeleton.js +2 -2
  74. package/es/payment/skeleton/donation.js +27 -7
  75. package/es/payment/skeleton/overview.js +22 -2
  76. package/es/payment/skeleton/payment.js +33 -5
  77. package/es/payment/success.d.ts +2 -9
  78. package/es/payment/success.js +41 -14
  79. package/es/payment/summary.d.ts +2 -17
  80. package/es/payment/summary.js +184 -111
  81. package/es/theme/index.d.ts +0 -5
  82. package/es/theme/index.js +2 -5
  83. package/es/theme/typography.d.ts +2 -2
  84. package/lib/checkout/donate.d.ts +1 -15
  85. package/lib/checkout/donate.js +75 -54
  86. package/lib/checkout/form.d.ts +1 -15
  87. package/lib/checkout/form.js +7 -15
  88. package/lib/checkout/table.js +4 -4
  89. package/lib/components/blockchain/gas.d.ts +1 -5
  90. package/lib/components/blockchain/gas.js +3 -2
  91. package/lib/components/blockchain/tx.d.ts +1 -8
  92. package/lib/components/blockchain/tx.js +11 -7
  93. package/lib/components/confirm.d.ts +1 -10
  94. package/lib/components/confirm.js +5 -11
  95. package/lib/components/country-select.d.ts +3 -2
  96. package/lib/components/country-select.js +23 -22
  97. package/lib/components/input.d.ts +11 -20
  98. package/lib/components/input.js +20 -23
  99. package/lib/components/lazy-loader.js +1 -1
  100. package/lib/components/link.d.ts +2 -9
  101. package/lib/components/link.js +3 -8
  102. package/lib/components/livemode.d.ts +2 -8
  103. package/lib/components/livemode.js +3 -7
  104. package/lib/components/loading-button.d.ts +6 -1
  105. package/lib/components/loading-button.js +9 -17
  106. package/lib/components/over-due-invoice-payment.d.ts +0 -18
  107. package/lib/components/over-due-invoice-payment.js +31 -33
  108. package/lib/components/payment-beneficiaries.d.ts +2 -7
  109. package/lib/components/payment-beneficiaries.js +12 -11
  110. package/lib/components/pricing-item.d.ts +0 -5
  111. package/lib/components/pricing-item.js +2 -5
  112. package/lib/components/pricing-table.d.ts +2 -10
  113. package/lib/components/pricing-table.js +5 -11
  114. package/lib/components/resume-subscription.d.ts +0 -10
  115. package/lib/components/resume-subscription.js +16 -16
  116. package/lib/components/table.js +1 -1
  117. package/lib/components/truncated-text.d.ts +2 -9
  118. package/lib/components/truncated-text.js +1 -6
  119. package/lib/contexts/donate.d.ts +0 -7
  120. package/lib/contexts/donate.js +4 -7
  121. package/lib/contexts/payment.d.ts +1 -4
  122. package/lib/contexts/payment.js +4 -7
  123. package/lib/history/invoice/list.d.ts +2 -18
  124. package/lib/history/invoice/list.js +49 -37
  125. package/lib/history/payment/list.js +30 -16
  126. package/lib/hooks/keyboard.d.ts +1 -1
  127. package/lib/hooks/mobile.js +1 -1
  128. package/lib/hooks/subscription.js +1 -1
  129. package/lib/index.js +2 -2
  130. package/lib/libs/api.js +1 -1
  131. package/lib/libs/dayjs.js +1 -1
  132. package/lib/libs/phone-validator.js +0 -2
  133. package/lib/libs/theme.js +1 -1
  134. package/lib/libs/util.js +1 -1
  135. package/lib/libs/validator.js +1 -1
  136. package/lib/locales/en.js +1 -1
  137. package/lib/locales/index.js +1 -1
  138. package/lib/locales/zh.js +1 -1
  139. package/lib/payment/amount.d.ts +2 -7
  140. package/lib/payment/amount.js +2 -6
  141. package/lib/payment/donation-form.d.ts +2 -10
  142. package/lib/payment/donation-form.js +33 -38
  143. package/lib/payment/error.d.ts +2 -8
  144. package/lib/payment/error.js +11 -13
  145. package/lib/payment/footer.d.ts +2 -3
  146. package/lib/payment/footer.js +5 -5
  147. package/lib/payment/form/addon.js +5 -3
  148. package/lib/payment/form/address.d.ts +2 -9
  149. package/lib/payment/form/address.js +5 -8
  150. package/lib/payment/form/currency.js +3 -3
  151. package/lib/payment/form/index.d.ts +2 -8
  152. package/lib/payment/form/index.js +19 -15
  153. package/lib/payment/form/phone.js +1 -1
  154. package/lib/payment/form/stripe/form.d.ts +2 -8
  155. package/lib/payment/form/stripe/form.js +3 -6
  156. package/lib/payment/header.js +8 -4
  157. package/lib/payment/index.d.ts +2 -9
  158. package/lib/payment/index.js +7 -16
  159. package/lib/payment/product-card.d.ts +2 -11
  160. package/lib/payment/product-card.js +13 -20
  161. package/lib/payment/product-donation.js +71 -66
  162. package/lib/payment/product-item.d.ts +2 -9
  163. package/lib/payment/product-item.js +24 -25
  164. package/lib/payment/product-skeleton.js +2 -2
  165. package/lib/payment/skeleton/donation.js +8 -4
  166. package/lib/payment/skeleton/overview.js +6 -2
  167. package/lib/payment/skeleton/payment.js +9 -3
  168. package/lib/payment/success.d.ts +2 -9
  169. package/lib/payment/success.js +12 -15
  170. package/lib/payment/summary.d.ts +2 -17
  171. package/lib/payment/summary.js +44 -45
  172. package/lib/theme/index.d.ts +0 -5
  173. package/lib/theme/index.js +2 -5
  174. package/lib/theme/typography.d.ts +2 -2
  175. package/package.json +40 -40
  176. package/src/checkout/donate.tsx +103 -35
  177. package/src/checkout/form.tsx +5 -14
  178. package/src/checkout/table.tsx +3 -3
  179. package/src/components/blockchain/gas.tsx +5 -3
  180. package/src/components/blockchain/tx.tsx +22 -8
  181. package/src/components/confirm.tsx +4 -11
  182. package/src/components/country-select.tsx +391 -378
  183. package/src/components/input.tsx +49 -45
  184. package/src/components/link.tsx +9 -7
  185. package/src/components/livemode.tsx +2 -6
  186. package/src/components/loading-button.tsx +63 -76
  187. package/src/components/over-due-invoice-payment.tsx +43 -28
  188. package/src/components/payment-beneficiaries.tsx +33 -14
  189. package/src/components/pricing-item.tsx +1 -4
  190. package/src/components/pricing-table.tsx +8 -8
  191. package/src/components/resume-subscription.tsx +20 -14
  192. package/src/components/table.tsx +2 -2
  193. package/src/components/truncated-text.tsx +0 -6
  194. package/src/contexts/donate.tsx +6 -7
  195. package/src/contexts/payment.tsx +7 -3
  196. package/src/history/invoice/list.tsx +74 -35
  197. package/src/history/payment/list.tsx +53 -16
  198. package/src/hooks/keyboard.ts +1 -1
  199. package/src/payment/amount.tsx +1 -6
  200. package/src/payment/donation-form.tsx +47 -29
  201. package/src/payment/error.tsx +16 -8
  202. package/src/payment/footer.tsx +11 -3
  203. package/src/payment/form/addon.tsx +6 -1
  204. package/src/payment/form/address.tsx +3 -7
  205. package/src/payment/form/currency.tsx +12 -2
  206. package/src/payment/form/index.tsx +30 -12
  207. package/src/payment/form/stripe/form.tsx +1 -5
  208. package/src/payment/header.tsx +14 -2
  209. package/src/payment/index.tsx +10 -21
  210. package/src/payment/product-card.tsx +41 -18
  211. package/src/payment/product-donation.tsx +85 -47
  212. package/src/payment/product-item.tsx +46 -24
  213. package/src/payment/product-skeleton.tsx +3 -2
  214. package/src/payment/skeleton/donation.tsx +12 -2
  215. package/src/payment/skeleton/overview.tsx +12 -2
  216. package/src/payment/skeleton/payment.tsx +16 -3
  217. package/src/payment/success.tsx +26 -15
  218. package/src/payment/summary.tsx +74 -42
  219. package/src/theme/index.tsx +5 -8
  220. package/src/theme/typography.ts +2 -2
@@ -1,21 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
- import { useMemo, forwardRef, useState, useEffect, ChangeEvent, useRef, KeyboardEvent } from 'react';
2
+ import { useMemo, useState, useEffect, ChangeEvent, useRef, KeyboardEvent } from 'react';
3
3
  import { Box, MenuItem, Typography, TextField, Dialog, DialogContent, IconButton, Select, Slide } from '@mui/material';
4
- import { TransitionProps } from '@mui/material/transitions';
5
4
  import { useFormContext } from 'react-hook-form';
6
5
  import { FlagEmoji, defaultCountries, parseCountry } from 'react-international-phone';
7
- import type { SxProps } from '@mui/material';
6
+ import type { SlideProps, SxProps } from '@mui/material';
8
7
  import type { CountryIso2 } from 'react-international-phone';
9
8
  import { useMobile } from '../hooks/mobile';
10
9
 
11
- const Transition = forwardRef(function Transition(
12
- props: TransitionProps & {
13
- children: React.ReactElement;
14
- },
15
- ref: React.Ref<unknown>
16
- ) {
10
+ function Transition({ ref, ...props }: { ref: React.RefObject<unknown> } & SlideProps) {
17
11
  return <Slide direction="up" ref={ref} timeout={200} {...props} />;
18
- });
12
+ }
19
13
 
20
14
  export type CountrySelectProps = {
21
15
  value: CountryIso2;
@@ -25,407 +19,426 @@ export type CountrySelectProps = {
25
19
  showDialCode?: boolean;
26
20
  };
27
21
 
28
- const CountrySelect = forwardRef<HTMLDivElement, CountrySelectProps>(
29
- ({ value, onChange, name, sx, showDialCode = false }, ref) => {
30
- const { setValue } = useFormContext();
31
- const [open, setOpen] = useState(false);
32
- const [searchText, setSearchText] = useState('');
33
- const inputRef = useRef<HTMLInputElement>(null);
34
- const menuRef = useRef<HTMLDivElement>(null);
35
- const listRef = useRef<HTMLDivElement>(null);
36
- const [focusedIndex, setFocusedIndex] = useState(-1);
37
- const itemHeightRef = useRef<number>(40);
38
- const { isMobile } = useMobile();
39
- const measuredRef = useRef(false);
40
-
41
- // Handle window resize
42
- useEffect(() => {
43
- if (!open) return () => {};
44
- const handleResize = () => {
45
- measuredRef.current = false;
46
- };
47
-
48
- window.addEventListener('resize', handleResize);
49
- return () => {
50
- window.removeEventListener('resize', handleResize);
51
- };
52
- // eslint-disable-next-line react-hooks/exhaustive-deps
53
- }, [open]);
54
-
55
- const scrollToTop = () => {
56
- if (listRef.current) {
57
- listRef.current.scrollTop = 0;
58
- }
22
+ export default function CountrySelect({
23
+ ref = undefined,
24
+ value,
25
+ onChange,
26
+ name,
27
+ sx = {},
28
+ showDialCode = false,
29
+ }: CountrySelectProps & {
30
+ ref?: React.RefObject<HTMLDivElement>;
31
+ }) {
32
+ const { setValue } = useFormContext();
33
+ const [open, setOpen] = useState(false);
34
+ const [searchText, setSearchText] = useState('');
35
+ const inputRef = useRef<HTMLInputElement>(null);
36
+ const menuRef = useRef<HTMLDivElement>(null);
37
+ const listRef = useRef<HTMLDivElement>(null);
38
+ const [focusedIndex, setFocusedIndex] = useState(-1);
39
+ const itemHeightRef = useRef<number>(40);
40
+ const { isMobile } = useMobile();
41
+ const measuredRef = useRef(false);
42
+
43
+ // Handle window resize
44
+ useEffect(() => {
45
+ if (!open) return () => {};
46
+ const handleResize = () => {
47
+ measuredRef.current = false;
59
48
  };
60
49
 
61
- const measureItemHeight = () => {
62
- if (!listRef.current || !open) return;
63
-
64
- const items = listRef.current.querySelectorAll('.MuiMenuItem-root');
65
- if (items.length > 0) {
66
- const firstItem = items[0] as HTMLElement;
67
- if (firstItem.offsetHeight > 0) {
68
- itemHeightRef.current = firstItem.offsetHeight;
69
- }
70
- }
50
+ window.addEventListener('resize', handleResize);
51
+ return () => {
52
+ window.removeEventListener('resize', handleResize);
71
53
  };
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, [open]);
56
+
57
+ const scrollToTop = () => {
58
+ if (listRef.current) {
59
+ listRef.current.scrollTop = 0;
60
+ }
61
+ };
72
62
 
73
- const controlScrollPosition = (index: number) => {
74
- if (!listRef.current) return;
63
+ const measureItemHeight = () => {
64
+ if (!listRef.current || !open) return;
75
65
 
76
- // Always measure height when dropdown is open for accuracy
77
- if (open && !measuredRef.current) {
78
- measureItemHeight();
79
- measuredRef.current = true;
66
+ const items = listRef.current.querySelectorAll('.MuiMenuItem-root');
67
+ if (items.length > 0) {
68
+ const firstItem = items[0] as HTMLElement;
69
+ if (firstItem.offsetHeight > 0) {
70
+ itemHeightRef.current = firstItem.offsetHeight;
80
71
  }
72
+ }
73
+ };
81
74
 
82
- const listHeight = listRef.current.clientHeight;
83
- const targetPosition = index * itemHeightRef.current;
75
+ const controlScrollPosition = (index: number) => {
76
+ if (!listRef.current) return;
84
77
 
85
- if (index < 2) {
86
- listRef.current.scrollTop = 0;
87
- } else if (index > filteredCountries.length - 3) {
88
- listRef.current.scrollTop = listRef.current.scrollHeight - listHeight;
89
- } else {
90
- const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2;
91
- listRef.current.scrollTop = Math.max(0, scrollPosition);
92
- }
93
- };
78
+ // Always measure height when dropdown is open for accuracy
79
+ if (open && !measuredRef.current) {
80
+ measureItemHeight();
81
+ measuredRef.current = true;
82
+ }
94
83
 
95
- useEffect(() => {
96
- let timeout: NodeJS.Timeout | null = null;
97
- if (open) {
98
- timeout = setTimeout(() => {
99
- scrollToTop();
100
- if (!isMobile && inputRef.current) {
101
- inputRef.current.focus();
102
- }
103
- }, 100);
104
- } else {
105
- setSearchText('');
106
- setFocusedIndex(-1);
107
- }
108
- return () => {
109
- if (timeout) {
110
- clearTimeout(timeout);
111
- }
112
- };
113
- }, [open, isMobile]);
114
-
115
- const filteredCountries = useMemo(() => {
116
- if (!searchText) return defaultCountries;
117
-
118
- return defaultCountries.filter((c) => {
119
- const parsed = parseCountry(c);
120
- return (
121
- parsed.name.toLowerCase().includes(searchText.toLowerCase()) ||
122
- parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) ||
123
- `+${parsed.dialCode}`.includes(searchText)
124
- );
125
- });
126
- }, [searchText]);
84
+ const listHeight = listRef.current.clientHeight;
85
+ const targetPosition = index * itemHeightRef.current;
127
86
 
128
- useEffect(() => {
129
- scrollToTop();
87
+ if (index < 2) {
88
+ listRef.current.scrollTop = 0;
89
+ } else if (index > filteredCountries.length - 3) {
90
+ listRef.current.scrollTop = listRef.current.scrollHeight - listHeight;
91
+ } else {
92
+ const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2;
93
+ listRef.current.scrollTop = Math.max(0, scrollPosition);
94
+ }
95
+ };
96
+
97
+ useEffect(() => {
98
+ let timeout: NodeJS.Timeout | null = null;
99
+ if (open) {
100
+ timeout = setTimeout(() => {
101
+ scrollToTop();
102
+ if (!isMobile && inputRef.current) {
103
+ inputRef.current.focus();
104
+ }
105
+ }, 100);
106
+ } else {
107
+ setSearchText('');
130
108
  setFocusedIndex(-1);
131
- }, [searchText]);
132
-
133
- useEffect(() => {
134
- let timeout: NodeJS.Timeout | null = null;
135
- if (focusedIndex >= 0) {
136
- timeout = setTimeout(() => {
137
- controlScrollPosition(focusedIndex);
138
- }, 10);
109
+ }
110
+ return () => {
111
+ if (timeout) {
112
+ clearTimeout(timeout);
139
113
  }
140
- return () => {
141
- if (timeout) {
142
- clearTimeout(timeout);
143
- }
144
- };
145
- // eslint-disable-next-line react-hooks/exhaustive-deps
146
- }, [focusedIndex, filteredCountries.length]);
147
-
148
- const countryDetail = useMemo(() => {
149
- const item = defaultCountries.find((v) => v[1] === value);
150
- return value && item ? parseCountry(item) : { name: '' };
151
- }, [value]);
152
-
153
- const handleCountryClick = (code: CountryIso2) => {
154
- onChange(code);
155
- setValue(name, code);
156
- setOpen(false);
157
114
  };
115
+ }, [open, isMobile]);
116
+
117
+ const filteredCountries = useMemo(() => {
118
+ if (!searchText) return defaultCountries;
158
119
 
159
- const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
160
- e.stopPropagation();
161
- setSearchText(e.target.value);
120
+ return defaultCountries.filter((c) => {
121
+ const parsed = parseCountry(c);
122
+ return (
123
+ parsed.name.toLowerCase().includes(searchText.toLowerCase()) ||
124
+ parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) ||
125
+ `+${parsed.dialCode}`.includes(searchText)
126
+ );
127
+ });
128
+ }, [searchText]);
129
+
130
+ useEffect(() => {
131
+ scrollToTop();
132
+ setFocusedIndex(-1);
133
+ }, [searchText]);
134
+
135
+ useEffect(() => {
136
+ let timeout: NodeJS.Timeout | null = null;
137
+ if (focusedIndex >= 0) {
138
+ timeout = setTimeout(() => {
139
+ controlScrollPosition(focusedIndex);
140
+ }, 10);
141
+ }
142
+ return () => {
143
+ if (timeout) {
144
+ clearTimeout(timeout);
145
+ }
162
146
  };
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ }, [focusedIndex, filteredCountries.length]);
163
149
 
164
- const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
165
- e.stopPropagation();
150
+ const countryDetail = useMemo(() => {
151
+ const item = defaultCountries.find((v) => v[1] === value);
152
+ return value && item ? parseCountry(item) : { name: '' };
153
+ }, [value]);
166
154
 
167
- if (e.key === 'Escape') {
168
- setOpen(false);
169
- return;
170
- }
155
+ const handleCountryClick = (code: CountryIso2) => {
156
+ onChange(code);
157
+ setValue(name, code);
158
+ setOpen(false);
159
+ };
171
160
 
172
- const handleNavigation = (direction: 'next' | 'prev') => {
173
- e.preventDefault();
174
- setFocusedIndex((prev) => {
175
- if (direction === 'next') {
176
- if (prev === -1) return 0;
177
- return prev >= filteredCountries.length - 1 ? 0 : prev + 1;
178
- }
179
- if (prev === -1) return filteredCountries.length - 1;
180
- return prev <= 0 ? filteredCountries.length - 1 : prev - 1;
181
- });
182
- };
183
-
184
- if (e.key === 'Tab') {
185
- handleNavigation(e.shiftKey ? 'prev' : 'next');
186
- return;
187
- }
161
+ const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
162
+ e.stopPropagation();
163
+ setSearchText(e.target.value);
164
+ };
188
165
 
189
- if (e.key === 'ArrowDown') {
190
- handleNavigation('next');
191
- return;
192
- }
166
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
167
+ e.stopPropagation();
193
168
 
194
- if (e.key === 'ArrowUp') {
195
- handleNavigation('prev');
196
- return;
197
- }
169
+ if (e.key === 'Escape') {
170
+ setOpen(false);
171
+ return;
172
+ }
198
173
 
199
- if (e.key === 'Enter' && focusedIndex >= 0 && focusedIndex < filteredCountries.length) {
200
- e.preventDefault();
201
- const country = parseCountry(filteredCountries[focusedIndex]);
202
- handleCountryClick(country.iso2);
203
- }
174
+ const handleNavigation = (direction: 'next' | 'prev') => {
175
+ e.preventDefault();
176
+ setFocusedIndex((prev) => {
177
+ if (direction === 'next') {
178
+ if (prev === -1) return 0;
179
+ return prev >= filteredCountries.length - 1 ? 0 : prev + 1;
180
+ }
181
+ if (prev === -1) return filteredCountries.length - 1;
182
+ return prev <= 0 ? filteredCountries.length - 1 : prev - 1;
183
+ });
204
184
  };
205
185
 
206
- const countryListContent = (
207
- <>
208
- <Box
209
- sx={{
210
- position: 'sticky',
211
- top: 0,
212
- zIndex: 1,
213
- bgcolor: 'background.paper',
214
- p: 1,
215
- }}
216
- onClick={(e) => {
217
- e.stopPropagation();
218
- }}>
219
- <TextField
220
- inputRef={inputRef}
221
- autoFocus={!isMobile}
222
- fullWidth
223
- placeholder="Search country..."
224
- value={searchText}
225
- onChange={handleSearchChange}
226
- onKeyDown={handleKeyDown}
227
- onClick={(e) => e.stopPropagation()}
228
- size="small"
229
- variant="outlined"
230
- />
231
- </Box>
232
- <Box
233
- ref={listRef}
234
- sx={{
235
- flex: 1,
236
- overflowY: 'auto',
237
- overflowX: 'hidden',
238
- maxHeight: isMobile ? 'calc(60vh - 80px)' : 'calc(300px - 65px)',
239
- scrollBehavior: 'smooth',
240
- }}>
241
- {filteredCountries.length > 0 ? (
242
- filteredCountries.map((c: any, index) => {
243
- const parsed = parseCountry(c);
244
- const isFocused = index === focusedIndex;
245
-
246
- return (
247
- <MenuItem
248
- key={parsed.iso2}
249
- value={parsed.iso2}
250
- selected={parsed.iso2 === value}
251
- onClick={() => handleCountryClick(parsed.iso2)}
252
- sx={{
253
- display: 'flex',
254
- alignItems: 'center',
255
- '&.Mui-selected': {
256
- backgroundColor: 'primary.lighter',
257
- },
258
- '&:hover': {
259
- backgroundColor: 'grey.100',
260
- },
261
- ...(isFocused
262
- ? {
263
- backgroundColor: 'grey.100',
264
- outline: 'none',
265
- }
266
- : {}),
267
- }}>
268
- <FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px' }} />
269
- <Typography>{parsed.name}</Typography>
270
- {showDialCode && (
271
- <Typography sx={{ ml: 1 }} color="text.secondary">
272
- {`+${parsed.dialCode}`}
273
- </Typography>
274
- )}
275
- </MenuItem>
276
- );
277
- })
278
- ) : (
279
- <MenuItem disabled>
280
- <Typography color="text.secondary">No countries found</Typography>
281
- </MenuItem>
282
- )}
283
- </Box>
284
- </>
285
- );
186
+ if (e.key === 'Tab') {
187
+ handleNavigation(e.shiftKey ? 'prev' : 'next');
188
+ return;
189
+ }
286
190
 
287
- if (!isMobile) {
288
- return (
289
- <Select
290
- ref={ref}
291
- open={open}
292
- onOpen={() => setOpen(true)}
293
- onClose={() => setOpen(false)}
294
- MenuProps={{
295
- style: {
296
- maxHeight: '300px',
297
- top: '10px',
298
- },
299
- anchorOrigin: {
300
- vertical: 'bottom',
301
- horizontal: 'left',
302
- },
303
- transformOrigin: {
304
- vertical: 'top',
305
- horizontal: 'left',
306
- },
307
- PaperProps: {
308
- ref: menuRef,
309
- sx: {
310
- display: 'flex',
311
- '& .MuiList-root': {
312
- pt: 0,
191
+ if (e.key === 'ArrowDown') {
192
+ handleNavigation('next');
193
+ return;
194
+ }
195
+
196
+ if (e.key === 'ArrowUp') {
197
+ handleNavigation('prev');
198
+ return;
199
+ }
200
+
201
+ if (e.key === 'Enter' && focusedIndex >= 0 && focusedIndex < filteredCountries.length) {
202
+ e.preventDefault();
203
+ const country = parseCountry(filteredCountries[focusedIndex]);
204
+ handleCountryClick(country.iso2);
205
+ }
206
+ };
207
+
208
+ const countryListContent = (
209
+ <>
210
+ <Box
211
+ sx={{
212
+ position: 'sticky',
213
+ top: 0,
214
+ zIndex: 1,
215
+ bgcolor: 'background.paper',
216
+ p: 1,
217
+ }}
218
+ onClick={(e) => {
219
+ e.stopPropagation();
220
+ }}>
221
+ <TextField
222
+ inputRef={inputRef}
223
+ autoFocus={!isMobile}
224
+ fullWidth
225
+ placeholder="Search country..."
226
+ value={searchText}
227
+ onChange={handleSearchChange}
228
+ onKeyDown={handleKeyDown}
229
+ onClick={(e) => e.stopPropagation()}
230
+ size="small"
231
+ variant="outlined"
232
+ />
233
+ </Box>
234
+ <Box
235
+ ref={listRef}
236
+ sx={{
237
+ flex: 1,
238
+ overflowY: 'auto',
239
+ overflowX: 'hidden',
240
+ maxHeight: isMobile ? 'calc(60vh - 80px)' : 'calc(300px - 65px)',
241
+ scrollBehavior: 'smooth',
242
+ }}>
243
+ {filteredCountries.length > 0 ? (
244
+ filteredCountries.map((c: any, index) => {
245
+ const parsed = parseCountry(c);
246
+ const isFocused = index === focusedIndex;
247
+
248
+ return (
249
+ <MenuItem
250
+ key={parsed.iso2}
251
+ value={parsed.iso2}
252
+ selected={parsed.iso2 === value}
253
+ onClick={() => handleCountryClick(parsed.iso2)}
254
+ sx={{
313
255
  display: 'flex',
314
- flexDirection: 'column',
315
- overflowY: 'hidden',
316
- },
256
+ alignItems: 'center',
257
+ '&.Mui-selected': {
258
+ backgroundColor: 'primary.lighter',
259
+ },
260
+ '&:hover': {
261
+ backgroundColor: 'grey.100',
262
+ },
263
+ ...(isFocused
264
+ ? {
265
+ backgroundColor: 'grey.100',
266
+ outline: 'none',
267
+ }
268
+ : {}),
269
+ }}>
270
+ <FlagEmoji iso2={parsed.iso2} style={{ marginRight: '8px' }} />
271
+ <Typography>{parsed.name}</Typography>
272
+ {showDialCode && (
273
+ <Typography
274
+ sx={{
275
+ color: 'text.secondary',
276
+ ml: 1,
277
+ }}>
278
+ {`+${parsed.dialCode}`}
279
+ </Typography>
280
+ )}
281
+ </MenuItem>
282
+ );
283
+ })
284
+ ) : (
285
+ <MenuItem disabled>
286
+ <Typography
287
+ sx={{
288
+ color: 'text.secondary',
289
+ }}>
290
+ No countries found
291
+ </Typography>
292
+ </MenuItem>
293
+ )}
294
+ </Box>
295
+ </>
296
+ );
297
+
298
+ if (!isMobile) {
299
+ return (
300
+ <Select
301
+ ref={ref}
302
+ open={open}
303
+ onOpen={() => setOpen(true)}
304
+ onClose={() => setOpen(false)}
305
+ MenuProps={{
306
+ style: {
307
+ maxHeight: '300px',
308
+ top: '10px',
309
+ },
310
+ anchorOrigin: {
311
+ vertical: 'bottom',
312
+ horizontal: 'left',
313
+ },
314
+ transformOrigin: {
315
+ vertical: 'top',
316
+ horizontal: 'left',
317
+ },
318
+ PaperProps: {
319
+ ref: menuRef,
320
+ sx: {
321
+ display: 'flex',
322
+ '& .MuiList-root': {
323
+ pt: 0,
324
+ display: 'flex',
325
+ flexDirection: 'column',
326
+ overflowY: 'hidden',
317
327
  },
318
328
  },
319
- }}
320
- sx={{
321
- width: '100%',
322
- minWidth: '60px',
329
+ },
330
+ }}
331
+ sx={{
332
+ width: '100%',
333
+ minWidth: '60px',
334
+ fieldset: {
335
+ display: 'none',
336
+ },
337
+ '&.Mui-focused:has(div[aria-expanded="false"])': {
323
338
  fieldset: {
324
- display: 'none',
325
- },
326
- '&.Mui-focused:has(div[aria-expanded="false"])': {
327
- fieldset: {
328
- display: 'block',
329
- },
330
- },
331
- '.MuiSelect-select': {
332
- padding: '8px',
333
- paddingRight: '24px !important',
334
- },
335
- svg: {
336
- right: 0,
337
- },
338
- '.MuiMenuItem-root': {
339
- justifyContent: 'flex-start',
339
+ display: 'block',
340
340
  },
341
- ...sx,
342
- }}
343
- value={value}
344
- onChange={(e) => {
345
- onChange(e.target.value as CountryIso2);
346
- setValue(name, e.target.value);
347
- }}
348
- renderValue={(code) => (
349
- <Box display="flex" alignItems="center" flexWrap="nowrap" gap={0.5} sx={{ cursor: 'pointer' }}>
350
- <FlagEmoji iso2={code} style={{ display: 'flex' }} />
351
- <Typography>{countryDetail?.name}</Typography>
352
- </Box>
353
- )}>
354
- {countryListContent}
355
- </Select>
356
- );
357
- }
358
-
359
- return (
360
- <Box ref={ref} sx={{ ...sx }}>
361
- <Box
362
- onClick={() => setOpen(true)}
363
- display="flex"
364
- alignItems="center"
365
- flexWrap="nowrap"
366
- gap={0.5}
367
- sx={{
368
- cursor: 'pointer',
341
+ },
342
+ '.MuiSelect-select': {
369
343
  padding: '8px',
370
- paddingRight: '24px',
371
- position: 'relative',
372
- border: 'none',
373
- '-webkit-tap-highlight-color': 'transparent',
374
- userSelect: 'none',
375
- background: 'none',
376
- '&:hover, &:focus, &:active': {
377
- backgroundColor: 'transparent',
378
- outline: 'none',
379
- },
380
- touchAction: 'manipulation',
381
- }}>
382
- <FlagEmoji iso2={value} style={{ display: 'flex' }} />
383
- <Typography>{countryDetail?.name}</Typography>
344
+ paddingRight: '24px !important',
345
+ },
346
+ svg: {
347
+ right: 0,
348
+ },
349
+ '.MuiMenuItem-root': {
350
+ justifyContent: 'flex-start',
351
+ },
352
+ ...sx,
353
+ }}
354
+ value={value}
355
+ onChange={(e) => {
356
+ onChange(e.target.value as CountryIso2);
357
+ setValue(name, e.target.value);
358
+ }}
359
+ renderValue={(code) => (
384
360
  <Box
385
361
  sx={{
386
- position: 'absolute',
387
- right: '8px',
388
- width: 0,
389
- height: 0,
390
- borderLeft: '5px solid transparent',
391
- borderRight: '5px solid transparent',
392
- borderTop: '5px solid currentColor',
393
- }}
394
- />
395
- </Box>
396
-
397
- <Dialog
398
- open={open}
399
- onClose={() => setOpen(false)}
400
- fullWidth
401
- maxWidth="xs"
402
- TransitionComponent={Transition}
403
- PaperProps={{
404
- sx: {
405
- position: 'absolute',
406
- bottom: 0,
407
- m: 0,
408
- p: 0,
409
- borderBottomLeftRadius: 0,
410
- borderBottomRightRadius: 0,
411
- width: '100%',
412
- },
413
- }}>
414
- <Box sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
415
- <Typography variant="h6">Select Country</Typography>
416
- <IconButton edge="end" onClick={() => setOpen(false)} sx={{ '-webkit-tap-highlight-color': 'transparent' }}>
417
-
418
- </IconButton>
362
+ display: 'flex',
363
+ alignItems: 'center',
364
+ flexWrap: 'nowrap',
365
+ gap: 0.5,
366
+ cursor: 'pointer',
367
+ }}>
368
+ <FlagEmoji iso2={code} style={{ display: 'flex' }} />
369
+ <Typography>{countryDetail?.name}</Typography>
419
370
  </Box>
420
- <DialogContent sx={{ '&.MuiDialogContent-root': { p: 0 } }}>{countryListContent}</DialogContent>
421
- </Dialog>
422
- </Box>
371
+ )}>
372
+ {countryListContent}
373
+ </Select>
423
374
  );
424
375
  }
425
- );
426
376
 
427
- CountrySelect.defaultProps = {
428
- sx: {},
429
- showDialCode: false,
430
- };
431
- export default CountrySelect;
377
+ return (
378
+ <Box ref={ref} sx={{ ...sx }}>
379
+ <Box
380
+ onClick={() => setOpen(true)}
381
+ sx={{
382
+ display: 'flex',
383
+ alignItems: 'center',
384
+ flexWrap: 'nowrap',
385
+ gap: 0.5,
386
+ cursor: 'pointer',
387
+ padding: '8px',
388
+ paddingRight: '24px',
389
+ position: 'relative',
390
+ border: 'none',
391
+ '-webkit-tap-highlight-color': 'transparent',
392
+ userSelect: 'none',
393
+ background: 'none',
394
+
395
+ '&:hover, &:focus, &:active': {
396
+ backgroundColor: 'transparent',
397
+ outline: 'none',
398
+ },
399
+
400
+ touchAction: 'manipulation',
401
+ }}>
402
+ <FlagEmoji iso2={value} style={{ display: 'flex' }} />
403
+ <Typography>{countryDetail?.name}</Typography>
404
+ <Box
405
+ sx={{
406
+ position: 'absolute',
407
+ right: '8px',
408
+ width: 0,
409
+ height: 0,
410
+ borderLeft: '5px solid transparent',
411
+ borderRight: '5px solid transparent',
412
+ borderTop: '5px solid currentColor',
413
+ }}
414
+ />
415
+ </Box>
416
+ <Dialog
417
+ open={open}
418
+ onClose={() => setOpen(false)}
419
+ fullWidth
420
+ maxWidth="xs"
421
+ // @ts-expect-error
422
+ TransitionComponent={Transition}
423
+ PaperProps={{
424
+ sx: {
425
+ position: 'absolute',
426
+ bottom: 0,
427
+ m: 0,
428
+ p: 0,
429
+ borderBottomLeftRadius: 0,
430
+ borderBottomRightRadius: 0,
431
+ width: '100%',
432
+ },
433
+ }}>
434
+ <Box sx={{ p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
435
+ <Typography variant="h6">Select Country</Typography>
436
+ <IconButton edge="end" onClick={() => setOpen(false)} sx={{ '-webkit-tap-highlight-color': 'transparent' }}>
437
+
438
+ </IconButton>
439
+ </Box>
440
+ <DialogContent sx={{ '&.MuiDialogContent-root': { p: 0 } }}>{countryListContent}</DialogContent>
441
+ </Dialog>
442
+ </Box>
443
+ );
444
+ }