Skip to content

Commit e7ecdd2

Browse files
authored
Mandatory Indication in placeholder (#2791)
* Added view driver * Added view tests - without using the driver * Added View driver and a test to check background color utilizing it * Removed view tests * Exported new view test driver * Changed indication logic - if no label is given the indication will be on the placeholder * Added tests for the mandatory indication * Added more sanity checks
1 parent 47a9d6a commit e7ecdd2

File tree

6 files changed

+118
-45
lines changed

6 files changed

+118
-45
lines changed

src/incubator/TextField/FloatingPlaceholder.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
1818
floatingPlaceholderStyle,
1919
validationMessagePosition,
2020
extraOffset = 0,
21-
testID
21+
testID,
22+
showMandatoryIndication
2223
} = props;
2324
const context = useContext(FieldContext);
2425
const [placeholderOffset, setPlaceholderOffset] = useState({
@@ -29,6 +30,7 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
2930
const shouldFloat = shouldPlaceholderFloat(props, context.isFocused, context.hasValue, context.value);
3031
const animation = useRef(new Animated.Value(Number(shouldFloat))).current;
3132
const hidePlaceholder = !context.isValid && validationMessagePosition === ValidationMessagePosition.TOP;
33+
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;
3234

3335
useDidUpdate(() => {
3436
Animated.timing(animation, {
@@ -86,7 +88,7 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
8688
testID={testID}
8789
recorderTag={'unmask'}
8890
>
89-
{placeholder}
91+
{shouldRenderIndication ? placeholder?.concat('*') : placeholder}
9092
</Text>
9193
</View>
9294
);

src/incubator/TextField/Input.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const Input = ({
3737
readonly,
3838
recorderTag,
3939
pointerEvents,
40+
showMandatoryIndication,
4041
...props
4142
}: InputProps & ForwardRefInjectedProps) => {
4243
const inputRef = useImperativeInputHandle(forwardedRef, {onChangeText: props.onChangeText});
@@ -46,6 +47,7 @@ const Input = ({
4647
const placeholderTextColor = getColorByState(props.placeholderTextColor, context);
4748
const value = formatter && !context.isFocused ? formatter(props.value) : props.value;
4849
const disabled = props.editable === false || readonly;
50+
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;
4951

5052
const TextInput = useMemo(() => {
5153
if (useGestureHandlerInput) {
@@ -65,7 +67,7 @@ const Input = ({
6567
{...props}
6668
editable={!disabled}
6769
value={value}
68-
placeholder={placeholder}
70+
placeholder={shouldRenderIndication ? placeholder?.concat('*') : placeholder}
6971
placeholderTextColor={placeholderTextColor}
7072
// @ts-expect-error
7173
ref={inputRef}

src/incubator/TextField/Label.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const Label = ({
2929
return [styles.label, labelStyle, floatingPlaceholder && styles.dummyPlaceholder];
3030
}, [labelStyle, floatingPlaceholder]);
3131
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;
32-
3332
if ((label || floatingPlaceholder) && !forceHidingLabel) {
3433
return (
3534
<Text

src/incubator/TextField/__tests__/index.driver.spec.tsx

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,15 @@ describe('TextField', () => {
146146

147147
describe('validation message', () => {
148148
it('should not render validationMessage if enableErrors prop not supplied', async () => {
149-
const component = (
150-
<TestCase
151-
value={''}
152-
validationMessage={'mock message'}
153-
validateOnStart
154-
/>);
149+
const component = <TestCase value={''} validationMessage={'mock message'} validateOnStart/>;
155150

156151
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
157152

158153
expect(await textFieldDriver.isValidationMsgExists()).toBe(false);
159154
});
160155

161156
it('should render validationMessage on start if input required and validateOnStart passed', async () => {
162-
const component = (
163-
<TestCase
164-
value={''}
165-
validationMessage={'mock message'}
166-
enableErrors
167-
validateOnStart
168-
/>);
157+
const component = <TestCase value={''} validationMessage={'mock message'} enableErrors validateOnStart/>;
169158
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
170159

171160
expect(await textFieldDriver.isValidationMsgExists()).toBe(true);
@@ -174,13 +163,8 @@ describe('TextField', () => {
174163

175164
it('should render validationMessage when input is requires after changing the input to empty string', async () => {
176165
const component = (
177-
<TestCase
178-
value={''}
179-
validate={'required'}
180-
validationMessage={'mock message'}
181-
enableErrors
182-
validateOnChange
183-
/>);
166+
<TestCase value={''} validate={'required'} validationMessage={'mock message'} enableErrors validateOnChange/>
167+
);
184168
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
185169

186170
expect(await textFieldDriver.isValidationMsgExists()).toBe(false);
@@ -242,13 +226,7 @@ describe('TextField', () => {
242226

243227
describe('validateOnBlur', () => {
244228
it('validate is called with undefined when defaultValue is not given', async () => {
245-
const component = (
246-
<TestCase
247-
validateOnBlur
248-
validationMessage={'Not valid'}
249-
validate={[validate]}
250-
/>
251-
);
229+
const component = <TestCase validateOnBlur validationMessage={'Not valid'} validate={[validate]}/>;
252230
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
253231
textFieldDriver.focus();
254232
textFieldDriver.blur();
@@ -259,12 +237,7 @@ describe('TextField', () => {
259237
it('validate is called with defaultValue when defaultValue is given', async () => {
260238
const defaultValue = '1';
261239
const component = (
262-
<TestCase
263-
validateOnBlur
264-
validationMessage={'Not valid'}
265-
validate={[validate]}
266-
defaultValue={defaultValue}
267-
/>
240+
<TestCase validateOnBlur validationMessage={'Not valid'} validate={[validate]} defaultValue={defaultValue}/>
268241
);
269242
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
270243
textFieldDriver.focus();
@@ -273,4 +246,95 @@ describe('TextField', () => {
273246
await waitFor(() => expect(validate).toHaveBeenCalledWith(defaultValue));
274247
});
275248
});
249+
describe('Mandatory Indication', () => {
250+
const getTestCaseDriver = (props: TextFieldProps) => {
251+
const component = <TestCase {...props}/>;
252+
return new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
253+
};
254+
const starReg = /.*\*$/;
255+
256+
//Sanity
257+
it('Should show mandatory indication on the label', async () => {
258+
const textFieldDriver = getTestCaseDriver({label: 'Label', validate: 'required', showMandatoryIndication: true});
259+
const labelContent = await textFieldDriver.getLabelContent();
260+
expect(labelContent).toMatch(starReg);
261+
});
262+
it('Should show mandatory indication on the label', async () => {
263+
const textFieldDriver = getTestCaseDriver({
264+
label: 'Label',
265+
validate: ['required'],
266+
showMandatoryIndication: true
267+
});
268+
const labelContent = await textFieldDriver.getLabelContent();
269+
expect(labelContent).toMatch(starReg);
270+
});
271+
it('Should not show mandatory indication on label', async () => {
272+
const textFieldDriver = getTestCaseDriver({label: 'label', showMandatoryIndication: true});
273+
const labelText = await textFieldDriver.getLabelContent();
274+
expect(labelText).not.toMatch(starReg);
275+
});
276+
it('Should not show mandatory indication on label', async () => {
277+
const textFieldDriver = getTestCaseDriver({label: 'label', validate: 'required'});
278+
const labelText = await textFieldDriver.getLabelContent();
279+
expect(labelText).not.toMatch(starReg);
280+
});
281+
it('Should have mandatory on the placeholder', async () => {
282+
const textFieldDriver = getTestCaseDriver({
283+
placeholder: 'placeholder',
284+
showMandatoryIndication: true,
285+
validate: 'required'
286+
});
287+
const placeholderText = await textFieldDriver.getPlaceholderContent();
288+
expect(placeholderText).toMatch(starReg);
289+
});
290+
it('Should not have any mandatory - 1', async () => {
291+
const textFieldDriver = getTestCaseDriver({
292+
placeholder: 'placeholder',
293+
showMandatoryIndication: true,
294+
// validate: 'required',
295+
label: 'label'
296+
});
297+
const placeholderText = await textFieldDriver.getPlaceholderContent();
298+
const labelText = await textFieldDriver.getLabelContent();
299+
expect(placeholderText).not.toMatch(starReg);
300+
expect(labelText).not.toMatch(starReg);
301+
});
302+
it('Should not have any mandatory - 2', async () => {
303+
const textFieldDriver = getTestCaseDriver({
304+
placeholder: 'placeholder',
305+
// showMandatoryIndication: true,
306+
validate: 'required',
307+
label: 'label'
308+
});
309+
const placeholderText = await textFieldDriver.getPlaceholderContent();
310+
const labelText = await textFieldDriver.getLabelContent();
311+
expect(placeholderText).not.toMatch(starReg);
312+
expect(labelText).not.toMatch(starReg);
313+
});
314+
it('Should have mandatory on the floating placeholder', async () => {
315+
const textFieldDriver = getTestCaseDriver({
316+
placeholder: 'placeholder',
317+
floatingPlaceholder: true,
318+
floatOnFocus: true,
319+
showMandatoryIndication: true,
320+
validate: 'required'
321+
});
322+
const placeholderText = await textFieldDriver.getPlaceholderContent();
323+
expect(placeholderText).toMatch(starReg);
324+
});
325+
326+
// Special cases
327+
it('Should have mandatory on the label and not on the placeholder', async () => {
328+
const textFieldDriver = getTestCaseDriver({
329+
placeholder: 'placeholder',
330+
showMandatoryIndication: true,
331+
validate: 'required',
332+
label: 'label'
333+
});
334+
const labelText = await textFieldDriver.getLabelContent();
335+
const placeholderText = await textFieldDriver.getPlaceholderContent();
336+
expect(labelText).toMatch(starReg);
337+
expect(placeholderText).not.toMatch(starReg);
338+
});
339+
});
276340
});

src/incubator/TextField/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ const TextField = (props: InternalTextFieldProps) => {
173173
validationMessagePosition={validationMessagePosition}
174174
extraOffset={leadingAccessoryMeasurements?.width}
175175
testID={`${props.testID}.floatingPlaceholder`}
176+
showMandatoryIndication={showMandatoryIndication}
176177
/>
177178
)}
178179
<Input
@@ -186,6 +187,7 @@ const TextField = (props: InternalTextFieldProps) => {
186187
onChangeText={onChangeText}
187188
placeholder={placeholder}
188189
hint={hint}
190+
showMandatoryIndication={showMandatoryIndication && !label}
189191
/>
190192
</View>
191193
)}

src/incubator/TextField/types.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface FieldStateProps extends InputProps {
3838
validateOnBlur?: boolean;
3939
/**
4040
* Callback for when field validated and failed
41-
*/
41+
*/
4242
onValidationFailed?: (failedValidatorIndex: number) => void;
4343
/**
4444
* A single or multiple validator. Can be a string (required, email) or custom function.
@@ -54,7 +54,14 @@ export interface FieldStateProps extends InputProps {
5454
onChangeValidity?: (isValid: boolean) => void;
5555
}
5656

57-
export interface LabelProps {
57+
interface MandatoryIndication {
58+
/**
59+
* Whether to show a mandatory field indication.
60+
*/
61+
showMandatoryIndication?: boolean;
62+
}
63+
64+
export interface LabelProps extends MandatoryIndication {
5865
/**
5966
* Field label
6067
*/
@@ -74,10 +81,9 @@ export interface LabelProps {
7481
validationMessagePosition?: ValidationMessagePositionType;
7582
floatingPlaceholder?: boolean;
7683
testID?: string;
77-
showMandatoryIndication?: boolean;
7884
}
7985

80-
export interface FloatingPlaceholderProps {
86+
export interface FloatingPlaceholderProps extends MandatoryIndication {
8187
/**
8288
* The placeholder for the field
8389
*/
@@ -137,6 +143,7 @@ export interface CharCounterProps {
137143
export interface InputProps
138144
extends Omit<TextInputProps, 'placeholderTextColor'>,
139145
Omit<React.ComponentPropsWithRef<typeof TextInput>, 'placeholderTextColor'>,
146+
MandatoryIndication,
140147
RecorderProps {
141148
/**
142149
* A hint text to display when focusing the field
@@ -171,6 +178,7 @@ export type TextFieldProps = MarginModifiers &
171178
InputProps &
172179
LabelProps &
173180
Omit<FloatingPlaceholderProps, 'testID'> &
181+
MandatoryIndication &
174182
// We're declaring these props explicitly here for react-docgen (which can't read hooks)
175183
// FieldStateProps &
176184
ValidationMessageProps &
@@ -252,10 +260,6 @@ export type TextFieldProps = MarginModifiers &
252260
* Set an alignment fit for inline behavior (when rendered inside a row container)
253261
*/
254262
inline?: boolean;
255-
/**
256-
* Whether to show a mandatory field indication.
257-
*/
258-
showMandatoryIndication?: boolean;
259263
};
260264

261265
export type InternalTextFieldProps = PropsWithChildren<

0 commit comments

Comments
 (0)