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

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

View File

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

View File

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

View File

@@ -43,10 +43,10 @@ interface ListModeProps extends BaseProps {
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
// ========== PageWrapper Props (Android-specific keyboard handling) ==========
interface PageWrapperBaseProps extends BaseProps {
// ========== Keyboard Handling Props (Android only) ==========
interface KeyboardHandlingProps {
/**
* Enable keyboard handling (Android only - NewWrapper_V2)
* Enable keyboard handling with auto-scroll (Android only)
* iOS ignores this prop
* @default false
*/
@@ -74,81 +74,74 @@ interface PageWrapperBaseProps extends BaseProps {
contentPadding?: number;
}
interface PageWrapperStaticProps extends PageWrapperBaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
// ========== Final Props Types ==========
type OS_WrapperStaticProps = StaticModeProps & KeyboardHandlingProps;
type OS_WrapperListProps = ListModeProps & KeyboardHandlingProps;
interface PageWrapperListProps extends PageWrapperBaseProps {
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;
type OS_WrapperProps = OS_WrapperStaticProps | OS_WrapperListProps;
/**
* 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
* <OS_Wrapper>
* <YourContent />
* </OS_Wrapper>
* ```
*
* @example List Mode
*
* @example List Mode (with pagination)
* ```tsx
* <OS_Wrapper
* listData={data}
* renderItem={({ item }) => <ItemCard item={item} />}
* 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) {
const {
enableKeyboardHandling = false,
keyboardScrollOffset = 100,
contentPaddingBottom = 250,
contentPadding = 0,
...wrapperProps
} = props;
// iOS uses IOSWrapper (based on NewWrapper)
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)
return <AndroidWrapper {...props} />;
}
/**
* PageWrapper - OS_Wrapper with keyboard handling support (Android only)
* Use this for forms with input fields
*
* @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} />;
// Android uses AndroidWrapper (with keyboard handling support)
return (
<AndroidWrapper
{...(wrapperProps as any)}
enableKeyboardHandling={enableKeyboardHandling}
keyboardScrollOffset={keyboardScrollOffset}
contentPaddingBottom={contentPaddingBottom}
contentPadding={contentPadding}
/>
);
}
// Re-export individual wrappers for direct usage if needed