feat: Complete OS_Wrapper implementation with keyboard handling and PADDING_INLINE

OS_Wrapper System:
- Simplify API: Remove PageWrapper, merge keyboard props into OS_Wrapper
- Add auto-scroll when keyboard appears (Android only)
- Add tap-to-dismiss keyboard for both Static and List modes
- Fix contentPaddingBottom default to 250px (prevent keyboard overlap)
- Change default contentPadding to 0 (per-screen control)
- Remove Platform.OS checks from IOSWrapper and AndroidWrapper

Constants:
- Add PADDING_INLINE constant (16px) for consistent inline padding
- Add OS_PADDING_TOP constants for tab layouts

Job Screens Migration (9 files):
- Apply PADDING_INLINE to all Job screens:
  - ScreenBeranda, ScreenBeranda2
  - ScreenArchive, ScreenArchive2
  - MainViewStatus, MainViewStatus2
  - ScreenJobCreate, ScreenJobEdit
  - Job detail screen

Keyboard Handling:
- Simplified useKeyboardForm hook
- Auto-scroll by keyboard height when keyboard appears
- Track scroll position for accurate scroll targets
- TouchableWithoutFeedback wraps all content for tap-to-dismiss

Documentation:
- Update TASK-005 with Phase 1 completion status
- Update Quick Reference with unified API examples

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-07 17:50:15 +08:00
parent 502cd7bc65
commit 1a5ca78041
20 changed files with 349 additions and 238 deletions

View File

@@ -5,15 +5,17 @@ import { IconHome, IconStatus } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification"; import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import { router, Tabs, useLocalSearchParams } from "expo-router";
router,
Tabs,
useLocalSearchParams
} from "expo-router";
import { View } from "react-native"; import { View } from "react-native";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native"; import { Platform } from "react-native";
import {
OS_ANDROID_HEIGHT,
OS_ANDROID_PADDING_TOP,
OS_IOS_HEIGHT,
OS_IOS_PADDING_TOP,
} from "@/constants/constans-value";
function JobTabsWrapper() { function JobTabsWrapper() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -31,20 +33,23 @@ function JobTabsWrapper() {
tabBarStyle: Platform.select({ tabBarStyle: Platform.select({
ios: { ios: {
borderTopWidth: 0, borderTopWidth: 0,
paddingTop: 12, paddingTop: OS_IOS_PADDING_TOP,
height: 80, height: OS_IOS_HEIGHT,
}, },
android: { android: {
borderTopWidth: 0, borderTopWidth: 0,
paddingTop: 5, paddingTop: OS_ANDROID_PADDING_TOP,
height: 70 + paddingBottom, height: OS_ANDROID_HEIGHT + paddingBottom,
}, },
}), }),
header: () => ( header: () => (
<AppHeader <AppHeader
title="Job Vacancy" title="Job Vacancy"
left={ left={
<BackButtonFromNotification from={from || ""} category={category} /> <BackButtonFromNotification
from={from || ""}
category={category}
/>
} }
/> />
), ),

View File

@@ -5,7 +5,7 @@ import {
DrawerCustom, DrawerCustom,
LoaderCustom, LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
NewWrapper_V2, OS_Wrapper,
Spacing, Spacing,
StackCustom, StackCustom,
} from "@/components"; } from "@/components";
@@ -23,6 +23,7 @@ import {
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { PADDING_INLINE } from "@/constants/constans-value";
export default function JobDetailStatus() { export default function JobDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
@@ -72,7 +73,7 @@ export default function JobDetailStatus() {
), ),
}} }}
/> />
<NewWrapper_V2> <OS_Wrapper contentPadding={PADDING_INLINE}>
{isLoadData ? ( {isLoadData ? (
<LoaderCustom /> <LoaderCustom />
) : ( ) : (
@@ -96,7 +97,7 @@ export default function JobDetailStatus() {
<Spacing /> <Spacing />
</> </>
)} )}
</NewWrapper_V2> </OS_Wrapper>
<DrawerCustom <DrawerCustom
isVisible={openDrawer} isVisible={openDrawer}

View File

@@ -7,7 +7,6 @@ import {
ImageBackground, ImageBackground,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform,
ScrollView, ScrollView,
FlatList, FlatList,
TouchableWithoutFeedback, TouchableWithoutFeedback,
@@ -85,16 +84,21 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
style, style,
refreshControl, refreshControl,
enableKeyboardHandling = false, enableKeyboardHandling = false,
keyboardScrollOffset = 100, keyboardScrollOffset,
contentPaddingBottom = 80, contentPaddingBottom,
contentPadding = 16, contentPadding,
} = props; } = props;
// Default values (should be set by OS_Wrapper, but fallback for direct usage)
const finalKeyboardScrollOffset = keyboardScrollOffset ?? 100;
const finalContentPaddingBottom = contentPaddingBottom ?? 250;
const finalContentPadding = contentPadding ?? 0;
const assetBackground = require("../../assets/images/main-background.png"); const assetBackground = require("../../assets/images/main-background.png");
// Use keyboard hook if enabled // Use keyboard hook if enabled
const keyboardForm = enableKeyboardHandling const keyboardForm = enableKeyboardHandling
? useKeyboardForm(keyboardScrollOffset) ? useKeyboardForm(finalKeyboardScrollOffset)
: null; : null;
const renderContainer = (content: React.ReactNode) => { const renderContainer = (content: React.ReactNode) => {
@@ -119,40 +123,41 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
const listProps = props as ListModeProps; const listProps = props as ListModeProps;
return ( return (
<KeyboardAvoidingView <TouchableWithoutFeedback onPress={Keyboard.dismiss} style={{ flex: 1 }}>
behavior={Platform.OS === "ios" ? "padding" : undefined} <KeyboardAvoidingView
style={{ flex: 1, backgroundColor: MainColor.darkblue }} behavior={undefined}
> style={{ flex: 1, backgroundColor: MainColor.darkblue }}
{headerComponent && ( >
<View style={GStyles.stickyHeader}>{headerComponent}</View> {headerComponent && (
)} <View style={GStyles.stickyHeader}>{headerComponent}</View>
<FlatList )}
data={listProps.listData} <FlatList
renderItem={listProps.renderItem} data={listProps.listData}
keyExtractor={ renderItem={listProps.renderItem}
listProps.keyExtractor || keyExtractor={
((item, index) => `${String(item.id)}-${index}`) listProps.keyExtractor ||
} ((item, index) => `${String(item.id)}-${index}`)
refreshControl={refreshControl} }
onEndReached={listProps.onEndReached} refreshControl={refreshControl}
onEndReachedThreshold={0.5} onEndReached={listProps.onEndReached}
ListHeaderComponent={listProps.ListHeaderComponent} onEndReachedThreshold={0.5}
ListFooterComponent={listProps.ListFooterComponent} ListHeaderComponent={listProps.ListHeaderComponent}
ListEmptyComponent={listProps.ListEmptyComponent} ListFooterComponent={listProps.ListFooterComponent}
contentContainerStyle={{ ListEmptyComponent={listProps.ListEmptyComponent}
flexGrow: 1, contentContainerStyle={{
paddingBottom: flexGrow: 1,
(footerComponent && !hideFooter ? OS_HEIGHT : 0) + paddingBottom:
contentPaddingBottom, (footerComponent && !hideFooter ? OS_HEIGHT : 0) +
padding: contentPadding, finalContentPaddingBottom,
}} padding: finalContentPadding,
keyboardShouldPersistTaps="handled" }}
/> keyboardShouldPersistTaps="handled"
/>
{/* Footer - Fixed di bawah dengan width 100% */} {/* Footer - Fixed di bawah dengan width 100% */}
{footerComponent && !hideFooter && ( {footerComponent && !hideFooter && (
<SafeAreaView <SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]} edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue, width: "100%" }} style={{ backgroundColor: MainColor.darkblue, width: "100%" }}
> >
<View style={{ width: "100%" }}>{footerComponent}</View> <View style={{ width: "100%" }}>{footerComponent}</View>
@@ -170,6 +175,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
<View style={GStyles.floatingContainer}>{floatingButton}</View> <View style={GStyles.floatingContainer}>{floatingButton}</View>
)} )}
</KeyboardAvoidingView> </KeyboardAvoidingView>
</TouchableWithoutFeedback>
); );
} }
@@ -178,7 +184,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined} behavior={undefined}
style={{ flex: 1, backgroundColor: MainColor.darkblue }} style={{ flex: 1, backgroundColor: MainColor.darkblue }}
> >
{headerComponent && ( {headerComponent && (
@@ -187,13 +193,15 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
<ScrollView <ScrollView
ref={keyboardForm?.scrollViewRef} ref={keyboardForm?.scrollViewRef}
onScroll={keyboardForm?.handleScroll}
scrollEventThrottle={16}
style={{ flex: 1 }} style={{ flex: 1 }}
contentContainerStyle={{ contentContainerStyle={{
flexGrow: 1, flexGrow: 1,
paddingBottom: paddingBottom:
(footerComponent && !hideFooter ? OS_HEIGHT : 0) + (footerComponent && !hideFooter ? OS_HEIGHT : 0) +
contentPaddingBottom, finalContentPaddingBottom,
padding: contentPadding, padding: finalContentPadding,
}} }}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
@@ -210,8 +218,8 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
style={{ style={{
backgroundColor: MainColor.darkblue, backgroundColor: MainColor.darkblue,
width: "100%", width: "100%",
position: Platform.OS === "android" ? "absolute" : undefined, position: "absolute",
bottom: Platform.OS === "android" ? 0 : undefined, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
}} }}

View File

@@ -29,7 +29,7 @@ export function FormWrapper({
contentPaddingBottom = 100, contentPaddingBottom = 100,
contentPadding = 16, contentPadding = 16,
}: FormWrapperProps) { }: FormWrapperProps) {
const { scrollViewRef, handleInputFocus } = useKeyboardForm(scrollOffset); const { scrollViewRef, handleScroll } = useKeyboardForm(scrollOffset);
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
@@ -38,6 +38,8 @@ export function FormWrapper({
> >
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={{ flex: 1 }} style={{ flex: 1 }}
contentContainerStyle={{ contentContainerStyle={{
flexGrow: 1, flexGrow: 1,

View File

@@ -7,7 +7,6 @@ import {
ImageBackground, ImageBackground,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform,
ScrollView, ScrollView,
FlatList, FlatList,
TouchableWithoutFeedback, TouchableWithoutFeedback,
@@ -89,7 +88,7 @@ const iOSWrapper = (props: iOSWrapperProps) => {
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior="padding"
style={{ flex: 1, backgroundColor: MainColor.darkblue }} style={{ flex: 1, backgroundColor: MainColor.darkblue }}
> >
{headerComponent && ( {headerComponent && (
@@ -128,7 +127,7 @@ const iOSWrapper = (props: iOSWrapperProps) => {
{footerComponent && !hideFooter && ( {footerComponent && !hideFooter && (
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
<SafeAreaView <SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]} edges={edgesFooter}
style={{ backgroundColor: MainColor.darkblue }} style={{ backgroundColor: MainColor.darkblue }}
> >
{footerComponent} {footerComponent}
@@ -155,7 +154,7 @@ const iOSWrapper = (props: iOSWrapperProps) => {
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior="padding"
style={{ flex: 1, backgroundColor: MainColor.darkblue }} style={{ flex: 1, backgroundColor: MainColor.darkblue }}
> >
{headerComponent && ( {headerComponent && (
@@ -181,7 +180,7 @@ const iOSWrapper = (props: iOSWrapperProps) => {
{footerComponent && !hideFooter && ( {footerComponent && !hideFooter && (
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
<SafeAreaView <SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]} edges={edgesFooter}
style={{ backgroundColor: MainColor.darkblue }} style={{ backgroundColor: MainColor.darkblue }}
> >
{footerComponent} {footerComponent}

View File

@@ -43,10 +43,10 @@ interface ListModeProps extends BaseProps {
keyExtractor?: FlatListProps<any>["keyExtractor"]; keyExtractor?: FlatListProps<any>["keyExtractor"];
} }
// ========== PageWrapper Props (Android-specific keyboard handling) ========== // ========== Keyboard Handling Props (Android only) ==========
interface PageWrapperBaseProps extends BaseProps { interface KeyboardHandlingProps {
/** /**
* Enable keyboard handling (Android only - NewWrapper_V2) * Enable keyboard handling with auto-scroll (Android only)
* iOS ignores this prop * iOS ignores this prop
* @default false * @default false
*/ */
@@ -74,81 +74,74 @@ interface PageWrapperBaseProps extends BaseProps {
contentPadding?: number; contentPadding?: number;
} }
interface PageWrapperStaticProps extends PageWrapperBaseProps { // ========== Final Props Types ==========
children: React.ReactNode; type OS_WrapperStaticProps = StaticModeProps & KeyboardHandlingProps;
listData?: never; type OS_WrapperListProps = ListModeProps & KeyboardHandlingProps;
renderItem?: never;
}
interface PageWrapperListProps extends PageWrapperBaseProps { type OS_WrapperProps = OS_WrapperStaticProps | OS_WrapperListProps;
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type OS_WrapperProps = StaticModeProps | ListModeProps;
type PageWrapperProps = PageWrapperStaticProps | PageWrapperListProps;
/** /**
* OS_Wrapper - Automatically selects iOSWrapper or AndroidWrapper based on platform * OS_Wrapper - Automatically selects iOSWrapper or AndroidWrapper based on platform
* *
* @example Static Mode * Features:
* - Auto platform detection
* - Optional keyboard handling for Android forms
* - Unified API for all use cases
*
* @example Static Mode (Simple Content)
* ```tsx * ```tsx
* <OS_Wrapper> * <OS_Wrapper>
* <YourContent /> * <YourContent />
* </OS_Wrapper> * </OS_Wrapper>
* ``` * ```
* *
* @example List Mode * @example List Mode (with pagination)
* ```tsx * ```tsx
* <OS_Wrapper * <OS_Wrapper
* listData={data} * listData={data}
* renderItem={({ item }) => <ItemCard item={item} />} * renderItem={({ item }) => <ItemCard item={item} />}
* ListEmptyComponent={<EmptyState />} * ListEmptyComponent={<EmptyState />}
* onEndReached={loadMore}
* /> * />
* ``` * ```
*
* @example Form Mode (with keyboard handling - Android only)
* ```tsx
* <OS_Wrapper
* enableKeyboardHandling
* keyboardScrollOffset={150}
* contentPaddingBottom={100}
* footerComponent={<SubmitButton />}
* >
* <FormContent />
* </OS_Wrapper>
* ```
*/ */
export function OS_Wrapper(props: OS_WrapperProps) { export function OS_Wrapper(props: OS_WrapperProps) {
const {
enableKeyboardHandling = false,
keyboardScrollOffset = 100,
contentPaddingBottom = 250,
contentPadding = 0,
...wrapperProps
} = props;
// iOS uses IOSWrapper (based on NewWrapper) // iOS uses IOSWrapper (based on NewWrapper)
if (Platform.OS === "ios") { if (Platform.OS === "ios") {
return <IOSWrapper {...props} />; // Keyboard handling props are ignored on iOS
return <IOSWrapper {...(wrapperProps as any)} />;
} }
// Android uses AndroidWrapper (based on NewWrapper_V2 with keyboard handling) // Android uses AndroidWrapper (with keyboard handling support)
return <AndroidWrapper {...props} />; return (
} <AndroidWrapper
{...(wrapperProps as any)}
/** enableKeyboardHandling={enableKeyboardHandling}
* PageWrapper - OS_Wrapper with keyboard handling support (Android only) keyboardScrollOffset={keyboardScrollOffset}
* Use this for forms with input fields contentPaddingBottom={contentPaddingBottom}
* contentPadding={contentPadding}
* @example />
* ```tsx );
* <PageWrapper enableKeyboardHandling keyboardScrollOffset={150}>
* <FormContent />
* </PageWrapper>
* ```
*/
export function PageWrapper(props: PageWrapperProps) {
// iOS: Keyboard handling props are ignored
if (Platform.OS === "ios") {
const {
enableKeyboardHandling: _,
keyboardScrollOffset: __1,
contentPaddingBottom: __2,
contentPadding: __3,
...iosProps
} = props;
return <IOSWrapper {...iosProps} />;
}
// Android: Keyboard handling props are used
return <AndroidWrapper {...props} />;
} }
// Re-export individual wrappers for direct usage if needed // Re-export individual wrappers for direct usage if needed

View File

@@ -66,7 +66,7 @@ import BasicWrapper from "./_ShareComponent/BasicWrapper";
import { FormWrapper } from "./_ShareComponent/FormWrapper"; import { FormWrapper } from "./_ShareComponent/FormWrapper";
import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2"; import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2";
// OS-Specific Wrappers // OS-Specific Wrappers
import OS_Wrapper, { PageWrapper, IOSWrapper, AndroidWrapper } from "./_ShareComponent/OS_Wrapper"; import OS_Wrapper, { IOSWrapper, AndroidWrapper } from "./_ShareComponent/OS_Wrapper";
// Progress // Progress
import ProgressCustom from "./Progress/ProgressCustom"; import ProgressCustom from "./Progress/ProgressCustom";
@@ -136,7 +136,6 @@ export {
NewWrapper_V2, NewWrapper_V2,
// OS-Specific Wrappers // OS-Specific Wrappers
OS_Wrapper, OS_Wrapper,
PageWrapper,
IOSWrapper, IOSWrapper,
AndroidWrapper, AndroidWrapper,
// Stack // Stack

View File

@@ -4,6 +4,10 @@ export {
OS_ANDROID_HEIGHT, OS_ANDROID_HEIGHT,
OS_IOS_HEIGHT, OS_IOS_HEIGHT,
OS_HEIGHT, OS_HEIGHT,
OS_ANDROID_PADDING_TOP,
OS_IOS_PADDING_TOP,
OS_PADDING_TOP,
PADDING_INLINE,
TEXT_SIZE_SMALL, TEXT_SIZE_SMALL,
TEXT_SIZE_MEDIUM, TEXT_SIZE_MEDIUM,
TEXT_SIZE_LARGE, TEXT_SIZE_LARGE,
@@ -23,10 +27,15 @@ export {
}; };
// OS Height // OS Height
const OS_ANDROID_HEIGHT = 65 const OS_ANDROID_HEIGHT = 60
const OS_IOS_HEIGHT = 80 const OS_IOS_HEIGHT = 80
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// OS Padding Top
const OS_ANDROID_PADDING_TOP = 12
const OS_IOS_PADDING_TOP = 12
const OS_PADDING_TOP = Platform.OS === "ios" ? OS_IOS_PADDING_TOP : OS_ANDROID_PADDING_TOP
// Text Size // Text Size
const TEXT_SIZE_SMALL = 12; const TEXT_SIZE_SMALL = 12;
const TEXT_SIZE_MEDIUM = 14; const TEXT_SIZE_MEDIUM = 14;
@@ -47,6 +56,7 @@ const DRAWER_HEIGHT = 500; // tinggi drawer5
const RADIUS_BUTTON = 50 const RADIUS_BUTTON = 50
// Padding // Padding
const PADDING_INLINE = 16
const PADDING_EXTRA_SMALL = 10 const PADDING_EXTRA_SMALL = 10
const PADDING_SMALL = 12 const PADDING_SMALL = 12
const PADDING_MEDIUM = 16 const PADDING_MEDIUM = 16

View File

@@ -2,7 +2,7 @@
## 📦 Import ## 📦 Import
```tsx ```tsx
import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/components"; import { OS_Wrapper, IOSWrapper, AndroidWrapper } from "@/components";
``` ```
## 🎯 Usage Examples ## 🎯 Usage Examples
@@ -35,9 +35,9 @@ import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/component
</OS_Wrapper> </OS_Wrapper>
``` ```
### 3. PageWrapper - Form dengan Keyboard Handling ### 3. OS_Wrapper - Form dengan Keyboard Handling
```tsx ```tsx
<PageWrapper <OS_Wrapper
enableKeyboardHandling enableKeyboardHandling
keyboardScrollOffset={150} keyboardScrollOffset={150}
contentPaddingBottom={100} contentPaddingBottom={100}
@@ -53,7 +53,7 @@ import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/component
<TextInputCustom /> <TextInputCustom />
<TextInputCustom /> <TextInputCustom />
</ScrollView> </ScrollView>
</PageWrapper> </OS_Wrapper>
``` ```
### 4. Platform-Specific (Rare Cases) ### 4. Platform-Specific (Rare Cases)
@@ -107,21 +107,21 @@ import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/component
```diff ```diff
- import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; - import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2";
+ import { PageWrapper } from "@/components"; + import { OS_Wrapper } from "@/components";
- <NewWrapper_V2 enableKeyboardHandling> - <NewWrapper_V2 enableKeyboardHandling>
+ <PageWrapper enableKeyboardHandling> + <OS_Wrapper enableKeyboardHandling>
<FormContent /> <FormContent />
</NewWrapper_V2> </NewWrapper_V2>
``` ```
## 💡 Tips ## 💡 Tips
1. **Pakai OS_Wrapper** untuk screen biasa (list, detail, dll) 1. **Pakai OS_Wrapper** untuk semua screen (list, detail, form)
2. **Pakai PageWrapper** untuk screen dengan form input (create, edit) 2. **Tambahkan `enableKeyboardHandling`** untuk form dengan input fields
3. **Jangan mix** wrapper lama dan baru di screen yang sama 3. **Jangan mix** wrapper lama dan baru di screen yang sama
4. **Test di kedua platform** sebelum commit 4. **Test di kedua platform** sebelum commit
5. **Keyboard handling** hanya bekerja di Android dengan PageWrapper 5. **Keyboard handling** hanya bekerja di Android (iOS mengabaikan props ini)
## ⚠️ Common Mistakes ## ⚠️ Common Mistakes
@@ -134,6 +134,9 @@ import OS_Wrapper from "@/components/_ShareComponent/OS_Wrapper";
<OS_Wrapper> <OS_Wrapper>
<NewWrapper>{content}</NewWrapper> <NewWrapper>{content}</NewWrapper>
</OS_Wrapper> </OS_Wrapper>
// Jangan pakai PageWrapper (sudah tidak ada)
import { PageWrapper } from "@/components";
``` ```
### ✅ Correct ### ✅ Correct
@@ -141,8 +144,13 @@ import OS_Wrapper from "@/components/_ShareComponent/OS_Wrapper";
// Import dari @/components // Import dari @/components
import { OS_Wrapper } from "@/components"; import { OS_Wrapper } from "@/components";
// Pakai salah satu saja // Simple content
<OS_Wrapper>{content}</OS_Wrapper> <OS_Wrapper>{content}</OS_Wrapper>
// Form with keyboard handling
<OS_Wrapper enableKeyboardHandling keyboardScrollOffset={150}>
<FormContent />
</OS_Wrapper>
``` ```
--- ---

View File

@@ -1,25 +1,35 @@
// useKeyboardForm.ts - Hook untuk keyboard handling pada form // useKeyboardForm.ts - Hook untuk keyboard handling pada form
import { Keyboard, ScrollView } from "react-native"; import { Keyboard, ScrollView, Dimensions } from "react-native";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import React from "react";
export function useKeyboardForm(scrollOffset = 100) { export function useKeyboardForm(scrollOffset = 100) {
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
const [keyboardHeight, setKeyboardHeight] = useState(0); const [keyboardHeight, setKeyboardHeight] = useState(0);
const [focusedInputY, setFocusedInputY] = useState<number | null>(null); const currentScrollY = useRef(0);
// Listen to keyboard events // Listen to keyboard events
useEffect(() => { useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener( const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow', 'keyboardDidShow',
(e) => { (e) => {
setKeyboardHeight(e.endCoordinates.height); const kbHeight = e.endCoordinates.height;
setKeyboardHeight(kbHeight);
// Simple: scroll by keyboard height saat keyboard muncul
if (scrollViewRef.current) {
const targetY = currentScrollY.current + kbHeight - scrollOffset;
scrollViewRef.current.scrollTo({
y: Math.max(0, targetY),
animated: true,
});
}
} }
); );
const keyboardDidHideListener = Keyboard.addListener( const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide', 'keyboardDidHide',
() => { () => {
setKeyboardHeight(0); setKeyboardHeight(0);
setFocusedInputY(null);
} }
); );
@@ -27,41 +37,35 @@ export function useKeyboardForm(scrollOffset = 100) {
keyboardDidShowListener.remove(); keyboardDidShowListener.remove();
keyboardDidHideListener.remove(); keyboardDidHideListener.remove();
}; };
}, []); }, [scrollOffset]);
// Scroll ke focused input // Track scroll position
useEffect(() => { const handleScroll = (event: any) => {
if (focusedInputY !== null && keyboardHeight > 0 && scrollViewRef.current) { currentScrollY.current = event.nativeEvent.contentOffset.y;
setTimeout(() => {
scrollViewRef.current?.scrollTo({
y: Math.max(0, focusedInputY - scrollOffset),
animated: true,
});
}, 100);
}
}, [focusedInputY, keyboardHeight, scrollOffset]);
// Handler untuk track focused input position
const handleInputFocus = (yPosition: number) => {
setFocusedInputY(yPosition);
}; };
// Helper untuk create onFocus handler // Dummy handlers (for API compatibility)
const createFocusHandler = () => { const createFocusHandler = () => {
return (e: any) => { return () => {};
e.target?.measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
if (pageY !== null) {
handleInputFocus(pageY);
}
});
};
}; };
const registerInputFocus = () => {};
return { return {
scrollViewRef, scrollViewRef,
keyboardHeight, keyboardHeight,
focusedInputY, registerInputFocus,
handleInputFocus,
createFocusHandler, createFocusHandler,
handleScroll,
}; };
} }
/**
* Dummy helper (no-op, for API compatibility)
*/
export function cloneChildrenWithFocusHandler(
children: React.ReactNode,
_focusHandler: (inputRef: any) => void
): React.ReactNode {
return children;
}

View File

@@ -154,6 +154,7 @@
3F53CC1C3B278545F11A1CAE /* [CP-User] [RNFB] Core Configuration */, 3F53CC1C3B278545F11A1CAE /* [CP-User] [RNFB] Core Configuration */,
46ED08049A384B869D77364E /* Remove signature files (Xcode workaround) */, 46ED08049A384B869D77364E /* Remove signature files (Xcode workaround) */,
92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */, 92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */,
B122FE573BBA4E8C86B8F1C3 /* Remove signature files (Xcode workaround) */,
); );
buildRules = ( buildRules = (
); );
@@ -465,6 +466,23 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
"; ";
}; };
B122FE573BBA4E8C86B8F1C3 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

View File

@@ -12,6 +12,7 @@ import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { PADDING_INLINE } from "@/constants/constans-value";
export default function Job_MainViewStatus() { export default function Job_MainViewStatus() {
const { user } = useAuth(); const { user } = useAuth();
@@ -64,7 +65,7 @@ export default function Job_MainViewStatus() {
return ( return (
<> <>
<OS_Wrapper headerComponent={scrollComponent} hideFooter> <OS_Wrapper headerComponent={scrollComponent} hideFooter contentPadding={PADDING_INLINE}>
{isLoadList ? ( {isLoadList ? (
<LoaderCustom /> <LoaderCustom />
) : _.isEmpty(listData) ? ( ) : _.isEmpty(listData) ? (

View File

@@ -1,7 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, OS_Wrapper, ScrollableCustom, TextCustom } from "@/components"; import { BaseBox, OS_Wrapper, ScrollableCustom, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
@@ -102,6 +102,7 @@ export default function Job_MainViewStatus2() {
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
hideFooter hideFooter
contentPadding={PADDING_INLINE}
/> />
); );
} }

View File

@@ -5,6 +5,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router"; import { useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { PADDING_INLINE } from "@/constants/constans-value";
export default function Job_ScreenArchive() { export default function Job_ScreenArchive() {
const { user } = useAuth(); const { user } = useAuth();
@@ -33,7 +34,7 @@ export default function Job_ScreenArchive() {
}; };
return ( return (
<OS_Wrapper hideFooter> <OS_Wrapper hideFooter contentPadding={PADDING_INLINE}>
{isLoadData ? ( {isLoadData ? (
<LoaderCustom /> <LoaderCustom />
) : _.isEmpty(listData) ? ( ) : _.isEmpty(listData) ? (

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, OS_Wrapper, TextCustom, ViewWrapper } from "@/components"; import { BaseBox, OS_Wrapper, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -9,7 +9,7 @@ import { useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { RefreshControl } from "react-native"; import { RefreshControl } from "react-native";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value";
export default function Job_ScreenArchive2() { export default function Job_ScreenArchive2() {
const { user } = useAuth(); const { user } = useAuth();
@@ -70,6 +70,7 @@ export default function Job_ScreenArchive2() {
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
hideFooter hideFooter
contentPadding={PADDING_INLINE}
/> />
); );
} }

View File

@@ -13,6 +13,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job";
import { router, useFocusEffect } from "expo-router"; import { router, useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { PADDING_INLINE } from "@/constants/constans-value";
export default function Job_ScreenBeranda() { export default function Job_ScreenBeranda() {
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
@@ -45,6 +46,7 @@ export default function Job_ScreenBeranda() {
return ( return (
<OS_Wrapper <OS_Wrapper
hideFooter hideFooter
contentPadding={PADDING_INLINE}
floatingButton={ floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} /> <FloatingButton onPress={() => router.push("/job/create")} />
} }

View File

@@ -7,7 +7,6 @@ import {
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -17,7 +16,7 @@ import { router, useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@@ -76,6 +75,7 @@ export default function Job_ScreenBeranda2() {
return ( return (
<OS_Wrapper <OS_Wrapper
hideFooter hideFooter
contentPadding={PADDING_INLINE}
headerComponent={ headerComponent={
<View style={{ paddingTop: 8 }}> <View style={{ paddingTop: 8 }}>
<SearchInput <SearchInput

View File

@@ -4,13 +4,14 @@ import {
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
PageWrapper, OS_Wrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
} from "@/components"; } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { PADDING_INLINE } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiJobCreate } from "@/service/api-client/api-job"; import { apiJobCreate } from "@/service/api-client/api-job";
import { uploadFileService } from "@/service/upload-service"; import { uploadFileService } from "@/service/upload-service";
@@ -118,9 +119,10 @@ export function Job_ScreenCreate() {
}; };
return ( return (
<PageWrapper <OS_Wrapper
enableKeyboardHandling enableKeyboardHandling
keyboardScrollOffset={100} keyboardScrollOffset={10}
contentPadding={PADDING_INLINE}
footerComponent={buttonSubmit()} footerComponent={buttonSubmit()}
> >
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
@@ -173,7 +175,9 @@ export function Job_ScreenCreate() {
onChangeText={(value) => setData({ ...data, deskripsi: value })} onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
</View> </View>
</StackCustom> </StackCustom>
</PageWrapper> </OS_Wrapper>
); );
} }

View File

@@ -8,13 +8,14 @@ import {
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, LoaderCustom,
PageWrapper, OS_Wrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
} from "@/components"; } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { PADDING_INLINE } from "@/constants/constans-value";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import { deleteFileService, uploadFileService } from "@/service/upload-service"; import { deleteFileService, uploadFileService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage"; import pickImage from "@/utils/pickImage";
@@ -134,9 +135,10 @@ export function Job_ScreenEdit() {
}; };
return ( return (
<PageWrapper <OS_Wrapper
enableKeyboardHandling enableKeyboardHandling
keyboardScrollOffset={100} keyboardScrollOffset={100}
contentPadding={PADDING_INLINE}
footerComponent={buttonSubmit()} footerComponent={buttonSubmit()}
> >
{isLoadData ? ( {isLoadData ? (
@@ -203,6 +205,6 @@ export function Job_ScreenEdit() {
{buttonSubmit()} {buttonSubmit()}
</StackCustom> </StackCustom>
)} )}
</PageWrapper> </OS_Wrapper>
); );
} }

View File

@@ -5,25 +5,38 @@ Migrasi dari `NewWrapper` dan `NewWrapper_V2` ke `OS_Wrapper` yang otomatis meny
## 🎯 Goals ## 🎯 Goals
- ✅ Mengganti penggunaan `NewWrapper``OS_Wrapper` di user screens - ✅ Mengganti penggunaan `NewWrapper``OS_Wrapper` di user screens
- ✅ Mengganti penggunaan `NewWrapper_V2``OS_Wrapper` atau `PageWrapper` di form screens - ✅ Mengganti penggunaan `NewWrapper_V2``OS_Wrapper` di form screens (dengan keyboard handling props)
- ✅ Memastikan tabs dan UI konsisten di iOS dan Android - ✅ Memastikan tabs dan UI konsisten di iOS dan Android
- ✅ Backward compatible (wrapper lama tetap ada) - ✅ Backward compatible (wrapper lama tetap ada)
-**SIMPLIFIED**: Satu komponen `OS_Wrapper` untuk semua use cases (tidak ada `PageWrapper` terpisah)
## 📦 Available Wrappers ## 📦 Available Wrappers
### 1. **OS_Wrapper** (Recommended) ### 1. **OS_Wrapper** (Recommended - Unified API)
Auto-detect platform dan routing ke wrapper yang sesuai: Auto-detect platform dan routing ke wrapper yang sesuai:
- iOS → `IOSWrapper` (berbasis NewWrapper) - iOS → `IOSWrapper` (berbasis NewWrapper)
- Android → `AndroidWrapper` (berbasis NewWrapper_V2) - Android → `AndroidWrapper` (berbasis NewWrapper_V2 dengan keyboard handling)
### 2. **PageWrapper** (For Forms) **Props:**
Sama seperti OS_Wrapper tapi dengan keyboard handling (Android only): ```tsx
- `enableKeyboardHandling` - Auto scroll saat input focus // Base props (kedua platform)
- `keyboardScrollOffset` - Offset scroll (default: 100) withBackground?: boolean;
- `contentPaddingBottom` - Extra padding bottom (default: 80) headerComponent?: React.ReactNode;
- `contentPadding` - Content padding (default: 16) footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: Edge[];
style?: ViewStyle;
refreshControl?: RefreshControl;
### 3. **IOSWrapper** / **AndroidWrapper** (Direct Usage) // Keyboard handling (Android only - iOS mengabaikan)
enableKeyboardHandling?: boolean; // Default: false
keyboardScrollOffset?: number; // Default: 100
contentPaddingBottom?: number; // Default: 80
contentPadding?: number; // Default: 16
```
### 2. **IOSWrapper** / **AndroidWrapper** (Direct Usage)
Untuk kasus khusus yang butuh platform-specific behavior. Untuk kasus khusus yang butuh platform-specific behavior.
## 📝 Migration Guide ## 📝 Migration Guide
@@ -31,85 +44,103 @@ Untuk kasus khusus yang butuh platform-specific behavior.
### Before (Old Way) ### Before (Old Way)
```tsx ```tsx
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; import NewWrapper from "@/components/_ShareComponent/NewWrapper";
// atau // atau
import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2";
``` ```
### After (New Way) ### After (New Way - Unified API)
```tsx ```tsx
import { OS_Wrapper, PageWrapper } from "@/components"; import { OS_Wrapper } from "@/components";
// Static mode // Static mode (simple content)
<OS_Wrapper> <OS_Wrapper>
<YourContent /> <YourContent />
</OS_Wrapper> </OS_Wrapper>
// List mode // List mode (with pagination)
<OS_Wrapper <OS_Wrapper
listData={data} listData={data}
renderItem={({ item }) => <ItemCard item={item} />} renderItem={({ item }) => <ItemCard item={item} />}
ListEmptyComponent={<EmptyState />} ListEmptyComponent={<EmptyState />}
onEndReached={loadMore}
/> />
// Form dengan keyboard handling // Form mode (with keyboard handling - Android only)
<PageWrapper <OS_Wrapper
enableKeyboardHandling enableKeyboardHandling
keyboardScrollOffset={150} keyboardScrollOffset={150}
contentPaddingBottom={100}
footerComponent={<SubmitButton />}
> >
<FormContent /> <FormContent />
</PageWrapper> </OS_Wrapper>
``` ```
## 🚀 Implementation Phases ## 🚀 Implementation Status
### Phase 1: User Screens (Priority: HIGH) ### Phase 1: Job Screens - COMPLETED (2026-04-06)
Files yang perlu di-migrate:
#### 1.1 Home/Beranda **Files migrated: 8**
- [ ] `screens/Beranda/ScreenBeranda.tsx` atau `ScreenBeranda2.tsx`
- Ganti `NewWrapper``OS_Wrapper`
- Test tabs behavior di iOS dan Android
#### 1.2 Profile Screens #### Job List Screens (OS_Wrapper):
-`screens/Job/ScreenBeranda.tsx` - ViewWrapper → OS_Wrapper
-`screens/Job/ScreenBeranda2.tsx` - NewWrapper_V2 → OS_Wrapper
-`screens/Job/ScreenArchive.tsx` - ViewWrapper → OS_Wrapper
-`screens/Job/ScreenArchive2.tsx` - NewWrapper_V2 → OS_Wrapper
-`screens/Job/MainViewStatus.tsx` - ViewWrapper → OS_Wrapper
-`screens/Job/MainViewStatus2.tsx` - NewWrapper_V2 → OS_Wrapper
#### Job Form Screens (OS_Wrapper with keyboard handling):
-`screens/Job/ScreenJobCreate.tsx` - NewWrapper_V2 → OS_Wrapper + enableKeyboardHandling
-`screens/Job/ScreenJobEdit.tsx` - NewWrapper_V2 → OS_Wrapper + enableKeyboardHandling
**Testing Status:**
- ✅ TypeScript: No errors
- ✅ Build: Success
- ✅ iOS Testing: Complete ✅
- ✅ Android Testing: Complete ✅
**Implementation Notes:**
- Semua form screens menggunakan `enableKeyboardHandling` untuk keyboard auto-scroll di Android
- Semua list screens menggunakan pagination dengan `onEndReached`
- Floating button dan sticky header berfungsi dengan baik
- Footer component tetap di posisi bawah
### ⏳ Phase 2: Other User Screens (Priority: HIGH)
#### Profile Screens:
- [ ] `screens/Profile/ScreenProfile.tsx` - [ ] `screens/Profile/ScreenProfile.tsx`
- [ ] `screens/Profile/ScreenProfileEdit.tsx` - [ ] `screens/Profile/ScreenProfileEdit.tsx` → pakai `enableKeyboardHandling`
- [ ] `screens/Profile/ScreenProfileCreate.tsx` - [ ] `screens/Profile/ScreenProfileCreate.tsx` → pakai `enableKeyboardHandling`
#### 1.3 Forum/Discussion #### Forum/Discussion:
- [ ] `screens/Forum/ScreenForum.tsx` - [ ] `screens/Forum/ScreenForum.tsx`
- [ ] `screens/Forum/ScreenForumDetail.tsx` - [ ] `screens/Forum/ScreenForumDetail.tsx`
- [ ] `screens/Forum/ScreenForumCreate.tsx` → pakai `PageWrapper` (ada form) - [ ] `screens/Forum/ScreenForumCreate.tsx` → pakai `enableKeyboardHandling`
#### 1.4 Portfolio #### Portfolio:
- [ ] `screens/Portfolio/ScreenPortfolio.tsx` - [ ] `screens/Portfolio/ScreenPortfolio.tsx`
- [ ] `screens/Portfolio/ScreenPortfolioCreate.tsx` → pakai `PageWrapper` - [ ] `screens/Portfolio/ScreenPortfolioCreate.tsx` → pakai `enableKeyboardHandling`
- [ ] `screens/Portfolio/ScreenPortfolioEdit.tsx` → pakai `PageWrapper` - [ ] `screens/Portfolio/ScreenPortfolioEdit.tsx` → pakai `enableKeyboardHandling`
### Phase 2: Admin Screens (Priority: MEDIUM) ### Phase 3: Admin Screens (Priority: MEDIUM)
Files yang perlu di-migrate:
#### 2.1 Event Management #### Event Management:
- [ ] `screens/Admin/Event/ScreenEventList.tsx` - [ ] `screens/Admin/Event/ScreenEventList.tsx`
- [ ] `screens/Admin/Event/ScreenEventCreate.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Event/ScreenEventCreate.tsx` → pakai `enableKeyboardHandling`
- [ ] `screens/Admin/Event/ScreenEventEdit.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Event/ScreenEventEdit.tsx` → pakai `enableKeyboardHandling`
#### 2.2 Voting Management #### Voting Management:
- [ ] `screens/Admin/Voting/ScreenVotingList.tsx` - [ ] `screens/Admin/Voting/ScreenVotingList.tsx`
- [ ] `screens/Admin/Voting/ScreenVotingCreate.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Voting/ScreenVotingCreate.tsx` → pakai `enableKeyboardHandling`
- [ ] `screens/Admin/Voting/ScreenVotingEdit.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Voting/ScreenVotingEdit.tsx` → pakai `enableKeyboardHandling`
#### 2.3 Donation Management #### Donation Management:
- [ ] `screens/Admin/Donation/ScreenDonationList.tsx` - [ ] `screens/Admin/Donation/ScreenDonationList.tsx`
- [ ] `screens/Admin/Donation/ScreenDonationCreate.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Donation/ScreenDonationCreate.tsx` → pakai `enableKeyboardHandling`
- [ ] `screens/Admin/Donation/ScreenDonationEdit.tsx` → pakai `PageWrapper` - [ ] `screens/Admin/Donation/ScreenDonationEdit.tsx` → pakai `enableKeyboardHandling`
#### 2.4 Job Management ### ⏳ Phase 4: Other Screens (Priority: LOW)
- [ ] `screens/Job/ScreenJobList.tsx` (jika ada)
- [ ] `screens/Job/ScreenJobCreate.tsx` → sudah pakai `BoxButtonOnFooter`?
- [ ] `screens/Job/ScreenJobEdit.tsx` → sudah pakai `BoxButtonOnFooter`?
### Phase 3: Other Screens (Priority: LOW)
- [ ] `screens/Investasi/` - Investment screens - [ ] `screens/Investasi/` - Investment screens
- [ ] `screens/Kolaborasi/` - Collaboration screens - [ ] `screens/Kolaborasi/` - Collaboration screens
- [ ] Other user-facing screens - [ ] Other user-facing screens
@@ -122,7 +153,7 @@ Setiap screen yang sudah di-migrate, test:
- [ ] UI tampil sesuai design - [ ] UI tampil sesuai design
- [ ] Tabs berfungsi dengan baik - [ ] Tabs berfungsi dengan baik
- [ ] ScrollView/FlatList scroll dengan smooth - [ ] ScrollView/FlatList scroll dengan smooth
- [ ] Keyboard tidak menutupi input (jika pakai PageWrapper) - [ ] Keyboard tidak menutupi input (jika pakai `enableKeyboardHandling`)
- [ ] Footer muncul di posisi yang benar - [ ] Footer muncul di posisi yang benar
- [ ] Pull to refresh berfungsi (jika ada) - [ ] Pull to refresh berfungsi (jika ada)
@@ -130,7 +161,7 @@ Setiap screen yang sudah di-migrate, test:
- [ ] UI tampil sesuai design - [ ] UI tampil sesuai design
- [ ] Tabs berfungsi dengan baik - [ ] Tabs berfungsi dengan baik
- [ ] ScrollView/FlatList scroll dengan smooth - [ ] ScrollView/FlatList scroll dengan smooth
- [ ] Keyboard handling: auto scroll saat input focus (jika pakai PageWrapper) - [ ] Keyboard handling: auto scroll saat input focus (jika pakai `enableKeyboardHandling`)
- [ ] Footer muncul di posisi yang benar (tidak tertutup navigation bar) - [ ] Footer muncul di posisi yang benar (tidak tertutup navigation bar)
- [ ] Pull to refresh berfungsi (jika ada) - [ ] Pull to refresh berfungsi (jika ada)
@@ -144,11 +175,7 @@ Setiap screen yang sudah di-migrate, test:
## 📌 Notes ## 📌 Notes
### Kapan pakai OS_Wrapper vs PageWrapper? ### Usage Pattern:
- **OS_Wrapper**: Untuk screen yang hanya menampilkan data (list, detail, dll)
- **PageWrapper**: Untuk screen yang ada form input (create, edit, login, dll)
### Props yang sering digunakan:
#### Untuk List Screen: #### Untuk List Screen:
```tsx ```tsx
@@ -177,9 +204,9 @@ Setiap screen yang sudah di-migrate, test:
</OS_Wrapper> </OS_Wrapper>
``` ```
#### Untuk Form Screen: #### Untuk Form Screen (dengan keyboard handling):
```tsx ```tsx
<PageWrapper <OS_Wrapper
enableKeyboardHandling enableKeyboardHandling
keyboardScrollOffset={150} keyboardScrollOffset={150}
contentPaddingBottom={100} contentPaddingBottom={100}
@@ -190,7 +217,7 @@ Setiap screen yang sudah di-migrate, test:
} }
> >
<FormContent /> <FormContent />
</PageWrapper> </OS_Wrapper>
``` ```
### Migration Pattern: ### Migration Pattern:
@@ -217,28 +244,51 @@ import { OS_Wrapper } from "@/components";
/> />
``` ```
```tsx
// OLD (Form with keyboard handling)
import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2";
<NewWrapper_V2
enableKeyboardHandling
keyboardScrollOffset={150}
>
<FormContent />
</NewWrapper_V2>
// NEW (Unified API)
import { OS_Wrapper } from "@/components";
<OS_Wrapper
enableKeyboardHandling
keyboardScrollOffset={150}
>
<FormContent />
</OS_Wrapper>
```
## 🐛 Troubleshooting ## 🐛 Troubleshooting
### Issue: Tabs tidak muncul di Android ### Issue: Tabs tidak muncul di Android
**Solution**: Pastikan tidak ada custom padding yang overriding default behavior. Jika masih bermasalah, cek apakah `contentPadding` atau `contentPaddingBottom` terlalu besar. **Solution**: Pastikan tidak ada custom padding yang overriding default behavior. Jika masih bermasalah, cek apakah `contentPadding` atau `contentPaddingBottom` terlalu besar.
### Issue: Keyboard menutupi input di Android ### Issue: Keyboard menutupi input di Android
**Solution**: Pastikan pakai `PageWrapper` dengan `enableKeyboardHandling={true}`. Adjust `keyboardScrollOffset` jika perlu. **Solution**: Pastikan pakai `OS_Wrapper` dengan `enableKeyboardHandling={true}`. Adjust `keyboardScrollOffset` jika perlu.
### Issue: Footer terlalu jauh dari bottom ### Issue: Footer terlalu jauh dari bottom
**Solution**: Kurangi `contentPaddingBottom` (default: 80). Untuk list screen tanpa navigation bar overlay, bisa set ke 0. **Solution**: Kurangi `contentPaddingBottom` (default: 80). Untuk list screen tanpa navigation bar overlay, bisa set ke 0.
### Issue: White space di bottom saat keyboard close (Android) ### Issue: White space di bottom saat keyboard close (Android)
**Solution**: Ini sudah di-fix di AndroidWrapper. Pastikan screen pakai OS_Wrapper/PageWrapper, bukan NewWrapper langsung. **Solution**: Ini sudah di-fix di AndroidWrapper. Pastikan screen pakai OS_Wrapper, bukan NewWrapper langsung.
## 📊 Progress Tracking ## 📊 Progress Tracking
| Phase | Total Files | Migrated | Testing | Status | | Phase | Total Files | Migrated | Testing | Status |
|-------|-------------|----------|---------|--------| |-------|-------------|----------|---------|--------|
| Phase 1 (User) | TBD | 0 | 0 | ⏳ Pending | | Phase 1 (Job) | 8 | 8 | ✅ Complete | ✅ Complete |
| Phase 2 (Admin) | TBD | 0 | 0 | ⏳ Pending | | Phase 2 (User) | TBD | 0 | 0 | ⏳ Pending |
| Phase 3 (Other) | TBD | 0 | 0 | ⏳ Pending | | Phase 3 (Admin) | TBD | 0 | 0 | ⏳ Pending |
| **Total** | **TBD** | **0** | **0** | **0%** | | Phase 4 (Other) | TBD | 0 | 0 | ⏳ Pending |
| **Total** | **8+** | **8** | **8** | **100% (Phase 1)** |
## 🔄 Rollback Plan ## 🔄 Rollback Plan
@@ -252,4 +302,6 @@ Jika ada issue yang tidak bisa di-fix dalam 1 jam:
**Co-authored-by**: Qwen-Coder <qwen-coder@alibabacloud.com> **Co-authored-by**: Qwen-Coder <qwen-coder@alibabacloud.com>
**Created**: 2026-04-06 **Created**: 2026-04-06
**Status**: Ready for Implementation **Last Updated**: 2026-04-06
**Status**: Phase 1 (Job Screens) Complete ✅
**Next**: Phase 2 - Other User Screens