Skip to content

Commit 2bea3df

Browse files
authored
Feat/new chips input (#1681)
* Remove redundant internal view wrapper in Incubator.TextField to allow more control with fieldStyle * Create new ChipsInput component under Incubator * Add Incubator.ChipsInput example screen * Fix issue with chip is still marked while entering new text * Change defaultChipProps prop to be optional * Remove ts-expect-error and use stylesheet * Support maximum chips in ChipsInput * Forward ref in Incubator.ChipsInput * Support change reason in onChange callback * Add api json to ChipsInput * Remove unused styles * Add missing prop descriptions * Add TODOs * Cleanup comments * Remove redundant imports
1 parent 9dc3492 commit 2bea3df

File tree

14 files changed

+286
-27
lines changed

14 files changed

+286
-27
lines changed

demo/src/screens/MenuStructure.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export const navigationData = {
153153
Incubator: {
154154
title: 'Incubator (Experimental)',
155155
screens: [
156+
{title: '(New) ChipsInput', tags: 'chips input', screen: 'unicorn.components.IncubatorChipsInputScreen'},
156157
{title: 'Native TouchableOpacity', tags: 'touchable native', screen: 'unicorn.incubator.TouchableOpacityScreen'},
157158
{title: '(New) Dialog', tags: 'dialog modal popup alert', screen: 'unicorn.incubator.IncubatorDialogScreen'},
158159
{title: '(New) TextField', tags: 'text field input', screen: 'unicorn.components.IncubatorTextFieldScreen'},
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React, {Component} from 'react';
2+
import {View, Text, Card, TextField, Button, Colors, Incubator} from 'react-native-ui-lib'; //eslint-disable-line
3+
4+
export default class ChipsInputScreen extends Component {
5+
state = {
6+
chips: [{label: 'one'}, {label: 'two'}],
7+
chips2: []
8+
};
9+
10+
render() {
11+
return (
12+
<View flex padding-20>
13+
<Text h1 marginB-s4>
14+
ChipsInput
15+
</Text>
16+
<Incubator.ChipsInput
17+
placeholder="Enter chips"
18+
defaultChipProps={{
19+
backgroundColor: Colors.primary,
20+
labelStyle: {color: Colors.white},
21+
containerStyle: {borderWidth: 0},
22+
dismissColor: Colors.white
23+
}}
24+
chips={this.state.chips}
25+
leadingAccessory={<Text>TO: </Text>}
26+
onChange={newChips => {
27+
this.setState({chips: newChips});
28+
}}
29+
/>
30+
31+
<Incubator.ChipsInput
32+
label="Max 3 chips"
33+
placeholder="Enter chips..."
34+
chips={this.state.chips2}
35+
onChange={newChips => this.setState({chips2: newChips})}
36+
maxChips={3}
37+
/>
38+
</View>
39+
);
40+
}
41+
}

demo/src/screens/incubatorScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
22

33
export function registerScreens(registrar) {
4+
registrar('unicorn.components.IncubatorChipsInputScreen', () => require('./IncubatorChipsInputScreen').default);
45
registrar('unicorn.incubator.TouchableOpacityScreen', () =>
56
gestureHandlerRootHOC(require('./TouchableOpacityScreen').default));
67
registrar('unicorn.incubator.IncubatorDialogScreen', () => require('./IncubatorDialogScreen').default);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { TextFieldProps } from '../TextField';
3+
import { ChipProps } from '../../components/chip';
4+
export declare enum ChipsInputChangeReason {
5+
Added = "added",
6+
Removed = "removed"
7+
}
8+
export declare type ChipsInputProps = Omit<TextFieldProps, 'ref'> & {
9+
/**
10+
* Chip items to render in the input
11+
*/
12+
chips?: ChipProps[];
13+
/**
14+
* A default set of chip props to pass to all chips
15+
*/
16+
defaultChipProps?: ChipProps;
17+
/**
18+
* Change callback for when chips changed (either added or removed)
19+
*/
20+
onChange?: (chips: ChipProps[], changeReason: ChipsInputChangeReason, updatedChip: ChipProps) => void;
21+
/**
22+
* Maximum chips
23+
*/
24+
maxChips?: number;
25+
};
26+
declare const _default: React.ForwardRefExoticComponent<Omit<TextFieldProps, "ref"> & {
27+
/**
28+
* Chip items to render in the input
29+
*/
30+
chips?: ChipProps[] | undefined;
31+
/**
32+
* A default set of chip props to pass to all chips
33+
*/
34+
defaultChipProps?: ChipProps | undefined;
35+
/**
36+
* Change callback for when chips changed (either added or removed)
37+
*/
38+
onChange?: ((chips: ChipProps[], changeReason: ChipsInputChangeReason, updatedChip: ChipProps) => void) | undefined;
39+
/**
40+
* Maximum chips
41+
*/
42+
maxChips?: number | undefined;
43+
} & React.RefAttributes<any>>;
44+
export default _default;

generatedTypes/src/incubator/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { default as ChipsInput, ChipsInputProps, ChipsInputChangeReason } from './ChipsInput';
12
export { default as ExpandableOverlay } from './expandableOverlay';
23
export { default as TextField, TextFieldProps, FieldContextType } from './TextField';
34
export { default as TouchableOpacity, TouchableOpacityProps } from './TouchableOpacity';
159 Bytes
Loading
184 Bytes
Loading
215 Bytes
Loading
268 Bytes
Loading
310 Bytes
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "ChipsInput",
3+
"category": "incubator",
4+
"description": "A chips input",
5+
"extends": ["TextField"],
6+
"modifiers": ["margin", "color", "typography"],
7+
"example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/IncubatorChipsInputScreen.tsx",
8+
"images": [],
9+
"props": [
10+
{"name": "chips", "type": "ChipProps[]", "description": "List of chips to render"},
11+
{
12+
"name": "defaultChipProps",
13+
"type": "ChipProps",
14+
"description": "Default set of props to pass by default to all chips"
15+
},
16+
{
17+
"name": "onChange",
18+
"type": "(newChips, changeReason, updatedChip) => void",
19+
"description": "Callback for chips change (adding or removing chip)"
20+
},
21+
{"name": "maxChips", "type": "number", "description": "The maximum chips to allow adding"}
22+
]
23+
}

src/incubator/ChipsInput/index.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, {useCallback, useMemo, useRef, useState, forwardRef} from 'react';
2+
import {StyleSheet, NativeSyntheticEvent, TextInputKeyPressEventData} from 'react-native';
3+
import {isUndefined, map} from 'lodash';
4+
import {Constants} from '../../helpers';
5+
import {useCombinedRefs} from '../../hooks';
6+
import TextField, {TextFieldProps} from '../TextField';
7+
import Chip, {ChipProps} from '../../components/chip';
8+
9+
const removeIcon = require('./assets/xSmall.png');
10+
11+
export enum ChipsInputChangeReason {
12+
Added = 'added',
13+
Removed = 'removed'
14+
}
15+
16+
export type ChipsInputProps = Omit<TextFieldProps, 'ref'> & {
17+
/**
18+
* Chip items to render in the input
19+
*/
20+
chips?: ChipProps[];
21+
/**
22+
* A default set of chip props to pass to all chips
23+
*/
24+
defaultChipProps?: ChipProps;
25+
/**
26+
* Change callback for when chips changed (either added or removed)
27+
*/
28+
onChange?: (chips: ChipProps[], changeReason: ChipsInputChangeReason, updatedChip: ChipProps) => void;
29+
/**
30+
* Maximum chips
31+
*/
32+
maxChips?: number;
33+
};
34+
35+
const ChipsInput = (props: ChipsInputProps, refToForward: React.Ref<any>) => {
36+
const fieldRef = useCombinedRefs(refToForward);
37+
const {chips = [], defaultChipProps, leadingAccessory, onChange, fieldStyle, maxChips, ...others} = props;
38+
const [markedForRemoval, setMarkedForRemoval] = useState<number | undefined>(undefined);
39+
const fieldValue = useRef(others.value);
40+
41+
const addChip = useCallback(() => {
42+
const reachedMaximum = maxChips && chips?.length >= maxChips;
43+
if (fieldValue.current && !reachedMaximum) {
44+
const newChip = {label: fieldValue.current};
45+
onChange?.([...chips, newChip], ChipsInputChangeReason.Added, newChip);
46+
setMarkedForRemoval(undefined);
47+
// @ts-expect-error
48+
fieldRef.current.clear();
49+
fieldValue.current = '';
50+
}
51+
}, [onChange, chips, maxChips]);
52+
53+
const removeMarkedChip = useCallback(() => {
54+
if (!isUndefined(markedForRemoval)) {
55+
const removedChip = chips?.splice(markedForRemoval, 1);
56+
onChange?.([...chips], ChipsInputChangeReason.Removed, removedChip?.[0]);
57+
setMarkedForRemoval(undefined);
58+
}
59+
}, [chips, markedForRemoval, onChange]);
60+
61+
const onChipPress = useCallback(({customValue: index}) => {
62+
const selectedChip = chips[index];
63+
selectedChip?.onPress?.();
64+
65+
setMarkedForRemoval(index);
66+
},
67+
[chips]);
68+
69+
const onChangeText = useCallback(value => {
70+
fieldValue.current = value;
71+
props.onChangeText?.(value);
72+
73+
if (!isUndefined(markedForRemoval)) {
74+
setMarkedForRemoval(undefined);
75+
}
76+
},
77+
[props.onChangeText, markedForRemoval]);
78+
79+
const onKeyPress = useCallback((event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
80+
props.onKeyPress?.(event);
81+
const keyCode = event?.nativeEvent?.key;
82+
const pressedBackspace = keyCode === Constants.backspaceKey;
83+
84+
if (pressedBackspace && !fieldValue.current) {
85+
if (isUndefined(markedForRemoval) || markedForRemoval !== chips.length - 1) {
86+
setMarkedForRemoval(chips.length - 1);
87+
} else {
88+
removeMarkedChip();
89+
}
90+
}
91+
},
92+
[chips, props.onKeyPress, markedForRemoval, removeMarkedChip]);
93+
94+
const chipList = useMemo(() => {
95+
return (
96+
<>
97+
{leadingAccessory}
98+
{map(chips, (chip, index) => {
99+
const isMarkedForRemoval = index === markedForRemoval;
100+
return (
101+
<Chip
102+
key={index}
103+
customValue={index}
104+
// resetSpacings
105+
// paddingH-s2
106+
marginR-s2
107+
marginB-s2
108+
dismissIcon={removeIcon}
109+
{...defaultChipProps}
110+
{...chip}
111+
onPress={onChipPress}
112+
onDismiss={isMarkedForRemoval ? removeMarkedChip : undefined}
113+
/>
114+
);
115+
})}
116+
</>
117+
);
118+
}, [chips, leadingAccessory, defaultChipProps, removeMarkedChip, markedForRemoval]);
119+
120+
return (
121+
<TextField
122+
// @ts-expect-error
123+
ref={fieldRef}
124+
leadingAccessory={chipList}
125+
blurOnSubmit={false}
126+
{...others}
127+
onChangeText={onChangeText}
128+
onSubmitEditing={addChip}
129+
fieldStyle={[fieldStyle, styles.fieldStyle]}
130+
onKeyPress={onKeyPress}
131+
accessibilityHint={props.editable ? 'press keyboard delete button to remove last tag' : undefined}
132+
/>
133+
);
134+
};
135+
136+
const styles = StyleSheet.create({
137+
fieldStyle: {
138+
flexWrap: 'wrap'
139+
}
140+
});
141+
142+
ChipsInput.changeReasons = {
143+
ADDED: 'added',
144+
REMOVED: 'removed'
145+
};
146+
147+
export default forwardRef(ChipsInput);

src/incubator/TextField/index.tsx

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -184,34 +184,34 @@ const TextField = (props: InternalTextFieldProps) => {
184184
testID={`${props.testID}.validationMessage`}
185185
/>
186186
)}
187-
<View style={[paddings, fieldStyle]}>
188-
<View row centerV>
189-
{leadingAccessory}
190-
<View flexG>
191-
{floatingPlaceholder && (
192-
<FloatingPlaceholder
193-
placeholder={placeholder}
194-
floatingPlaceholderStyle={[typographyStyle, floatingPlaceholderStyle]}
195-
floatingPlaceholderColor={floatingPlaceholderColor}
196-
floatOnFocus={floatOnFocus}
197-
validationMessagePosition={validationMessagePosition}
198-
/>
199-
)}
200-
{children || (
201-
<Input
202-
placeholderTextColor={hidePlaceholder ? 'transparent' : Colors.grey30}
203-
{...others}
204-
style={[typographyStyle, colorStyle, others.style]}
205-
onFocus={onFocus}
206-
onBlur={onBlur}
207-
onChangeText={onChangeText}
208-
placeholder={placeholder}
209-
hint={hint}
210-
/>
211-
)}
212-
</View>
213-
{trailingAccessory}
187+
<View style={[paddings, fieldStyle]} row centerV>
188+
{/* <View row centerV> */}
189+
{leadingAccessory}
190+
<View flexG>
191+
{floatingPlaceholder && (
192+
<FloatingPlaceholder
193+
placeholder={placeholder}
194+
floatingPlaceholderStyle={[typographyStyle, floatingPlaceholderStyle]}
195+
floatingPlaceholderColor={floatingPlaceholderColor}
196+
floatOnFocus={floatOnFocus}
197+
validationMessagePosition={validationMessagePosition}
198+
/>
199+
)}
200+
{children || (
201+
<Input
202+
placeholderTextColor={hidePlaceholder ? 'transparent' : Colors.grey30}
203+
{...others}
204+
style={[typographyStyle, colorStyle, others.style]}
205+
onFocus={onFocus}
206+
onBlur={onBlur}
207+
onChangeText={onChangeText}
208+
placeholder={placeholder}
209+
hint={hint}
210+
/>
211+
)}
214212
</View>
213+
{trailingAccessory}
214+
{/* </View> */}
215215
</View>
216216
<View row spread>
217217
{validationMessagePosition === ValidationMessagePosition.BOTTOM && (

src/incubator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export {default as ChipsInput, ChipsInputProps, ChipsInputChangeReason} from './ChipsInput';
12
export {default as ExpandableOverlay} from './expandableOverlay';
23
// @ts-ignore
34
export {default as TextField, TextFieldProps, FieldContextType} from './TextField';

0 commit comments

Comments
 (0)