@cleartrip/ct-design-field 4.0.0 → 5.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 (134) hide show
  1. package/README.md +87 -0
  2. package/dist/Field.d.ts +4 -4
  3. package/dist/Field.d.ts.map +1 -1
  4. package/dist/FieldAction/index.d.ts +0 -1
  5. package/dist/FieldAction/index.d.ts.map +1 -1
  6. package/dist/FieldAction/type.d.ts +1 -1
  7. package/dist/FieldAction/type.d.ts.map +1 -1
  8. package/dist/FieldIcon/index.d.ts +0 -1
  9. package/dist/FieldIcon/index.d.ts.map +1 -1
  10. package/dist/FieldIcon/type.d.ts +4 -2
  11. package/dist/FieldIcon/type.d.ts.map +1 -1
  12. package/dist/Input.d.ts +8 -0
  13. package/dist/Input.d.ts.map +1 -0
  14. package/dist/Input.native.d.ts +7 -0
  15. package/dist/Input.native.d.ts.map +1 -0
  16. package/dist/InputField.d.ts +4 -0
  17. package/dist/InputField.d.ts.map +1 -0
  18. package/dist/Label.d.ts +6 -0
  19. package/dist/Label.d.ts.map +1 -0
  20. package/dist/Label.native.d.ts +6 -0
  21. package/dist/Label.native.d.ts.map +1 -0
  22. package/dist/TextArea.d.ts +7 -0
  23. package/dist/TextArea.d.ts.map +1 -0
  24. package/dist/TextAreaInput.d.ts +7 -0
  25. package/dist/TextAreaInput.d.ts.map +1 -0
  26. package/dist/TextAreaInput.native.d.ts +3 -0
  27. package/dist/TextAreaInput.native.d.ts.map +1 -0
  28. package/dist/constants.d.ts +10 -0
  29. package/dist/constants.d.ts.map +1 -0
  30. package/dist/ct-design-field.browser.cjs.js +11 -1
  31. package/dist/ct-design-field.browser.cjs.js.map +1 -1
  32. package/dist/ct-design-field.browser.esm.js +11 -1
  33. package/dist/ct-design-field.browser.esm.js.map +1 -1
  34. package/dist/ct-design-field.cjs.js +1003 -396
  35. package/dist/ct-design-field.cjs.js.map +1 -1
  36. package/dist/ct-design-field.esm.js +997 -387
  37. package/dist/ct-design-field.esm.js.map +1 -1
  38. package/dist/ct-design-field.umd.js +2721 -537
  39. package/dist/ct-design-field.umd.js.map +1 -1
  40. package/dist/index.d.ts +6 -9
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/style.d.ts +146 -38
  43. package/dist/style.d.ts.map +1 -1
  44. package/dist/type.d.ts +114 -25
  45. package/dist/type.d.ts.map +1 -1
  46. package/dist/variants/Card/index.d.ts +13 -0
  47. package/dist/variants/Card/index.d.ts.map +1 -0
  48. package/dist/variants/Card/index.native.d.ts +13 -0
  49. package/dist/variants/Card/index.native.d.ts.map +1 -0
  50. package/dist/variants/Card/type.d.ts +5 -0
  51. package/dist/variants/Card/type.d.ts.map +1 -0
  52. package/dist/variants/OTP/index.d.ts +4 -0
  53. package/dist/variants/OTP/index.d.ts.map +1 -0
  54. package/dist/variants/OTP/type.d.ts +25 -0
  55. package/dist/variants/OTP/type.d.ts.map +1 -0
  56. package/dist/variants/Phone/Prefix/index.d.ts +4 -0
  57. package/dist/variants/Phone/Prefix/index.d.ts.map +1 -0
  58. package/dist/variants/Phone/Prefix/type.d.ts.map +1 -0
  59. package/dist/variants/Phone/index.d.ts +5 -0
  60. package/dist/variants/Phone/index.d.ts.map +1 -0
  61. package/dist/variants/Phone/index.native.d.ts +5 -0
  62. package/dist/variants/Phone/index.native.d.ts.map +1 -0
  63. package/dist/variants/Phone/type.d.ts +7 -0
  64. package/dist/variants/Phone/type.d.ts.map +1 -0
  65. package/package.json +33 -19
  66. package/src/Field.tsx +201 -0
  67. package/src/FieldAction/index.tsx +47 -0
  68. package/src/FieldAction/type.ts +15 -0
  69. package/src/FieldIcon/index.tsx +48 -0
  70. package/src/FieldIcon/type.ts +52 -0
  71. package/src/Input.native.tsx +284 -0
  72. package/src/Input.tsx +242 -0
  73. package/src/InputField.tsx +22 -0
  74. package/src/Label.native.tsx +83 -0
  75. package/src/Label.tsx +91 -0
  76. package/src/TextArea.tsx +14 -0
  77. package/src/TextAreaInput.native.tsx +4 -0
  78. package/src/TextAreaInput.tsx +243 -0
  79. package/src/constants.ts +10 -0
  80. package/src/index.ts +8 -0
  81. package/src/style.ts +353 -0
  82. package/src/type.ts +243 -0
  83. package/src/variants/Card/index.native.tsx +46 -0
  84. package/src/variants/Card/index.tsx +89 -0
  85. package/src/variants/Card/type.ts +5 -0
  86. package/src/variants/OTP/index.tsx +343 -0
  87. package/src/variants/OTP/type.ts +34 -0
  88. package/src/variants/Phone/Prefix/index.tsx +87 -0
  89. package/src/variants/Phone/Prefix/type.ts +24 -0
  90. package/src/variants/Phone/index.native.tsx +84 -0
  91. package/src/variants/Phone/index.tsx +79 -0
  92. package/src/variants/Phone/type.ts +13 -0
  93. package/dist/CardField/CardField.d.ts +0 -6
  94. package/dist/CardField/CardField.d.ts.map +0 -1
  95. package/dist/CardField/index.d.ts +0 -3
  96. package/dist/CardField/index.d.ts.map +0 -1
  97. package/dist/CardField/type.d.ts +0 -16
  98. package/dist/CardField/type.d.ts.map +0 -1
  99. package/dist/OTPField/OTPField.d.ts +0 -6
  100. package/dist/OTPField/OTPField.d.ts.map +0 -1
  101. package/dist/OTPField/SingleOTPInput.d.ts +0 -6
  102. package/dist/OTPField/SingleOTPInput.d.ts.map +0 -1
  103. package/dist/OTPField/index.d.ts +0 -3
  104. package/dist/OTPField/index.d.ts.map +0 -1
  105. package/dist/OTPField/type.d.ts +0 -23
  106. package/dist/OTPField/type.d.ts.map +0 -1
  107. package/dist/PhoneField/PhoneField.d.ts +0 -6
  108. package/dist/PhoneField/PhoneField.d.ts.map +0 -1
  109. package/dist/PhoneField/index.d.ts +0 -3
  110. package/dist/PhoneField/index.d.ts.map +0 -1
  111. package/dist/PhoneField/type.d.ts +0 -11
  112. package/dist/PhoneField/type.d.ts.map +0 -1
  113. package/dist/PhoneFieldPrefix/index.d.ts +0 -5
  114. package/dist/PhoneFieldPrefix/index.d.ts.map +0 -1
  115. package/dist/PhoneFieldPrefix/type.d.ts.map +0 -1
  116. package/dist/StyledField/StyledField.d.ts +0 -7
  117. package/dist/StyledField/StyledField.d.ts.map +0 -1
  118. package/dist/StyledField/index.d.ts +0 -2
  119. package/dist/StyledField/index.d.ts.map +0 -1
  120. package/dist/StyledField/type.d.ts +0 -12
  121. package/dist/StyledField/type.d.ts.map +0 -1
  122. package/dist/StyledFieldContainer/StyledFieldContainer.d.ts +0 -7
  123. package/dist/StyledFieldContainer/StyledFieldContainer.d.ts.map +0 -1
  124. package/dist/StyledFieldContainer/index.d.ts +0 -2
  125. package/dist/StyledFieldContainer/index.d.ts.map +0 -1
  126. package/dist/StyledFieldContainer/type.d.ts +0 -8
  127. package/dist/StyledFieldContainer/type.d.ts.map +0 -1
  128. package/dist/StyledFieldPlaceholder/StyledFieldPlaceholder.d.ts +0 -7
  129. package/dist/StyledFieldPlaceholder/StyledFieldPlaceholder.d.ts.map +0 -1
  130. package/dist/StyledFieldPlaceholder/index.d.ts +0 -2
  131. package/dist/StyledFieldPlaceholder/index.d.ts.map +0 -1
  132. package/dist/StyledFieldPlaceholder/type.d.ts +0 -7
  133. package/dist/StyledFieldPlaceholder/type.d.ts.map +0 -1
  134. /package/dist/{PhoneFieldPrefix → variants/Phone/Prefix}/type.d.ts +0 -0
@@ -0,0 +1,89 @@
1
+ import { forwardRef, useEffect, useRef } from 'react';
2
+
3
+ import { FieldChangeEvent, IFieldProps, IFieldRef } from '../../type';
4
+ import { FieldType } from '../../constants';
5
+ import InputField from '../../Field';
6
+ import useMergeRefs from '@cleartrip/ct-design-use-merge-refs';
7
+
8
+ export interface ICursorRef {
9
+ cursorPos: number;
10
+ operation: string;
11
+ }
12
+
13
+ export enum CURSOR_OPERATION {
14
+ NONE = 'NONE',
15
+ ADD = 'ADD',
16
+ SUBSTRACT = 'SUBSTRACT',
17
+ }
18
+
19
+ /**
20
+ * CardField is a wrapper component which is used add a space after a specific character limit
21
+ * For Eg: 1234 5678 9012 3456
22
+ *
23
+ * Use cases
24
+ *
25
+ * 1.) Gift Voucher
26
+ * 2.) Credit/Debit Card
27
+ */
28
+ const CardField = forwardRef<IFieldRef, IFieldProps>(({ onChange, value, ...rest }, forwardedRef) => {
29
+ const ref = useRef<IFieldRef | null>(null);
30
+ const mergedRef = useMergeRefs(forwardedRef, ref);
31
+ const cursorRef = useRef<ICursorRef>({
32
+ cursorPos: 0,
33
+ operation: CURSOR_OPERATION.NONE,
34
+ });
35
+
36
+ /**
37
+ * Update the cursor position for on each re-render.
38
+ */
39
+ useEffect(() => {
40
+ ref.current?.setSelection(cursorRef.current.cursorPos, cursorRef.current.cursorPos);
41
+
42
+ cursorRef.current = {
43
+ cursorPos: cursorRef.current.cursorPos,
44
+ operation: CURSOR_OPERATION.NONE,
45
+ };
46
+ }, [value]);
47
+
48
+ const handleChange = (e: FieldChangeEvent) => {
49
+ onChange?.(e);
50
+ let selectionStart = e.target.selectionStart || 0;
51
+
52
+ //Update the cursor on the basis of operations.
53
+ switch (cursorRef.current.operation) {
54
+ case CURSOR_OPERATION.SUBSTRACT: {
55
+ --selectionStart;
56
+ break;
57
+ }
58
+
59
+ case CURSOR_OPERATION.ADD: {
60
+ ++selectionStart;
61
+ break;
62
+ }
63
+
64
+ default:
65
+ break;
66
+ }
67
+
68
+ //Update the ref
69
+ cursorRef.current = {
70
+ cursorPos: selectionStart,
71
+ operation: CURSOR_OPERATION.NONE,
72
+ };
73
+ };
74
+
75
+ return (
76
+ <InputField
77
+ ref={mergedRef}
78
+ onChange={handleChange}
79
+ {...rest}
80
+ type={FieldType.NUMBER}
81
+ value={value}
82
+ inputMode='numeric'
83
+ />
84
+ );
85
+ });
86
+
87
+ CardField.displayName = 'CardField';
88
+
89
+ export default CardField;
@@ -0,0 +1,5 @@
1
+ import { IFieldProps } from '../../type';
2
+
3
+ export interface ICardFieldProps extends IFieldProps {
4
+ gap: number;
5
+ }
@@ -0,0 +1,343 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { IOtpFieldProps } from './type';
3
+ import { Container } from '@cleartrip/ct-design-container';
4
+ import { Typography, TypographyColor, TypographyVariant } from '@cleartrip/ct-design-typography';
5
+ import { IFieldRef, FieldChangeEvent, FieldKeyDownEvent, FieldFocusEvent } from '../../type';
6
+ import { useTheme } from '@cleartrip/ct-design-theme';
7
+ import Field from '../../Input';
8
+ import { isIOS } from '@cleartrip/ct-design-common-utils';
9
+ import { makeStyles, useStyles } from '@cleartrip/ct-design-style-manager';
10
+
11
+ const styles = makeStyles((theme) => ({
12
+ rootContainer: {
13
+ justifyContent: 'center',
14
+ },
15
+ otpContainer: {
16
+ display: 'flex',
17
+ flexDirection: 'row',
18
+ gap: theme.spacing[3],
19
+ },
20
+ otpBox: {
21
+ width: theme.size[15],
22
+ height: theme.size[15],
23
+ borderRadius: theme?.border.radius[8],
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ borderStyle: theme.border.style.solid,
27
+ },
28
+ otpInput: {
29
+ width: '100%',
30
+ height: '100%',
31
+ fontSize: theme?.typography.size[20],
32
+ fontWeight: theme?.typography.weight.semibold,
33
+ color: theme.color.text.primary,
34
+ textAlign: 'center',
35
+ },
36
+ errorText: {
37
+ marginVertical: theme?.spacing[2],
38
+ fontSize: theme?.typography.size[14],
39
+ fontWeight: theme?.typography.weight.semibold,
40
+ color: theme?.color.text.warning,
41
+ },
42
+ }));
43
+
44
+ const OtpBox: React.FC<Omit<IOtpFieldProps, 'onFocus'> & { focus: boolean; onFocus: (e: FieldFocusEvent) => void }> = ({
45
+ value,
46
+ focus,
47
+ inputMode,
48
+ onChange,
49
+ onFocus,
50
+ readOnly,
51
+ onBlur,
52
+ onPressIn,
53
+ onKeyDown,
54
+ onSelect,
55
+ maxLength,
56
+ prompt,
57
+ styleConfig,
58
+ }) => {
59
+ const theme = useTheme();
60
+ const ref = useRef<IFieldRef>(null);
61
+ const { hasError = false } = prompt || {};
62
+ const { otpBox = [], otpRootInput = [] } = styleConfig || {};
63
+ useEffect(() => {
64
+ if (focus && ref.current) {
65
+ ref.current.focus();
66
+ }
67
+ }, [focus]);
68
+
69
+ const dynamicStyles = useStyles(
70
+ (theme) => {
71
+ if (hasError) {
72
+ return {
73
+ otpBox: {
74
+ borderColor: theme.color.border.warning,
75
+ borderWidth: theme.border.width.lg,
76
+ },
77
+ };
78
+ }
79
+ return {
80
+ otpBox: {
81
+ borderColor: focus ? theme.color.border.primary : theme.color.border.disabledDark,
82
+ borderWidth: focus ? theme.border.width.lg : theme.border.width.sm,
83
+ },
84
+ };
85
+ },
86
+ [hasError, focus],
87
+ );
88
+
89
+ return (
90
+ <Container
91
+ styleConfig={{
92
+ root: [styles.otpBox, dynamicStyles.otpBox, ...otpBox],
93
+ }}
94
+ >
95
+ <Field
96
+ id={`otp-box`}
97
+ disabled={readOnly}
98
+ ref={ref}
99
+ value={value}
100
+ maxLength={maxLength}
101
+ inputMode={inputMode}
102
+ onChange={onChange}
103
+ onBlur={onBlur}
104
+ onPressIn={onPressIn}
105
+ onKeyDown={onKeyDown}
106
+ onSelect={onSelect}
107
+ autoCapitalize='characters'
108
+ cursorColor={theme.color.text.grapetini900}
109
+ onFocus={onFocus}
110
+ focus={focus}
111
+ styleConfig={{ root: [styles.otpInput, ...otpRootInput] }}
112
+ />
113
+ </Container>
114
+ );
115
+ };
116
+
117
+ const OtpField: React.FC<IOtpFieldProps> = ({
118
+ value = '',
119
+ numberOfBoxes = 4,
120
+ inputMode = 'numeric',
121
+ autoFocus = true,
122
+ refocus = false,
123
+ onChange,
124
+ readOnly = false,
125
+ onComplete,
126
+ onKeyDown,
127
+ onSelect,
128
+ onBlur,
129
+ onPressIn,
130
+ onFocus,
131
+ prompt,
132
+ styleConfig = {},
133
+ }) => {
134
+ const [focusedIndex, setFocusedIndex] = useState<number>(autoFocus ? 0 : -1);
135
+ const { hasError = false, message: errorMessage } = prompt || {};
136
+ const { promptStyles, otpRootContainer = [], otpRootInput = [] } = styleConfig || {};
137
+ const { promptMessageTypography } = promptStyles || {};
138
+
139
+ // Convert value string to array for easy access
140
+ const otpValues = Array(numberOfBoxes)
141
+ .fill('')
142
+ .map((_, index) => {
143
+ const char = value[index];
144
+ return char && char !== ' ' ? char : '';
145
+ });
146
+ // Handle auto focus on mount
147
+ useEffect(() => {
148
+ if (autoFocus && !readOnly) {
149
+ setFocusedIndex(0);
150
+ }
151
+ }, [autoFocus, readOnly]);
152
+
153
+ useEffect(() => {
154
+ if (refocus) {
155
+ setFocusedIndex(0);
156
+ }
157
+ }, [refocus]);
158
+
159
+ const validateInput = useCallback(
160
+ (text: string): string => {
161
+ if (inputMode === 'numeric') {
162
+ return text.replace(/[^0-9]/g, '');
163
+ }
164
+ return text.replace(/[^0-9A-Za-z]/g, '').toUpperCase();
165
+ },
166
+ [inputMode],
167
+ );
168
+
169
+ /**
170
+ * handleInputChange - Memoized event handler for OTP box input change.
171
+ *
172
+ * This function is memoized as much as possible; however, since it relies on otpValues, which is derived
173
+ * from component props/state and changes as the value prop changes, it's not possible to avoid recalculating
174
+ * otpValues within this function. To optimize, recompute otpValues inside the handler,
175
+ * use useCallback only for the core handler, and avoid including otpValues directly in the dependency array.
176
+ */
177
+ const handleInputChange = useCallback(
178
+ (e: FieldChangeEvent, index: number) => {
179
+ const validatedText = validateInput(e.target.value);
180
+
181
+ // Recompute otpValues - treat space as empty placeholder
182
+ const otpString = value || '';
183
+ const currentOtpValues = Array(numberOfBoxes)
184
+ .fill('')
185
+ .map((_, idx) => {
186
+ const char = otpString[idx];
187
+ return char && char !== ' ' ? char : '';
188
+ });
189
+
190
+ // Handle iOS auto-fill (when multiple characters are pasted)
191
+ if (validatedText.length > 1) {
192
+ const newValues = [...currentOtpValues];
193
+
194
+ // Start filling from the current focused index
195
+ for (let i = 0; i < validatedText.length && index + i < numberOfBoxes; i++) {
196
+ newValues[index + i] = validatedText[i];
197
+ }
198
+
199
+ const newValue = newValues.join('');
200
+
201
+ onChange?.({
202
+ target: { value: newValue, selectionStart: index },
203
+ currentTarget: e.currentTarget,
204
+ preventDefault: e.preventDefault,
205
+ });
206
+ // Move focus to next empty box or last box
207
+ const nextEmptyIndex = newValues.findIndex((val) => !val);
208
+ if (nextEmptyIndex !== -1) {
209
+ setFocusedIndex(nextEmptyIndex);
210
+ } else {
211
+ setFocusedIndex(numberOfBoxes - 1);
212
+ }
213
+
214
+ // Check if OTP is complete (no empty boxes)
215
+ if (!newValues.includes('')) {
216
+ onComplete?.(newValue);
217
+ }
218
+ return;
219
+ }
220
+
221
+ // Single character input - fill the FOCUSED box
222
+ if (validatedText) {
223
+ const newValues = [...currentOtpValues];
224
+ newValues[index] = validatedText;
225
+
226
+ const newValue = newValues.join('');
227
+ onChange?.({
228
+ target: { value: newValue, selectionStart: index },
229
+ currentTarget: e.currentTarget,
230
+ preventDefault: e.preventDefault,
231
+ });
232
+
233
+ // Move focus to next box sequentially
234
+ if (index < numberOfBoxes - 1) {
235
+ setFocusedIndex(index + 1);
236
+ }
237
+
238
+ // Check if OTP is complete (no empty boxes)
239
+ if (!newValues.includes('')) {
240
+ onComplete?.(newValue);
241
+ }
242
+ }
243
+ },
244
+ [validateInput, value, numberOfBoxes, onChange, onComplete],
245
+ );
246
+
247
+ const handleKeyPress = useCallback(
248
+ (e: FieldKeyDownEvent, index: number) => {
249
+ const key = e.target.key;
250
+
251
+ if (key === 'Backspace') {
252
+ // Recompute to avoid stale closure - treat space as empty
253
+ const otpString = value || '';
254
+ const currentOtpValues = Array(numberOfBoxes)
255
+ .fill('')
256
+ .map((_, idx) => {
257
+ const char = otpString[idx];
258
+ return char && char !== ' ' ? char : '';
259
+ });
260
+
261
+ if (currentOtpValues[index]) {
262
+ // Clear current box - use SPACE as placeholder to preserve position
263
+ const newValues = [...currentOtpValues];
264
+ newValues[index] = ' ';
265
+ onChange?.({
266
+ target: { value: newValues.join(''), selectionStart: index },
267
+ currentTarget: e.currentTarget,
268
+ preventDefault: e.preventDefault,
269
+ });
270
+ } else if (index > 0) {
271
+ // Move to previous box if current is empty
272
+ setFocusedIndex(index - 1);
273
+ }
274
+ } else if (key === 'ArrowLeft' && index > 0) {
275
+ e.preventDefault?.();
276
+ setFocusedIndex(index - 1);
277
+ } else if (key === 'ArrowRight' && index < numberOfBoxes - 1) {
278
+ e.preventDefault?.();
279
+ setFocusedIndex(index + 1);
280
+ }
281
+ onKeyDown?.({
282
+ target: { value, key },
283
+ currentTarget: e.currentTarget,
284
+ preventDefault: e.preventDefault,
285
+ });
286
+ },
287
+ [numberOfBoxes, onKeyDown, value, onChange],
288
+ );
289
+
290
+ const handleFocus = useCallback(
291
+ (e: FieldFocusEvent, index: number) => {
292
+ onFocus?.({
293
+ target: { value, index },
294
+ currentTarget: e.currentTarget,
295
+ preventDefault: e.preventDefault,
296
+ });
297
+ setFocusedIndex(index);
298
+ },
299
+ [onFocus, value],
300
+ );
301
+
302
+ return (
303
+ <Container styleConfig={{ root: [styles.rootContainer, ...otpRootContainer] }}>
304
+ <Container styleConfig={{ root: [styles.otpContainer, ...otpRootInput] }}>
305
+ {Array(numberOfBoxes)
306
+ .fill(null)
307
+ .map((_, index) => (
308
+ <OtpBox
309
+ key={index}
310
+ value={otpValues[index]}
311
+ focus={focusedIndex === index}
312
+ prompt={{ hasError }}
313
+ inputMode={inputMode}
314
+ maxLength={isIOS() ? numberOfBoxes : 1}
315
+ onFocus={(e) => handleFocus(e, index)}
316
+ onChange={(e) => handleInputChange(e, index)}
317
+ readOnly={readOnly}
318
+ onPressIn={onPressIn}
319
+ onKeyDown={(e) => handleKeyPress(e, index)}
320
+ onSelect={onSelect}
321
+ onBlur={onBlur}
322
+ styleConfig={styleConfig}
323
+ />
324
+ ))}
325
+ </Container>
326
+
327
+ {hasError && !!errorMessage && (
328
+ <Typography
329
+ variant={TypographyVariant.P3}
330
+ color={TypographyColor.WARNING}
331
+ styleConfig={{
332
+ root: [styles.errorText],
333
+ ...promptMessageTypography,
334
+ }}
335
+ >
336
+ {errorMessage}
337
+ </Typography>
338
+ )}
339
+ </Container>
340
+ );
341
+ };
342
+
343
+ export default OtpField;
@@ -0,0 +1,34 @@
1
+ import { Styles } from '@cleartrip/ct-design-types';
2
+ import { FieldFocusEvent, IFieldProps } from '../../type';
3
+
4
+ type OtpFieldStyleConfig = {
5
+ otpBox?: Styles[];
6
+ otpRootInput?: Styles[];
7
+ otpRootContainer?: Styles[];
8
+ } & IFieldProps['styleConfig'];
9
+
10
+ export interface OtpFieldFocusEvent extends FieldFocusEvent {
11
+ target: {
12
+ value: string;
13
+ index: number;
14
+ };
15
+ }
16
+ interface OwnOtpFieldProps extends IFieldProps {
17
+ /** Number of OTP input boxes to display (default: 4) */
18
+ numberOfBoxes?: number;
19
+
20
+ /** Callback fired when all OTP boxes are filled */
21
+ onComplete?: (otp: string) => void;
22
+
23
+ /** Whether to refocus the first otp input box (default: false) */
24
+ refocus?: boolean;
25
+ /**
26
+ * Max length of the OTP input box
27
+ */
28
+ maxLength?: number;
29
+ }
30
+
31
+ export type IOtpFieldProps = Omit<OwnOtpFieldProps, 'onFocus' | 'styleConfig'> & {
32
+ onFocus?: (e: OtpFieldFocusEvent) => void;
33
+ styleConfig?: OtpFieldStyleConfig;
34
+ };
@@ -0,0 +1,87 @@
1
+ import { makeStyles } from '@cleartrip/ct-design-style-manager';
2
+ import { IPhoneFieldPrefix } from './type';
3
+ import { Container } from '@cleartrip/ct-design-container';
4
+ import { Divider } from '@cleartrip/ct-design-divider';
5
+ import { Box } from '@cleartrip/ct-design-box';
6
+ import { Typography } from '@cleartrip/ct-design-typography';
7
+
8
+ const styles = makeStyles((theme) => {
9
+ return {
10
+ wrapper: {
11
+ alignItems: 'center',
12
+ justifyContent: 'center',
13
+ flexDirection: 'row',
14
+ paddingLeft: 16,
15
+ },
16
+ flagContainer: {
17
+ display: 'flex',
18
+ alignItems: 'center',
19
+ justifyContent: 'center',
20
+ flexDirection: 'row',
21
+ height: theme.size[6],
22
+ width: theme.size[6],
23
+ },
24
+ countryCode: {
25
+ alignSelf: 'flex-start',
26
+ },
27
+ divider: {
28
+ borderRightWidth: 1,
29
+ height: '100%',
30
+ borderRightColor: theme.color.background.disabledDark,
31
+ },
32
+ disabledColor: {
33
+ color: theme.color.text.disabled,
34
+ },
35
+ enabledColor: {
36
+ color: theme.color.text.primary,
37
+ },
38
+ container: {
39
+ flexDirection: 'row',
40
+ alignItems: 'center',
41
+ gap: theme.spacing[2],
42
+ },
43
+ };
44
+ });
45
+
46
+ const DownChevronV2 = ({ color = '#1A1A1A', ...rest }) => {
47
+ return (
48
+ <svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' viewBox='0 0 12 12' {...rest}>
49
+ <path stroke={color} strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='M2 4l4 4 4-4'></path>
50
+ </svg>
51
+ );
52
+ };
53
+
54
+ const PhoneFieldPrefix: React.FC<IPhoneFieldPrefix> = ({
55
+ countryCode,
56
+ disabled = false,
57
+ flagIcon,
58
+ onDropdownClick,
59
+ showDropdownIcon,
60
+ }) => {
61
+ const onClick = () => {
62
+ if (disabled) return;
63
+ onDropdownClick?.();
64
+ };
65
+
66
+ return (
67
+ <Box boxSize='micro' styleConfig={{ root: [styles.wrapper] }} onClick={onClick}>
68
+ <Container
69
+ styleConfig={{
70
+ root: [styles.container],
71
+ }}
72
+ >
73
+ {flagIcon && <Container styleConfig={{ root: [styles.flagContainer] }}>{flagIcon}</Container>}
74
+ <Typography variant='B1' color={disabled ? 'disabled' : 'primary'} styleConfig={{ root: [styles.countryCode] }}>
75
+ {countryCode}
76
+ </Typography>
77
+
78
+ {showDropdownIcon && (
79
+ <DownChevronV2 color={disabled ? styles.disabledColor.color : styles.enabledColor.color} />
80
+ )}
81
+ </Container>
82
+ <Divider dividerWidth={1} dividerLength={0} orientation='vertical' styleConfig={{ root: [styles.divider] }} />
83
+ </Box>
84
+ );
85
+ };
86
+
87
+ export default PhoneFieldPrefix;
@@ -0,0 +1,24 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ export interface IPhoneFieldPrefix {
4
+ /**
5
+ * The icon to display for the country flag.
6
+ */
7
+ flagIcon?: ReactNode;
8
+ /**
9
+ * The country code to display.
10
+ */
11
+ countryCode: string;
12
+ /**
13
+ * Whether to show the dropdown icon.
14
+ */
15
+ showDropdownIcon?: boolean;
16
+ /**
17
+ * The function to call when the dropdown icon is clicked.
18
+ */
19
+ onDropdownClick?: () => void;
20
+ /**
21
+ * Whether the field is disabled.
22
+ */
23
+ disabled?: boolean;
24
+ }
@@ -0,0 +1,84 @@
1
+ import { forwardRef, useState } from 'react';
2
+ import { FieldChangeEvent, IFieldRef } from '../../type';
3
+ import { FieldType } from '../../constants';
4
+ import { IPhoneField } from './type';
5
+ import Field from '../../Field';
6
+ import PhoneFieldPrefix from './Prefix';
7
+ import { ONLY_NUMERIC } from '@cleartrip/ct-design-common-constants';
8
+ import { View } from 'react-native';
9
+ import { makeStyles, useStyles } from '@cleartrip/ct-design-style-manager';
10
+
11
+ const styles = makeStyles((theme) => {
12
+ return {
13
+ fieldConfig: {
14
+ flex: 1,
15
+ paddingTop: 0,
16
+ paddingBottom: 0,
17
+ paddingVertical: 0,
18
+ lineHeight: 18,
19
+ paddingLeft: theme.spacing[4],
20
+ },
21
+ };
22
+ });
23
+
24
+ const PhoneField = forwardRef<IFieldRef, IPhoneField>(
25
+ (
26
+ { flagIcon, countryCode, showDropdownIcon, onDropdownClick, disabled, max, onChange, styleConfig, ...rest },
27
+ forwardedRef,
28
+ ) => {
29
+ const [prefixWidth, setPrefixWidth] = useState(0);
30
+ const handleChange = (event: FieldChangeEvent) => {
31
+ const value = event.target.value;
32
+ const isValidValue = value ? new RegExp(ONLY_NUMERIC).test(value) : true;
33
+ if (value.length <= max && isValidValue) {
34
+ onChange?.(event);
35
+ }
36
+ };
37
+ const customPlaceHolderStyling = useStyles(() => {
38
+ return {
39
+ placeholderStyles: {
40
+ marginLeft: prefixWidth,
41
+ },
42
+ };
43
+ }, [prefixWidth]);
44
+ return (
45
+ <Field
46
+ {...rest}
47
+ type={FieldType.PHONE}
48
+ ref={forwardedRef}
49
+ disabled={disabled}
50
+ onChange={handleChange}
51
+ prefix={
52
+ <View
53
+ onLayout={(e) => {
54
+ const layout = e.nativeEvent.layout.width;
55
+ setPrefixWidth(layout);
56
+ }}
57
+ >
58
+ <PhoneFieldPrefix
59
+ flagIcon={flagIcon}
60
+ countryCode={countryCode}
61
+ showDropdownIcon={showDropdownIcon}
62
+ onDropdownClick={onDropdownClick}
63
+ disabled={disabled}
64
+ />
65
+ </View>
66
+ }
67
+ inputMode='numeric'
68
+ styleConfig={{
69
+ field: [styles.fieldConfig, ...(styleConfig?.field || [])],
70
+ placeholder: {
71
+ placeholderLabel: [
72
+ customPlaceHolderStyling.placeholderStyles,
73
+ ...(styleConfig?.placeholder?.placeholderLabel || []),
74
+ ],
75
+ },
76
+ ...styleConfig,
77
+ }}
78
+ />
79
+ );
80
+ },
81
+ );
82
+
83
+ PhoneField.displayName = 'PhoneField';
84
+ export default PhoneField;