-
Notifications
You must be signed in to change notification settings - Fork 372
fix: animation stutter on edit #3537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
678cceb
fix: animation stutter on editing
isekovanic 0949395
refactor: move PWC concern in a separate hook
isekovanic fa1546f
refactor: clean hooks up properly
isekovanic 471c31a
fix: cleanup
isekovanic fbcbb0c
fix: lint and tests
isekovanic 78e6557
fix: lint issue
isekovanic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { useEffect, useRef } from 'react'; | ||
| import { EventSubscription, Keyboard, Platform } from 'react-native'; | ||
|
|
||
| import { useKeyboardVisibility } from './useKeyboardVisibility'; | ||
|
|
||
| import { useStableCallback } from './useStableCallback'; | ||
|
|
||
| import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; | ||
| import { useMessageInputContext } from '../contexts/messageInputContext/MessageInputContext'; | ||
|
|
||
| /** | ||
| * A utility hook that returns a stable callback which focuses the message input | ||
| * and invokes the callback once the keyboard is open. | ||
| * | ||
| * @param callback - callback we want to run once the keyboard is ready | ||
| * @returns A stable callback that will wait for the keyboard to be open before executing. | ||
| */ | ||
| export const useAfterKeyboardOpenCallback = <T extends unknown[]>( | ||
| callback: (...args: T) => void, | ||
| ) => { | ||
| const isKeyboardVisible = useKeyboardVisibility(); | ||
| const { inputBoxRef } = useMessageInputContext(); | ||
| const keyboardSubscriptionRef = useRef<EventSubscription | undefined>(undefined); | ||
| // This callback runs from a keyboard event listener, so it must stay fresh across rerenders. | ||
| const stableCallback = useStableCallback(callback); | ||
|
|
||
| /** Clears the pending keyboard listener, if any. */ | ||
| const clearKeyboardSubscription = useStableCallback(() => { | ||
| keyboardSubscriptionRef.current?.remove(); | ||
| keyboardSubscriptionRef.current = undefined; | ||
| }); | ||
|
|
||
| useEffect(() => clearKeyboardSubscription, [clearKeyboardSubscription]); | ||
|
|
||
| return useStableCallback((...args: T) => { | ||
| clearKeyboardSubscription(); | ||
|
|
||
| const runCallback = () => { | ||
| clearKeyboardSubscription(); | ||
| stableCallback(...args); | ||
| }; | ||
|
|
||
| if (!inputBoxRef.current) { | ||
| runCallback(); | ||
| return; | ||
| } | ||
|
|
||
| if (isKeyboardVisible) { | ||
| inputBoxRef.current.focus(); | ||
| runCallback(); | ||
| return; | ||
| } | ||
|
|
||
| const keyboardEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; | ||
|
|
||
| keyboardSubscriptionRef.current = KeyboardControllerPackage?.KeyboardEvents | ||
| ? KeyboardControllerPackage.KeyboardEvents.addListener(keyboardEvent, runCallback) | ||
| : Keyboard.addListener(keyboardEvent, runCallback); | ||
|
|
||
| inputBoxRef.current.focus(); | ||
| }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { useEffect, useRef } from 'react'; | ||
| import { Platform } from 'react-native'; | ||
|
|
||
| import { useStableCallback } from './useStableCallback'; | ||
|
|
||
| /** | ||
| * Number of frames we wait before invoking input focus sensitive work after the | ||
| * overlay closes. | ||
| */ | ||
| const SETTLE_FRAMES = Platform.OS === 'android' ? 2 : 0; | ||
|
|
||
| /** | ||
| * Runs a callback after a fixed number of animation frames. | ||
| * | ||
| * We use RAFs here because the settling work we care about is tied to the next | ||
| * rendered frames after the overlay close transition. | ||
| * | ||
| * @param callback - callback to run once the frame budget has elapsed | ||
| * @param frames - number of frames to wait | ||
| * @param rafIds - accumulator used for later cancellation/cleanup | ||
| */ | ||
| const scheduleAfterFrames = (callback: () => void, frames: number, rafIds: number[]) => { | ||
| if (frames <= 0) { | ||
| callback(); | ||
| return; | ||
| } | ||
|
|
||
| const rafId = requestAnimationFrame(() => scheduleAfterFrames(callback, frames - 1, rafIds)); | ||
| rafIds.push(rafId); | ||
| }; | ||
|
|
||
| /** | ||
| * Returns a stable callback that is safe to run after a `PortalWhileClosingView` | ||
| * has settled back into its original tree. | ||
| * | ||
| * Some followup actions are sensitive to that handoff window. If they run | ||
| * while a view is still being returned from a portal host to its in place host, | ||
| * they can target a node that is about to be reattached. On Android, that is | ||
| * especially noticeable with focus sensitive work, where the target can lose | ||
| * focus again mid keyboard animation. | ||
| * | ||
| * Two frames are intentional here: | ||
| * - frame 1 lets the portal retarget and React commit the component tree | ||
| * - frame 2 lets the native view hierarchy settle in its final host | ||
| * | ||
| * iOS does not currently need this settle window for this flow. | ||
| * | ||
| * A good example is the message composer edit action: after closing the message | ||
| * overlay, we wait for the portal handoff to settle before focusing the input | ||
| * and opening the keyboard. Doing this prematurely will result in the keyboard | ||
| * being immediately closed. | ||
| * | ||
| * Another good example would be having a button wrapped in a `PortalWhileClosingView`, | ||
| * that possibly renders (or morphs into) something when pressed. Handling `onPress` | ||
| * prematurely here may lead to the morphed button rendering into a completely different | ||
| * part of the UI hierarchy, causing unknown behaviour. This hook prevents that from | ||
| * happening. | ||
| * | ||
| * @param callback - callback we want to invoke once the portal handoff has settled | ||
| * @returns A stable callback gated behind the portal settle window. | ||
| */ | ||
| export const usePortalSettledCallback = <T extends unknown[]>(callback: (...args: T) => void) => { | ||
| const rafIdsRef = useRef<number[]>([]); | ||
| // This callback runs from deferred RAF work, so it must stay fresh across rerenders. | ||
| const stableCallback = useStableCallback(callback); | ||
|
|
||
| const clearScheduledFrames = useStableCallback(() => { | ||
| rafIdsRef.current.forEach((rafId) => cancelAnimationFrame(rafId)); | ||
| rafIdsRef.current = []; | ||
| }); | ||
|
|
||
| useEffect(() => clearScheduledFrames, [clearScheduledFrames]); | ||
|
|
||
| return useStableCallback((...args: T) => { | ||
| clearScheduledFrames(); | ||
| scheduleAfterFrames(() => stableCallback(...args), SETTLE_FRAMES, rafIdsRef.current); | ||
| }); | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely not used by the underlying unmemoized component but let's do the cleanup in a separate PR to make sure some parts of the state do not weirdly depend on this rerender.