Phase 3 - Portfolio Screens (6 files):
- [id]/index.tsx: ViewWrapper → OS_Wrapper (detail dengan pull-to-refresh)
- [id]/edit.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)
- [id]/edit-logo.tsx: ViewWrapper → OS_Wrapper (upload logo)
- [id]/edit-social-media.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)
- ViewListPortofolio.tsx: NewWrapper → OS_Wrapper (pagination list)
- ScreenPortofolioCreate.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)
Phase 4 - Maps Screens (2 files):
- ScreenMapsCreate.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)
- ScreenMapsEdit.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)
Bug Fixes:
- Perbaiki auto-scroll keyboard yang membuat input paling atas 'terlempar' keluar layar
- Gunakan UIManager.measure untuk mendapatkan posisi absolut input (pageY) secara akurat
- Logika conditional scroll:
* Jika input terlihat (di atas keyboard) → TIDAK SCROLL
* Jika input tertutup keyboard → Scroll secukupnya
- Helper cloneChildrenWithFocusHandler sekarang aktif menyuntikan onFocus handler ke semua TextInput/TextArea/PhoneInput/Select
- Hapus KeyboardAvoidingView dari AndroidWrapper static mode (tidak diperlukan lagi)
Pattern yang diterapkan:
- List screens: contentPaddingBottom=100 (default)
- Form screens: contentPaddingBottom={250} + enableKeyboardHandling
- NO PADDING_INLINE (sesuai preferensi user - mencegah box menyempit)
Dokumentasi:
- Update TASK-005 dengan status lengkap Phase 1-4 (27 files migrated)
- Tambahkan urutan phase baru: Event (Phase 5), Voting (Phase 6), Forum (Phase 7), Donation (Phase 8)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
// useKeyboardForm.ts - Hook untuk keyboard handling pada form
|
|
import { Keyboard, ScrollView, Dimensions, findNodeHandle, UIManager } from "react-native";
|
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
|
export function useKeyboardForm(scrollOffset = 100) {
|
|
const scrollViewRef = useRef<ScrollView>(null);
|
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
const currentScrollY = useRef(0);
|
|
const inputPageY = useRef(0);
|
|
const screenHeight = Dimensions.get('window').height;
|
|
|
|
// Fungsi untuk mengukur posisi absolut input
|
|
const handleInputFocus = useCallback((target: any) => {
|
|
const nodeHandle = findNodeHandle(target);
|
|
if (nodeHandle) {
|
|
UIManager.measure(nodeHandle, (x, y, width, height, pageX, pageY) => {
|
|
if (pageY !== undefined && pageY !== null) {
|
|
inputPageY.current = pageY;
|
|
}
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
// Listen to keyboard events
|
|
useEffect(() => {
|
|
const keyboardDidShowListener = Keyboard.addListener(
|
|
'keyboardDidShow',
|
|
(e) => {
|
|
const kbHeight = e.endCoordinates.height;
|
|
setKeyboardHeight(kbHeight);
|
|
|
|
// Conditional scroll: hanya scroll jika input tertutup keyboard
|
|
if (scrollViewRef.current) {
|
|
const touchAbsoluteY = inputPageY.current;
|
|
|
|
// Posisi Y teratas keyboard (dari atas layar)
|
|
const keyboardTopY = screenHeight - kbHeight;
|
|
|
|
// Jika input ADA DI BAWAH keyboard (tertutup)
|
|
if (touchAbsoluteY > keyboardTopY) {
|
|
// Hitung berapa harus scroll agar input terlihat di atas keyboard
|
|
const scrollBy = touchAbsoluteY - keyboardTopY + scrollOffset;
|
|
const targetY = currentScrollY.current + scrollBy;
|
|
|
|
scrollViewRef.current.scrollTo({
|
|
y: Math.max(0, targetY),
|
|
animated: true,
|
|
});
|
|
}
|
|
// Jika input SUDAH TERLIHAT (di atas keyboard), JANGAN SCROLL
|
|
}
|
|
}
|
|
);
|
|
|
|
const keyboardDidHideListener = Keyboard.addListener(
|
|
'keyboardDidHide',
|
|
() => {
|
|
setKeyboardHeight(0);
|
|
inputPageY.current = 0;
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
keyboardDidShowListener.remove();
|
|
keyboardDidHideListener.remove();
|
|
};
|
|
}, [scrollOffset, screenHeight]);
|
|
|
|
// Track scroll position
|
|
const handleScroll = (event: any) => {
|
|
currentScrollY.current = event.nativeEvent.contentOffset.y;
|
|
};
|
|
|
|
return {
|
|
scrollViewRef,
|
|
keyboardHeight,
|
|
handleInputFocus,
|
|
handleScroll,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper untuk inject onFocus handler ke semua TextInput/TextArea children
|
|
* Menggunakan UI.measure untuk mendapatkan posisi absolut input secara akurat
|
|
*/
|
|
export function cloneChildrenWithFocusHandler(
|
|
children: React.ReactNode,
|
|
focusHandler: (target: any) => void
|
|
): React.ReactNode {
|
|
if (!children) return children;
|
|
const React = require("react");
|
|
|
|
return React.Children.map(children, (child: any) => {
|
|
if (!React.isValidElement(child)) return child;
|
|
|
|
const childType = child.type;
|
|
const childProps = child.props as Record<string, any> || {};
|
|
|
|
// Check if it's a text input component
|
|
let isTextInput = false;
|
|
|
|
if (typeof childType === 'string') {
|
|
isTextInput = childType.toLowerCase().includes('textinput');
|
|
} else if (childType) {
|
|
isTextInput =
|
|
(childType as any).displayName?.includes('TextInput') ||
|
|
(childType as any).name?.includes('TextInput') ||
|
|
(childType as any).displayName?.includes('TextArea') ||
|
|
(childType as any).name?.includes('TextArea') ||
|
|
(childType as any).displayName?.includes('PhoneInput') ||
|
|
(childType as any).name?.includes('PhoneInput') ||
|
|
(childType as any).displayName?.includes('Select') ||
|
|
(childType as any).name?.includes('Select');
|
|
}
|
|
|
|
if (isTextInput) {
|
|
const existingOnFocus = childProps.onFocus;
|
|
return React.cloneElement(child, {
|
|
...childProps,
|
|
onFocus: (e: any) => {
|
|
existingOnFocus?.(e);
|
|
focusHandler(e.target);
|
|
},
|
|
} as any);
|
|
}
|
|
|
|
// Recursively clone nested children
|
|
if (childProps.children) {
|
|
return React.cloneElement(child, {
|
|
...childProps,
|
|
children: cloneChildrenWithFocusHandler(childProps.children, focusHandler),
|
|
} as any);
|
|
}
|
|
|
|
return child;
|
|
});
|
|
} |