Commit eaf55b41 by tungnq

TODO: Bổ sung componet tab view

parent 21d5a486
# TabView Component
A reusable TabView component that supports both filter and navigation modes.
## Features
- **Auto-generate tabs** from data array
- **Two modes**: Filter mode and Navigate mode
- **Scrollable tabs** when there are many tabs
- **Icon support** for each tab
- **Active state styling** with underline indicator
- **Disabled state** support
- **Auto-scroll** to active tab when scrollable
- **Edge case handling** (empty data, duplicate keys, etc.)
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `data` | Array | `[]` | Array of tab objects |
| `mode` | String | `'filter'` | `'filter'` or `'navigate'` |
| `defaultActiveKey` | String | `null` | Initial active tab key |
| `scrollable` | Boolean | `false` | Enable horizontal scrolling |
| `onTabChange` | Function | `null` | Called when tab changes |
| `onFilterChange` | Function | `null` | Called in filter mode |
| `onNavigate` | Function | `null` | Called in navigate mode |
| `style` | Object | `{}` | Container style |
| `tabStyle` | Object | `{}` | Individual tab style |
| `activeTabStyle` | Object | `{}` | Active tab style |
| `textStyle` | Object | `{}` | Tab text style |
| `activeTextStyle` | Object | `{}` | Active tab text style |
## Data Object Structure
```javascript
{
key: 'unique_key', // Required: unique identifier
label: 'Tab Label', // Required: display text
icon: 'icon_name', // Optional: MaterialIcons name
disabled: false, // Optional: disable tab
// ... any other custom properties
}
```
## Usage Examples
### 1. Filter Mode (Default)
```javascript
import React, { useState } from 'react';
import { View } from 'react-native';
import TabView from '../components/TabView';
const FilterScreen = () => {
const [filteredData, setFilteredData] = useState([]);
const [allData] = useState([
// Your data array
]);
const tabData = [
{ key: 'all', label: 'Tất cả', icon: 'list' },
{ key: 'public', label: 'Công khai', icon: 'public' },
{ key: 'private', label: 'Hạn chế', icon: 'lock' },
{ key: 'draft', label: 'Nháp', icon: 'edit' },
];
const handleFilterChange = (item) => {
if (item.key === 'all') {
setFilteredData(allData);
} else {
const filtered = allData.filter(data => data.status === item.key);
setFilteredData(filtered);
}
};
return (
<View>
<TabView
data={tabData}
mode="filter"
defaultActiveKey="all"
scrollable={true}
onFilterChange={handleFilterChange}
/>
{/* Render your filtered list here */}
</View>
);
};
```
### 2. Navigate Mode
```javascript
import React from 'react';
import { View } from 'react-native';
import TabView from '../components/TabView';
const NavigationScreen = ({ navigation }) => {
const tabData = [
{ key: 'home', label: 'Trang chủ', icon: 'home' },
{ key: 'profile', label: 'Hồ sơ', icon: 'person' },
{ key: 'settings', label: 'Cài đặt', icon: 'settings' },
{ key: 'help', label: 'Trợ giúp', icon: 'help' },
];
const handleNavigate = (item) => {
switch (item.key) {
case 'home':
navigation.navigate('HomeScreen');
break;
case 'profile':
navigation.navigate('ProfileScreen');
break;
case 'settings':
navigation.navigate('SettingsScreen');
break;
case 'help':
navigation.navigate('HelpScreen');
break;
}
};
return (
<View>
<TabView
data={tabData}
mode="navigate"
defaultActiveKey="home"
onNavigate={handleNavigate}
/>
</View>
);
};
```
### 3. Custom Styling
```javascript
<TabView
data={tabData}
style={{ backgroundColor: '#f8f9fa' }}
tabStyle={{
paddingHorizontal: 20,
paddingVertical: 15
}}
activeTabStyle={{
backgroundColor: '#e3f2fd'
}}
textStyle={{
fontSize: 16,
color: '#333'
}}
activeTextStyle={{
color: '#1976d2',
fontWeight: 'bold'
}}
/>
```
### 4. With Disabled Tabs
```javascript
const tabData = [
{ key: 'tab1', label: 'Available', icon: 'check' },
{ key: 'tab2', label: 'Disabled', icon: 'block', disabled: true },
{ key: 'tab3', label: 'Coming Soon', icon: 'schedule', disabled: true },
];
```
## Edge Cases Handled
- **Empty data**: Shows "Không có tab" placeholder
- **Invalid defaultActiveKey**: Falls back to first tab
- **Duplicate keys**: Shows warning in console
- **Many tabs**: Use `scrollable={true}` for horizontal scrolling
## Styling Notes
- Active tab has blue color (`#007AFF`) and underline
- Inactive tabs have gray color (`#666`)
- Disabled tabs have reduced opacity (0.5)
- Tab height is 40px with 16px horizontal padding
- Icons are 16px size with 6px right margin
import TabViewComponent from './view';
export default TabViewComponent;
import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TouchableOpacity,
ScrollView,
StyleSheet,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
const { width: screenWidth } = Dimensions.get('window');
/**
* Component TabView có thể tái sử dụng
* Hỗ trợ 2 chế độ: filter (lọc dữ liệu) và navigate (điều hướng)
*
* Props:
* - data: Mảng dữ liệu tab [{key, label, icon?, disabled?}]
* - mode: 'filter' (lọc) hoặc 'navigate' (điều hướng)
* - defaultActiveKey: Tab mặc định được chọn
* - scrollable: Cho phép cuộn ngang khi có nhiều tab
* - onTabChange: Callback khi thay đổi tab
* - onFilterChange: Callback cho chế độ filter
* - onNavigate: Callback cho chế độ navigate
* - style, tabStyle, activeTabStyle, textStyle, activeTextStyle: Tùy chỉnh giao diện
*/
const TabViewComponent = ({
data = [], // Mảng dữ liệu tab
mode = 'filter', // Chế độ: 'filter' | 'navigate'
defaultActiveKey = null, // Tab mặc định
scrollable = false, // Cho phép cuộn ngang
onTabChange = null, // Callback khi đổi tab
onFilterChange = null, // Callback cho chế độ lọc
onNavigate = null, // Callback cho chế độ điều hướng
style = {}, // Style container chính
tabStyle = {}, // Style từng tab
activeTabStyle = {}, // Style tab đang active
textStyle = {}, // Style text tab
activeTextStyle = {}, // Style text tab active
}) => {
// State lưu key của tab đang được chọn
const [activeKey, setActiveKey] = useState(null);
// Ref để điều khiển ScrollView
const scrollViewRef = useRef(null);
// Ref để lưu reference của từng tab (dùng cho auto scroll)
const tabRefs = useRef({});
// Khởi tạo tab active ban đầu
useEffect(() => {
if (data.length === 0) return;
let initialKey = defaultActiveKey;
// Kiểm tra xem defaultActiveKey có tồn tại trong data không
if (defaultActiveKey) {
const keyExists = data.some(item => item.key === defaultActiveKey);
if (!keyExists) {
console.warn(`TabView: defaultActiveKey "${defaultActiveKey}" không tìm thấy trong data. Sử dụng tab đầu tiên.`);
initialKey = data[0].key;
}
} else {
// Nếu không có defaultActiveKey, chọn tab đầu tiên
initialKey = data[0].key;
}
setActiveKey(initialKey);
}, [data, defaultActiveKey]);
// Kiểm tra key trùng lặp trong data
useEffect(() => {
if (data.length === 0) return;
const keys = data.map(item => item.key);
const uniqueKeys = [...new Set(keys)];
if (keys.length !== uniqueKeys.length) {
console.warn('TabView: Phát hiện key trùng lặp trong mảng data. Có thể gây lỗi không mong muốn.');
}
}, [data]);
// Tự động cuộn đến tab active khi scrollable = true
useEffect(() => {
if (!scrollable || !activeKey || !scrollViewRef.current) return;
const activeTabRef = tabRefs.current[activeKey];
if (activeTabRef) {
// Đo vị trí của tab active để cuộn đến giữa màn hình
activeTabRef.measureLayout(
scrollViewRef.current.getInnerViewNode(),
(x, y, width, height) => {
const tabCenter = x + width / 2; // Tâm của tab
const screenCenter = screenWidth / 2; // Tâm màn hình
const scrollToX = Math.max(0, tabCenter - screenCenter); // Vị trí cần cuộn
scrollViewRef.current.scrollTo({
x: scrollToX,
animated: true,
});
},
() => {} // Callback khi đo thất bại
);
}
}, [activeKey, scrollable]);
// Xử lý khi người dùng nhấn vào tab
const handleTabPress = (item) => {
// Không làm gì nếu tab bị disabled
if (item.disabled) return;
// Cập nhật tab active
setActiveKey(item.key);
// Gọi callback onTabChange nếu có
if (onTabChange) {
onTabChange(item);
}
// Xử lý theo chế độ sử dụng
if (mode === 'filter' && onFilterChange) {
// Chế độ lọc: gọi onFilterChange để parent lọc dữ liệu
onFilterChange(item);
} else if (mode === 'navigate' && onNavigate) {
// Chế độ điều hướng: gọi onNavigate để parent chuyển màn
onNavigate(item);
}
};
// Render từng tab item
const renderTab = (item, index) => {
const isActive = activeKey === item.key; // Kiểm tra tab có đang active không
const isDisabled = item.disabled || false; // Kiểm tra tab có bị disabled không
return (
<TouchableOpacity
key={item.key}
ref={ref => tabRefs.current[item.key] = ref} // Lưu ref để dùng cho auto scroll
style={[
styles.tab, // Style cơ bản
tabStyle, // Style tùy chỉnh từ props
isActive && styles.activeTab, // Style khi active
isActive && activeTabStyle, // Style active tùy chỉnh
isDisabled && styles.disabledTab, // Style khi disabled
]}
onPress={() => handleTabPress(item)}
disabled={isDisabled}
activeOpacity={0.7}
>
<View style={styles.tabContent}>
{/* Hiển thị icon nếu có */}
{item.icon && (
<Icon
name={item.icon}
size={16}
color={isActive ? '#007AFF' : '#666'} // Màu xanh khi active, xám khi không
style={styles.tabIcon}
/>
)}
{/* Text của tab */}
<Text
style={[
styles.tabText, // Style text cơ bản
textStyle, // Style text tùy chỉnh
isActive && styles.activeTabText, // Style text khi active
isActive && activeTextStyle, // Style text active tùy chỉnh
isDisabled && styles.disabledTabText, // Style text khi disabled
]}
numberOfLines={1}
>
{/* Ưu tiên label, sau đó title, name, cuối cùng là 'Tab' */}
{item.label || item.title || item.name || 'Tab'}
</Text>
</View>
{/* Thanh gạch dưới khi tab active */}
{isActive && <View style={styles.activeIndicator} />}
</TouchableOpacity>
);
};
// Xử lý trường hợp không có data
if (data.length === 0) {
return (
<View style={[styles.container, style]}>
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>Không có tab</Text>
</View>
</View>
);
}
// Chọn container: ScrollView nếu scrollable, View nếu không
const TabContainer = scrollable ? ScrollView : View;
const containerProps = scrollable
? {
ref: scrollViewRef, // Ref để điều khiển scroll
horizontal: true, // Cuộn ngang
showsHorizontalScrollIndicator: false, // Ẩn thanh cuộn
contentContainerStyle: styles.scrollContent,
}
: {
style: styles.tabContainer, // Style cho View thường
};
return (
<View style={[styles.container, style]}>
<TabContainer {...containerProps}>
{data.map((item, index) => renderTab(item, index))}
</TabContainer>
</View>
);
};
// Styles cho component
const styles = StyleSheet.create({
// Container chính của TabView
container: {
backgroundColor: '#fff', // Nền trắng
borderBottomWidth: 1, // Viền dưới mỏng
borderBottomColor: '#E5E5E5', // Màu viền xám nhạt
},
// Container cho các tab khi không scrollable
tabContainer: {
flexDirection: 'row', // Sắp xếp tab theo hàng ngang
},
// Style cho ScrollView content khi scrollable
scrollContent: {
flexDirection: 'row', // Sắp xếp tab theo hàng ngang
paddingHorizontal: 8, // Padding hai bên
},
// Style cơ bản cho mỗi tab
tab: {
paddingHorizontal: 16, // Padding ngang 16px
paddingVertical: 12, // Padding dọc 12px
minHeight: 40, // Chiều cao tối thiểu 40px
justifyContent: 'center', // Căn giữa theo chiều dọc
alignItems: 'center', // Căn giữa theo chiều ngang
position: 'relative', // Để có thể đặt activeIndicator absolute
},
// Style cho tab active (được xử lý bởi activeIndicator)
activeTab: {
// Style active chủ yếu được xử lý bởi activeIndicator và activeTabText
},
// Style cho tab bị disabled
disabledTab: {
opacity: 0.5, // Làm mờ 50%
},
// Container chứa icon và text của tab
tabContent: {
flexDirection: 'row', // Icon và text nằm ngang
alignItems: 'center', // Căn giữa theo chiều dọc
justifyContent: 'center', // Căn giữa theo chiều ngang
},
// Style cho icon của tab
tabIcon: {
marginRight: 6, // Khoảng cách 6px giữa icon và text
},
// Style text cơ bản của tab
tabText: {
fontSize: 14, // Cỡ chữ 14px
color: '#666', // Màu xám
fontWeight: '400', // Độ đậm bình thường
},
// Style text khi tab active
activeTabText: {
color: '#007AFF', // Màu xanh iOS
fontWeight: '600', // Đậm hơn
},
// Style text khi tab disabled
disabledTabText: {
color: '#CCC', // Màu xám nhạt
},
// Thanh gạch dưới cho tab active
activeIndicator: {
position: 'absolute', // Đặt absolute để nằm dưới cùng
bottom: 0, // Sát đáy
left: 0, // Từ trái
right: 0, // Đến phải
height: 2, // Cao 2px
backgroundColor: '#007AFF', // Màu xanh iOS
},
// Container khi không có data
emptyContainer: {
padding: 20, // Padding 20px
alignItems: 'center', // Căn giữa ngang
justifyContent: 'center', // Căn giữa dọc
},
// Text hiển thị khi không có data
emptyText: {
fontSize: 14, // Cỡ chữ 14px
color: '#999', // Màu xám
fontStyle: 'italic', // Chữ nghiêng
},
});
// Export component để sử dụng ở nơi khác
export default TabViewComponent;
...@@ -6,16 +6,32 @@ import SubButton from '../../components/FAB/sub_button'; ...@@ -6,16 +6,32 @@ import SubButton from '../../components/FAB/sub_button';
import Header from '../../components/Header/Header'; import Header from '../../components/Header/Header';
import Dropdown from '../../components/DropdownAlert/Dropdown'; import Dropdown from '../../components/DropdownAlert/Dropdown';
import R from '../../assets/R'; import R from '../../assets/R';
import TabViewComponent from '../../components/TabView';
const ListWorkView = (props) => { const ListWorkView = (props) => {
const { } = props; const { } = props;
const handleFilterChange = (item) => {
console.log(item);
};
return ( return (
<View <View
style={styles.container}> style={styles.container}>
<Header title={'Danh sách công việc'} isBack /> <Header title={'Danh sách công việc'} isBack />
<View style = {styles.body}> <View style = {styles.body}>
<TabViewComponent
data={[
{key: 'all', label: 'Tất cả', icon: 'list'},
{key: 'public', label: 'Công khai', icon: 'public'},
{key: 'private', label: 'Hạn chế', icon: 'lock'},
{key: 'draft', label: 'Nháp', icon: 'edit'},
]}
mode="filter"
defaultActiveKey="all"
scrollable={true}
onFilterChange={handleFilterChange}
/>
</View> </View>
<FAB> <FAB>
<SubButton <SubButton
......
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