Skip to content
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

Unnecessary blank area is created when scrolling down while keyboard is open #19642 #21298

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
87737bd
viewport scroll block on report screen
lukemorawski Jun 22, 2023
9ef0f83
refactored function declaration
lukemorawski Jun 22, 2023
c342cec
formatting, const naming and jsdocs
lukemorawski Jun 23, 2023
6445869
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Jun 28, 2023
21fb1b4
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Jul 18, 2023
906f744
linting
lukemorawski Jul 19, 2023
0d240cb
even more linting
lukemorawski Jul 20, 2023
75cf222
even more linting
lukemorawski Jul 20, 2023
f8c6243
bug fix/firing swipe down call back in SwipeableView on scrolling
lukemorawski Jul 27, 2023
1468987
fix/swipe down on android closes keyboard when scrolling
lukemorawski Aug 1, 2023
279e4c4
swipeableview props desctruct
lukemorawski Aug 4, 2023
4362760
formatting
lukemorawski Aug 4, 2023
6a5e329
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Oct 13, 2023
bc77e5c
linting
lukemorawski Oct 17, 2023
ae79e0a
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Oct 24, 2023
8dfc2b6
refactored SwipeableView for browsers to TS
lukemorawski Oct 24, 2023
04af0ba
native swipeable view compatibility with the browser one
lukemorawski Oct 24, 2023
f4e948c
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Nov 6, 2023
fdbfb79
linting
lukemorawski Nov 6, 2023
11bd396
fixes
lukemorawski Nov 9, 2023
4cd8620
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Nov 15, 2023
29d12dd
prettier
lukemorawski Nov 15, 2023
7ed0356
added comment to swipeableview style prop
lukemorawski Nov 17, 2023
2a5389a
refactor/nativewebkeyboard to TS
lukemorawski Nov 17, 2023
d94490b
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Nov 17, 2023
b704342
added comments to exported functions on NativeWebKeyboard
lukemorawski Nov 17, 2023
8896a4d
Merge branch 'main' into lukemorawski19642-blank_area_on_scroll
lukemorawski Nov 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions src/components/SwipeableView/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,40 @@ import {PanResponder, View} from 'react-native';
import CONST from '@src/CONST';
import SwipeableViewProps from './types';

function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
function SwipeableView({children, onSwipeDown, onSwipeUp}: SwipeableViewProps) {
const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT;
const oldYRef = useRef(0);
const directionRef = useRef<'UP' | 'DOWN' | null>(null);

const panResponder = useRef(
PanResponder.create({
// The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
// eslint-disable-next-line @typescript-eslint/naming-convention
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
onMoveShouldSetPanResponderCapture: (event, gestureState) => {
if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) {
directionRef.current = 'DOWN';
return true;
}

if (gestureState.dy - oldYRef.current < 0 && Math.abs(gestureState.dy) > minimumPixelDistance) {
directionRef.current = 'UP';
return true;
}
oldYRef.current = gestureState.dy;
return false;
},

// Calls the callback when the swipe down is released; after the completion of the gesture
onPanResponderRelease: onSwipeDown,
onPanResponderRelease: () => {
if (directionRef.current === 'DOWN' && onSwipeDown) {
onSwipeDown();
} else if (directionRef.current === 'UP' && onSwipeUp) {
onSwipeUp();
}
directionRef.current = null; // Reset the direction after the gesture completes
},
}),
).current;

return (
// eslint-disable-next-line react/jsx-props-no-spreading
<View {...panResponder.panHandlers}>{children}</View>
);
// eslint-disable-next-line react/jsx-props-no-spreading
return <View {...panResponder.panHandlers}>{children}</View>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should apply the style prop here as well same as web.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great spotting! But this is deliberate :) When the style is added in native it brakes the layout

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, It will make this component inconsistent across platforms. May be we can manage the styles in such a way that works across platforms.

Copy link
Member

@parasharrajat parasharrajat Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise, Add a comment here to explain why styles are not applied. I would still prefer it be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left the comment as I don't really have the capacity now to deal with that styling problem. Hopefully that's good enough.

}

SwipeableView.displayName = 'SwipeableView';
Expand Down
77 changes: 75 additions & 2 deletions src/components/SwipeableView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,77 @@
import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import DomUtils from '@libs/DomUtils';
import SwipeableViewProps from './types';

// Swipeable View is available just on Android/iOS for now.
export default ({children}: SwipeableViewProps) => children;
// Min delta y in px to trigger swipe
const MIN_DELTA_Y = 25;
lukemorawski marked this conversation as resolved.
Show resolved Hide resolved

function SwipeableView({onSwipeUp, onSwipeDown, style, children}: SwipeableViewProps) {
const ref = useRef<View | null>(null);
const scrollableChildRef = useRef<HTMLElement | null>(null);
const startY = useRef(0);
const isScrolling = useRef(false);

useEffect(() => {
if (!ref.current) {
return;
}

const element = ref.current as unknown as HTMLElement;

const handleTouchStart = (event: TouchEvent) => {
startY.current = event.touches[0].clientY;
};

const handleTouchEnd = (event: TouchEvent) => {
const deltaY = event.changedTouches[0].clientY - startY.current;
const isSelecting = DomUtils.isActiveTextSelection();
let canSwipeDown = true;
let canSwipeUp = true;
if (scrollableChildRef.current) {
canSwipeUp = scrollableChildRef.current.scrollHeight - scrollableChildRef.current.scrollTop === scrollableChildRef.current.clientHeight;
canSwipeDown = scrollableChildRef.current.scrollTop === 0;
}

if (deltaY > MIN_DELTA_Y && onSwipeDown && !isSelecting && canSwipeDown && !isScrolling.current) {
onSwipeDown();
}

if (deltaY < -MIN_DELTA_Y && onSwipeUp && !isSelecting && canSwipeUp && !isScrolling.current) {
onSwipeUp();
}
isScrolling.current = false;
};

const handleScroll = (event: Event) => {
isScrolling.current = true;
if (!event.target || scrollableChildRef.current) {
return;
}
scrollableChildRef.current = event.target as HTMLElement;
};

element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchend', handleTouchEnd);
element.addEventListener('scroll', handleScroll, true);

return () => {
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchend', handleTouchEnd);
element.removeEventListener('scroll', handleScroll);
};
}, [onSwipeDown, onSwipeUp]);

return (
<View
ref={ref}
style={style}
>
{children}
</View>
);
}

SwipeableView.displayName = 'SwipeableView';

export default SwipeableView;
9 changes: 8 additions & 1 deletion src/components/SwipeableView/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import {ReactNode} from 'react';
import {StyleProp, ViewStyle} from 'react-native';

type SwipeableViewProps = {
/** The content to be rendered within the SwipeableView */
children: ReactNode;

/** Callback to fire when the user swipes down on the child content */
onSwipeDown: () => void;
onSwipeDown?: () => void;

/** Callback to fire when the user swipes up on the child content */
onSwipeUp?: () => void;

/** Style for the wrapper View */
style?: StyleProp<ViewStyle>;
};

export default SwipeableViewProps;
15 changes: 15 additions & 0 deletions src/hooks/useBlockViewportScroll/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* A hook that blocks viewport scroll when the keyboard is visible.
* It does this by capturing the current scrollY position when the keyboard is shown, then scrolls back to this position smoothly on 'touchend' event.
* This scroll blocking is removed when the keyboard hides.
* This hook is doing nothing on native platforms.
*
* @example
* useBlockViewportScroll();
*/
function useBlockViewportScroll() {
// This hook is doing nothing on native platforms.
// Check index.ts for web implementation.
}

export default useBlockViewportScroll;
43 changes: 43 additions & 0 deletions src/hooks/useBlockViewportScroll/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {useEffect, useRef} from 'react';
import Keyboard from '@libs/NativeWebKeyboard';

/**
* A hook that blocks viewport scroll when the keyboard is visible.
* It does this by capturing the current scrollY position when the keyboard is shown, then scrolls back to this position smoothly on 'touchend' event.
* This scroll blocking is removed when the keyboard hides.
* This hook is doing nothing on native platforms.
*
* @example
* useBlockViewportScroll();
*/
function useBlockViewportScroll() {
const optimalScrollY = useRef(0);
const keyboardShowListenerRef = useRef(() => {});
const keyboardHideListenerRef = useRef(() => {});

useEffect(() => {
const handleTouchEnd = () => {
window.scrollTo({top: optimalScrollY.current, behavior: 'smooth'});
};

const handleKeybShow = () => {
optimalScrollY.current = window.scrollY;
window.addEventListener('touchend', handleTouchEnd);
};

const handleKeybHide = () => {
window.removeEventListener('touchend', handleTouchEnd);
};

keyboardShowListenerRef.current = Keyboard.addListener('keyboardDidShow', handleKeybShow);
keyboardHideListenerRef.current = Keyboard.addListener('keyboardDidHide', handleKeybHide);

return () => {
keyboardShowListenerRef.current();
keyboardHideListenerRef.current();
window.removeEventListener('touchend', handleTouchEnd);
};
}, []);
}

export default useBlockViewportScroll;
16 changes: 16 additions & 0 deletions src/libs/DomUtils/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import GetActiveElement from './types';

const getActiveElement: GetActiveElement = () => null;

/**
* Checks if there is a text selection within the currently focused input or textarea element.
*
* This function determines whether the currently focused element is an input or textarea,
* and if so, it checks whether there is a text selection (i.e., whether the start and end
* of the selection are at different positions). It assumes that only inputs and textareas
* can have text selections.
* Works only on web. Throws an error on native.
*
* @returns True if there is a text selection within the focused element, false otherwise.
*/
const isActiveTextSelection = () => {
throw new Error('Not implemented in React Native. Use only for web.');
};

const requestAnimationFrame = (callback: () => void) => {
if (!callback) {
return;
Expand All @@ -12,5 +27,6 @@ const requestAnimationFrame = (callback: () => void) => {

export default {
getActiveElement,
isActiveTextSelection,
requestAnimationFrame,
};
23 changes: 23 additions & 0 deletions src/libs/DomUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,30 @@ import GetActiveElement from './types';

const getActiveElement: GetActiveElement = () => document.activeElement;

/**
* Checks if there is a text selection within the currently focused input or textarea element.
*
* This function determines whether the currently focused element is an input or textarea,
* and if so, it checks whether there is a text selection (i.e., whether the start and end
* of the selection are at different positions). It assumes that only inputs and textareas
* can have text selections.
* Works only on web. Throws an error on native.
*
* @returns True if there is a text selection within the focused element, false otherwise.
*/
const isActiveTextSelection = (): boolean => {
const focused = document.activeElement as HTMLInputElement | HTMLTextAreaElement | null;
if (!focused) {
return false;
}
if (typeof focused.selectionStart === 'number' && typeof focused.selectionEnd === 'number') {
return focused.selectionStart !== focused.selectionEnd;
}
return false;
};

export default {
getActiveElement,
isActiveTextSelection,
requestAnimationFrame: window.requestAnimationFrame.bind(window),
};
Loading
Loading