diff --git a/example/src/Examples/TextInputExample.tsx b/example/src/Examples/TextInputExample.tsx
index de05e0e640..d60bf3d69c 100644
--- a/example/src/Examples/TextInputExample.tsx
+++ b/example/src/Examples/TextInputExample.tsx
@@ -1,10 +1,21 @@
import * as React from 'react';
-import { StyleSheet, View, KeyboardAvoidingView, Platform } from 'react-native';
+import {
+ StyleSheet,
+ View,
+ KeyboardAvoidingView,
+ Platform,
+ Text,
+} from 'react-native';
import { TextInput, HelperText, useTheme } from 'react-native-paper';
import Icon from 'react-native-vector-icons/FontAwesome';
import { inputReducer, State } from '../../utils';
import ScreenWrapper from '../ScreenWrapper';
-import { amber900, pink400, transparent } from '../../../src/styles/colors';
+import {
+ amber900,
+ pink400,
+ red500,
+ transparent,
+} from '../../../src/styles/colors';
const MAX_LENGTH = 20;
@@ -18,6 +29,7 @@ const initialState: State = {
outlinedLargeText: '',
outlinedTextPassword: '',
nameNoPadding: '',
+ nameRequired: '',
flatDenseText: '',
flatDense: '',
outlinedDenseText: '',
@@ -71,6 +83,7 @@ const TextInputExample = () => {
outlinedLargeText,
outlinedTextPassword,
nameNoPadding,
+ nameRequired,
flatDenseText,
flatDense,
outlinedDenseText,
@@ -456,6 +469,25 @@ const TextInputExample = () => {
Error: Only letters are allowed
+
+
+ * Label as component
+
+ }
+ style={styles.noPaddingInput}
+ placeholder="Enter username, required"
+ value={nameRequired}
+ error={!nameRequired}
+ onChangeText={(nameRequired) =>
+ inputActionHandler('nameRequired', nameRequired)
+ }
+ />
+
+ Error: Username is required
+
+
{
const isValueChanged = prevState.value !== this.state.value;
const isLabelLayoutChanged =
prevState.labelLayout !== this.state.labelLayout;
- const isLabelChanged = prevProps.label !== this.props.label;
+ const isLabelChanged = !areLabelsEqual(prevProps.label, this.props.label);
const isErrorChanged = prevProps.error !== this.props.error;
-
if (
isFocusChanged ||
isValueChanged ||
diff --git a/src/components/TextInput/helpers.tsx b/src/components/TextInput/helpers.tsx
index 1e037c2f00..4c10c97d6d 100644
--- a/src/components/TextInput/helpers.tsx
+++ b/src/components/TextInput/helpers.tsx
@@ -7,6 +7,7 @@ import {
FLAT_INPUT_OFFSET,
} from './constants';
import { AdornmentType, AdornmentSide } from './Adornment/enums';
+import type { TextInputLabelProp } from './types';
type PaddingProps = {
height: number | null;
@@ -16,7 +17,7 @@ type PaddingProps = {
topPosition: number;
fontSize: number;
lineHeight?: number;
- label?: string | null;
+ label?: TextInputLabelProp | null;
scale: number;
offset: number;
isAndroid: boolean;
@@ -286,3 +287,95 @@ export const calculateFlatInputHorizontalPadding = ({
return { paddingLeft, paddingRight };
};
+
+export function areLabelsEqual(
+ label1?: TextInputLabelProp,
+ label2?: TextInputLabelProp
+): boolean {
+ if (label1 === label2) {
+ // will also take care of equality for `string` type, or if both are undefined.
+ return true;
+ }
+
+ // Return true if both of them are falsy.
+ if (!(label1 || label2)) {
+ return true;
+ }
+
+ // At this point, both of them cannot be false.
+ // So, return false if any of them is falsy.
+ if (!(label1 && label2)) {
+ return false;
+ }
+
+ // At this point, both of them has to be truthy.
+ // So, return false if they are not of the same type.
+ if (typeof label1 !== typeof label2) {
+ return false;
+ }
+
+ // At this point, both of them has to be of the same datatype.
+ if (
+ typeof label1 === 'string' ||
+ label1 instanceof String ||
+ // These last two OR checks are only here for Typescript's sake.
+ typeof label2 === 'string' ||
+ label2 instanceof String
+ ) {
+ // They're strings, so they won't be equal; otherwise
+ // we would have returned 'true' earlier.
+ return false;
+ }
+
+ // At this point, both of them has to be of the datatype: `React.ReactElement`.
+ if (label1.type !== label2.type) {
+ return false;
+ }
+
+ // Preliminary equality check: do they stringify to the same string?
+ const label1Props = label1.props || {};
+ const label2Props = label2.props || {};
+ if (JSON.stringify(label1Props) !== JSON.stringify(label2Props)) {
+ return false;
+ }
+
+ // We now know they stringify to the same string.
+ // Return true if both of them DO NOT have children
+ if (!(label1Props.children || label2Props.children)) {
+ return true; // since there's nothing else to check
+ }
+
+ // Return false if only one of them has children
+ if (!(label1Props.children && label2Props.children)) {
+ return false;
+ }
+
+ // Both have children...
+ // Handle for when both the children are arrays
+ const label1IsArray = Array.isArray(label1Props.children);
+ const label2IsArray = Array.isArray(label2Props.children);
+ if (label1IsArray && label2IsArray) {
+ const children1 = label1Props.children as any[];
+ const children2 = label2Props.children as any[];
+ if (children1.length !== children2.length) {
+ return false; // no point proceeding
+ }
+
+ // all the children must also be equal
+ for (let i = 0; i < children1.length; i++) {
+ if (!areLabelsEqual(children1[i], children2[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ // Only one of them can be an array at this point. If any is array, return false
+ if (label1IsArray || label2IsArray) {
+ return false;
+ }
+
+ // both children are not arrays, so recur.
+ return areLabelsEqual(label1Props.children, label2Props.children);
+}
diff --git a/src/components/TextInput/types.tsx b/src/components/TextInput/types.tsx
index b45f817aa0..aa47c3a420 100644
--- a/src/components/TextInput/types.tsx
+++ b/src/components/TextInput/types.tsx
@@ -8,6 +8,8 @@ import type {
import type { TextInputProps } from './TextInput';
import type { $Omit } from './../../types';
+export type TextInputLabelProp = string | React.ReactElement;
+
export type RenderProps = {
ref: (a?: NativeTextInput | null) => void;
onChangeText?: (a: string) => void;
@@ -62,7 +64,7 @@ export type LabelProps = {
labelTranslationXOffset?: number;
placeholderColor: string | null;
backgroundColor?: ColorValue;
- label?: string | null;
+ label?: TextInputLabelProp | null;
hasActiveOutline?: boolean | null;
activeColor: string;
errorColor?: string;
diff --git a/src/components/__tests__/TextInput.test.js b/src/components/__tests__/TextInput.test.js
index 82d0327086..f1571d2a0e 100644
--- a/src/components/__tests__/TextInput.test.js
+++ b/src/components/__tests__/TextInput.test.js
@@ -1,8 +1,9 @@
import * as React from 'react';
-import { StyleSheet } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
import { fireEvent, render } from 'react-native-testing-library';
import TextInput from '../TextInput/TextInput';
-import { red500 } from '../../styles/colors';
+import { areLabelsEqual } from '../TextInput/helpers';
+import { blue500, red500 } from '../../styles/colors';
const style = StyleSheet.create({
inputStyle: {
@@ -148,3 +149,117 @@ it('correctly applies focused state Outline TextInput', () => {
expect.arrayContaining([expect.objectContaining({ borderWidth: 2 })])
);
});
+
+it('correctly applies a component as the text label', () => {
+ const { toJSON } = render(
+ Flat input}
+ placeholder="Type something"
+ value={'Some test value'}
+ />
+ );
+
+ expect(toJSON()).toMatchSnapshot();
+});
+
+it('correctly compares labels when both are string', () => {
+ expect(areLabelsEqual('Comments', 'Comments')).toBe(true);
+ expect(areLabelsEqual('Comments', 'No Comment')).toBe(false);
+});
+
+it('correctly compares labels when one is string and one is a component', () => {
+ expect(areLabelsEqual(Comments, 'Comments')).toBe(false);
+});
+
+it('correctly compares labels when both labels are falsy', () => {
+ // We're treating all falsy values as equivalent
+ expect(areLabelsEqual()).toBe(true);
+ expect(areLabelsEqual(undefined, undefined)).toBe(true);
+ expect(areLabelsEqual(null, null)).toBe(true);
+ expect(areLabelsEqual(undefined, '')).toBe(true);
+ expect(areLabelsEqual(null, '')).toBe(true);
+ expect(areLabelsEqual(undefined, null)).toBe(true);
+});
+
+it('correctly compares labels when both labels are components', () => {
+ // Same component; same props, same children
+ const component1 = Comments;
+
+ let component2 = Comments;
+ expect(areLabelsEqual(component1, component2)).toBe(true);
+
+ // Same component; same props, different children
+ component2 = Another Comment;
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Different component; same props, same children
+ component2 = Comments;
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Same component; different props, same children
+ component2 = (
+
+ Comments
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+});
+
+it('correctly compares labels for nested components', () => {
+ // Same component; same props, same children
+ const component1 = (
+
+ * Comments
+
+ );
+
+ let component2 = (
+
+ * Comments
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(true);
+
+ // Same component; same props, different children
+ component2 = (
+
+ Comments continues
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Different component; same props, same children
+ component2 = (
+
+ * Comments
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Same component; different props, same children
+ component2 = (
+
+ * Comments
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Same component; same props, different number of children
+ component2 = (
+
+ *
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+
+ // Same component; different props in inner component, same children
+ component2 = (
+
+
+ *
+ {' '}
+ Comments
+
+ );
+ expect(areLabelsEqual(component1, component2)).toBe(false);
+});
diff --git a/src/components/__tests__/__snapshots__/TextInput.test.js.snap b/src/components/__tests__/__snapshots__/TextInput.test.js.snap
index dde1a764c8..c9fa6cd821 100644
--- a/src/components/__tests__/__snapshots__/TextInput.test.js.snap
+++ b/src/components/__tests__/__snapshots__/TextInput.test.js.snap
@@ -1,5 +1,198 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`correctly applies a component as the text label 1`] = `
+
+
+
+
+
+
+ Flat input
+
+
+
+
+ Flat input
+
+
+
+
+
+
+`;
+
exports[`correctly applies default textAlign based on default RTL 1`] = `