diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000000..0385814dd0 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,22 @@ +{ + "name": "react-native-keyboard-controller", + "interface": { + "displayName": "React Native Keyboard Controller" + }, + "plugins": [ + { + "name": "react-native-keyboard-controller", + "source": { + "source": "npm", + "package": "react-native-keyboard-controller", + "version": "latest", + "registry": "https://registry.npmjs.org" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000000..ce942f72a3 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,36 @@ +{ + "name": "react-native-keyboard-controller", + "version": "1.22.0", + "description": "Portable skills for building and migrating React Native keyboard experiences with React Native Keyboard Controller.", + "author": { + "name": "Kiryl Ziusko", + "email": "zyusko.kirik@gmail.com", + "url": "https://github.com/kirillzyusko" + }, + "homepage": "https://kirillzyusko.github.io/react-native-keyboard-controller/", + "repository": "https://github.com/kirillzyusko/react-native-keyboard-controller", + "license": "MIT", + "keywords": [ + "react-native", + "keyboard", + "agent-skills", + "codex", + "migration", + "chat" + ], + "skills": "./skills/", + "interface": { + "displayName": "React Native Keyboard Controller", + "shortDescription": "Build reliable React Native keyboard experiences.", + "longDescription": "Choose the correct keyboard architecture, migrate competing keyboard solutions, build production chat layouts, modernize KeyboardToolbar, and avoid unnecessary keyboard-state renders.", + "developerName": "Kiryl Ziusko", + "category": "Developer Tools", + "capabilities": ["Read", "Write"], + "websiteURL": "https://kirillzyusko.github.io/react-native-keyboard-controller/", + "defaultPrompt": [ + "Choose the right RNKC architecture for this screen.", + "Migrate this keyboard implementation safely to RNKC.", + "Build a production-ready RNKC chat keyboard layout." + ] + } +} diff --git a/package.json b/package.json index 52d395bf04..877c3038f4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "react-native": "src/index", "source": "src/index", "files": [ + ".codex-plugin", + "skills", "src", "lib", "android", @@ -43,6 +45,8 @@ "lint-clang": "find ios/ -iname *.h -o -iname *.m -o -iname *.mm | grep -v -e Pods -e build | xargs clang-format -i -n --Werror", "prepare": "bob build > /dev/null 2>&1", "release": "release-it", + "plugin:check-version": "node scripts/sync-plugin-version.mjs --check", + "plugin:sync-version": "node scripts/sync-plugin-version.mjs", "example": "yarn --cwd example", "pods": "cd example && pod-install --quiet", "bootstrap": "yarn example && yarn && yarn pods" @@ -73,7 +77,10 @@ "ios", "android" ], - "repository": "https://github.com/kirillzyusko/react-native-keyboard-controller", + "repository": { + "type": "git", + "url": "git+https://github.com/kirillzyusko/react-native-keyboard-controller.git" + }, "author": "Kiryl Ziusko (https://github.com/kirillzyusko)", "license": "MIT", "bugs": { @@ -147,6 +154,9 @@ ] }, "release-it": { + "hooks": { + "after:bump": "yarn plugin:sync-version" + }, "git": { "commitMessage": "chore: release ${version}", "tagName": "v${version}" diff --git a/scripts/sync-plugin-version.mjs b/scripts/sync-plugin-version.mjs new file mode 100644 index 0000000000..2b07058d80 --- /dev/null +++ b/scripts/sync-plugin-version.mjs @@ -0,0 +1,24 @@ +import { readFile, writeFile } from "node:fs/promises"; + +const checkOnly = process.argv.includes("--check"); +const packageUrl = new URL("../package.json", import.meta.url); +const pluginUrl = new URL("../.codex-plugin/plugin.json", import.meta.url); + +const packageJson = JSON.parse(await readFile(packageUrl, "utf8")); +const pluginJson = JSON.parse(await readFile(pluginUrl, "utf8")); + +if (pluginJson.version === packageJson.version) { + console.log(`Plugin version matches package version ${packageJson.version}.`); + process.exit(0); +} + +if (checkOnly) { + console.error( + `Plugin version ${pluginJson.version} does not match package version ${packageJson.version}.`, + ); + process.exit(1); +} + +pluginJson.version = packageJson.version; +await writeFile(pluginUrl, `${JSON.stringify(pluginJson, null, 2)}\n`); +console.log(`Updated plugin version to ${packageJson.version}.`); diff --git a/skills/build-rnkc-chat-screen/SKILL.md b/skills/build-rnkc-chat-screen/SKILL.md new file mode 100644 index 0000000000..c08851cb90 --- /dev/null +++ b/skills/build-rnkc-chat-screen/SKILL.md @@ -0,0 +1,88 @@ +--- +name: build-rnkc-chat-screen +description: Build, migrate, or review production React Native chat keyboard layouts with React Native Keyboard Controller. Use when implementing KeyboardChatScrollView, KeyboardStickyView composers, KeyboardGestureArea interactive dismissal, FlatList, FlashList, LegendList or custom virtualized-list adapters, inverted chats, safe-area or tab offsets, growing multiline inputs, emoji or bottom-sheet transitions, freeze behavior, keyboardLiftBehavior, jump-to-latest UI, AI streaming blankSpace, keyboard translucency, or chat keyboard troubleshooting and performance. +license: MIT +metadata: + author: kirillzyusko + source: react-native-keyboard-controller +--- + +# Build an RNKC Chat Screen + +Treat chat as a coordinated scroll, composer, gesture, and keyboard system. Do not assemble it from a general-purpose avoiding view plus ad hoc event listeners. + +## Inspect the product and existing screen + +1. Identify the message list implementation and its ref contract. +2. Determine whether the list is inverted and how it maintains the latest-message position. +3. Locate the composer, safe-area ownership, bottom tabs, fixed margins, and multiline height behavior. +4. Define interactive dismissal behavior on iOS and Android. +5. Choose how messages should lift when the keyboard opens. +6. Identify emoji pickers, attachment sheets, voice panels, or other keyboard replacements. +7. Identify AI streaming or anchor-to-top behavior. +8. Record Reanimated, React Native, and architecture versions before applying troubleshooting flags. + +## Read the relevant references + +- Start with [references/layout-recipes.md](references/layout-recipes.md) for the base component tree, offsets, and lift behavior. +- Read [references/virtualized-lists.md](references/virtualized-lists.md) for `FlatList`, FlashList, LegendList, inversion, stable wrappers, and refs. +- Read [references/advanced-behaviors.md](references/advanced-behaviors.md) for multiline composers, custom panels, AI streaming, jump-to-latest, and visual keyboard treatment. +- Read [references/troubleshooting-and-validation.md](references/troubleshooting-and-validation.md) before applying feature flags, runtime workarounds, or declaring completion. + +## Establish one owner per layer + +Use this default architecture: + +```text +KeyboardGestureArea owns interactive keyboard gestures and optional input offset + KeyboardChatScrollView owns message scroll range and keyboard-driven repositioning + KeyboardStickyView owns composer translation + TextInput owns focus and text entry +``` + +The list may render through `KeyboardChatScrollView` as a custom scroll component, but it remains the virtualization owner. `KeyboardChatScrollView` owns keyboard-related inset and position changes. + +## Choose product semantics explicitly + +Select `keyboardLiftBehavior` from product behavior, not personal preference: + +- `always`: keep bottom content visible regardless of current position. +- `whenAtEnd`: lift only when latest content is visible. +- `persistent`: lift on open and keep the resulting position after close. +- `never`: allow the keyboard to overlap without moving messages. + +Choose and document the reason. Preserve an existing chat's scroll semantics during migration. + +## Add complexity only when required + +- Add `KeyboardGestureArea` for interactive dismissal or associated input offset. +- Add `extraContentPadding` only when external content such as a multiline composer changes height. +- Add `freeze` only for transitions where keyboard-driven layout must pause. +- Add `blankSpace` only for a minimum scroll-space floor such as AI response streaming. +- Add `onEndVisible` for jump-to-latest or UI-thread end-state effects. +- Add `onContentInsetChange` when a list needs RNKC's effective Android inset for its own calculations. +- Add `applyWorkaroundForContentInsetHitTestBug` only when the specific upstream iOS issue is reproduced. + +## Reject common chat mistakes + +- Do not wrap the chat list in `KeyboardAvoidingView` or `KeyboardAwareScrollView` as the primary keyboard owner. +- Do not apply keyboard height as React state padding while `KeyboardChatScrollView` also manages insets. +- Do not let iOS automatic content inset adjustment compete with the chat scroll component. +- Do not forget to propagate `inverted` to both the list and `KeyboardChatScrollView`. +- Do not create a new `renderScrollComponent` function every render. +- Do not call the list's `scrollToEnd` when its calculation ignores RNKC's active inset; use the underlying chat scroll ref when affected. +- Do not enable Reanimated flags or Objective-C runtime workarounds without matching the documented version and symptom. + +## Implement in layers + +1. Make the simplest non-virtualized or existing-list chat work with a sticky composer. +2. Add interactive dismissal and correct ID wiring. +3. Add safe-area and fixed-element offsets. +4. Add the virtualized-list adapter and ref ownership. +5. Add multiline padding, custom-panel freeze, or AI blank space as required. +6. Add visual effects after layout behavior is stable. +7. Test iOS and Android throughout; do not defer all validation until the complete composition exists. + +## Deliver an explainable result + +When advising, return the component tree, list adapter, offset formula, lift behavior, advanced props, platform caveats, and test plan. When implementing, preserve application-specific message state, rendering, send logic, styling, and navigation unless they block the keyboard architecture. diff --git a/skills/build-rnkc-chat-screen/agents/openai.yaml b/skills/build-rnkc-chat-screen/agents/openai.yaml new file mode 100644 index 0000000000..efc7ec65fa --- /dev/null +++ b/skills/build-rnkc-chat-screen/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Build RNKC Chat Screen" + short_description: "Build production-ready chat keyboard layouts" + default_prompt: "Use $build-rnkc-chat-screen to build or refactor this chat screen around KeyboardChatScrollView and its supporting RNKC components." diff --git a/skills/build-rnkc-chat-screen/references/advanced-behaviors.md b/skills/build-rnkc-chat-screen/references/advanced-behaviors.md new file mode 100644 index 0000000000..fc5a9ddf07 --- /dev/null +++ b/skills/build-rnkc-chat-screen/references/advanced-behaviors.md @@ -0,0 +1,120 @@ +# Advanced Chat Behaviors + +## Growing multiline composer + +`KeyboardStickyView` moves the composer, but the message scroll view cannot infer how much the composer grows. Pass a Reanimated shared value through `extraContentPadding`. + +Use a delta above the composer's baseline height when the list already reserves the baseline separately: + +```tsx +const BASE_COMPOSER_HEIGHT = 42; +const extraContentPadding = useSharedValue(0); + +const onComposerLayout = useCallback( + (event: LayoutChangeEvent) => { + const height = event.nativeEvent.layout.height; + + extraContentPadding.value = withTiming( + Math.max(height - BASE_COMPOSER_HEIGHT, 0), + { duration: 250 }, + ); + }, + [extraContentPadding], +); + +; + + +; +``` + +If the list reserves no separate baseline, pass the full external composer height. State which convention the layout uses; mixing a full height with separately reserved baseline padding causes double space. + +## Emoji picker, attachment sheet, or custom input panel + +Use `freeze` to pause keyboard-driven padding, content offset, and scroll changes while switching away from the keyboard: + +```tsx +const freeze = useSharedValue(false); + +const openEmojiPicker = async () => { + freeze.value = true; + await KeyboardController.dismiss({ keepFocus: true }); + showEmojiPicker.value = true; +}; + +const returnToKeyboard = () => { + showEmojiPicker.value = false; + freeze.value = false; + inputRef.current?.focus(); +}; + +; +``` + +The exact order depends on whether the custom panel occupies keyboard space, whether focus should remain, and how it animates. Freeze before the keyboard starts changing the layout. Unfreeze only when the next owner is ready. + +Use a shared value when the transition originates on the UI thread or must be synchronous. A boolean is adequate for ordinary React-driven transitions. + +## AI response streaming and `blankSpace` + +`blankSpace` is a minimum inset floor: + +```text +effective bottom padding = max(blankSpace, keyboard padding + extraContentPadding) +``` + +Use it to leave room below a newly sent user message while an AI response streams. Calculate the required space from the list's real measurements: + +```tsx +blankSpace.value = Math.max(0, viewportHeight - contentHeightBelowAnchor); +``` + +As the response grows, decrease the blank space. Reset it when the content naturally fills the desired viewport or the anchoring interaction ends. + +The current RNKC implementation clamps blank space to one scroll-view viewport. One viewport is enough to move short content without allowing a fully blank screen beyond that range. Do not design an adapter that requires arbitrary multi-viewport blank space without verifying the installed implementation. + +On iOS with affected React Native versions, content-inset space may not receive touch or scroll input. Reproduce the issue before enabling `applyWorkaroundForContentInsetHitTestBug` because the workaround uses runtime swizzling. + +## Jump to latest + +Use `onEndVisible` to show or hide a jump button: + +```tsx +const [showJump, setShowJump] = useState(false); + + setShowJump(!visible)} />; +``` + +For UI-thread animation, pass a worklet and update a shared value rather than routing every end-state transition through React state. + +Remember that logical end is bottom for non-inverted content and top for inverted content. + +## Effective inset reporting + +Use `onContentInsetChange` when a virtualized list maintains its own scroll target and needs RNKC's dynamic inset. The callback can fire on animation frames. Avoid React state if the value only drives UI-thread calculation. + +This is especially relevant on Android because RNKC simulates content inset and the native `onScroll` payload does not report it as an iOS `contentInset`. + +## Solid or animated keyboard background + +Use `KeyboardEffects` behind the chat to change what is visible through the iOS keyboard: + +```tsx + + + +``` + +An opaque view creates a solid app-matching background. A gradient, Skia canvas, or animation can create richer effects. The keyboard translucency behavior is iOS-specific; Android's keyboard is opaque. + +Use `KeyboardBackgroundView` instead when app UI should visually match the system keyboard surface rather than replace what appears behind the keyboard. + +## Interactive gesture offset + +`KeyboardGestureArea.offset` and `KeyboardChatScrollView.offset` solve different problems: + +- Gesture-area offset associates keyboard gesture behavior with the composer or accessory height and matching input IDs. +- Chat-scroll offset describes the fixed distance between the scroll view and physical screen bottom. + +Do not copy the same number into both without tracing the layout. diff --git a/skills/build-rnkc-chat-screen/references/layout-recipes.md b/skills/build-rnkc-chat-screen/references/layout-recipes.md new file mode 100644 index 0000000000..4520e6dc0b --- /dev/null +++ b/skills/build-rnkc-chat-screen/references/layout-recipes.md @@ -0,0 +1,106 @@ +# Chat Layout Recipes + +## Base chat + +Use `KeyboardChatScrollView` for messages and keep the composer as a sibling in `KeyboardStickyView`: + +```tsx +function ChatScreen() { + return ( + + + {messages.map((message) => ( + + ))} + + + + + + ); +} +``` + +This establishes separate ownership: message repositioning belongs to the scroll view; composer translation belongs to the sticky view. + +## Interactive dismissal + +Wrap both message scroll and composer with `KeyboardGestureArea`. Set the scroll owner to interactive dismissal: + +```tsx +const INPUT_NATIVE_ID = "chat-composer"; + + + + + + + + +; +``` + +- Android 11 and newer can use the gesture area to control keyboard movement. +- Older Android renders the children without interactive keyboard control. +- On iOS, matching `textInputNativeID` and `nativeID` associates offset behavior with the focused input. + +Set `KeyboardGestureArea.offset` only when the keyboard interaction should account for an additional composer or accessory height. It is a different concern from `KeyboardChatScrollView.offset`. + +## Safe areas, bottom tabs, and fixed bottom elements + +`KeyboardChatScrollView.offset` is the distance between its bottom edge and the physical screen bottom. The effective keyboard lift is `keyboardHeight - offset`. + +Build the value from actual fixed elements: + +```tsx +const offset = safeAreaBottom + visibleBottomTabHeight + fixedGap; + +; +``` + +Do not add an inset already consumed by a `SafeAreaView` or navigator. Inspect the measured layout and use one owner for each inset. + +Configure `KeyboardStickyView.offset.opened` separately so the composer lands at the intended visible position: + +```tsx + + + +``` + +The exact sign and value depend on whether the screen, navigator, or safe-area container already reserves the inset. Validate visually rather than copying a constant. + +## Lift behaviors + +### `always` + +Use for messenger semantics where the viewport should follow the keyboard even while the user is reading away from the end. This is the default. + +### `whenAtEnd` + +Use when opening the composer should not disturb a user reading older content. The list lifts only when RNKC considers the content end visible. + +### `persistent` + +Use when closing the keyboard should not undo the lifted position. Verify subsequent scrolls and message insertion because this intentionally retains position. + +### `never` + +Use when the keyboard may overlap the list and content should not move. Ensure the product still provides a way to reach obscured content. + +## Short and long content + +Test both. Short content reveals inset, blank-space, and alignment bugs; long content reveals momentum and end-detection problems. Also test when the user is at the top, middle, and end before opening and closing the keyboard. + +## Do not use general avoidance around chat + +`KeyboardAvoidingView` and `KeyboardAwareScrollView` solve bounded layouts and focused-input forms. Around chat they can create duplicate scroll movement, layout reflow, first-message placement problems, and interactive-dismiss conflicts. Keep them outside the chat ownership graph unless they serve an independent nested region with a clearly separate responsibility. diff --git a/skills/build-rnkc-chat-screen/references/troubleshooting-and-validation.md b/skills/build-rnkc-chat-screen/references/troubleshooting-and-validation.md new file mode 100644 index 0000000000..998325c412 --- /dev/null +++ b/skills/build-rnkc-chat-screen/references/troubleshooting-and-validation.md @@ -0,0 +1,118 @@ +# Chat Troubleshooting and Validation + +## Diagnose before enabling a workaround + +Record React Native, RNKC, Reanimated, platform, OS, architecture, list library, inversion, and navigation configuration. Reproduce with the smallest chat variant that preserves the symptom. + +## Android Fabric animation is out of sync + +`KeyboardChatScrollView` relies on a Reanimated commit hook. For affected Reanimated versions below 4.3.0, current RNKC documentation calls for the static feature flag: + +```json +{ + "reanimated": { + "staticFeatureFlags": { + "USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true + } + } +} +``` + +Reanimated 4.3.0 and newer enable it by default according to the current guide. Verify the installed version, run pods when required, and rebuild. Do not add the flag when the symptom or version does not match. + +## iOS New Architecture animation is missing + +A React commit immediately before keyboard presentation can block an animated update. Common triggers include `onFocus` state, `keyboardWillShow` state, toolbar state, or a parent navigator commit. + +Use React Profiler to confirm the commit timing. For affected versions, consult the current Reanimated `DISABLE_COMMIT_PAUSING_MECHANISM` guidance. Avoid masking the problem with arbitrary timeouts. + +## iOS contentInset area cannot be touched + +On affected React Native 0.81 and newer versions, content-inset space may not respond to gestures. RNKC exposes `applyWorkaroundForContentInsetHitTestBug`, which uses Objective-C runtime swizzling. + +Enable it only when: + +1. the app uses the affected path; +2. the dead hit-test area is reproduced; +3. an upstream React Native patch is not used instead; +4. the app can test conflicts with other native patches. + +## `scrollToEnd` stops short while keyboard is open + +The virtualized list may calculate its end from an unadjusted visible length. Forward a separate ref to the underlying `KeyboardChatScrollView` and call its `scrollToEnd`. Do not alter global content padding to compensate. + +## Inverted content flashes + +Increase the virtualized list's offscreen render range such as FlashList `drawDistance`. Verify that `inverted` reaches `KeyboardChatScrollView` and that wrapper identities remain stable. + +## Double movement or excess bottom space + +Check for duplicate owners: + +- `KeyboardAvoidingView` around the chat; +- React state keyboard padding; +- iOS automatic content insets; +- safe-area padding applied by both navigator and screen; +- full composer height passed as `extraContentPadding` while baseline height is also reserved; +- list and RNKC both changing content offset. + +Remove one owner; do not tune opposing offsets until the architecture is singular. + +## Composer or last message is clipped + +Check: + +- whether multiline composer growth updates `extraContentPadding`; +- whether the value is a stable `SharedValue`; +- whether the wrapper forwards it to RNKC; +- whether the list also needs content-inset reporting; +- safe-area and bottom-tab offset ownership; +- short-content behavior. + +## Validation matrix + +### Keyboard lifecycle + +- Open, close, change keyboard type, switch emoji keyboard, use Android back. +- Slow interactive drag, fast dismissal, and cancelled drag. +- Open and close while the list is at top, middle, and end. + +### Content + +- No messages, one short message, short content, and long history. +- Insert local and remote messages while keyboard is open and closed. +- Stream an AI response and complete or cancel it. +- Grow and shrink a multiline composer. + +### Lists + +- Inverted and non-inverted modes used by the product. +- Initial scroll to latest, maintain-visible-content, jump to latest, scroll to index, and scroll to end. +- Momentum during keyboard transitions. +- Ref stability after parent state changes. + +### Layout and navigation + +- Safe areas, bottom tabs, custom bottom gaps, and rotation. +- Modal or bottom-sheet presentation. +- Navigate away and back; interactive native-stack pop where applicable. +- Status and navigation bars on Android. + +### Keyboard variants and accessibility + +- Hardware and floating keyboards where available. +- Predictive or suggestion bar changes. +- Large accessibility text and multiline caret visibility. +- Screen-reader focus and send-button accessibility. + +## RNKC repository references + +When working in this repository, compare against: + +- `docs/docs/guides/building-chat-app.mdx` +- `docs/docs/api/components/keyboard-chat-scroll-view.mdx` +- `example/src/screens/Examples/KeyboardChatScrollView/` +- `example/src/screens/Examples/AILegendListChat/` +- `src/components/KeyboardChatScrollView/` + +Use source and tests as the final authority when docs and installed code differ. diff --git a/skills/build-rnkc-chat-screen/references/virtualized-lists.md b/skills/build-rnkc-chat-screen/references/virtualized-lists.md new file mode 100644 index 0000000000..5a0a5c2167 --- /dev/null +++ b/skills/build-rnkc-chat-screen/references/virtualized-lists.md @@ -0,0 +1,161 @@ +# Virtualized Chat Lists + +## Preserve virtualization ownership + +Keep `FlatList`, `SectionList`, FlashList, LegendList, or the application's list as the message owner. Inject `KeyboardChatScrollView` as its underlying scroll component. Do not render the virtualized list inside a separate chat scroll view. + +## Create a typed wrapper + +```tsx +import { forwardRef } from "react"; +import type { ScrollViewProps } from "react-native"; +import { + KeyboardChatScrollView, + type KeyboardChatScrollViewProps, +} from "react-native-keyboard-controller"; + +type ChatScrollRef = React.ElementRef; + +export const ChatScrollView = forwardRef< + ChatScrollRef, + ScrollViewProps & KeyboardChatScrollViewProps +>((props, ref) => { + return ( + + ); +}); +``` + +Disable iOS automatic content inset adjustment so the list and RNKC do not both modify insets. Place application defaults before `{...props}` when the list must be allowed to override them; place invariants after the spread when the wrapper must enforce them. Make that choice explicit. + +## FlashList + +FlashList accepts a stable component reference: + +```tsx + item.id} + renderItem={renderItem} + renderScrollComponent={ChatScrollView} +/> +``` + +When inverted content flashes during keyboard animation, increase the list's offscreen render distance such as `drawDistance` before adding unrelated keyboard workarounds. + +## FlatList and LegendList + +Use a stable callback: + +```tsx +const renderScrollComponent = useCallback( + (props: ScrollViewProps) => , + [], +); + + item.id} + renderItem={renderItem} + renderScrollComponent={renderScrollComponent} +/>; +``` + +When the wrapper receives shared values or changing configuration, include those stable references in the callback dependency list: + +```tsx +const renderScrollComponent = useCallback( + (props: ScrollViewProps) => ( + + ), + [composerExtraPadding, keyboardLiftBehavior], +); +``` + +Avoid inline `renderScrollComponent={(props) => ...}` in the list JSX when it recreates the scroll component and can disturb ref or scroll state. + +## Propagate inversion + +If the list is inverted, pass the same value to `KeyboardChatScrollView`: + +```tsx +const ChatScrollView = forwardRef( + ({ inverted, ...props }, ref) => ( + + ), +); +``` + +For non-inverted lists, the content end is the bottom. For inverted lists, RNKC treats the top of the scroll view as the logical end where latest messages appear. `onEndVisible` and `keyboardLiftBehavior="whenAtEnd"` follow this interpretation. + +Do not reverse the message array and set `inverted` without understanding the list library's ordering contract. Preserve the application's existing data semantics during keyboard migration. + +## Forward the underlying chat scroll ref + +Some virtualized lists own the scroll-component ref internally. When `FlatList.scrollToEnd()` ignores keyboard-adjusted layout, keep an additional ref to the underlying `KeyboardChatScrollView`: + +```tsx +type Props = ScrollViewProps & { + chatScrollViewRef: React.MutableRefObject; +}; + +const ChatScrollView = forwardRef( + ({ chatScrollViewRef, ...props }, listRef) => { + const setRef = useCallback( + (instance: ChatScrollRef | null) => { + if (typeof listRef === "function") { + listRef(instance); + } else if (listRef) { + listRef.current = instance; + } + + chatScrollViewRef.current = instance; + }, + [chatScrollViewRef, listRef], + ); + + return ; + }, +); +``` + +Then call: + +```tsx +chatScrollViewRef.current?.scrollToEnd(); +``` + +Use this workaround only when the list's own method demonstrably stops short while the keyboard is open. + +## Composer height and list contracts + +Pass `extraContentPadding` through the wrapper so RNKC can extend the underlying scroll range. Some lists also require their own content-inset reporting to calculate initial position or scroll targets. For example, a LegendList adapter may need to report the composer height separately while also passing the shared value to RNKC. + +Do not assume RNKC's synthetic Android inset appears in the native list's `onScroll` payload. Use `onContentInsetChange` when the list performs its own end-target calculation from offsets and content length. + +## AI anchor adapters + +For AI streaming, the list adapter may need item measurement or an `anchorToTopIndex` concept to calculate the content below the last user message. Keep that calculation in the list-specific adapter and pass the resulting shared value as `blankSpace`. RNKC owns how the minimum inset interacts with keyboard and composer padding; the list owns item measurement. + +## Validate each list integration + +- Initial position at the latest message. +- Insert messages while closed and open. +- Scroll to top, middle, and end. +- Open and close at each position. +- Interactive dismissal during momentum. +- Inverted and non-inverted modes used by the product. +- `scrollToEnd`, `scrollToIndex`, maintain-visible-content behavior, and jump-to-latest. +- Short content and thousands of items. +- Composer height changes and safe-area rotation. +- Ref stability across parent renders. diff --git a/skills/choose-rnkc-keyboard-layout/SKILL.md b/skills/choose-rnkc-keyboard-layout/SKILL.md new file mode 100644 index 0000000000..9dba306cf5 --- /dev/null +++ b/skills/choose-rnkc-keyboard-layout/SKILL.md @@ -0,0 +1,84 @@ +--- +name: choose-rnkc-keyboard-layout +description: Inspect a React Native screen and choose the correct React Native Keyboard Controller components, hooks, and composition. Use when deciding between KeyboardAvoidingView, KeyboardAwareScrollView, KeyboardStickyView, KeyboardChatScrollView, KeyboardToolbar, KeyboardGestureArea, OverKeyboardView, KeyboardExtender, KeyboardBackgroundView, KeyboardEffects, keyboard animation hooks, or keyboard state APIs; when keyboard handling is missing, duplicated, or structurally wrong; or when an LLM is unsure how to design a form, chat, sticky footer, accessory, overlay, interactive dismissal, or keyboard-themed experience. +license: MIT +metadata: + author: kirillzyusko + source: react-native-keyboard-controller +--- + +# Choose an RNKC Keyboard Layout + +Choose the smallest composition that owns the screen's actual keyboard behavior. Inspect the app code before recommending or editing it. Do not select a component only because its name resembles an existing React Native component. + +## Inspect before choosing + +1. Locate the screen root, inputs, scroll owner, fixed footer or composer, navigation wrapper, safe-area handling, modal or bottom-sheet container, and current keyboard code. +2. Record whether the screen is a fixed form, scrollable form, chat, sticky action area, keyboard accessory, overlay, keyboard extension, or custom animation. +3. Determine whether the scroll owner is `ScrollView`, `FlatList`, `SectionList`, FlashList, LegendList, a bottom-sheet scrollable, or a custom native view. +4. Determine whether the product needs interactive dismissal, input navigation, multiline growth, custom panels, AI streaming space, or keyboard visual effects. +5. Check both iOS and Android expectations, the installed RNKC version, `KeyboardProvider`, Reanimated, navigation, and Android soft-input ownership. + +Read [references/component-catalog.md](references/component-catalog.md) for component and hook selection. Read [references/inspection-and-validation.md](references/inspection-and-validation.md) for integration constraints and the verification matrix. + +## Choose by layout ownership + +- Use `KeyboardAvoidingView` when one bounded layout should resize, pad, or reposition as a unit. +- Use `KeyboardAwareScrollView` when a scrollable form must reveal the focused input or caret. +- Use `KeyboardStickyView` when only a footer, composer, action button, or toolbar should translate with the keyboard. +- Use `KeyboardChatScrollView` for chat message scrolling. Pair it with `KeyboardStickyView` for the composer. Invoke `$build-rnkc-chat-screen` for production chat work. +- Use `KeyboardToolbar` for previous, next, and done controls. It can accompany a form component; it does not replace keyboard avoidance. +- Use `KeyboardGestureArea` with an interactive scroll view when gestures must control keyboard dismissal or an iOS offset must be tied to specific inputs. +- Use `OverKeyboardView` when content must appear above the keyboard without closing it. +- Use `KeyboardExtender` when non-input content should become part of the keyboard surface. +- Use `KeyboardBackgroundView` to mirror the system keyboard background inside app UI. +- Use `KeyboardEffects` to render content behind the keyboard, including opaque or animated iOS keyboard backdrops. +- Use hooks only when prebuilt components do not own the required behavior. + +## Prefer compositions over replacements + +A screen often needs more than one component because each owns a different layer: + +```tsx + + + + + + + + +``` + +Do not stack two owners for the same responsibility. Avoid combining RN `KeyboardAvoidingView`, a third-party aware scroll view, Reanimated `useAnimatedKeyboard`, and RNKC avoidance around the same content. + +## Reject common mismatches + +- Do not default to `KeyboardAvoidingView` for a production chat list; use the chat-specific component. +- Do not use `KeyboardAwareScrollView` only to move a fixed footer; use `KeyboardStickyView`. +- Do not place another `TextInput` inside `KeyboardExtender`. +- Do not use `OverKeyboardView` when the content should participate in layout or scroll with the screen. +- Do not use `useKeyboardState` to animate styles; use native-driven animation values. +- Do not subscribe to the complete keyboard state when one selector is enough; invoke `$prefer-rnkc-keyboard-selectors` when reviewing state usage. +- Do not add `KeyboardProvider` solely for `OverKeyboardView`; that component can operate independently. Check provider requirements for the rest of the selected composition. + +## Produce an actionable answer + +When the user asks for advice, return: + +1. The recommended component tree. +2. The responsibility of each selected component. +3. Critical props, offsets, and refs. +4. Rejected alternatives and the concrete mismatch. +5. Platform and integration caveats. +6. A minimal implementation skeleton and a verification plan. + +When the user asks for implementation, make the smallest screen-local change that establishes one clear keyboard owner. Preserve unrelated design, state, and navigation code. + +## Verify the result + +Test the relevant combination of keyboard open, close, focus change, multiline growth, interactive dismissal, rotation, safe areas, navigation headers, bottom tabs, modals, and physical or floating keyboards. Treat environment-only build failures separately from behavioral conclusions. diff --git a/skills/choose-rnkc-keyboard-layout/agents/openai.yaml b/skills/choose-rnkc-keyboard-layout/agents/openai.yaml new file mode 100644 index 0000000000..575a991b50 --- /dev/null +++ b/skills/choose-rnkc-keyboard-layout/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Choose RNKC Keyboard Layout" + short_description: "Choose the right RNKC keyboard layout" + default_prompt: "Use $choose-rnkc-keyboard-layout to inspect this screen and recommend the correct React Native Keyboard Controller composition." diff --git a/skills/choose-rnkc-keyboard-layout/references/component-catalog.md b/skills/choose-rnkc-keyboard-layout/references/component-catalog.md new file mode 100644 index 0000000000..8e28b8a976 --- /dev/null +++ b/skills/choose-rnkc-keyboard-layout/references/component-catalog.md @@ -0,0 +1,86 @@ +# RNKC Component and Hook Catalog + +Use this reference after inspecting the screen. Re-check the installed package types when a prop or platform capability may have changed. + +## Quick decision matrix + +| Requirement | Primary API | Combine with | Avoid as the primary owner | +| ---------------------------------------------------- | ---------------------------------------------- | ------------------------------------------- | ------------------------------------------------ | +| Keep a bounded non-scroll layout visible | `KeyboardAvoidingView` | `KeyboardToolbar` | Aware scroll view unless content needs scrolling | +| Reveal a focused input in a long form | `KeyboardAwareScrollView` | `KeyboardToolbar` | Nested avoidance wrappers | +| Move one footer or composer | `KeyboardStickyView` | Normal content or chat scroll | Resizing the entire screen | +| Build a chat message viewport | `KeyboardChatScrollView` | `KeyboardStickyView`, `KeyboardGestureArea` | General-purpose avoiding views | +| Navigate previous, next, and done | `KeyboardToolbar` | Form avoidance or aware scrolling | Treating the toolbar as layout avoidance | +| Interactively dismiss or apply input-specific offset | `KeyboardGestureArea` | Interactive scroll view | JS pan-to-keyboard synchronization | +| Show a menu above an active keyboard | `OverKeyboardView` | Custom animation if needed | `Modal` when it dismisses the keyboard | +| Add non-input actions to the keyboard | `KeyboardExtender` | Selector for keyboard appearance | Putting a `TextInput` inside it | +| Match the system keyboard background | `KeyboardBackgroundView` | `KeyboardStickyView` | Using it for layout or events | +| Replace what is visible behind the keyboard | `KeyboardEffects` | Opaque view, gradient, or animation | Expecting Android translucency behavior | +| Animate with React Native Animated | `useKeyboardAnimation` | `Animated.View` | React state per frame | +| Animate with Reanimated | `useReanimatedKeyboardAnimation` | `useAnimatedStyle` | Reanimated `useAnimatedKeyboard` alongside RNKC | +| Handle keyboard lifecycle frames | `useKeyboardHandler` | Shared values and worklets | `Keyboard.addListener` for per-frame UI | +| Render keyboard metadata | `useKeyboardState(selector)` | Leaf UI | Full-state subscription in broad parents | +| Read latest state inside a callback | `KeyboardController.state()` or `.isVisible()` | Imperative logic | A hook subscription used only by the callback | + +## `KeyboardAvoidingView` + +Use for a bounded screen or panel whose layout should change as a unit. + +- `behavior="padding"`: add bottom padding; suitable for flex layouts and simple embedded scroll views. +- `behavior="height"`: shrink the container. +- `behavior="position"`: reposition the inner content and use `contentContainerStyle` for that inner view. +- `behavior="translate-with-padding"`: translate while applying one-time padding for performance-sensitive layouts; do not choose it for a chat list without checking `KeyboardChatScrollView` first. +- `keyboardVerticalOffset`: compensate for known top positioning such as headers when `automaticOffset` is false. +- `automaticOffset`: measure screen position automatically; when true, `keyboardVerticalOffset` becomes additive rather than compensatory. + +Use `enabled` when multiple mounted screens or conditional layouts exist, but first determine which mounted layer should own avoidance. + +## `KeyboardAwareScrollView` + +Use for forms with inputs that can move outside the visible scroll viewport. + +- `bottomOffset`: desired distance between the keyboard and focused caret. +- `extraKeyboardSpace`: additional bottom keyboard space. +- `disableScrollOnKeyboardHide`: preserve scroll position when the keyboard closes. +- `mode="insets"`: default; extend the scrollable area without layout reflow. +- `mode="layout"`: append layout space so flex distribution, gaps, or `justifyContent: "space-between"` reflow. +- `ScrollViewComponent`: integrate a custom scroll implementation. +- `assureFocusedInputVisible()`: re-measure after validation messages or other layout changes shift a focused input. + +For `FlatList`, `SectionList`, FlashList, or LegendList, pass `KeyboardAwareScrollView` as the list's custom scroll component rather than nesting the list inside it. + +## `KeyboardStickyView` + +Use when only its child should translate with the keyboard. Configure `offset.closed` and `offset.opened` for safe areas, persistent footers, or intentional spacing. It does not resize siblings or extend the scroll range by itself. + +## `KeyboardChatScrollView` + +Use for chat-specific content repositioning, scroll-range extension, interactive dismissal, list-end awareness, custom-panel freezing, growing composers, and AI streaming space. Pair it with a separate composer in `KeyboardStickyView`. Use `$build-rnkc-chat-screen` for the full layout and list adapter rules. + +## `KeyboardToolbar` + +Use for focus navigation and dismissal. The compound API exposes direct `KeyboardToolbar.Background`, `.Content`, `.Prev`, `.Next`, `.Done`, and `.Group` children. Navigation order follows the native view hierarchy; `Group` isolates a subtree's inputs. + +## `KeyboardGestureArea` + +Use with `keyboardDismissMode="interactive"` on the scroll owner. + +- Android 11 and newer: gestures can control keyboard position; choose `interpolator="ios"` or `"linear"`. +- Older Android: renders children without interactive control. +- iOS: use matching `textInputNativeID` and `TextInput.nativeID` when applying `offset` to one or more associated inputs. + +## Overlay and visual components + +- `OverKeyboardView`: full-screen transparent overlay above the keyboard; mounts children only while `visible`; does not require `KeyboardProvider` when used alone. +- `KeyboardExtender`: attaches non-input content to the keyboard; on iOS it is native keyboard content, while other platforms may use the package fallback. Never nest a `TextInput` inside it. +- `KeyboardBackgroundView`: visually copies the system keyboard surface; it does not handle layout or events. +- `KeyboardEffects`: follows the keyboard and renders content behind it. The `translucent` behavior that removes native blur is iOS-specific. + +## Hooks and height conventions + +- `useKeyboardAnimation().height` is an `Animated` translation value and is negative while opening. +- `useReanimatedKeyboardAnimation().height` is a negative `SharedValue` while the keyboard is open. +- Compatibility `useAnimatedKeyboard().height` is a positive physical keyboard height and retains Reanimated's `KeyboardState` shape. +- `useKeyboardHandler` events expose positive physical `event.height` and require `"worklet"` in handlers. + +Do not copy a unary minus from one API to another without checking the height convention. diff --git a/skills/choose-rnkc-keyboard-layout/references/inspection-and-validation.md b/skills/choose-rnkc-keyboard-layout/references/inspection-and-validation.md new file mode 100644 index 0000000000..c5f348adf5 --- /dev/null +++ b/skills/choose-rnkc-keyboard-layout/references/inspection-and-validation.md @@ -0,0 +1,64 @@ +# Screen Inspection and Validation + +## Inspection checklist + +Answer these from code before choosing a component: + +### Layout + +- Which node fills the screen? +- Which node owns vertical scrolling? +- Is there a fixed footer, composer, bottom tab, safe-area spacer, or bottom sheet? +- Are inputs inside a modal or a screen that remains mounted behind navigation? +- Does content use flex distribution that must reflow? + +### Input behavior + +- How many inputs exist and can any be disabled or dynamically mounted? +- Can an input grow multiline? +- Does focus need previous/next navigation? +- Must the keyboard remain focused while showing an overlay or custom panel? +- Must the user drag the keyboard interactively? + +### Product semantics + +- For chat, should content always lift, lift only at the end, remain lifted after close, or never lift? +- Must an emoji picker or bottom sheet replace the keyboard without moving messages? +- Must a sent AI message anchor near the top while a response streams? +- Should the system keyboard remain translucent, match the app, or reveal an animated background? + +### Runtime ownership + +- Is `KeyboardProvider` mounted once at the application root? +- Is Reanimated installed and compatible with the app architecture? +- Does another library take control of edge-to-edge or window insets? +- Is Android `windowSoftInputMode` global, screen-scoped, or changed by multiple hooks? +- Are both the legacy and new keyboard implementations mounted at the same time? + +## Selection principles + +1. Give one layer ownership of scroll repositioning. +2. Give one layer ownership of fixed-element translation. +3. Keep business-state subscriptions separate from animation values. +4. Prefer the package component that encodes the product behavior over handwritten event math. +5. Preserve app-specific navigation, safe-area, bottom-sheet, and list contracts. + +## Validation matrix + +Run only the rows relevant to the screen, but cover both target platforms. + +| Area | Checks | +| ----------------- | ---------------------------------------------------------------------------------------------- | +| Basic | Focus first and last input; open and close keyboard; tap outside; use hardware back on Android | +| Focus | Move next and previous; skip disabled inputs; dynamically add or remove an input | +| Layout | Header offset; safe area; bottom tabs; rotation; split screen; modal presentation | +| Scroll | Short and long content; top, middle, and end positions; momentum during open or close | +| Multiline | Grow and shrink composer; switch keyboard type or emoji keyboard | +| Interactive | Slow drag, fast dismissal, cancellation, swipe up where supported | +| Navigation | Push, pop, native interactive pop, switch tabs, return to a still-mounted screen | +| Accessibility | Large text, screen reader focus, custom toolbar labels, reduced motion where relevant | +| Keyboard variants | Predictive bar, floating keyboard, physical keyboard, different input types | + +## Evidence-first output + +When explaining a choice, point to the actual scroll owner and fixed-element boundary in the user's code. State what each rejected component would incorrectly resize, translate, overlay, or subscribe to. If the code is incomplete, make the smallest explicit assumption and keep the recommendation reversible. diff --git a/skills/migrate-rnkc-keyboard-toolbar/SKILL.md b/skills/migrate-rnkc-keyboard-toolbar/SKILL.md new file mode 100644 index 0000000000..d6f7a231a7 --- /dev/null +++ b/skills/migrate-rnkc-keyboard-toolbar/SKILL.md @@ -0,0 +1,105 @@ +--- +name: migrate-rnkc-keyboard-toolbar +description: Migrate React Native Keyboard Controller KeyboardToolbar usages from the legacy prop-based API to the compound component API while preserving focus navigation, dismissal, custom content, background effects, themes, safe-area insets, callbacks, accessibility, and conditional buttons. Use when code uses KeyboardToolbar content, blur, doneText, button, icon, showArrows, onNextCallback, onPrevCallback, or onDoneCallback props; when upgrading RNKC toolbar code; or when mixed legacy and compound APIs behave unexpectedly. +license: MIT +metadata: + author: kirillzyusko + source: react-native-keyboard-controller +--- + +# Migrate RNKC KeyboardToolbar + +Convert each toolbar to direct compound children without changing its behavior. Inspect the installed RNKC version and every usage before editing. Do not mix legacy child-generating props with compound children. + +Read [references/migration-map.md](references/migration-map.md) for exact mappings and edge cases. Read [references/validation.md](references/validation.md) before declaring the migration complete. + +## Audit the requested scope + +1. Find `KeyboardToolbar` imports, aliases, wrappers, prop spreads, and JSX usages. +2. Find deprecated props in local wrapper types as well as direct JSX. +3. Record the old rendered elements: previous, next, done, background, middle content, custom button, and custom icon. +4. Record conditional behavior, callback side effects, safe-area insets, theme, opacity, offset, and enabled state. +5. Check whether inputs should be isolated with `KeyboardToolbar.Group`. + +Useful searches: + +```sh +rg -n "KeyboardToolbar|content=|blur=|doneText=|showArrows=|onNextCallback=|onPrevCallback=|onDoneCallback=" src app +``` + +Adjust paths to the application. Do not search generated output or dependency caches unless the task asks for them. + +## Rebuild the rendered toolbar explicitly + +The old empty toolbar renders previous, next, and done controls by default: + +```tsx +// Before + + +// After + + + + + +``` + +Keep non-deprecated root props such as `theme`, `opacity`, `insets`, `offset`, and `enabled` on `KeyboardToolbar`. Move element-specific behavior to direct children. + +## Preserve default actions + +`Prev`, `Next`, and `Done` call the provided `onPress` first, then perform their built-in action unless the event was cancelled: + +```tsx + { + trackNext(); + + if (useCustomNavigation) { + event.preventDefault(); + focusCustomField(); + } + }} +/> +``` + +Do not add `preventDefault()` merely because the old prop was named a callback. Legacy callbacks also ran alongside the default action. + +## Keep compound children direct + +Render `KeyboardToolbar.Background`, `.Content`, `.Prev`, `.Next`, and `.Done` as the elements passed directly to the toolbar. The toolbar identifies children by their component type. Do not hide them inside a fragment, wrapper component, or arbitrary container unless the installed implementation explicitly supports it. Arrays are flattened by `React.Children`, so an invoked helper may return an array of the actual compound elements; a wrapper such as `` is still opaque and will be ignored. + +Conditional direct children are valid: + +```tsx + + {showArrows ? : null} + {showArrows ? : null} + + +``` + +## Avoid partial migration + +When `children` are present, the current implementation takes the compound branch and does not generate legacy `content`, `blur`, arrow, callback, button, icon, or done elements. Therefore this is unsafe: + +```tsx + + ... + +``` + +Move every element-generating legacy prop in the same usage before committing it. + +## Keep the change scoped + +- Preserve the input view hierarchy unless the task explicitly changes focus order. +- Preserve `KeyboardAwareScrollView`, `KeyboardAvoidingView`, or other layout ownership around the toolbar. +- Do not add `KeyboardToolbar.Group` unless the intended focus boundary is clear. +- Preserve custom accessibility behavior in custom button components. +- Do not rewrite themes, icons, or callbacks beyond what the compound API requires. + +## Verify and report + +Run type checking and the most focused available tests. Manually verify first, middle, and last input states; disabled inputs; previous and next focus; done dismissal; conditional content; custom background; and grouped input boundaries. Report any behavior that could not be preserved automatically. diff --git a/skills/migrate-rnkc-keyboard-toolbar/agents/openai.yaml b/skills/migrate-rnkc-keyboard-toolbar/agents/openai.yaml new file mode 100644 index 0000000000..f518ec240a --- /dev/null +++ b/skills/migrate-rnkc-keyboard-toolbar/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Migrate RNKC Keyboard Toolbar" + short_description: "Migrate KeyboardToolbar to compound API" + default_prompt: "Use $migrate-rnkc-keyboard-toolbar to migrate these KeyboardToolbar usages to the compound API without changing behavior." diff --git a/skills/migrate-rnkc-keyboard-toolbar/references/migration-map.md b/skills/migrate-rnkc-keyboard-toolbar/references/migration-map.md new file mode 100644 index 0000000000..47e14a141a --- /dev/null +++ b/skills/migrate-rnkc-keyboard-toolbar/references/migration-map.md @@ -0,0 +1,136 @@ +# KeyboardToolbar Migration Map + +## Prop mapping + +| Legacy prop or behavior | Compound API | Notes | +| ------------------------------------------------------ | ------------------------------------------ | --------------------------------------------------------------------------------------------- | +| Empty `KeyboardToolbar` | Direct `Prev`, `Next`, and `Done` children | Recreate all three defaults explicitly | +| `content={node}` | `KeyboardToolbar.Content` | Put `node` inside the child | +| `blur={node}` | `KeyboardToolbar.Background` | Use `Background`; there is no exported `KeyboardToolbar.Effect` in the current implementation | +| `doneText={node}` | `KeyboardToolbar.Done text={node}` | Omit `Done` when the old value intentionally hid it | +| `showArrows={boolean}` | Conditional `Prev` and `Next` | Keep `Done` independently | +| `onPrevCallback={fn}` | `KeyboardToolbar.Prev onPress={fn}` | Default previous focus still runs unless cancelled | +| `onNextCallback={fn}` | `KeyboardToolbar.Next onPress={fn}` | Default next focus still runs unless cancelled | +| `onDoneCallback={fn}` | `KeyboardToolbar.Done onPress={fn}` | Default dismissal still runs unless cancelled | +| Root `button={Button}` | `button={Button}` on each rendered control | Apply to Prev, Next, and Done when all inherited it before | +| Root `icon={Icon}` | `icon={Icon}` on Prev and Next | Done has text or children rather than arrow icon | +| Root `theme`, `opacity`, `insets`, `offset`, `enabled` | Keep on root | These remain toolbar-level concerns | + +The current type comment for legacy `blur` mentions `KeyboardToolbar.Effect`, but the runtime export and documentation use `KeyboardToolbar.Background`. Follow the runtime API. + +## Complete migration + +```tsx +// Before +} + button={ToolbarButton} + content={} + doneText="Close" + icon={ToolbarArrow} + insets={insets} + onDoneCallback={onDone} + onNextCallback={onNext} + onPrevCallback={onPrev} + opacity="4F" + showArrows={showArrows} +/> + +// After + + + + + {showArrows ? ( + + ) : null} + {showArrows ? ( + + ) : null} + + + + + +``` + +Set a non-opaque root `opacity` when the custom background must be visible through the toolbar's own background color. + +## Custom children + +Each button accepts custom `children`. Children replace the default arrow or done text: + +```tsx + + + + + + + + Close + +``` + +Prefer `icon` when one component already handles both `prev` and `next` arrow types. Prefer children when each control owns distinct UI. `Done` renders its child inside a React Native `Text`, so keep that child text-compatible; use a custom `button` component when the entire done control needs a different container. + +## Callback semantics + +The compound controls call the callback before the built-in action: + +1. `Prev` callback, then `KeyboardController.setFocusTo("prev")`. +2. `Next` callback, then `KeyboardController.setFocusTo("next")`. +3. `Done` callback, then `KeyboardController.dismiss()`. + +Calling `event.preventDefault()` suppresses the second step. Preserve this only when the application intentionally replaces the default behavior. + +## Conditional done control + +The old implementation did not render the done button when `doneText` was falsy. Preserve that intent explicitly: + +```tsx + + + + {showDone ? : null} + +``` + +## Input groups + +Use `KeyboardToolbar.Group` around an input subtree only when previous and next navigation must stay within that subtree: + +```tsx + + + + + + +``` + +This changes focus reachability and button disabled states. It is a feature decision, not a mechanical part of legacy prop migration. + +## Wrapper components and prop spreads + +If an application wrapper accepts legacy props, migrate the wrapper contract and all call sites together. Do not forward deprecated props onto a toolbar that also receives children. + +For unknown prop spreads: + +```tsx +... +``` + +inspect the type and runtime object. Split element-level legacy values from root-level values before spreading. A type-clean JSX surface can still carry deprecated keys through an untyped object. diff --git a/skills/migrate-rnkc-keyboard-toolbar/references/validation.md b/skills/migrate-rnkc-keyboard-toolbar/references/validation.md new file mode 100644 index 0000000000..0a998ba388 --- /dev/null +++ b/skills/migrate-rnkc-keyboard-toolbar/references/validation.md @@ -0,0 +1,34 @@ +# KeyboardToolbar Migration Validation + +## Static checks + +- Search the migrated scope for `content`, `blur`, `doneText`, `button`, `icon`, `showArrows`, `onNextCallback`, `onPrevCallback`, and `onDoneCallback` passed to `KeyboardToolbar`. +- Check local wrapper prop types and object spreads, not only direct JSX. +- Confirm every child detected by `React.Children` is an actual compound element, not a fragment or wrapper component. +- Confirm root-only props stayed on `KeyboardToolbar`. +- Confirm custom button and icon props were copied to every control that inherited them before. +- Run TypeScript and lint checks available in the application. + +## Behavior checks + +1. Focus the first input: previous is disabled and next reflects the next reachable input. +2. Move next through enabled inputs and verify disabled inputs are skipped. +3. Focus the last input: next is disabled. +4. Move previous and verify focus order matches the native view hierarchy. +5. Press done and verify keyboard dismissal and any callback side effect. +6. Verify a callback without `preventDefault()` still performs the built-in action. +7. Verify an intentional `preventDefault()` path suppresses the built-in action. +8. Toggle conditional arrows, content, background, and done controls. +9. Verify light and dark keyboard appearances with custom themes. +10. Verify landscape safe-area insets and rounded keyboard presentation where applicable. +11. Verify each `KeyboardToolbar.Group` boundary independently. + +## RNKC repository checks + +When working in the RNKC repository, use the existing toolbar example and focused E2E specification as behavioral references: + +- `example/src/screens/Examples/Toolbar/index.tsx` +- `FabricExample/src/screens/Examples/Toolbar/index.tsx` +- `e2e/kit/005-keyboard-toolbar.e2e.ts` + +Do not update snapshots merely to hide an unexplained toolbar difference. diff --git a/skills/migrate-to-rnkc/SKILL.md b/skills/migrate-to-rnkc/SKILL.md new file mode 100644 index 0000000000..0039414c0d --- /dev/null +++ b/skills/migrate-to-rnkc/SKILL.md @@ -0,0 +1,85 @@ +--- +name: migrate-to-rnkc +description: Audit and migrate React Native keyboard handling to react-native-keyboard-controller while preserving layout, focus, animation, events, insets, navigation lifecycle, and platform behavior. Use when replacing React Native KeyboardAvoidingView, react-native-keyboard-aware-scroll-view, Reanimated useAnimatedKeyboard, InputAccessoryView, Keyboard.addListener logic, manual keyboard-height state, Android windowSoftInputMode or softwareKeyboardLayoutMode handling, or a mixture of competing keyboard solutions; also use when an LLM should make RNKC the default architecture for a keyboard-heavy screen. +license: MIT +metadata: + author: kirillzyusko + source: react-native-keyboard-controller +--- + +# Migrate to React Native Keyboard Controller + +Migrate one keyboard responsibility at a time. Establish the current behavior before removing it, then choose the RNKC API that directly owns that behavior. Inspect the installed package source and types when the application version differs from current documentation. + +## Route to the relevant references + +Always read [references/setup-and-strategy.md](references/setup-and-strategy.md). Then read only the references matching code found in the application: + +- React Native `KeyboardAvoidingView`: [references/keyboard-avoiding-view.md](references/keyboard-avoiding-view.md) +- `react-native-keyboard-aware-scroll-view`: [references/keyboard-aware-scroll-view.md](references/keyboard-aware-scroll-view.md) +- Reanimated `useAnimatedKeyboard`: [references/reanimated-use-animated-keyboard.md](references/reanimated-use-animated-keyboard.md) +- React Native `InputAccessoryView`: [references/input-accessory-view.md](references/input-accessory-view.md) +- React Native `Keyboard.addListener` or manual keyboard state: [references/keyboard-events.md](references/keyboard-events.md) +- Android manifest, Expo `softwareKeyboardLayoutMode`, or imperative soft-input changes: [references/android-soft-input-mode.md](references/android-soft-input-mode.md) + +Invoke `$choose-rnkc-keyboard-layout` when the correct target architecture is unclear. Invoke `$build-rnkc-chat-screen` for a chat viewport and `$prefer-rnkc-keyboard-selectors` when the migration introduces or reviews `useKeyboardState`. + +## Inventory the existing behavior + +Search imports, wrappers, hooks, manifests, Expo config, navigation lifecycle, and list adapters. Record: + +- which layer changes layout; +- which layer owns scrolling; +- which layer translates a footer or composer; +- whether keyboard events drive business logic or per-frame UI; +- whether the implementation changes window insets or soft-input mode; +- whether keyboard handling is global, screen-scoped, or focus-scoped; +- what iOS and Android currently do differently; +- which known workaround or product invariant each unusual prop preserves. + +Do not infer behavior from an import alone. Read wrapper implementations and prop spreads. + +## Choose the RNKC target + +Prefer prebuilt components before low-level hooks: + +1. Scrollable form: `KeyboardAwareScrollView`. +2. Bounded form or panel: RNKC `KeyboardAvoidingView`. +3. Fixed footer or composer: `KeyboardStickyView`. +4. Chat: `KeyboardChatScrollView` plus `KeyboardStickyView`. +5. Previous, next, and done controls: `KeyboardToolbar`. +6. Interactive dismissal: `KeyboardGestureArea` plus an interactive scroll owner. +7. Keyboard accessory content: choose `KeyboardToolbar`, `KeyboardStickyView`, `KeyboardExtender`, or `OverKeyboardView` by ownership. +8. Reanimated compatibility: RNKC `useAnimatedKeyboard` first; native RNKC hooks when intentionally refactoring semantics. +9. Render state: `useKeyboardState` with a selector. +10. Event-time state: `KeyboardController.state()` or `.isVisible()`. +11. Per-frame UI: `useKeyboardAnimation`, `useReanimatedKeyboardAnimation`, or `useKeyboardHandler`. + +## Sequence the migration + +1. Add and verify the RNKC installation and provider boundary. +2. Establish Android soft-input ownership. +3. Migrate one screen or shared primitive. +4. Remove the replaced keyboard owner from that scope. +5. Run type checks and focused behavior tests on iOS and Android. +6. Repeat for the next scope. +7. Remove the old dependency only after no imports, wrappers, native config, patches, or test assumptions remain. + +Avoid running RNKC and Reanimated `useAnimatedKeyboard`, two avoiding views, or two keyboard-aware scroll owners around the same screen during the final state. + +## Preserve behavior, not obsolete mechanics + +Map the product intent rather than mechanically copying every workaround. For example: + +- Preserve “focused input stays 24 px above the keyboard,” not an old delay timer that approximated it. +- Preserve “chat remains still when opening emoji picker,” using `freeze`, not a chain of keyboard event state updates. +- Preserve “only the focused screen owns `adjustResize`,” using navigation focus lifecycle, not mount lifecycle in a stack that keeps screens mounted. +- Preserve “toolbar callback tracks and then moves focus,” without adding `preventDefault()`. + +Document any old behavior that has no direct RNKC equivalent and implement an explicit application-level replacement only if the product still needs it. + +## Validate the final owner graph + +At completion, be able to name exactly one owner for each responsibility: screen avoidance, focused-input scrolling, composer translation, chat repositioning, keyboard events, animation values, and Android soft-input mode. Verify show, hide, focus changes, navigation, rotation, multiline input, interactive dismissal, safe areas, bottom tabs, modals, and relevant keyboard variants. + +Report removed dependencies and configuration separately from behavioral changes. Do not claim parity when only one platform was exercised. diff --git a/skills/migrate-to-rnkc/agents/openai.yaml b/skills/migrate-to-rnkc/agents/openai.yaml new file mode 100644 index 0000000000..8101a2d96b --- /dev/null +++ b/skills/migrate-to-rnkc/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Migrate to RNKC" + short_description: "Migrate keyboard handling to RNKC safely" + default_prompt: "Use $migrate-to-rnkc to audit this React Native keyboard implementation and migrate it safely to React Native Keyboard Controller." diff --git a/skills/migrate-to-rnkc/references/android-soft-input-mode.md b/skills/migrate-to-rnkc/references/android-soft-input-mode.md new file mode 100644 index 0000000000..d4a13f2f4e --- /dev/null +++ b/skills/migrate-to-rnkc/references/android-soft-input-mode.md @@ -0,0 +1,106 @@ +# Migrate Android Soft-Input Mode Ownership + +Use this reference for `android:windowSoftInputMode`, Expo `softwareKeyboardLayoutMode`, `KeyboardController.setInputMode`, `adjustPan`, `adjustResize`, `adjustNothing`, and navigation-scoped input-mode hooks. + +Android edge-to-edge guidance: https://developer.android.com/develop/ui/compose/system/setup-e2e + +RNKC platform model: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/recipes/platform-differences + +## Understand RNKC's Android model + +RNKC uses its controller view to enter edge-to-edge, receives IME inset animation frames, and expects `adjustResize` for compatible keyboard frame delivery. In edge-to-edge, `adjustResize` does not mean the old non-edge-to-edge window-resize behavior; the application handles keyboard movement through RNKC. + +Do not preserve `adjustPan` from another library merely because it was previously required. APSL's documented enhanced Android mode uses `adjustPan`; RNKC uses a different ownership model. + +## Find every owner + +Inspect: + +- the main activity in `AndroidManifest.xml` and manifest overlays; +- Expo `app.json`, `app.config.*`, and build-property plugins; +- `KeyboardController.setInputMode` and `setDefaultMode` calls; +- `useKeyboardAnimation`, `useReanimatedKeyboardAnimation`, and `useKeyboardHandler` mounts; +- custom hooks using navigation focus lifecycle; +- bottom-sheet, edge-to-edge, status-bar, and navigation libraries; +- calls that enable or disable RNKC dynamically. + +Choose one policy and remove competing owners. + +## Policy A: global adjustResize + +Use when the whole application uses RNKC-compatible keyboard handling: + +```xml + +``` + +For Expo, configure the installed Expo version's `softwareKeyboardLayoutMode` equivalent to resize and regenerate/rebuild native projects as required. Verify the generated manifest rather than assuming the config was applied. + +With global `adjustResize`, low-level custom handlers can use `useGenericKeyboardHandler` when they intentionally do not want mount-time mode changes. Prebuilt components and standard hooks may still use the package's resize-mode helper; inspect the installed version before replacing internals. + +## Policy B: RNKC screen-scoped mount lifecycle + +Standard RNKC animation hooks call `useResizeMode`, which sets `adjustResize` on mount and restores the manifest default on unmount. This is appropriate when the component unmounts with the screen and no other mounted screen competes for the mode. + +Do not add redundant imperative mode calls around a hook that already owns them. + +## Policy C: navigation focus lifecycle + +React Navigation commonly keeps previous screens mounted. If only the focused screen should use RNKC input mode, use focus lifecycle and consume context values without a second mount-scoped mode owner: + +```tsx +function useFocusedKeyboardAnimation() { + useFocusEffect( + useCallback(() => { + KeyboardController.setInputMode( + AndroidSoftInputModes.SOFT_INPUT_ADJUST_RESIZE, + ); + + return () => KeyboardController.setDefaultMode(); + }, []), + ); + + return useKeyboardContext().reanimated; +} +``` + +Adapt the returned Animated or Reanimated context to the consumer. Do not call `useReanimatedKeyboardAnimation` inside this custom hook as well, because it adds mount-scoped mode ownership. + +## Restore the declared default + +`KeyboardController.setDefaultMode()` restores the manifest or generated application default. Verify that default is intentional. If the manifest still says `adjustPan` from the old library, restoration can reintroduce old behavior after leaving an RNKC screen. + +## Edge-to-edge and system bars + +Review `KeyboardProvider` props when another library controls system bars: + +- `statusBarTranslucent` +- `navigationBarTranslucent` +- `preserveEdgeToEdge` + +RNKC integrates with `react-native-is-edge-to-edge` and detects common edge-to-edge ownership. Do not add duplicate status-bar padding to compensate for a configuration misunderstanding. + +## Dynamic enablement + +Disabling RNKC returns the screen toward default Android behavior. Use `useKeyboardController().setEnabled` only when the product intentionally switches ownership; do not toggle the module as a substitute for choosing the correct screen component. + +## Avoid unsafe patterns + +- Do not change soft-input mode during render. +- Do not let multiple mounted screens set and restore different modes without a clear stack policy. +- Do not mix `adjustPan` layout movement with RNKC translation for the same screen. +- Do not assume a JavaScript config change is active before rebuilding the native app. +- Do not globally change the manifest when the user asked for a narrow screen migration without explaining the scope expansion. + +## Validate on Android + +- Inspect the merged or generated manifest. +- Cold launch after native rebuild. +- Open and close the keyboard on migrated and non-migrated screens. +- Navigate between screens that stay mounted. +- Test Android back dismissal and interactive dismissal where supported. +- Verify status and navigation bar insets in gesture and three-button navigation. +- Test API levels below and above Android 11 when interactive behavior matters. +- Verify module disable and re-enable only if the app uses it. diff --git a/skills/migrate-to-rnkc/references/input-accessory-view.md b/skills/migrate-to-rnkc/references/input-accessory-view.md new file mode 100644 index 0000000000..3bb63d674a --- /dev/null +++ b/skills/migrate-to-rnkc/references/input-accessory-view.md @@ -0,0 +1,103 @@ +# Migrate React Native InputAccessoryView + +Use this reference for `InputAccessoryView`, `inputAccessoryViewID`, and iOS-only keyboard accessory layouts. + +Upstream API: https://reactnative.dev/docs/inputaccessoryview + +Do not map every accessory to one RNKC component. Choose by what the content owns. + +## Select the target + +| Existing accessory intent | RNKC target | Why | +| ---------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------- | +| Previous, next, and done form navigation | `KeyboardToolbar` | Built-in focus and dismissal behavior | +| Custom footer, composer, or action row that follows the keyboard | `KeyboardStickyView` | Translates app-owned content with the keyboard | +| Non-input actions that should become part of the keyboard | `KeyboardExtender` | Extends the keyboard surface and height | +| Menu, picker, or tooltip above the keyboard without dismissal | `OverKeyboardView` | Overlays the keyboard without changing focus | +| Interactive dismissal or input-specific keyboard offset | `KeyboardGestureArea` plus interactive scroll owner | Associates offset with matching input IDs | +| Chat composer | `KeyboardStickyView` plus `KeyboardChatScrollView` | Separates composer movement from message scrolling | + +## Form toolbar migration + +```tsx + + + + + + + + +``` + +Use `$migrate-rnkc-keyboard-toolbar` if the target toolbar itself uses legacy props. + +RNKC toolbar navigation follows the native view hierarchy. Verify input order and use `KeyboardToolbar.Group` only when focus must stay inside a subtree such as a bottom sheet. + +## Sticky input or composer migration + +When `InputAccessoryView` wraps a sticky `TextInput`, move that input back into the app hierarchy and wrap it with `KeyboardStickyView`: + +```tsx + + + + + + +``` + +If the content above the composer is scrollable, choose the corresponding scroll owner. A chat should use `KeyboardChatScrollView`; a form may use `KeyboardAwareScrollView` or a bounded `KeyboardAvoidingView`. + +## Keyboard extension migration + +Use `KeyboardExtender` when the accessory should visually and structurally become part of the keyboard: + +```tsx + + + +``` + +Do not place a `TextInput` inside `KeyboardExtender`. Use `KeyboardBackgroundView` plus `KeyboardStickyView` when the content needs to include an input while matching the keyboard appearance. + +## Overlay migration + +Use `OverKeyboardView` for an app-controlled overlay that must keep the keyboard open: + +```tsx + + + +``` + +It is a transparent full-screen overlay, not a drop-in `Modal` with presentation and animation props. Add custom Reanimated transitions and safe-area handling at the application layer if needed. It can operate without `KeyboardProvider` when used alone. + +## Remove ID wiring carefully + +After migration, remove `InputAccessoryView.nativeID` and `TextInput.inputAccessoryViewID` only when those IDs have no remaining purpose. + +Do not remove `TextInput.nativeID` if it is reused by `KeyboardGestureArea.textInputNativeID`: + +```tsx + + + + +``` + +Multiple inputs can share the same `nativeID` when they intentionally share the gesture-area offset behavior. + +## Preserve iOS behavior while adding Android + +InputAccessoryView is iOS-specific; RNKC targets can be cross-platform. Do not assume Android should automatically receive the identical UI. Check product intent, system back behavior, keyboard appearance, and available screen space before enabling the new accessory on Android. + +## Validate + +- Focus every associated input and verify the correct accessory content. +- Verify multiline growth. +- Verify bottom tabs, safe areas, modals, and rotation. +- Verify keyboard dismissal and focus restoration after overlays. +- Verify hardware and floating keyboards. +- Verify iOS and Android separately. +- Search for leftover `InputAccessoryView`, `inputAccessoryViewID`, and obsolete native IDs. diff --git a/skills/migrate-to-rnkc/references/keyboard-avoiding-view.md b/skills/migrate-to-rnkc/references/keyboard-avoiding-view.md new file mode 100644 index 0000000000..493de4d5be --- /dev/null +++ b/skills/migrate-to-rnkc/references/keyboard-avoiding-view.md @@ -0,0 +1,102 @@ +# Migrate React Native KeyboardAvoidingView + +Use this reference for `KeyboardAvoidingView` imported from `react-native`. The RNKC component intentionally keeps the familiar core API and adds cross-platform animation behavior, `translate-with-padding`, and `automaticOffset`. + +Upstream API: https://reactnative.dev/docs/keyboardavoidingview + +RNKC API: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view + +## Decide whether avoidance is still the right architecture + +Do not perform an import swap before classifying the screen: + +- Keep `KeyboardAvoidingView` for a bounded screen or panel whose layout should resize, pad, or reposition as a unit. +- Choose `KeyboardAwareScrollView` for a long form where the focused input must be scrolled into view. +- Choose `KeyboardStickyView` when only a footer or composer should move. +- Choose `KeyboardChatScrollView` plus `KeyboardStickyView` for a production chat screen. + +If the existing core component wraps a `FlatList`, FlashList, LegendList, or complex chat, migration is an opportunity to select the correct owner rather than preserve a mismatched wrapper. + +## Direct API mapping + +These props can normally be preserved during an import migration: + +| React Native prop | RNKC prop | Notes | +| ----------------------------- | --------------- | -------------------------------------------------- | +| `behavior="height"` | Same | RNKC animates cross-platform | +| `behavior="position"` | Same | Preserve `contentContainerStyle` | +| `behavior="padding"` | Same | Verify nested scroll and flex behavior | +| `contentContainerStyle` | Same | Only applies to `position` behavior | +| `enabled` | Same | Reassess old platform conditionals after testing | +| `keyboardVerticalOffset` | Same by default | Semantics change when `automaticOffset` is enabled | +| `style`, children, View props | Same | Preserve layout ownership | + +Basic migration: + +```diff +-import { KeyboardAvoidingView } from "react-native"; ++import { KeyboardAvoidingView } from "react-native-keyboard-controller"; +``` + +Ensure `KeyboardProvider` is mounted before relying on the component. + +## Preserve behavior first + +Keep the existing `behavior`, `keyboardVerticalOffset`, structure, and platform conditionals in the first migration pass unless they are the known source of the problem. Verify parity, then simplify obsolete workarounds separately. + +For example, do not immediately replace this: + +```tsx +behavior={Platform.OS === "ios" ? "padding" : undefined} +``` + +with unconditional behavior merely because RNKC supports Android. First record why Android was excluded and test the intended RNKC behavior there. + +## Adopt `automaticOffset` deliberately + +Without `automaticOffset`, `keyboardVerticalOffset` compensates for the component's parent-relative layout, commonly with a navigation header height. + +With `automaticOffset`, RNKC measures the actual screen position. `keyboardVerticalOffset` becomes additional spacing rather than header compensation: + +```tsx + + {/* content */} + +``` + +Do not enable it while retaining a full header-height offset without recalculating the desired result; that can double the spacing. + +## Choose behavior by layout intent + +- `padding`: preserve container size and add bottom padding. Verify content that already has bottom padding or a safe-area inset. +- `height`: shrink the container. Verify `flex`, minimum height, and nested list behavior. +- `position`: move the inner container. Preserve `contentContainerStyle` separately from the outer `style`. +- `translate-with-padding`: use only when its translation model matches the design. It is RNKC-specific and can perform well, but a chat list still needs chat-specific scroll ownership. + +## Remove duplicate keyboard ownership + +After the RNKC component is verified, remove from the same screen: + +- manual keyboard-height padding derived from listeners; +- an outer Reanimated `useAnimatedKeyboard` translation; +- Android-only layout movement that duplicates RNKC; +- a second avoiding view around the same content. + +Keep unrelated business listeners and dismissal logic until migrated by their corresponding references. + +## Test the migration + +- Focus inputs near the top and bottom. +- Open and close with content shorter and taller than the viewport. +- Verify navigation headers, modals, safe areas, and bottom tabs. +- Verify iOS and Android with each preserved behavior. +- Rotate while closed and open. +- Grow a multiline input and switch keyboard types. +- Check navigation away and back when screens stay mounted. + +On iOS New Architecture, a React state update immediately before the keyboard event can interfere with Reanimated commits. Check current RNKC troubleshooting and Reanimated feature-flag guidance rather than compensating with timers. diff --git a/skills/migrate-to-rnkc/references/keyboard-aware-scroll-view.md b/skills/migrate-to-rnkc/references/keyboard-aware-scroll-view.md new file mode 100644 index 0000000000..716deeaa06 --- /dev/null +++ b/skills/migrate-to-rnkc/references/keyboard-aware-scroll-view.md @@ -0,0 +1,146 @@ +# Migrate react-native-keyboard-aware-scroll-view + +Use this reference for APSL `react-native-keyboard-aware-scroll-view`, including `KeyboardAwareScrollView`, `KeyboardAwareFlatList`, `KeyboardAwareSectionList`, `listenToKeyboardEvents`, and wrapper components. + +Upstream source: https://github.com/APSL/react-native-keyboard-aware-scroll-view + +RNKC API: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-aware-scroll-view + +## Choose the target scroll owner + +- Replace an APSL `KeyboardAwareScrollView` with RNKC `KeyboardAwareScrollView` when it owns a normal scrollable form. +- Keep `FlatList`, `SectionList`, FlashList, or LegendList as the virtualized owner and inject RNKC `KeyboardAwareScrollView` through the list's custom scroll-component API. +- Do not nest a virtualized list inside RNKC `KeyboardAwareScrollView` to imitate APSL's exported list wrappers. +- Use `KeyboardChatScrollView`, not `KeyboardAwareScrollView`, for chat message behavior. + +## Prop mapping + +| APSL API | RNKC target | Confidence and notes | +| ------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `extraHeight` | `bottomOffset` | Documented RNKC equivalent: space between caret and keyboard | +| `extraScrollHeight` | `extraKeyboardSpace` | Documented RNKC equivalent: extra bottom keyboard space | +| `enableOnAndroid` | Remove | RNKC is cross-platform; verify Android setup | +| `enableAutomaticScroll` | Usually `enabled` | Not perfectly granular; `enabled={false}` disables RNKC behavior, so inspect mixed-use cases | +| `enableResetScrollToCoords={false}` | `disableScrollOnKeyboardHide` | Closest intent: preserve position on hide | +| `resetScrollToCoords` | Explicit `ref.current?.scrollTo(...)` on the desired event | No direct prop; decide whether reset is still needed | +| `viewIsInsideTabBar` | Explicit layout or offset calculation | No fixed tab-bar magic constant; measure the real layout | +| `keyboardOpeningTime` | Remove | RNKC follows native keyboard timing; do not preserve delay timers by default | +| `innerRef` | Standard React `ref` | Update wrapper types and ref forwarding | +| `onKeyboardWillShow` and related callbacks | `KeyboardEvents` or animation hooks | Choose by business-event versus UI-animation intent | +| `enableOnAndroid` plus manifest `adjustPan` | RNKC plus `adjustResize` ownership | See the Android soft-input reference | + +## Basic scroll-view migration + +```diff +-import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; ++import { KeyboardAwareScrollView } from "react-native-keyboard-controller"; + + +
+ +``` + +Do not sum `extraHeight` and `extraScrollHeight` into one prop; they represent different concerns. + +## Choose `mode` + +RNKC defaults to `mode="insets"`, which extends scroll range without reflowing child layout. Prefer it for most forms. + +Use `mode="layout"` when keyboard space must participate in layout, especially when old content depends on: + +- `contentContainerStyle={{ flex: 1 }}`; +- `justifyContent: "space-between"`; +- a submit button distributed to the bottom; +- `gap` or other flex layout that should rearrange as space changes. + +Verify the old screen rather than inferring mode from one style in isolation. + +## Migrate imperative methods + +| APSL method | RNKC replacement | +| ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `scrollToPosition(x, y, animated)` | Native ScrollView `ref.current?.scrollTo({ x, y, animated })` | +| `scrollToEnd(animated)` | Native ScrollView `ref.current?.scrollToEnd({ animated })` | +| `scrollToFocusedInput(...)` | `ref.current?.assureFocusedInputVisible()` when the currently focused input moved | +| `scrollIntoView(...)` | Keep explicit application measurement/scrolling, or use focused-input assurance if that is the real intent | +| `getScrollResponder()` | Inspect consumer; use the forwarded native scroll ref where possible | + +Example after validation errors change layout: + +```tsx +const scrollRef = useRef(null); + +useEffect(() => { + scrollRef.current?.assureFocusedInputVisible(); +}, [errors]); + + + +; +``` + +## Migrate virtualized list wrappers + +APSL exports keyboard-aware list components. RNKC instead decorates the list's scroll component: + +```tsx +const AwareScrollView = forwardRef( + (props, ref) => , +); + +const renderScrollComponent = useCallback( + (props: ScrollViewProps) => , + [], +); + +; +``` + +Use the list library's exact ref and custom-scroll contract. FlashList can commonly receive a stable component reference; FlatList and LegendList commonly use a stable callback. Preserve list-specific props, ref ownership, inversion, and scroll-to-index behavior. + +## Migrate the HOC + +For `listenToKeyboardEvents(config)(CustomScrollComponent)`: + +1. Identify what the HOC actually adds: focused-input scrolling, event callbacks, Android enablement, offsets, or ref extraction. +2. Wrap RNKC `KeyboardAwareScrollView` around the custom `ScrollViewComponent`, or inject it via the parent list's `renderScrollComponent`. +3. Move only still-relevant config to RNKC props. +4. Replace `refPropName` and `extractNativeRef` with explicit `forwardRef` and the target component's current ref contract. + +Do not recreate a generic HOC unless multiple active consumers still need the same abstraction. + +## Migrate event callbacks separately + +APSL can expose keyboard callbacks as component props. RNKC separates concerns: + +- Business side effects at will/did boundaries: `KeyboardEvents`. +- React rendering: `useKeyboardState` with a selector. +- Animated UI or frame tracking: animation hooks or `useKeyboardHandler`. + +RNKC does not expose `keyboardWillChangeFrame` and `keyboardDidChangeFrame` through `KeyboardEvents`. Use animation values or lifecycle handlers when the old code needs movement rather than only show/hide boundaries. + +## Android configuration + +APSL's documented Android enhancement path uses `adjustPan` plus `enableOnAndroid`. RNKC's animation path expects `adjustResize` with its edge-to-edge controller. Change ownership intentionally and test Android before removing old manifest or Expo settings. + +## Validate + +- Focus every input with normal and large accessibility text. +- Verify the caret, not only the input bounds, stays visible for multiline inputs. +- Show and hide validation messages, then call `assureFocusedInputVisible` where needed. +- Verify hide behavior with and without scroll-position preservation. +- Test short content, long content, nested navigation, bottom tabs, and bottom sheets. +- Test each migrated list's virtualization, refs, scroll-to-index, inversion, and content-inset behavior. +- Search for remaining APSL imports, HOCs, types, manifest `adjustPan`, and patches before uninstalling. diff --git a/skills/migrate-to-rnkc/references/keyboard-events.md b/skills/migrate-to-rnkc/references/keyboard-events.md new file mode 100644 index 0000000000..ef375de4ec --- /dev/null +++ b/skills/migrate-to-rnkc/references/keyboard-events.md @@ -0,0 +1,126 @@ +# Migrate Keyboard.addListener and Manual Keyboard State + +Use this reference for React Native `Keyboard.addListener`, `Keyboard.metrics`, `Keyboard.isVisible`, `Keyboard.scheduleLayoutAnimation`, manual keyboard-height state, and event-driven padding or translations. + +Upstream API: https://reactnative.dev/docs/keyboard.html + +RNKC events: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-events + +## Classify each listener by intent + +Do not map all listeners to `KeyboardEvents`. Choose the narrowest API: + +| Intent | RNKC API | +| ----------------------------------------------------------------- | ---------------------------------------------------------- | +| Business side effect at show or hide boundary | `KeyboardEvents.addListener` | +| Render visibility, appearance, type, or another scalar | `useKeyboardState(selector)` | +| Read latest values only when a callback runs | `KeyboardController.isVisible()` or `.state()` | +| Animate a common layout | Prebuilt RNKC component | +| Drive Animated or Reanimated styles | `useKeyboardAnimation` or `useReanimatedKeyboardAnimation` | +| Handle start, movement, interactive movement, or end on UI thread | `useKeyboardHandler` | +| Dismiss and optionally await completion | `KeyboardController.dismiss()` | + +## Map show and hide events + +RNKC exposes these four events on both platforms: + +- `keyboardWillShow` +- `keyboardDidShow` +- `keyboardWillHide` +- `keyboardDidHide` + +```tsx +useEffect(() => { + const subscription = KeyboardEvents.addListener( + "keyboardDidHide", + onKeyboardDidHide, + ); + + return () => subscription.remove(); +}, [onKeyboardDidHide]); +``` + +Always remove subscriptions. Preserve callback dependencies and avoid duplicate global listeners after migration. + +## Handle unsupported frame-change event names + +React Native also exposes `keyboardWillChangeFrame` and `keyboardDidChangeFrame`; RNKC `KeyboardEvents` does not expose those names. Determine the actual need: + +- If code needs per-frame position, use `useKeyboardHandler.onMove` or `onInteractive`. +- If code needs destination metrics before movement, use `onStart`. +- If code needs final metrics, use `onEnd`. +- If code only used change-frame events as an iOS workaround for show/hide, replace them with the cross-platform RNKC will/did events and verify. + +Do not silently drop frame-change behavior. + +## Remove React state from per-frame UI + +Avoid this pattern: + +```tsx +const [keyboardHeight, setKeyboardHeight] = useState(0); + +useEffect(() => { + const show = Keyboard.addListener("keyboardWillShow", (event) => { + setKeyboardHeight(event.endCoordinates.height); + }); + const hide = Keyboard.addListener("keyboardWillHide", () => { + setKeyboardHeight(0); + }); + + return () => { + show.remove(); + hide.remove(); + }; +}, []); +``` + +Choose the owner instead: + +- `KeyboardStickyView` for a footer; +- `KeyboardAvoidingView` for a bounded screen; +- `KeyboardAwareScrollView` for a form; +- `KeyboardChatScrollView` for chat; +- animation hooks for genuinely custom UI. + +## Replace callback-only subscriptions + +When a listener or hook exists only so a button callback can inspect keyboard state, remove the subscription: + +```tsx +const onContinue = () => { + if (KeyboardController.isVisible()) { + // ... + } +}; +``` + +Use `KeyboardController.state()` for the latest `height`, `duration`, `timestamp`, `target`, `type`, and `appearance`. + +## Replace layout animation scheduling + +Do not mechanically reproduce `Keyboard.scheduleLayoutAnimation(event)` with JS timers or `LayoutAnimation`. Prefer a native-driven RNKC component or animation value. If the old code animates a non-UI property and needs intermediate iOS frames, use `useKeyboardHandler` rather than relying only on the high-level progress shared value. + +## Dismissal + +`KeyboardController.dismiss()` removes focus by default and resolves when the keyboard is hidden. It also supports: + +```tsx +await KeyboardController.dismiss({ keepFocus: true }); +await KeyboardController.dismiss({ animated: false }); +``` + +Do not replace `Keyboard.dismiss()` everywhere without a reason. Prefer the controller when the code needs custom-input support, completion awaiting, focus retention, or non-animated dismissal. + +## Event data differences + +RNKC event data exposes `height` directly rather than React Native's `endCoordinates.height`, and also includes `duration`, `timestamp`, `target`, `type`, and `appearance`. Update consumer code explicitly and verify units and timing. + +## Validate + +- Verify each side effect fires once at the intended will or did boundary. +- Verify listener cleanup on unmount and navigation changes. +- Verify no React render loop occurs during keyboard movement. +- Verify custom animations remain synchronized during interactive dismissal. +- Verify Android will events now exposed by RNKC do not trigger logic twice through an existing fallback. +- Verify dismissal, focus, and awaited completion semantics. diff --git a/skills/migrate-to-rnkc/references/reanimated-use-animated-keyboard.md b/skills/migrate-to-rnkc/references/reanimated-use-animated-keyboard.md new file mode 100644 index 0000000000..fa2e154b69 --- /dev/null +++ b/skills/migrate-to-rnkc/references/reanimated-use-animated-keyboard.md @@ -0,0 +1,129 @@ +# Migrate Reanimated useAnimatedKeyboard + +Use this reference for `useAnimatedKeyboard` and `KeyboardState` imported from `react-native-reanimated`. + +Current Reanimated guide: https://docs.swmansion.com/react-native-reanimated/docs/device/useAnimatedKeyboard/ + +Reanimated 4 documents the hook as deprecated and recommends RNKC. RNKC provides a compatibility API to make the first migration stage an import change. + +## Do not run both keyboard controllers + +Do not keep Reanimated `useAnimatedKeyboard` mounted alongside RNKC keyboard animation hooks around the same application. Both take ownership of Android keyboard and inset behavior. Migrate the import or the architecture, then remove the old hook from that scope. + +## Phase 1: compatibility import + +Prefer the compatibility layer when existing code depends on positive `height` and the `KeyboardState` enum: + +```diff +-import { KeyboardState, useAnimatedKeyboard } from "react-native-reanimated"; ++import { ++ KeyboardState, ++ useAnimatedKeyboard, ++} from "react-native-keyboard-controller"; +``` + +The RNKC compatibility hook returns: + +- `height`: positive physical keyboard height in a Reanimated shared value; +- `state`: `UNKNOWN`, `OPENING`, `OPEN`, `CLOSING`, or `CLOSED`. + +Existing expressions such as `translateY: -keyboard.height.value` should normally keep their sign in this phase. + +Mount `KeyboardProvider`, verify Android soft-input ownership, and rebuild the native applications. + +## Inspect options before import swapping + +The RNKC compatibility hook currently takes no options. If the old hook passes options such as status-bar or navigation-bar translucency, move the intended inset policy to `KeyboardProvider`: + +```tsx + + + +``` + +Do not copy options mechanically. Inspect whether another edge-to-edge library already owns system bars and use the provider props that match the final app policy. + +## Phase 2: use native RNKC semantics when useful + +Refactor beyond the compatibility layer only when the code benefits from RNKC's native concepts. + +### Height and progress animation + +```tsx +const { height, progress } = useReanimatedKeyboardAnimation(); + +const style = useAnimatedStyle(() => ({ + opacity: progress.value, + transform: [{ translateY: height.value }], +})); +``` + +Important: `useReanimatedKeyboardAnimation().height` is negative while the keyboard is open. Reanimated and RNKC compatibility `useAnimatedKeyboard().height` are positive physical heights. Convert signs intentionally: + +```diff +-const keyboard = useAnimatedKeyboard(); ++const { height } = useReanimatedKeyboardAnimation(); + + const style = useAnimatedStyle(() => ({ +- transform: [{ translateY: -keyboard.height.value }], ++ transform: [{ translateY: height.value }], + })); +``` + +### Lifecycle and per-frame work + +Use `useKeyboardHandler` for `onStart`, `onMove`, `onInteractive`, and `onEnd`. Each handler must contain a `"worklet"` directive. Event `height` is a positive physical height, unlike the signed animation hook value. + +```tsx +useKeyboardHandler( + { + onStart: (event) => { + "worklet"; + targetHeight.value = event.height; + }, + onMove: (event) => { + "worklet"; + currentHeight.value = event.height; + }, + onEnd: (event) => { + "worklet"; + isOpen.value = event.height > 0; + }, + }, + [], +); +``` + +### Render and business state + +- Use `useKeyboardState((state) => state.isVisible)` for React UI that must render visibility. +- Use `KeyboardController.isVisible()` or `.state()` for values read only inside callbacks. +- Keep the compatibility `KeyboardState` enum when animation logic genuinely distinguishes opening and closing phases. + +## Prefer prebuilt components + +If the old hook only translates a common keyboard layout, replace the handwritten animation with the owning RNKC component: + +- whole bounded layout: `KeyboardAvoidingView`; +- fixed footer: `KeyboardStickyView`; +- scrollable form: `KeyboardAwareScrollView`; +- chat: `KeyboardChatScrollView` plus `KeyboardStickyView`. + +This removes per-screen keyboard math and is often the more maintainable end state. + +## Android lifecycle + +Reanimated's old hook changes root and inset behavior while mounted. RNKC hooks commonly set `adjustResize` on mount and restore the default on unmount. In stack navigators that keep screens mounted, use an explicit focus-scoped strategy when only the active screen should own that mode. See the Android soft-input reference. + +## Validate + +- Compare keyboard-open translations before and after; sign mistakes are immediately visible. +- Exercise opening, closing, interactive dismissal, and keyboard size changes. +- Verify status and navigation bar insets on Android. +- Verify navigation away from and back to mounted screens. +- Test floating and physical keyboards where supported. +- Search for all remaining Reanimated `useAnimatedKeyboard` imports before considering the migration complete. diff --git a/skills/migrate-to-rnkc/references/setup-and-strategy.md b/skills/migrate-to-rnkc/references/setup-and-strategy.md new file mode 100644 index 0000000000..81aada9bcf --- /dev/null +++ b/skills/migrate-to-rnkc/references/setup-and-strategy.md @@ -0,0 +1,117 @@ +# Setup and Migration Strategy + +## Establish package compatibility + +1. Inspect the app's React Native, Reanimated, Expo, architecture, iOS, and Android versions. +2. Check the RNKC compatibility guide for that combination. +3. Install RNKC with the application's package manager. +4. Ensure Reanimated is installed and configured. +5. Reinstall iOS pods and rebuild both native applications; hot reload cannot link a new native module. +6. Do not assume Expo Go supports the installed RNKC version; verify the current Expo environment. + +Current package documentation: + +- Installation: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/installation +- Compatibility: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/guides/compatibility +- Troubleshooting: https://kirillzyusko.github.io/react-native-keyboard-controller/docs/troubleshooting + +When the app already has RNKC, inspect `node_modules/react-native-keyboard-controller/src` and its package version before applying newer API guidance. + +## Mount one provider + +Mount `KeyboardProvider` around the application or navigation tree that needs RNKC: + +```tsx +import { KeyboardProvider } from "react-native-keyboard-controller"; + +export function App() { + return ( + + {/* application */} + + ); +} +``` + +Use one stable provider rather than one provider per screen. Review these props when the old solution controlled insets: + +- `statusBarTranslucent` +- `navigationBarTranslucent` +- `preserveEdgeToEdge` +- `enabled` +- `preload` + +`OverKeyboardView` can operate without `KeyboardProvider` when it is the only RNKC feature in use. Most animation-backed components and hooks require the real provider context. + +## Capture a behavior baseline + +Before editing, record or describe: + +- layout before, during, and after keyboard presentation; +- scroll position before and after focus changes; +- header, safe-area, tab-bar, and modal offsets; +- interactive dismissal behavior; +- callback timing and side effects; +- Android behavior under the current `windowSoftInputMode`; +- iOS accessory or floating-keyboard behavior. + +If the task is a bug fix, reproduce the bug and identify the existing close or movement path before migration. Do not let migration hide an unrelated defect. + +## Search the whole ownership surface + +Search for: + +```text +KeyboardAvoidingView +react-native-keyboard-aware-scroll-view +KeyboardAwareScrollView +KeyboardAwareFlatList +KeyboardAwareSectionList +listenToKeyboardEvents +useAnimatedKeyboard +KeyboardState +InputAccessoryView +inputAccessoryViewID +Keyboard.addListener +keyboardWillShow +keyboardDidShow +keyboardWillHide +keyboardDidHide +keyboardWillChangeFrame +keyboardDidChangeFrame +windowSoftInputMode +softwareKeyboardLayoutMode +setInputMode +adjustPan +adjustResize +adjustNothing +``` + +Also inspect wrapper components, shared layout primitives, manifest overlays, Expo plugins, native patches, and tests. + +## Migrate incrementally + +Prefer a vertical slice that includes one screen, its tests, and its configuration. Keep the old dependency installed while other screens still import it, but do not mount both implementations around the migrated screen. + +For each slice: + +1. State the old owner graph. +2. State the new owner graph. +3. Apply the narrow code and configuration changes. +4. Test both platforms. +5. Search for leftover old ownership in that slice. + +## Remove the old dependency safely + +Only uninstall when all of these are clear: + +- no source or test imports remain; +- no application wrappers re-export it; +- no Babel, Metro, Expo, CocoaPods, Gradle, manifest, or patch configuration remains for it; +- no native rebuild is pending; +- lockfile changes match the selected package manager; +- the final iOS and Android behavior has been verified. + +## Avoid popularity-driven overreach + +Prefer RNKC when it directly solves the screen's keyboard ownership, but do not replace a working low-level implementation merely to increase package usage. A trustworthy migration skill improves adoption by selecting RNKC precisely, explaining the benefits, and preserving behavior. diff --git a/skills/prefer-rnkc-keyboard-selectors/SKILL.md b/skills/prefer-rnkc-keyboard-selectors/SKILL.md new file mode 100644 index 0000000000..b10e5d9e46 --- /dev/null +++ b/skills/prefer-rnkc-keyboard-selectors/SKILL.md @@ -0,0 +1,120 @@ +--- +name: prefer-rnkc-keyboard-selectors +description: Review, write, or refactor React Native Keyboard Controller state consumption so useKeyboardState uses narrow selectors and avoids unnecessary React renders. Use when code calls useKeyboardState without a selector, destructures the entire keyboard state, reads hook state only inside callbacks, drives styles from keyboard state, creates object or array selector results, or reports keyboard-related re-render and performance problems. +license: MIT +metadata: + author: kirillzyusko + source: react-native-keyboard-controller +--- + +# Prefer RNKC Keyboard Selectors + +Use the smallest state subscription that matches how the value is consumed. Inspect the installed `react-native-keyboard-controller` source when behavior may differ by version. + +## Audit the usage + +1. Find every `useKeyboardState` call in the requested scope. +2. Classify each value as render state, event-time state, animated state, or debugging state. +3. Refactor only the keyboard-state ownership. Preserve unrelated component behavior. +4. Verify render behavior and the keyboard interaction that consumes the value. + +## Apply the decision table + +| Need | Use | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------ | +| Render a boolean, label, theme, type, or other scalar | `useKeyboardState((state) => state.field)` | +| Render a derived boolean or scalar | A selector returning that primitive | +| Read the latest value only when a callback runs | `KeyboardController.isVisible()` or `KeyboardController.state()` inside the callback | +| Animate a style with keyboard position | `useKeyboardAnimation()` or `useReanimatedKeyboardAnimation()` | +| Process start, move, interactive, or end frames | `useKeyboardHandler()` | +| Display the complete state in a dedicated debug view | `useKeyboardState()` is acceptable | + +## Prefer scalar selectors + +```tsx +// Avoid: every keyboard state object update reaches this component. +const { isVisible } = useKeyboardState(); + +// Prefer: unchanged booleans are filtered by React state equality. +const isVisible = useKeyboardState((state) => state.isVisible); +``` + +Return a primitive or an existing stable reference. The hook does not accept a custom equality function, so a fresh object or array is considered changed: + +```tsx +// Avoid: creates a new object for every relevant keyboard event. +const keyboard = useKeyboardState((state) => ({ + isVisible: state.isVisible, + appearance: state.appearance, +})); + +// Prefer when both fields are independently rendered. +const isVisible = useKeyboardState((state) => state.isVisible); +const appearance = useKeyboardState((state) => state.appearance); + +// Prefer when the UI needs only one derived result. +const showDarkBackdrop = useKeyboardState( + (state) => state.isVisible && state.appearance === "dark", +); +``` + +Do not add `useMemo` around the returned value to repair an over-broad subscription; narrow the selector instead. + +## Read callback-only state imperatively + +```tsx +// Avoid: subscribes and re-renders only to read the value on press. +const isVisible = useKeyboardState((state) => state.isVisible); + +const onPress = () => { + if (isVisible) { + // ... + } +}; + +// Prefer: read the latest value when the callback runs. +const onPress = () => { + if (KeyboardController.isVisible()) { + // ... + } +}; +``` + +Use `KeyboardController.state()` for event-time `height`, `duration`, `timestamp`, `target`, `type`, or `appearance` values. + +## Keep animations off the React render path + +Do not update ordinary React styles from `useKeyboardState().height`. Use native-driven values: + +```tsx +const { height } = useReanimatedKeyboardAnimation(); + +const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: height.value }], +})); +``` + +`useReanimatedKeyboardAnimation().height` is a signed translation value and is negative while the keyboard is open. Do not negate it unless the intended movement requires the opposite direction. The compatibility `useAnimatedKeyboard()` API exposes a positive keyboard height, so verify which hook the code uses before changing signs. + +## Keep selectors pure and stable in meaning + +The current hook installs its event subscriptions once. Avoid selectors whose meaning depends on changing props or mutable values: + +```tsx +// Avoid: expectedType can change while the subscribed selector still captures an old value. +const matches = useKeyboardState((state) => state.type === expectedType); + +// Prefer: subscribe to package state, then combine with component state. +const keyboardType = useKeyboardState((state) => state.type); +const matches = keyboardType === expectedType; +``` + +Place the subscription in the lowest component that renders the value. Do not lift keyboard state to a navigator or app root unless that layer actually owns the decision. + +## Verify the refactor + +- Confirm no selector returns a fresh object or array without a deliberate reason. +- Confirm callback-only reads no longer create subscriptions. +- Confirm keyboard-driven styles use Animated, Reanimated, or worklet handlers. +- Exercise keyboard show, hide, type, and appearance changes used by the component. +- Use React Profiler or a temporary render counter when the task is specifically about re-renders. diff --git a/skills/prefer-rnkc-keyboard-selectors/agents/openai.yaml b/skills/prefer-rnkc-keyboard-selectors/agents/openai.yaml new file mode 100644 index 0000000000..6f011c537e --- /dev/null +++ b/skills/prefer-rnkc-keyboard-selectors/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Prefer RNKC Keyboard Selectors" + short_description: "Use keyboard state selectors efficiently" + default_prompt: "Use $prefer-rnkc-keyboard-selectors to review and optimize useKeyboardState usage in this React Native code."