Skip to content

Commit 8fc5485

Browse files
feat: add props for code input (#119)
* feat: add props for code input * feat: update CodeInput readme * fix: address feedback to remove incorrect theme props
1 parent 17bee1c commit 8fc5485

File tree

6 files changed

+545
-88
lines changed

6 files changed

+545
-88
lines changed

src/components/CodeInput/CodeInput.tsx

Lines changed: 200 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@ import React, {
77
useRef,
88
useState,
99
} from 'react'
10-
import {KeyboardTypeOptions, StyleProp, TextInput, TextInputProps, TextStyle, ViewStyle} from 'react-native'
10+
import {
11+
KeyboardTypeOptions,
12+
StyleProp,
13+
TextInput,
14+
TextInputProps,
15+
TextProps,
16+
TextStyle,
17+
ViewStyle,
18+
} from 'react-native'
1119
import styled from 'styled-components/native'
1220
import {useTheme} from '../../hooks'
1321
import {metrics} from '../../helpers'
1422
import {Cursor} from './Cursor'
1523
import {Text} from '../Text/Text'
24+
import {ErrorText, HelperText} from './components'
1625

1726
// Types
1827
type CodeInputValue = string
@@ -102,9 +111,64 @@ interface CodeInputProps extends Omit<TextInputProps, 'value' | 'onChangeText' |
102111

103112
/** Test ID for the component */
104113
testID?: string
105-
}
106114

107-
// Default constants moved to theme configuration
115+
/** Style for outer container */
116+
containerStyle?: StyleProp<ViewStyle>
117+
118+
/** Label text displayed above the code input */
119+
label?: string
120+
121+
/** Custom label component to replace default label text */
122+
labelComponent?: ReactNode
123+
124+
/** Styling for the label */
125+
labelStyle?: StyleProp<TextStyle>
126+
127+
/** Props to be passed to the label Text component */
128+
labelProps?: TextProps
129+
130+
/** Show asterisk beside label for required fields */
131+
isRequire?: boolean
132+
133+
/** Helper text displayed below the code input */
134+
helperText?: string
135+
136+
/** Custom helper component to replace default helper text */
137+
helperComponent?: ReactNode
138+
139+
/** Props to be passed to the helper text component */
140+
helperTextProps?: TextProps
141+
142+
/** Error text displayed below the code input */
143+
errorText?: string
144+
145+
/** Props to be passed to the error text component */
146+
errorProps?: TextProps
147+
148+
/** Enable error state styling */
149+
error?: boolean
150+
151+
/** Style for cell in error state */
152+
errorCellStyle?: StyleProp<ViewStyle>
153+
154+
/** Enable success state styling */
155+
success?: boolean
156+
157+
/** Style for cell in success state */
158+
successCellStyle?: StyleProp<ViewStyle>
159+
160+
/** Style for cell in disabled state */
161+
disabledCellStyle?: StyleProp<ViewStyle>
162+
163+
/** Style for cell in active state */
164+
activeCellStyle?: StyleProp<ViewStyle>
165+
166+
/** React node to be rendered on the left side of the code input */
167+
leftComponent?: ReactNode
168+
169+
/** React node to be rendered on the right side of the code input */
170+
rightComponent?: ReactNode
171+
}
108172

109173
// Main component
110174
export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
@@ -135,6 +199,25 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
135199
autoFocus,
136200
disabled,
137201
testID = 'code-input',
202+
containerStyle,
203+
label,
204+
labelComponent,
205+
labelStyle,
206+
labelProps,
207+
isRequire,
208+
helperText,
209+
helperComponent,
210+
helperTextProps,
211+
errorText,
212+
errorProps,
213+
error,
214+
errorCellStyle,
215+
success,
216+
successCellStyle,
217+
disabledCellStyle,
218+
activeCellStyle,
219+
leftComponent,
220+
rightComponent,
138221
...textInputProps
139222
},
140223
ref,
@@ -145,7 +228,7 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
145228
const [isFocused, setIsFocused] = useState(false)
146229

147230
// Use theme defaults with props override
148-
const actualLength = length ?? CodeInputTheme.length
231+
const actualLength = length ?? 6 // Default length is 6
149232

150233
// Use controlled or uncontrolled value
151234
const code = controlledValue !== undefined ? controlledValue : internalValue
@@ -295,7 +378,15 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
295378
const isCellFocused = isFocused && cellIndex === focusedCellIndex
296379
const hasCellValue = Boolean(cellValue)
297380

298-
const cellStyles = [cellStyle, hasCellValue && filledCellStyle, isCellFocused && focusCellStyle]
381+
// Apply styles in priority order: disabled > error > success > focused > filled > default
382+
const cellStyles = [
383+
cellStyle,
384+
hasCellValue && filledCellStyle,
385+
isCellFocused && (activeCellStyle ?? focusCellStyle),
386+
success && (successCellStyle ?? CodeInputTheme.successCellStyle),
387+
error && (errorCellStyle ?? CodeInputTheme.errorCellStyle),
388+
disabled && (disabledCellStyle ?? CodeInputTheme.disabledCellStyle),
389+
]
299390

300391
const wrapperStyles = [cellWrapperStyle, isCellFocused && focusCellWrapperStyle]
301392

@@ -311,7 +402,11 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
311402
accessibilityLabel={`Code input cell ${cellIndex + 1} of ${length}${
312403
cellValue ? `, contains ${cellValue}` : ', empty'
313404
}`}
314-
accessibilityHint={`Tap to ${cellValue ? 'clear and ' : ''}enter code digit`}>
405+
accessibilityHint={`Tap to ${cellValue ? 'clear and ' : ''}enter code digit`}
406+
accessibilityState={{
407+
disabled: !!disabled,
408+
selected: isCellFocused,
409+
}}>
315410
{renderCellContent(cellIndex, cellValue)}
316411
</Cell>
317412
</CellWrapperStyled>
@@ -324,13 +419,20 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
324419
cellStyle,
325420
filledCellStyle,
326421
focusCellStyle,
422+
activeCellStyle,
327423
cellWrapperStyle,
328424
focusCellWrapperStyle,
329425
handleCellPress,
330426
disabled,
427+
error,
428+
success,
429+
errorCellStyle,
430+
successCellStyle,
431+
disabledCellStyle,
331432
length,
332433
testID,
333434
renderCellContent,
435+
CodeInputTheme,
334436
],
335437
)
336438

@@ -349,34 +451,74 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
349451
}
350452

351453
return (
352-
<Container testID={testID}>
353-
<HiddenTextInput
354-
testID={`${testID}-hidden-input`}
355-
ref={textInputRef}
356-
value={code}
357-
onChangeText={handleValueChange}
358-
onFocus={handleFocus}
359-
onBlur={handleBlur}
360-
maxLength={actualLength}
361-
keyboardType={keyboardType ?? CodeInputTheme.keyboardType}
362-
textContentType="oneTimeCode"
363-
autoComplete="sms-otp"
364-
autoFocus={autoFocus ?? CodeInputTheme.autoFocus}
365-
editable={!(disabled ?? CodeInputTheme.disabled)}
366-
accessible={true}
367-
accessibilityLabel={`Code input with ${actualLength} digits`}
368-
accessibilityHint={`Enter ${actualLength} digit code`}
369-
accessibilityValue={{
370-
text: `${code.length} of ${actualLength} digits entered`,
371-
}}
372-
{...textInputProps}
373-
/>
374-
<CellContainer
375-
style={cellContainerStyle}
376-
accessible={true}
377-
accessibilityLabel={`Code input cells, ${code.length} of ${actualLength} filled`}>
378-
{cells}
379-
</CellContainer>
454+
<Container testID={testID} style={containerStyle}>
455+
{labelComponent ? (
456+
<LabelContainer testID={`${testID}-label-container`}>
457+
{labelComponent}
458+
{isRequire && <RequiredStar testID={`${testID}-required`}> *</RequiredStar>}
459+
</LabelContainer>
460+
) : (
461+
!!label && (
462+
<LabelText
463+
testID={`${testID}-label`}
464+
style={labelStyle ?? CodeInputTheme.labelStyle}
465+
{...labelProps}>
466+
{label}
467+
{isRequire && <RequiredStar testID={`${testID}-required`}> *</RequiredStar>}
468+
</LabelText>
469+
)
470+
)}
471+
<InputWrapper>
472+
<HiddenTextInput
473+
testID={`${testID}-hidden-input`}
474+
ref={textInputRef}
475+
value={code}
476+
onChangeText={handleValueChange}
477+
onFocus={handleFocus}
478+
onBlur={handleBlur}
479+
maxLength={actualLength}
480+
keyboardType={keyboardType ?? 'number-pad'}
481+
textContentType="oneTimeCode"
482+
autoComplete="sms-otp"
483+
autoFocus={autoFocus}
484+
editable={!disabled}
485+
accessible={true}
486+
accessibilityLabel={`Code input with ${actualLength} digits${error ? ', error' : ''}${
487+
success ? ', success' : ''
488+
}${disabled ? ', disabled' : ''}`}
489+
accessibilityHint={`Enter ${actualLength} digit code`}
490+
accessibilityValue={{
491+
text: `${code.length} of ${actualLength} digits entered`,
492+
}}
493+
accessibilityState={{
494+
disabled: !!disabled,
495+
}}
496+
{...textInputProps}
497+
/>
498+
<ComponentRow>
499+
{!!leftComponent && leftComponent}
500+
<CellContainer
501+
style={cellContainerStyle}
502+
accessible={true}
503+
accessibilityLabel={`Code input cells, ${code.length} of ${actualLength} filled${
504+
error ? ', error' : ''
505+
}${success ? ', success' : ''}${disabled ? ', disabled' : ''}`}
506+
accessibilityState={{
507+
disabled: !!disabled,
508+
}}>
509+
{cells}
510+
</CellContainer>
511+
{!!rightComponent && rightComponent}
512+
</ComponentRow>
513+
</InputWrapper>
514+
{!!errorText && <ErrorText errorText={errorText} errorProps={errorProps} />}
515+
{!errorText && (!!helperText || !!helperComponent) && (
516+
<HelperText
517+
helperText={helperText}
518+
helperComponent={helperComponent}
519+
helperTextProps={helperTextProps}
520+
/>
521+
)}
380522
</Container>
381523
)
382524
},
@@ -385,10 +527,33 @@ export const CodeInput = forwardRef<CodeInputRef, CodeInputProps>(
385527
CodeInput.displayName = 'CodeInput'
386528

387529
// Styled components
388-
const Container = styled.View({
530+
const Container = styled.View({})
531+
532+
const InputWrapper = styled.View({
389533
position: 'relative',
390534
})
391535

536+
const LabelContainer = styled.View(({theme}) => ({
537+
marginBottom: theme?.spacing?.tiny || 8,
538+
flexDirection: 'row',
539+
alignItems: 'center',
540+
}))
541+
542+
const LabelText = styled.Text(({theme}) => ({
543+
fontSize: theme?.fontSizes?.sm || 14,
544+
color: theme?.colors?.darkText || '#333',
545+
marginBottom: theme?.spacing?.tiny || 8,
546+
}))
547+
548+
const RequiredStar = styled.Text(({theme}) => ({
549+
color: theme?.colors?.errorText || '#ff0000',
550+
}))
551+
552+
const ComponentRow = styled.View({
553+
flexDirection: 'row',
554+
alignItems: 'center',
555+
})
556+
392557
const CellWrapperStyled = styled.View({})
393558

394559
const Cell = styled.Pressable<{disabled?: boolean}>(({theme, disabled}) => ({

0 commit comments

Comments
 (0)