Skip to content

Mandatory Indication in placeholder #2791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/incubator/TextField/FloatingPlaceholder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
floatingPlaceholderStyle,
validationMessagePosition,
extraOffset = 0,
testID
testID,
showMandatoryIndication
} = props;
const context = useContext(FieldContext);
const [placeholderOffset, setPlaceholderOffset] = useState({
Expand All @@ -29,6 +30,7 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
const shouldFloat = shouldPlaceholderFloat(props, context.isFocused, context.hasValue, context.value);
const animation = useRef(new Animated.Value(Number(shouldFloat))).current;
const hidePlaceholder = !context.isValid && validationMessagePosition === ValidationMessagePosition.TOP;
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;

useDidUpdate(() => {
Animated.timing(animation, {
Expand Down Expand Up @@ -86,7 +88,7 @@ const FloatingPlaceholder = (props: FloatingPlaceholderProps) => {
testID={testID}
recorderTag={'unmask'}
>
{placeholder}
{shouldRenderIndication ? placeholder?.concat('*') : placeholder}
</Text>
</View>
);
Expand Down
4 changes: 3 additions & 1 deletion src/incubator/TextField/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const Input = ({
readonly,
recorderTag,
pointerEvents,
showMandatoryIndication,
...props
}: InputProps & ForwardRefInjectedProps) => {
const inputRef = useImperativeInputHandle(forwardedRef, {onChangeText: props.onChangeText});
Expand All @@ -46,6 +47,7 @@ const Input = ({
const placeholderTextColor = getColorByState(props.placeholderTextColor, context);
const value = formatter && !context.isFocused ? formatter(props.value) : props.value;
const disabled = props.editable === false || readonly;
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;

const TextInput = useMemo(() => {
if (useGestureHandlerInput) {
Expand All @@ -65,7 +67,7 @@ const Input = ({
{...props}
editable={!disabled}
value={value}
placeholder={placeholder}
placeholder={shouldRenderIndication ? placeholder?.concat('*') : placeholder}
placeholderTextColor={placeholderTextColor}
// @ts-expect-error
ref={inputRef}
Expand Down
1 change: 0 additions & 1 deletion src/incubator/TextField/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const Label = ({
return [styles.label, labelStyle, floatingPlaceholder && styles.dummyPlaceholder];
}, [labelStyle, floatingPlaceholder]);
const shouldRenderIndication = context.isMandatory && showMandatoryIndication;

if ((label || floatingPlaceholder) && !forceHidingLabel) {
return (
<Text
Expand Down
130 changes: 97 additions & 33 deletions src/incubator/TextField/__tests__/index.driver.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,26 +146,15 @@ describe('TextField', () => {

describe('validation message', () => {
it('should not render validationMessage if enableErrors prop not supplied', async () => {
const component = (
<TestCase
value={''}
validationMessage={'mock message'}
validateOnStart
/>);
const component = <TestCase value={''} validationMessage={'mock message'} validateOnStart/>;

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

expect(await textFieldDriver.isValidationMsgExists()).toBe(false);
});

it('should render validationMessage on start if input required and validateOnStart passed', async () => {
const component = (
<TestCase
value={''}
validationMessage={'mock message'}
enableErrors
validateOnStart
/>);
const component = <TestCase value={''} validationMessage={'mock message'} enableErrors validateOnStart/>;
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});

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

it('should render validationMessage when input is requires after changing the input to empty string', async () => {
const component = (
<TestCase
value={''}
validate={'required'}
validationMessage={'mock message'}
enableErrors
validateOnChange
/>);
<TestCase value={''} validate={'required'} validationMessage={'mock message'} enableErrors validateOnChange/>
);
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});

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

describe('validateOnBlur', () => {
it('validate is called with undefined when defaultValue is not given', async () => {
const component = (
<TestCase
validateOnBlur
validationMessage={'Not valid'}
validate={[validate]}
/>
);
const component = <TestCase validateOnBlur validationMessage={'Not valid'} validate={[validate]}/>;
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
textFieldDriver.focus();
textFieldDriver.blur();
Expand All @@ -259,12 +237,7 @@ describe('TextField', () => {
it('validate is called with defaultValue when defaultValue is given', async () => {
const defaultValue = '1';
const component = (
<TestCase
validateOnBlur
validationMessage={'Not valid'}
validate={[validate]}
defaultValue={defaultValue}
/>
<TestCase validateOnBlur validationMessage={'Not valid'} validate={[validate]} defaultValue={defaultValue}/>
);
const textFieldDriver = new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
textFieldDriver.focus();
Expand All @@ -273,4 +246,95 @@ describe('TextField', () => {
await waitFor(() => expect(validate).toHaveBeenCalledWith(defaultValue));
});
});
describe('Mandatory Indication', () => {
const getTestCaseDriver = (props: TextFieldProps) => {
const component = <TestCase {...props}/>;
return new TextFieldDriver({component, testID: TEXT_FIELD_TEST_ID});
};
const starReg = /.*\*$/;

//Sanity
it('Should show mandatory indication on the label', async () => {
const textFieldDriver = getTestCaseDriver({label: 'Label', validate: 'required', showMandatoryIndication: true});
const labelContent = await textFieldDriver.getLabelContent();
expect(labelContent).toMatch(starReg);
});
it('Should show mandatory indication on the label', async () => {
const textFieldDriver = getTestCaseDriver({
label: 'Label',
validate: ['required'],
showMandatoryIndication: true
});
const labelContent = await textFieldDriver.getLabelContent();
expect(labelContent).toMatch(starReg);
});
it('Should not show mandatory indication on label', async () => {
const textFieldDriver = getTestCaseDriver({label: 'label', showMandatoryIndication: true});
const labelText = await textFieldDriver.getLabelContent();
expect(labelText).not.toMatch(starReg);
});
it('Should not show mandatory indication on label', async () => {
const textFieldDriver = getTestCaseDriver({label: 'label', validate: 'required'});
const labelText = await textFieldDriver.getLabelContent();
expect(labelText).not.toMatch(starReg);
});
it('Should have mandatory on the placeholder', async () => {
const textFieldDriver = getTestCaseDriver({
placeholder: 'placeholder',
showMandatoryIndication: true,
validate: 'required'
});
const placeholderText = await textFieldDriver.getPlaceholderContent();
expect(placeholderText).toMatch(starReg);
});
it('Should not have any mandatory - 1', async () => {
const textFieldDriver = getTestCaseDriver({
placeholder: 'placeholder',
showMandatoryIndication: true,
// validate: 'required',
label: 'label'
});
const placeholderText = await textFieldDriver.getPlaceholderContent();
const labelText = await textFieldDriver.getLabelContent();
expect(placeholderText).not.toMatch(starReg);
expect(labelText).not.toMatch(starReg);
});
it('Should not have any mandatory - 2', async () => {
const textFieldDriver = getTestCaseDriver({
placeholder: 'placeholder',
// showMandatoryIndication: true,
validate: 'required',
label: 'label'
});
const placeholderText = await textFieldDriver.getPlaceholderContent();
const labelText = await textFieldDriver.getLabelContent();
expect(placeholderText).not.toMatch(starReg);
expect(labelText).not.toMatch(starReg);
});
it('Should have mandatory on the floating placeholder', async () => {
const textFieldDriver = getTestCaseDriver({
placeholder: 'placeholder',
floatingPlaceholder: true,
floatOnFocus: true,
showMandatoryIndication: true,
validate: 'required'
});
const placeholderText = await textFieldDriver.getPlaceholderContent();
expect(placeholderText).toMatch(starReg);
});

// Special cases
it('Should have mandatory on the label and not on the placeholder', async () => {
const textFieldDriver = getTestCaseDriver({
placeholder: 'placeholder',
showMandatoryIndication: true,
validate: 'required',
label: 'label'
});
const labelText = await textFieldDriver.getLabelContent();
const placeholderText = await textFieldDriver.getPlaceholderContent();
expect(labelText).toMatch(starReg);
expect(placeholderText).not.toMatch(starReg);
});
});
});
2 changes: 2 additions & 0 deletions src/incubator/TextField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const TextField = (props: InternalTextFieldProps) => {
validationMessagePosition={validationMessagePosition}
extraOffset={leadingAccessoryMeasurements?.width}
testID={`${props.testID}.floatingPlaceholder`}
showMandatoryIndication={showMandatoryIndication}
/>
)}
<Input
Expand All @@ -186,6 +187,7 @@ const TextField = (props: InternalTextFieldProps) => {
onChangeText={onChangeText}
placeholder={placeholder}
hint={hint}
showMandatoryIndication={showMandatoryIndication && !label}
/>
</View>
)}
Expand Down
20 changes: 12 additions & 8 deletions src/incubator/TextField/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface FieldStateProps extends InputProps {
validateOnBlur?: boolean;
/**
* Callback for when field validated and failed
*/
*/
onValidationFailed?: (failedValidatorIndex: number) => void;
/**
* A single or multiple validator. Can be a string (required, email) or custom function.
Expand All @@ -54,7 +54,14 @@ export interface FieldStateProps extends InputProps {
onChangeValidity?: (isValid: boolean) => void;
}

export interface LabelProps {
interface MandatoryIndication {
/**
* Whether to show a mandatory field indication.
*/
showMandatoryIndication?: boolean;
}

export interface LabelProps extends MandatoryIndication {
/**
* Field label
*/
Expand All @@ -74,10 +81,9 @@ export interface LabelProps {
validationMessagePosition?: ValidationMessagePositionType;
floatingPlaceholder?: boolean;
testID?: string;
showMandatoryIndication?: boolean;
}

export interface FloatingPlaceholderProps {
export interface FloatingPlaceholderProps extends MandatoryIndication {
/**
* The placeholder for the field
*/
Expand Down Expand Up @@ -137,6 +143,7 @@ export interface CharCounterProps {
export interface InputProps
extends Omit<TextInputProps, 'placeholderTextColor'>,
Omit<React.ComponentPropsWithRef<typeof TextInput>, 'placeholderTextColor'>,
MandatoryIndication,
RecorderProps {
/**
* A hint text to display when focusing the field
Expand Down Expand Up @@ -171,6 +178,7 @@ export type TextFieldProps = MarginModifiers &
InputProps &
LabelProps &
Omit<FloatingPlaceholderProps, 'testID'> &
MandatoryIndication &
// We're declaring these props explicitly here for react-docgen (which can't read hooks)
// FieldStateProps &
ValidationMessageProps &
Expand Down Expand Up @@ -252,10 +260,6 @@ export type TextFieldProps = MarginModifiers &
* Set an alignment fit for inline behavior (when rendered inside a row container)
*/
inline?: boolean;
/**
* Whether to show a mandatory field indication.
*/
showMandatoryIndication?: boolean;
};

export type InternalTextFieldProps = PropsWithChildren<
Expand Down