Skip to content

Commit 9e2e7f6

Browse files
authored
feat: focus TextInput on icon/affix press (#1850)
1 parent 49b3271 commit 9e2e7f6

File tree

7 files changed

+89
-44
lines changed

7 files changed

+89
-44
lines changed

src/components/TextInput/Adornment/Affix.tsx

+8-12
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type Props = {
2525
};
2626

2727
type ContextState = {
28-
affixTopPosition: number | null;
28+
topPosition: number | null;
2929
onLayout?: (event: LayoutChangeEvent) => void;
3030
visible?: Animated.Value;
3131
textStyle?: StyleProp<TextStyle>;
@@ -34,7 +34,7 @@ type ContextState = {
3434

3535
const AffixContext = React.createContext<ContextState>({
3636
textStyle: { fontFamily: '', color: '' },
37-
affixTopPosition: null,
37+
topPosition: null,
3838
side: AdornmentSide.Left,
3939
});
4040

@@ -45,7 +45,7 @@ export const AffixAdornment: React.FunctionComponent<{
4545
affix,
4646
side,
4747
textStyle,
48-
affixTopPosition,
48+
topPosition,
4949
onLayout,
5050
visible,
5151
}) => {
@@ -54,7 +54,7 @@ export const AffixAdornment: React.FunctionComponent<{
5454
value={{
5555
side,
5656
textStyle,
57-
affixTopPosition,
57+
topPosition,
5858
onLayout,
5959
visible,
6060
}}
@@ -65,20 +65,16 @@ export const AffixAdornment: React.FunctionComponent<{
6565
};
6666

6767
const TextInputAffix = ({ text, theme }: Props) => {
68-
const {
69-
textStyle,
70-
onLayout,
71-
affixTopPosition,
72-
side,
73-
visible,
74-
} = React.useContext(AffixContext);
68+
const { textStyle, onLayout, topPosition, side, visible } = React.useContext(
69+
AffixContext
70+
);
7571
const textColor = color(theme.colors.text)
7672
.alpha(theme.dark ? 0.7 : 0.54)
7773
.rgb()
7874
.string();
7975

8076
const style = {
81-
top: affixTopPosition,
77+
top: topPosition,
8278
[side]: AFFIX_OFFSET,
8379
};
8480

src/components/TextInput/Adornment/Icon.tsx

+32-7
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,60 @@ type Props = $Omit<
1717
export const ICON_SIZE = 24;
1818
const ICON_OFFSET = 12;
1919

20-
const StyleContext = React.createContext<{ style?: StyleProp<ViewStyle> }>({
20+
type StyleContextType = {
21+
style: StyleProp<ViewStyle>;
22+
isTextInputFocused: boolean;
23+
forceFocus: () => void;
24+
};
25+
26+
const StyleContext = React.createContext<StyleContextType>({
2127
style: {},
28+
isTextInputFocused: false,
29+
forceFocus: () => {},
2230
});
2331

2432
export const IconAdornment: React.FunctionComponent<{
2533
testID: string;
2634
icon: React.ReactNode;
27-
iconTopPosition: number;
35+
topPosition: number;
2836
side: 'left' | 'right';
29-
}> = ({ icon, iconTopPosition, side }) => {
37+
} & Omit<StyleContextType, 'style'>> = ({
38+
icon,
39+
topPosition,
40+
side,
41+
isTextInputFocused,
42+
forceFocus,
43+
}) => {
3044
const style = {
31-
top: iconTopPosition,
45+
top: topPosition,
3246
[side]: ICON_OFFSET,
3347
};
48+
const contextState = { style, isTextInputFocused, forceFocus };
3449

3550
return (
36-
<StyleContext.Provider value={{ style }}>{icon}</StyleContext.Provider>
51+
<StyleContext.Provider value={contextState}>{icon}</StyleContext.Provider>
3752
);
3853
};
3954

4055
const TextInputIcon = ({ name, onPress, ...rest }: Props) => {
41-
const { style } = React.useContext(StyleContext);
56+
const { style, isTextInputFocused, forceFocus } = React.useContext(
57+
StyleContext
58+
);
59+
60+
const onPressWithFocusControl = React.useCallback(() => {
61+
if (!isTextInputFocused) {
62+
forceFocus();
63+
}
64+
onPress?.();
65+
}, [forceFocus, isTextInputFocused, onPress]);
66+
4267
return (
4368
<View style={[styles.container, style]}>
4469
<IconButton
4570
icon={name}
4671
style={styles.iconButton}
4772
size={ICON_SIZE}
48-
onPress={onPress}
73+
onPress={onPressWithFocusControl}
4974
{...rest}
5075
/>
5176
</View>

src/components/TextInput/Adornment/TextInputAdornment.tsx

+27-19
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,24 @@ const captalize = (text: string) =>
9696
text.charAt(0).toUpperCase() + text.slice(1);
9797

9898
export interface TextInputAdornmentProps {
99+
forceFocus: () => void;
99100
adornmentConfig: AdornmentConfig[];
100-
affixTopPosition: {
101-
[AdornmentSide.Left]: number | null;
102-
[AdornmentSide.Right]: number | null;
101+
topPosition: {
102+
[AdornmentType.Affix]: {
103+
[AdornmentSide.Left]: number | null;
104+
[AdornmentSide.Right]: number | null;
105+
};
106+
[AdornmentType.Icon]: number;
103107
};
104108
onAffixChange: {
105109
[AdornmentSide.Left]: (event: LayoutChangeEvent) => void;
106110
[AdornmentSide.Right]: (event: LayoutChangeEvent) => void;
107111
};
108-
iconTopPosition: number;
109112
left?: React.ReactNode;
110113
right?: React.ReactNode;
111114
textStyle?: StyleProp<TextStyle>;
112115
visible?: Animated.Value;
116+
isTextInputFocused: boolean;
113117
}
114118

115119
const TextInputAdornment: React.FunctionComponent<TextInputAdornmentProps> = ({
@@ -118,40 +122,44 @@ const TextInputAdornment: React.FunctionComponent<TextInputAdornmentProps> = ({
118122
right,
119123
onAffixChange,
120124
textStyle,
121-
affixTopPosition,
122125
visible,
123-
iconTopPosition,
126+
topPosition,
127+
isTextInputFocused,
128+
forceFocus,
124129
}) => {
125130
if (adornmentConfig.length) {
126131
return (
127132
<>
128133
{adornmentConfig.map(({ type, side }: AdornmentConfig) => {
129-
let adornmentInputComponent;
134+
let inputAdornmentComponent;
130135
if (side === AdornmentSide.Left) {
131-
adornmentInputComponent = left;
136+
inputAdornmentComponent = left;
132137
} else if (side === AdornmentSide.Right) {
133-
adornmentInputComponent = right;
138+
inputAdornmentComponent = right;
134139
}
135140

141+
const commonProps = {
142+
key: side,
143+
side: side,
144+
testID: `${side}-${type}-adornment`,
145+
isTextInputFocused,
146+
};
136147
if (type === AdornmentType.Icon) {
137148
return (
138149
<IconAdornment
139-
testID={`${side}-icon-adornment`}
140-
key={side}
141-
icon={adornmentInputComponent}
142-
side={side}
143-
iconTopPosition={iconTopPosition}
150+
{...commonProps}
151+
icon={inputAdornmentComponent}
152+
topPosition={topPosition[AdornmentType.Icon]}
153+
forceFocus={forceFocus}
144154
/>
145155
);
146156
} else if (type === AdornmentType.Affix) {
147157
return (
148158
<AffixAdornment
149-
testID={`${side}-affix-adornment`}
150-
key={side}
151-
affix={adornmentInputComponent}
152-
side={side}
159+
{...commonProps}
160+
topPosition={topPosition[AdornmentType.Affix][side]}
161+
affix={inputAdornmentComponent}
153162
textStyle={textStyle}
154-
affixTopPosition={affixTopPosition[side]}
155163
onLayout={onAffixChange[side]}
156164
visible={visible}
157165
/>

src/components/TextInput/TextInput.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ class TextInput extends React.Component<TextInputProps, State> {
406406
});
407407
};
408408

409+
forceFocus = () => {
410+
return this.root?.focus();
411+
};
412+
409413
/**
410414
* @internal
411415
*/
@@ -440,7 +444,6 @@ class TextInput extends React.Component<TextInputProps, State> {
440444
blur() {
441445
return this.root && this.root.blur();
442446
}
443-
444447
render() {
445448
const { mode, ...rest } = this.props as $Omit<TextInputProps, 'ref'>;
446449

@@ -453,6 +456,7 @@ class TextInput extends React.Component<TextInputProps, State> {
453456
this.root = ref;
454457
}}
455458
onFocus={this.handleFocus}
459+
forceFocus={this.forceFocus}
456460
onBlur={this.handleBlur}
457461
onChangeText={this.handleChangeText}
458462
onLayoutAnimatedText={this.handleLayoutAnimatedText}
@@ -468,6 +472,7 @@ class TextInput extends React.Component<TextInputProps, State> {
468472
this.root = ref;
469473
}}
470474
onFocus={this.handleFocus}
475+
forceFocus={this.forceFocus}
471476
onBlur={this.handleBlur}
472477
onChangeText={this.handleChangeText}
473478
onLayoutAnimatedText={this.handleLayoutAnimatedText}

src/components/TextInput/TextInputFlat.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {
3737
getAdornmentConfig,
3838
getAdornmentStyleAdjustmentForNativeInput,
3939
} from './Adornment/TextInputAdornment';
40-
import { AdornmentSide } from './Adornment/enums';
40+
import { AdornmentSide, AdornmentType } from './Adornment/enums';
4141

4242
const MINIMIZED_LABEL_Y_OFFSET = -18;
4343

@@ -72,6 +72,7 @@ class TextInputFlat extends React.Component<ChildTextInputProps> {
7272
parentState,
7373
innerRef,
7474
onFocus,
75+
forceFocus,
7576
onBlur,
7677
onChangeText,
7778
onLayoutAnimatedText,
@@ -290,9 +291,13 @@ class TextInputFlat extends React.Component<ChildTextInputProps> {
290291

291292
let adornmentProps: TextInputAdornmentProps = {
292293
adornmentConfig,
293-
iconTopPosition,
294-
affixTopPosition,
294+
forceFocus,
295+
topPosition: {
296+
[AdornmentType.Affix]: affixTopPosition,
297+
[AdornmentType.Icon]: iconTopPosition,
298+
},
295299
onAffixChange,
300+
isTextInputFocused: this.props.parentState.focused,
296301
};
297302
if (adornmentConfig.length) {
298303
adornmentProps = {

src/components/TextInput/TextInputOutlined.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class TextInputOutlined extends React.Component<ChildTextInputProps> {
7070
parentState,
7171
innerRef,
7272
onFocus,
73+
forceFocus,
7374
onBlur,
7475
onChangeText,
7576
onLayoutAnimatedText,
@@ -252,9 +253,13 @@ class TextInputOutlined extends React.Component<ChildTextInputProps> {
252253

253254
let adornmentProps: TextInputAdornmentProps = {
254255
adornmentConfig,
255-
iconTopPosition,
256-
affixTopPosition,
256+
forceFocus,
257+
topPosition: {
258+
[AdornmentType.Icon]: iconTopPosition,
259+
[AdornmentType.Affix]: affixTopPosition,
260+
},
257261
onAffixChange,
262+
isTextInputFocused: parentState.focused,
258263
};
259264
if (adornmentConfig.length) {
260265
adornmentProps = {

src/components/TextInput/types.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type ChildTextInputProps = {
3939
innerRef: (ref: NativeTextInput | null | undefined) => void;
4040
onFocus?: (args: any) => void;
4141
onBlur?: (args: any) => void;
42+
forceFocus: () => void;
4243
onChangeText?: (value: string) => void;
4344
onLayoutAnimatedText: (args: any) => void;
4445
onLeftAffixLayoutChange: (event: LayoutChangeEvent) => void;

0 commit comments

Comments
 (0)