Commit 63f6f8af by tungnq

TODO: Đã hoàn thiện được componet chip trong email input fix được vùng giao diện…

TODO: Đã hoàn thiện được componet chip trong email input fix được vùng giao diện chết do wapper tạo ra không còn chỉ nhấn vào title mới sang được chế độ show input
parent a0073857
Subproject commit c95f83687e728b84c9eb4df75e9a89ad0afb1b1a
import React, {useRef, useState} from 'react'; import React, {useRef, useState} from 'react';
import {View, TextInput, TouchableOpacity, Image} from 'react-native'; import {View, TextInput, TouchableOpacity, Image, Text} from 'react-native';
import Chip from './chip'; import Chip from './chip';
import {BACKSPACE, DELIMITERS} from '../../config/constants'; import {BACKSPACE, DELIMITERS} from '../../config/constants';
import {isValidEmail} from '../../config/Functions'; import {isValidEmail} from '../../config/Functions';
import {Text} from 'react-native';
import R from '../../assets/R'; import R from '../../assets/R';
/**
* FUNCTIONALITY: Component cho phép nhập email và hiển thị dạng chip
@param {Object} props
- Props của component
@param {string[]} props.entries
- Danh sách email ban đầu
@param {Function} props.onSubmit
- Callback khi submit danh sách email
@param {string[]} props.delimiters
- Các ký tự phân tách để tự động thêm email
@param {Object} props.containerStyle
- Style cho container chính
@param {Object} props.chipContainerStyle
- Style cho từng chip
@param {Object} props.chipTextStyle
- Style cho text trong chip
@param {Object} props.inputContainerStyle
- Style cho container input
@param {Object} props.inputStyle
- Style cho TextInput
@param {React.Element} props.chipImage
- Icon hiển thị trong chip (thường là nút X)
@param {string} props.keyboardAppearance
- Appearance của keyboard
@param {string} props.clearButtonMode
- Mode của clear button
@param {string} props.placeholder
- Placeholder text cho input
@param {string} props.placeholderTextColor
- Màu placeholder
@param {string} props.autoCapitalize
- Auto capitalize setting
@param {boolean} props.autoCorrect
- Bật/tắt auto correct
@param {boolean} props.autoFocus
- Auto focus khi mount
@param {boolean} props.blurOnSubmit
- Blur input khi submit
@param {string} props.keyboardType
- Loại keyboard
@param {Object} props.TextInputProps
- Props bổ sung cho TextInput
@param {string} props.widthContainerInput
- Chiều rộng của container input
*/
export const EmailChipInput = ({ export const EmailChipInput = ({
title, title,
entries, entries,
...@@ -71,7 +24,6 @@ export const EmailChipInput = ({ ...@@ -71,7 +24,6 @@ export const EmailChipInput = ({
placeholder, placeholder,
autoCapitalize = 'none', autoCapitalize = 'none',
autoFocus = false, autoFocus = false,
blurOnSubmit = true,
keyboardType = 'email-address', keyboardType = 'email-address',
widthContainerInput = '100%', widthContainerInput = '100%',
}) => { }) => {
...@@ -79,81 +31,71 @@ export const EmailChipInput = ({ ...@@ -79,81 +31,71 @@ export const EmailChipInput = ({
const [emails, setEmails] = useState(entries); const [emails, setEmails] = useState(entries);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [showInput, setShowInput] = useState(false); const [showInput, setShowInput] = useState(true); // mở sẵn
// Thu gọn chỉ khi blur & không có giá trị
const onBlur = () => { const onBlur = () => {
if (!value) { if (!value) setShowInput(false);
setShowInput(false);
}
}; };
// Chip -> chuyển sang edit
const handleOnPressChip = index => { const handleOnPressChip = index => {
setValue(emails[index]); setValue(emails[index]);
setEmails(emails.filter((_, i) => i !== index)); setEmails(emails.filter((_, i) => i !== index));
setShowInput(true); // remount TextInput -> autoFocus
// UI/UX: Focus vào input để tiếp tục chỉnh sửa
return ref.current?.focus();
}; };
// PERFORMANCE: Tối ưu việc lấy entry cuối cùng
const lastEntry = emails[emails.length - 1]; const lastEntry = emails[emails.length - 1];
// FUNCTIONALITY: Xử lý thay đổi text trong input // Nhập text + thêm qua delimiter
const onTextChange = inputValue => { const onTextChange = inputValue => {
// BUG: Tránh duplicate khi edit email cuối cùng if (inputValue === lastEntry) return setValue('');
if (inputValue === lastEntry) {
return setValue('');
}
// FEATURE: Tự động thêm email khi gặp delimiter
if ( if (
isValidEmail(inputValue) && isValidEmail(inputValue) &&
delimiters.some(delimiter => inputValue.endsWith(delimiter)) delimiters.some(d => inputValue.endsWith(d))
) { ) {
setEmails([ const cleaned = inputValue.slice(0, -1).trim();
...emails, if (cleaned) {
inputValue.substring(0, inputValue.length - 1), // Loại bỏ delimiter const newEntries = [...emails, cleaned];
]); setEmails(newEntries);
setValue(''); setValue('');
onSubmit?.(newEntries);
setShowInput(true); // remount vẫn giữ mở
} else {
setValue('');
}
return; return;
} }
setValue(inputValue); setValue(inputValue);
}; };
// FUNCTIONALITY: Xử lý phím backspace để xóa chip // Backspace khi trống -> edit chip cuối
const onKeyPress = ({nativeEvent: {key}}) => { const onKeyPress = ({nativeEvent: {key}}) => {
// FEATURE: Backspace khi input trống sẽ edit chip cuối cùng
if (!value && key === BACKSPACE && lastEntry) { if (!value && key === BACKSPACE && lastEntry) {
setValue(lastEntry); setValue(lastEntry);
setEmails(emails.filter(email => email !== lastEntry)); setEmails(emails.filter(e => e !== lastEntry));
setShowInput(true); // remount -> autoFocus
} }
}; };
// FUNCTIONALITY: Xử lý blur/submit để finalize email // Submit bằng Enter nhưng KHÔNG đóng input
const handleBlur = () => { const submitCurrent = () => {
// VALIDATION: Nếu email không hợp lệ hoặc rỗng, chỉ submit danh sách hiện tại const v = value.trim();
if (!isValidEmail(value)) { if (!isValidEmail(v)) return;
return onSubmit(emails); const newEntries = [...emails, v];
}
// FEATURE: Thêm email hiện tại vào danh sách và submit
const newEntries = [...emails, value];
setEmails(newEntries); setEmails(newEntries);
setValue(''); setValue('');
onSubmit(newEntries); onSubmit?.(newEntries);
setShowInput(true); // vẫn mở để nhập tiếp
}; };
// FUNCTIONALITY: Toggle chế độ hiển thị chip const toggleExpanded = () => setIsExpanded(!isExpanded);
const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};
// FUNCTIONALITY: Render chip theo chế độ hiển thị
const renderChips = () => { const renderChips = () => {
if (emails.length === 0) return null; if (emails.length === 0) return null;
// Nếu chỉ có 1 chip hoặc đang ở chế độ mở rộng, hiển thị tất cả
if (emails.length === 1 || isExpanded) { if (emails.length === 1 || isExpanded) {
return emails.map((email, index) => ( return emails.map((email, index) => (
<Chip <Chip
...@@ -169,7 +111,6 @@ export const EmailChipInput = ({ ...@@ -169,7 +111,6 @@ export const EmailChipInput = ({
)); ));
} }
// Chế độ thu gọn: chỉ hiển thị chip đầu + text số lượng
return ( return (
<> <>
<Chip <Chip
...@@ -190,29 +131,39 @@ export const EmailChipInput = ({ ...@@ -190,29 +131,39 @@ export const EmailChipInput = ({
marginLeft: 5, marginLeft: 5,
alignSelf: 'center', alignSelf: 'center',
}}> }}>
+{emails.length - 1} more +{emails.length - 1}
</Text> </Text>
</> </>
); );
}; };
// Khi input ẩn: dùng TouchableOpacity để mở.
// Khi input hiện: dùng View + pointerEvents để không chặn touch của chip/nút X.
const Wrapper = showInput ? View : TouchableOpacity;
const wrapperProps = showInput
? {pointerEvents: 'box-none'}
: {
onPress: () => setShowInput(true),
activeOpacity: 0.7,
hitSlop: { top: 8, bottom: 8, left: 8, right: 8 },
};
return ( return (
<View style={containerStyle}> <View style={containerStyle}>
<View <View
style={[ style={{
{ flexDirection: 'row',
flexDirection: 'row', // flexWrap: emails.length > 0 ? 'wrap' : 'nowrap',
flexWrap: emails.length > 0 ? 'wrap' : 'nowrap', alignItems: 'center',
alignItems: 'center', }}>
}, <Wrapper
]}> {...wrapperProps}
{/* FEATURE: Render danh sách email chips */}
<TouchableOpacity
onPress={() => setShowInput(true)}
style={{ style={{
minHeight: 35,
flexDirection: 'row', flexDirection: 'row',
flexWrap: emails.length > 0 ? 'wrap' : 'nowrap', flexWrap: emails.length > 0 ? 'wrap' : 'nowrap',
alignItems: 'center', alignItems: 'center',
flex:1,
}}> }}>
<Text <Text
style={{ style={{
...@@ -224,48 +175,24 @@ export const EmailChipInput = ({ ...@@ -224,48 +175,24 @@ export const EmailChipInput = ({
{title} {title}
</Text> </Text>
{renderChips()} {renderChips()}
</TouchableOpacity> {showInput && (
{/* FEATURE: Nút toggle thu gọn/phóng to */}
{emails.length > 1 && (
<View style={{position: 'absolute', right: -3, top: 0}}>
<TouchableOpacity
onPress={toggleExpanded}
style={{
backgroundColor: R.colors.blue4,
borderRadius: 15,
padding: 3,
}}>
<Image
source={isExpanded ? R.images.icBack2 : R.images.icDrop}
style={{
width: 25,
height: 25,
tintColor: R.colors.black,
}}
/>
</TouchableOpacity>
</View>
)}
{showInput && (
<View <View
style={[ style={[
{width: '100%', height: 35, widthContainerInput}, { height: 35, widthContainerInput,},
inputContainerStyle, inputContainerStyle,
]}> ]}>
<TextInput <TextInput
value={value}
ref={ref} ref={ref}
value={value}
onChangeText={onTextChange} onChangeText={onTextChange}
onKeyPress={onKeyPress} onKeyPress={onKeyPress}
onSubmitEditing={handleBlur} onSubmitEditing={submitCurrent} // nhập liên tiếp
onBlur={onBlur} onBlur={onBlur} // chỉ đóng khi rỗng
blurOnSubmit={blurOnSubmit} blurOnSubmit={false} // Enter không blur
keyboardType={keyboardType} keyboardType={keyboardType}
clearButtonMode={clearButtonMode} clearButtonMode={clearButtonMode}
autoCorrect={autoCorrect} autoCorrect={autoCorrect}
autoFocus={autoFocus} autoFocus={showInput || autoFocus} // <-- tự focus khi mount
placeholder={placeholder} placeholder={placeholder}
autoCapitalize={autoCapitalize} autoCapitalize={autoCapitalize}
keyboardAppearance={keyboardAppearance} keyboardAppearance={keyboardAppearance}
...@@ -277,8 +204,8 @@ export const EmailChipInput = ({ ...@@ -277,8 +204,8 @@ export const EmailChipInput = ({
fontFamily: R.fonts.fontMedium, fontFamily: R.fonts.fontMedium,
fontWeight: '600', fontWeight: '600',
padding: 0, padding: 0,
height: '100%', height: 35,
paddingHorizontal: 0, paddingHorizontal: 10,
textAlignVertical: 'center', textAlignVertical: 'center',
}, },
inputStyle, inputStyle,
...@@ -287,10 +214,28 @@ export const EmailChipInput = ({ ...@@ -287,10 +214,28 @@ export const EmailChipInput = ({
/> />
</View> </View>
)} )}
</Wrapper>
{emails.length > 1 && (
<View style={{position: 'absolute', right: -3, top: 0}}>
<TouchableOpacity
onPress={toggleExpanded}
style={{
backgroundColor: R.colors.blue4,
borderRadius: 15,
padding: 3,
}}>
<Image
source={isExpanded ? R.images.icBack2 : R.images.icDrop}
style={{width: 25, height: 25, tintColor: R.colors.black}}
/>
</TouchableOpacity>
</View>
)}
</View> </View>
</View> </View>
); );
}; };
// PERFORMANCE: Memoize component để tránh re-render
export default React.memo(EmailChipInput); export default React.memo(EmailChipInput);
...@@ -52,6 +52,21 @@ const styles = StyleSheet.create({ ...@@ -52,6 +52,21 @@ const styles = StyleSheet.create({
iconClose:{ iconClose:{
width:15, width:15,
height:15 height:15
},
inputContainer:{
height:35,
flexDirection:'row',
alignItems:'center',
borderBottomWidth:1,
borderColor:R.colors.blue4,
borderRadius:5,
},
input:{
padding:0,
paddingHorizontal:10,
paddingVertical:0
} }
}) })
......
import React from 'react'; import React from 'react';
import {Text, View, TouchableOpacity, StyleSheet, Image} from 'react-native'; import {Text, View, TouchableOpacity, StyleSheet, Image, TextInput} from 'react-native';
import styles from './style'; import styles from './style';
import R from '../../../assets/R'; import R from '../../../assets/R';
import Button from '../../../components/Button'; import Button from '../../../components/Button';
...@@ -31,7 +31,6 @@ const renderHeader = () => { ...@@ -31,7 +31,6 @@ const renderHeader = () => {
style={styles.container}> style={styles.container}>
{renderHeader()} {renderHeader()}
<View style={styles.body}> <View style={styles.body}>
<EmailChipInput <EmailChipInput
title="To" title="To"
delimiters={dataList} delimiters={dataList}
...@@ -41,13 +40,84 @@ const renderHeader = () => { ...@@ -41,13 +40,84 @@ const renderHeader = () => {
chipTextStyle={{ color:R.colors.black }} chipTextStyle={{ color:R.colors.black }}
containerStyle={{ containerStyle={{
borderBottomWidth:1, borderBottomWidth:1,
borderColor: R.colors.blue4, borderColor: R.colors.grayBorderInputTextHeader,
borderRadius: 5, borderRadius: 5,
marginBottom:10
}} }}
chipImage={ chipImage={
<Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.black}/> <Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.black}/>
} }
/> />
<EmailChipInput
title="CC"
delimiters={dataList}
entries={chip}
onSubmit={handleChange}
chipContainerStyle={{ backgroundColor: R.colors.blue4}}
chipTextStyle={{ color:R.colors.black }}
containerStyle={{
borderBottomWidth:1,
borderColor: R.colors.grayBorderInputTextHeader,
borderRadius: 5,
marginBottom:10
}}
chipImage={
<Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.black}/>
}
/>
<EmailChipInput
title="BCC"
delimiters={dataList}
entries={chip}
onSubmit={handleChange}
chipContainerStyle={{ backgroundColor: R.colors.blue4}}
chipTextStyle={{ color:R.colors.black }}
containerStyle={{
borderBottomWidth:1,
borderColor: R.colors.grayBorderInputTextHeader,
borderRadius: 5,
marginBottom:10
}}
chipImage={
<Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.black}/>
}
/>
<EmailChipInput
title="From"
delimiters={dataList}
entries={chip}
onSubmit={handleChange}
chipContainerStyle={{ backgroundColor: R.colors.blue4}}
chipTextStyle={{ color:R.colors.black }}
containerStyle={{
borderBottomWidth:1,
borderColor: R.colors.grayBorderInputTextHeader,
borderRadius: 5,
marginBottom:10
}}
chipImage={
<Image source={R.images.icCancel} style={styles.iconClose} tintColor={R.colors.black}/>
}
/>
<View style={styles.inputContainer}>
<Text>
Subject
</Text>
<TextInput
placeholder="Subject"
style={styles.input}
/>
</View>
<View style={styles.inputContainer}>
<Text>
Content
</Text>
<TextInput
placeholder="Content"
style={styles.input}
/>
</View>
</View> </View>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment