Files
hipmi-mobile/hooks/useKeyboardForm.ts
bagasbanuna c3cf354c28 feat: Migrate Portfolio & Maps screens + perbaiki bug auto-scroll keyboard
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>
2026-04-08 17:27:06 +08:00

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;
});
}