Skip to content

Commit 89c0638

Browse files
committed
fix: rtl handling
1 parent d0cffca commit 89c0638

7 files changed

Lines changed: 202 additions & 14 deletions

File tree

src/components/TextField/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL = 12;
1616
// ==================
1717
// ACCESSORY
1818
// ==================
19-
export const ACCESORY_SIZE = 24;
19+
export const ACCESSORY_SIZE = 24;
2020

2121
// ===============
2222
// TYPOGRAPHY

src/components/TextField/filled/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
ACCESORY_SIZE,
2+
ACCESSORY_SIZE,
33
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
44
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL,
55
TEXT_FIELD_PADDING_VERTICAL,
@@ -10,7 +10,7 @@ import {
1010
// ==================
1111

1212
export const LABEL_LEFT_OFFSET_WITH_ACCESSORY =
13-
ACCESORY_SIZE +
13+
ACCESSORY_SIZE +
1414
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL +
1515
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL;
1616

src/components/TextField/filled/logic.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
1+
import {
2+
Animated,
3+
I18nManager,
4+
StyleProp,
5+
TextStyle,
6+
ViewStyle,
7+
} from 'react-native';
28

39
import {
410
ACTIVE_INDICATOR_SIZE,
@@ -53,6 +59,12 @@ export const getFilledTextFieldData = (
5359
$animatedLabelTextStyle,
5460
} = api;
5561

62+
// =======================
63+
// CONSTANTS
64+
// =======================
65+
66+
const { isRTL } = I18nManager;
67+
5668
// =======================
5769
// THEME TOKENS
5870
// =======================
@@ -125,20 +137,23 @@ export const getFilledTextFieldData = (
125137

126138
const $containerStyles = [$containerStyle, $containerStyleOverride];
127139

128-
const $helperStyles = [
140+
const $helperStyles: StyleProp<TextStyle> = [
129141
$helperStyle,
130142
{
131-
color: props.status === 'error' ? errorColor : onSurfaceVariant,
143+
color: hasError ? errorColor : onSurfaceVariant,
144+
writingDirection: isRTL ? 'rtl' : 'ltr',
132145
},
133146
disabled && $disabledStyle,
134147
helperProps?.style,
135148
];
136149

137-
const $inputStyles = [
150+
const $inputStyles: StyleProp<TextStyle> = [
138151
$inputStyle,
139152
{
140153
color: onSurface,
141154
fontSize: INPUT_FONT_SIZE,
155+
textAlign: isRTL ? 'right' : 'left',
156+
writingDirection: isRTL ? 'rtl' : 'ltr',
142157
},
143158
textInputProps.multiline && {
144159
height: 'auto' as TextStyle['height'],

src/components/TextField/outlined/constants.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { I18nManager } from 'react-native';
22

33
import {
4-
ACCESORY_SIZE,
4+
ACCESSORY_SIZE,
55
LINE_HEIGHT_DELTA,
66
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
77
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL,
@@ -21,7 +21,7 @@ const layoutSupportMultiplier = isRTL ? -1 : 1;
2121
export const LABEL_PADDING_HORIZONTAL = 4;
2222

2323
export const LABEL_LEFT_OFFSET_WITH_ACCESSORY =
24-
ACCESORY_SIZE +
24+
ACCESSORY_SIZE +
2525
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL +
2626
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL -
2727
LABEL_PADDING_HORIZONTAL;
@@ -34,7 +34,7 @@ export const ACTIVE_LABEL_TOP_POSITION =
3434

3535
export const LABEL_TRANSLATE_X_WITH_ACCESSORY =
3636
-layoutSupportMultiplier *
37-
(ACCESORY_SIZE +
37+
(ACCESSORY_SIZE +
3838
TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL -
3939
LABEL_PADDING_HORIZONTAL);
4040

src/components/TextField/styles.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { StyleProp, TextStyle, ViewStyle } from 'react-native';
22

33
import {
4-
ACCESORY_SIZE,
4+
ACCESSORY_SIZE,
55
HELPER_FONT_SIZE,
66
HELPER_MARGIN_TOP,
77
TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
@@ -27,14 +27,14 @@ export const $helperStyle: TextStyle = {
2727
};
2828

2929
export const $trailingAccessoryStyle: ViewStyle = {
30-
width: ACCESORY_SIZE,
30+
width: ACCESSORY_SIZE,
3131
marginEnd: TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
3232
justifyContent: 'center',
3333
alignItems: 'center',
3434
};
3535

3636
export const $leadingAccessoryStyle: ViewStyle = {
37-
width: ACCESORY_SIZE,
37+
width: ACCESSORY_SIZE,
3838
marginStart: TEXT_FIELD_ACCESSORY_MARGIN_HORIZONTAL,
3939
justifyContent: 'center',
4040
alignItems: 'center',

src/components/__tests__/TextField.test.tsx

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import * as React from 'react';
2-
import { TextInput, View } from 'react-native';
2+
import { I18nManager, StyleSheet, TextInput, View } from 'react-native';
33

44
import { fireEvent, render } from '@testing-library/react-native';
55

66
import TextField, {
77
type TextFieldAccessoryProps,
88
} from '../TextField/TextField';
99

10+
const defaultI18nIsRTL = I18nManager.isRTL;
11+
12+
afterEach(() => {
13+
I18nManager.isRTL = defaultI18nIsRTL;
14+
});
15+
16+
function firstIndexOfTestIdInTree(tree: unknown, testID: string): number {
17+
const serialized = JSON.stringify(tree);
18+
const match = new RegExp(`"testID":\\s*"${testID}"`).exec(serialized);
19+
return match ? match.index : -1;
20+
}
21+
1022
it('renders filled TextField with label and value', () => {
1123
const tree = render(
1224
<TextField label="Email" value="a@b.co" onChangeText={() => {}} />
@@ -277,3 +289,162 @@ it('applies helperProps to the helper Text', () => {
277289

278290
expect(getByTestId('helper-text').props.children).toBe('Hint');
279291
});
292+
293+
it('applies RTL text alignment and writing direction to the TextInput (filled)', () => {
294+
I18nManager.isRTL = true;
295+
296+
const { getByTestId } = render(
297+
<TextField
298+
label="Email"
299+
value="x"
300+
onChangeText={() => {}}
301+
testID="tf-input-rtl"
302+
/>
303+
);
304+
305+
expect(StyleSheet.flatten(getByTestId('tf-input-rtl').props.style)).toEqual(
306+
expect.objectContaining({
307+
textAlign: 'right',
308+
writingDirection: 'rtl',
309+
})
310+
);
311+
});
312+
313+
it('applies RTL text alignment and writing direction to the TextInput (outlined)', () => {
314+
I18nManager.isRTL = true;
315+
316+
const { getByTestId } = render(
317+
<TextField
318+
variant="outlined"
319+
label="Email"
320+
value="x"
321+
onChangeText={() => {}}
322+
testID="tf-input-rtl-outlined"
323+
/>
324+
);
325+
326+
expect(
327+
StyleSheet.flatten(getByTestId('tf-input-rtl-outlined').props.style)
328+
).toEqual(
329+
expect.objectContaining({
330+
textAlign: 'right',
331+
writingDirection: 'rtl',
332+
})
333+
);
334+
});
335+
336+
it('applies RTL writing direction to helper text', () => {
337+
I18nManager.isRTL = true;
338+
339+
const { getByTestId } = render(
340+
<TextField
341+
label="Email"
342+
value=""
343+
onChangeText={() => {}}
344+
helper="Hint"
345+
helperProps={{ testID: 'helper-rtl' }}
346+
/>
347+
);
348+
349+
expect(StyleSheet.flatten(getByTestId('helper-rtl').props.style)).toEqual(
350+
expect.objectContaining({
351+
writingDirection: 'rtl',
352+
})
353+
);
354+
});
355+
356+
it('places RightAccessory before LeftAccessory in the tree when RTL', () => {
357+
I18nManager.isRTL = true;
358+
359+
function LeftAccessory() {
360+
return <View testID="rtl-acc-from-left-prop" />;
361+
}
362+
363+
function RightAccessory() {
364+
return <View testID="rtl-acc-from-right-prop" />;
365+
}
366+
367+
const { toJSON } = render(
368+
<TextField
369+
label="Email"
370+
value=""
371+
onChangeText={() => {}}
372+
LeftAccessory={LeftAccessory}
373+
RightAccessory={RightAccessory}
374+
testID="tf-input-rtl-order"
375+
/>
376+
);
377+
378+
const tree = toJSON();
379+
expect(
380+
firstIndexOfTestIdInTree(tree, 'rtl-acc-from-right-prop')
381+
).toBeLessThan(firstIndexOfTestIdInTree(tree, 'rtl-acc-from-left-prop'));
382+
});
383+
384+
it('places LeftAccessory before RightAccessory in the tree when LTR', () => {
385+
I18nManager.isRTL = false;
386+
387+
function LeftAccessory() {
388+
return <View testID="ltr-acc-from-left-prop" />;
389+
}
390+
391+
function RightAccessory() {
392+
return <View testID="ltr-acc-from-right-prop" />;
393+
}
394+
395+
const { toJSON } = render(
396+
<TextField
397+
label="Email"
398+
value=""
399+
onChangeText={() => {}}
400+
LeftAccessory={LeftAccessory}
401+
RightAccessory={RightAccessory}
402+
testID="tf-input-ltr-order"
403+
/>
404+
);
405+
406+
const tree = toJSON();
407+
expect(firstIndexOfTestIdInTree(tree, 'ltr-acc-from-left-prop')).toBeLessThan(
408+
firstIndexOfTestIdInTree(tree, 'ltr-acc-from-right-prop')
409+
);
410+
});
411+
412+
it('maps a lone LeftAccessory to leading in LTR and trailing in RTL (tree order)', () => {
413+
function LoneLeftAccessory() {
414+
return <View testID="lone-left-acc" />;
415+
}
416+
417+
I18nManager.isRTL = false;
418+
419+
const { toJSON: toJsonLtr } = render(
420+
<TextField
421+
label="Email"
422+
value=""
423+
onChangeText={() => {}}
424+
LeftAccessory={LoneLeftAccessory}
425+
testID="tf-lone-ltr"
426+
/>
427+
);
428+
429+
I18nManager.isRTL = true;
430+
431+
const { toJSON: toJsonRtl } = render(
432+
<TextField
433+
label="Email"
434+
value=""
435+
onChangeText={() => {}}
436+
LeftAccessory={LoneLeftAccessory}
437+
testID="tf-lone-rtl"
438+
/>
439+
);
440+
441+
const ltrTree = toJsonLtr();
442+
expect(firstIndexOfTestIdInTree(ltrTree, 'lone-left-acc')).toBeLessThan(
443+
firstIndexOfTestIdInTree(ltrTree, 'tf-lone-ltr')
444+
);
445+
446+
const rtlTree = toJsonRtl();
447+
expect(firstIndexOfTestIdInTree(rtlTree, 'tf-lone-rtl')).toBeLessThan(
448+
firstIndexOfTestIdInTree(rtlTree, 'lone-left-acc')
449+
);
450+
});

src/components/__tests__/__snapshots__/TextField.test.tsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ exports[`renders filled TextField with label and value 1`] = `
136136
{
137137
"color": "rgba(29, 27, 32, 1)",
138138
"fontSize": 16,
139+
"textAlign": "left",
140+
"writingDirection": "ltr",
139141
},
140142
undefined,
141143
false,

0 commit comments

Comments
 (0)