docs/plans/keyboard-controller-integration.md
Add react-native-keyboard-controller integration to Tamagui following the same pattern as gesture-handler and teleport in @tamagui/native. This enables smooth keyboard + sheet coordination where:
The current moveOnKeyboardChange prop in Sheet (lines 508-539 of SheetImplementationCustom.tsx) uses basic Keyboard.addListener() which:
enableBlurKeyboardOnGesture - keyboard dismisses when drag startskeyboardBlurBehavior="restore" - sheet returns to pre-keyboard positionFollowing the exact pattern of setup-gesture-handler.ts:
keyboardControllerState.ts)const GLOBAL_KEY = '__tamagui_native_keyboard_controller_state__'
export interface KeyboardControllerState {
enabled: boolean
KeyboardProvider: any
KeyboardAwareScrollView: any
useKeyboardHandler: any
useReanimatedKeyboardAnimation: any
KeyboardController: any
KeyboardStickyView: any
}
function getGlobalState(): KeyboardControllerState {
/* ... */
}
export function isKeyboardControllerEnabled(): boolean
export function getKeyboardControllerState(): KeyboardControllerState
export function setKeyboardControllerState(
updates: Partial<KeyboardControllerState>
): void
setup-keyboard-controller.ts)import { setKeyboardControllerState } from './keyboardControllerState'
function setup() {
const g = globalThis as any
if (g.__tamagui_native_keyboard_controller_setup_complete) return
g.__tamagui_native_keyboard_controller_setup_complete = true
try {
const rnkc = require('react-native-keyboard-controller')
const {
KeyboardProvider,
KeyboardAwareScrollView,
useKeyboardHandler,
useReanimatedKeyboardAnimation,
KeyboardController,
KeyboardStickyView,
} = rnkc
if (useKeyboardHandler && KeyboardProvider) {
setKeyboardControllerState({
enabled: true,
KeyboardProvider,
KeyboardAwareScrollView,
useKeyboardHandler,
useReanimatedKeyboardAnimation,
KeyboardController,
KeyboardStickyView,
})
}
} catch {
// keyboard-controller not available
}
}
setup()
setup-keyboard-controller.web.ts)// no-op on web
Add to @tamagui/native/package.json:
{
"peerDependencies": {
"react-native-keyboard-controller": ">=1.0.0"
},
"peerDependenciesMeta": {
"react-native-keyboard-controller": {
"optional": true
}
},
"exports": {
"./setup-keyboard-controller": {
"react-native": {
"types": "./types/setup-keyboard-controller.d.ts",
"module": "./dist/esm/setup-keyboard-controller.native.js",
"import": "./dist/esm/setup-keyboard-controller.native.js",
"require": "./dist/cjs/setup-keyboard-controller.native.js"
},
"types": "./types/setup-keyboard-controller.d.ts",
"module": "./dist/esm/setup-keyboard-controller.mjs",
"import": "./dist/esm/setup-keyboard-controller.mjs",
"require": "./dist/cjs/setup-keyboard-controller.cjs"
}
}
}
New hook: useKeyboardControllerSheet.ts
import { isKeyboardControllerEnabled, getKeyboardControllerState } from '@tamagui/native'
import { useSharedValue } from 'react-native-reanimated'
export function useKeyboardControllerSheet({
enabled,
onKeyboardShow,
onKeyboardHide,
animatedNumber,
positions,
position,
screenSize,
}: UseKeyboardControllerSheetOptions) {
const keyboardControllerEnabled = isKeyboardControllerEnabled()
const keyboardHeight = useSharedValue(0)
const basePosition = useSharedValue(0)
if (!keyboardControllerEnabled || !enabled) {
return { keyboardHeight: null }
}
const { useKeyboardHandler } = getKeyboardControllerState()
useKeyboardHandler({
onStart: (e) => {
'worklet'
// store position before keyboard
basePosition.value = positions[position]
},
onMove: (e) => {
'worklet'
// frame-by-frame tracking
keyboardHeight.value = e.height
const newY = Math.max(basePosition.value - e.height, 0)
// update sheet position directly via shared value
},
onEnd: (e) => {
'worklet'
// keyboard fully shown/hidden
},
})
return { keyboardHeight }
}
The key insight from gorhom/bottom-sheet: when gesture starts, blur keyboard first.
In useGestureHandlerPan.ts, add:
const { KeyboardController } = getKeyboardControllerState()
// in onStart handler:
if (keyboardControllerEnabled && enableBlurKeyboardOnGesture) {
KeyboardController.dismiss()
}
interface SheetProps {
// existing
moveOnKeyboardChange?: boolean
// new with keyboard-controller
keyboardBehavior?: 'interactive' | 'extend' | 'fillParent'
keyboardBlurBehavior?: 'none' | 'restore'
enableBlurKeyboardOnGesture?: boolean
}
code/core/native/src/keyboardControllerState.tscode/core/native/src/setup-keyboard-controller.tscode/core/native/src/setup-keyboard-controller.web.tscode/ui/sheet/src/useKeyboardControllerSheet.tscode/ui/sheet/src/useKeyboardControllerSheet.native.tscode/kitchen-sink/src/usecases/SheetKeyboardDragCase.tsxcode/kitchen-sink/e2e/SheetKeyboardDrag.test.tscode/core/native/package.json - add peer dep + exportscode/core/native/src/index.ts - export state functionscode/ui/sheet/src/SheetImplementationCustom.tsx - integrate hookcode/ui/sheet/src/types.ts - add new propsdocs/using-ios.md - add keyboard test workflowdescribe('SheetKeyboardDrag - Keyboard Controller Integration', () => {
it('should show keyboard-controller enabled', async () => {
// verify setup worked
})
it('Case 1: open keyboard, sheet moves up smoothly', async () => {
// tap input -> keyboard shows -> sheet animates up
// verify sheet position changed
})
it('Case 2: drag sheet down dismisses keyboard first', async () => {
// open keyboard
// start dragging sheet down
// keyboard should dismiss FIRST
// then sheet can drag down
})
it('Case 3: keyboard dismiss, sheet returns to original position', async () => {
// open keyboard (sheet moves up)
// dismiss keyboard (tap outside)
// sheet should animate back to original snap point
})
it('Case 4: rapid keyboard show/hide is smooth', async () => {
// focus input -> unfocus -> focus -> unfocus
// no janky animation
})
})
# 1. Start metro in background
cd code/kitchen-sink && yarn start > /tmp/metro.log 2>&1 &
# 2. Build app once
detox build -c ios.sim.debug
# 3. Run keyboard tests iteratively
detox test --reuse --retries 0 -t "keyboard" -c ios.sim.debug
# 4. Check screenshots in e2e/artifacts/
# Look for smooth keyboard + sheet coordination
# 5. Filter metro logs for keyboard events
grep -E "keyboard|Keyboard" /tmp/metro.log
KeyboardAwareScrollView integration for Sheet.ScrollViewKeyboardStickyView for input toolbars that stick above keyboard