Skip to content

Commit

Permalink
feat: focus TextInput on icon/affix press (#1850)
Browse files Browse the repository at this point in the history
  • Loading branch information
matkoson committed May 25, 2020
1 parent 49b3271 commit 9e2e7f6
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 44 deletions.
20 changes: 8 additions & 12 deletions src/components/TextInput/Adornment/Affix.tsx
Expand Up @@ -25,7 +25,7 @@ type Props = {
};

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

const AffixContext = React.createContext<ContextState>({
textStyle: { fontFamily: '', color: '' },
affixTopPosition: null,
topPosition: null,
side: AdornmentSide.Left,
});

Expand All @@ -45,7 +45,7 @@ export const AffixAdornment: React.FunctionComponent<{
affix,
side,
textStyle,
affixTopPosition,
topPosition,
onLayout,
visible,
}) => {
Expand All @@ -54,7 +54,7 @@ export const AffixAdornment: React.FunctionComponent<{
value={{
side,
textStyle,
affixTopPosition,
topPosition,
onLayout,
visible,
}}
Expand All @@ -65,20 +65,16 @@ export const AffixAdornment: React.FunctionComponent<{
};

const TextInputAffix = ({ text, theme }: Props) => {
const {
textStyle,
onLayout,
affixTopPosition,
side,
visible,
} = React.useContext(AffixContext);
const { textStyle, onLayout, topPosition, side, visible } = React.useContext(
AffixContext
);
const textColor = color(theme.colors.text)
.alpha(theme.dark ? 0.7 : 0.54)
.rgb()
.string();

const style = {
top: affixTopPosition,
top: topPosition,
[side]: AFFIX_OFFSET,
};

Expand Down
39 changes: 32 additions & 7 deletions src/components/TextInput/Adornment/Icon.tsx
Expand Up @@ -17,35 +17,60 @@ type Props = $Omit<
export const ICON_SIZE = 24;
const ICON_OFFSET = 12;

const StyleContext = React.createContext<{ style?: StyleProp<ViewStyle> }>({
type StyleContextType = {
style: StyleProp<ViewStyle>;
isTextInputFocused: boolean;
forceFocus: () => void;
};

const StyleContext = React.createContext<StyleContextType>({
style: {},
isTextInputFocused: false,
forceFocus: () => {},
});

export const IconAdornment: React.FunctionComponent<{
testID: string;
icon: React.ReactNode;
iconTopPosition: number;
topPosition: number;
side: 'left' | 'right';
}> = ({ icon, iconTopPosition, side }) => {
} & Omit<StyleContextType, 'style'>> = ({
icon,
topPosition,
side,
isTextInputFocused,
forceFocus,
}) => {
const style = {
top: iconTopPosition,
top: topPosition,
[side]: ICON_OFFSET,
};
const contextState = { style, isTextInputFocused, forceFocus };

return (
<StyleContext.Provider value={{ style }}>{icon}</StyleContext.Provider>
<StyleContext.Provider value={contextState}>{icon}</StyleContext.Provider>
);
};

const TextInputIcon = ({ name, onPress, ...rest }: Props) => {
const { style } = React.useContext(StyleContext);
const { style, isTextInputFocused, forceFocus } = React.useContext(
StyleContext
);

const onPressWithFocusControl = React.useCallback(() => {
if (!isTextInputFocused) {
forceFocus();
}
onPress?.();
}, [forceFocus, isTextInputFocused, onPress]);

return (
<View style={[styles.container, style]}>
<IconButton
icon={name}
style={styles.iconButton}
size={ICON_SIZE}
onPress={onPress}
onPress={onPressWithFocusControl}
{...rest}
/>
</View>
Expand Down
46 changes: 27 additions & 19 deletions src/components/TextInput/Adornment/TextInputAdornment.tsx
Expand Up @@ -96,20 +96,24 @@ const captalize = (text: string) =>
text.charAt(0).toUpperCase() + text.slice(1);

export interface TextInputAdornmentProps {
forceFocus: () => void;
adornmentConfig: AdornmentConfig[];
affixTopPosition: {
[AdornmentSide.Left]: number | null;
[AdornmentSide.Right]: number | null;
topPosition: {
[AdornmentType.Affix]: {
[AdornmentSide.Left]: number | null;
[AdornmentSide.Right]: number | null;
};
[AdornmentType.Icon]: number;
};
onAffixChange: {
[AdornmentSide.Left]: (event: LayoutChangeEvent) => void;
[AdornmentSide.Right]: (event: LayoutChangeEvent) => void;
};
iconTopPosition: number;
left?: React.ReactNode;
right?: React.ReactNode;
textStyle?: StyleProp<TextStyle>;
visible?: Animated.Value;
isTextInputFocused: boolean;
}

const TextInputAdornment: React.FunctionComponent<TextInputAdornmentProps> = ({
Expand All @@ -118,40 +122,44 @@ const TextInputAdornment: React.FunctionComponent<TextInputAdornmentProps> = ({
right,
onAffixChange,
textStyle,
affixTopPosition,
visible,
iconTopPosition,
topPosition,
isTextInputFocused,
forceFocus,
}) => {
if (adornmentConfig.length) {
return (
<>
{adornmentConfig.map(({ type, side }: AdornmentConfig) => {
let adornmentInputComponent;
let inputAdornmentComponent;
if (side === AdornmentSide.Left) {
adornmentInputComponent = left;
inputAdornmentComponent = left;
} else if (side === AdornmentSide.Right) {
adornmentInputComponent = right;
inputAdornmentComponent = right;
}

const commonProps = {
key: side,
side: side,
testID: `${side}-${type}-adornment`,
isTextInputFocused,
};
if (type === AdornmentType.Icon) {
return (
<IconAdornment
testID={`${side}-icon-adornment`}
key={side}
icon={adornmentInputComponent}
side={side}
iconTopPosition={iconTopPosition}
{...commonProps}
icon={inputAdornmentComponent}
topPosition={topPosition[AdornmentType.Icon]}
forceFocus={forceFocus}
/>
);
} else if (type === AdornmentType.Affix) {
return (
<AffixAdornment
testID={`${side}-affix-adornment`}
key={side}
affix={adornmentInputComponent}
side={side}
{...commonProps}
topPosition={topPosition[AdornmentType.Affix][side]}
affix={inputAdornmentComponent}
textStyle={textStyle}
affixTopPosition={affixTopPosition[side]}
onLayout={onAffixChange[side]}
visible={visible}
/>
Expand Down
7 changes: 6 additions & 1 deletion src/components/TextInput/TextInput.tsx
Expand Up @@ -406,6 +406,10 @@ class TextInput extends React.Component<TextInputProps, State> {
});
};

forceFocus = () => {
return this.root?.focus();
};

/**
* @internal
*/
Expand Down Expand Up @@ -440,7 +444,6 @@ class TextInput extends React.Component<TextInputProps, State> {
blur() {
return this.root && this.root.blur();
}

render() {
const { mode, ...rest } = this.props as $Omit<TextInputProps, 'ref'>;

Expand All @@ -453,6 +456,7 @@ class TextInput extends React.Component<TextInputProps, State> {
this.root = ref;
}}
onFocus={this.handleFocus}
forceFocus={this.forceFocus}
onBlur={this.handleBlur}
onChangeText={this.handleChangeText}
onLayoutAnimatedText={this.handleLayoutAnimatedText}
Expand All @@ -468,6 +472,7 @@ class TextInput extends React.Component<TextInputProps, State> {
this.root = ref;
}}
onFocus={this.handleFocus}
forceFocus={this.forceFocus}
onBlur={this.handleBlur}
onChangeText={this.handleChangeText}
onLayoutAnimatedText={this.handleLayoutAnimatedText}
Expand Down
11 changes: 8 additions & 3 deletions src/components/TextInput/TextInputFlat.tsx
Expand Up @@ -37,7 +37,7 @@ import {
getAdornmentConfig,
getAdornmentStyleAdjustmentForNativeInput,
} from './Adornment/TextInputAdornment';
import { AdornmentSide } from './Adornment/enums';
import { AdornmentSide, AdornmentType } from './Adornment/enums';

const MINIMIZED_LABEL_Y_OFFSET = -18;

Expand Down Expand Up @@ -72,6 +72,7 @@ class TextInputFlat extends React.Component<ChildTextInputProps> {
parentState,
innerRef,
onFocus,
forceFocus,
onBlur,
onChangeText,
onLayoutAnimatedText,
Expand Down Expand Up @@ -290,9 +291,13 @@ class TextInputFlat extends React.Component<ChildTextInputProps> {

let adornmentProps: TextInputAdornmentProps = {
adornmentConfig,
iconTopPosition,
affixTopPosition,
forceFocus,
topPosition: {
[AdornmentType.Affix]: affixTopPosition,
[AdornmentType.Icon]: iconTopPosition,
},
onAffixChange,
isTextInputFocused: this.props.parentState.focused,
};
if (adornmentConfig.length) {
adornmentProps = {
Expand Down
9 changes: 7 additions & 2 deletions src/components/TextInput/TextInputOutlined.tsx
Expand Up @@ -70,6 +70,7 @@ class TextInputOutlined extends React.Component<ChildTextInputProps> {
parentState,
innerRef,
onFocus,
forceFocus,
onBlur,
onChangeText,
onLayoutAnimatedText,
Expand Down Expand Up @@ -252,9 +253,13 @@ class TextInputOutlined extends React.Component<ChildTextInputProps> {

let adornmentProps: TextInputAdornmentProps = {
adornmentConfig,
iconTopPosition,
affixTopPosition,
forceFocus,
topPosition: {
[AdornmentType.Icon]: iconTopPosition,
[AdornmentType.Affix]: affixTopPosition,
},
onAffixChange,
isTextInputFocused: parentState.focused,
};
if (adornmentConfig.length) {
adornmentProps = {
Expand Down
1 change: 1 addition & 0 deletions src/components/TextInput/types.tsx
Expand Up @@ -39,6 +39,7 @@ export type ChildTextInputProps = {
innerRef: (ref: NativeTextInput | null | undefined) => void;
onFocus?: (args: any) => void;
onBlur?: (args: any) => void;
forceFocus: () => void;
onChangeText?: (value: string) => void;
onLayoutAnimatedText: (args: any) => void;
onLeftAffixLayoutChange: (event: LayoutChangeEvent) => void;
Expand Down

0 comments on commit 9e2e7f6

Please sign in to comment.