Skip to content

Commit 80e66d9

Browse files
authored
Merge pull request #47 from stanleyugwu/develop
Develop
2 parents 441c790 + 01a5340 commit 80e66d9

3 files changed

Lines changed: 170 additions & 84 deletions

File tree

commitlint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const Configuration: UserConfig = {
3939
'🧪 test',
4040
],
4141
],
42+
'footer-max-line-length': [2, 'always', 400],
4243
},
4344
};
4445

src/components/bottomSheet/index.tsx

Lines changed: 162 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import React, {
22
forwardRef,
33
useCallback,
4+
useEffect,
45
useImperativeHandle,
6+
useLayoutEffect,
57
useMemo,
68
useRef,
79
useState,
8-
useLayoutEffect,
9-
useEffect,
1010
type ComponentRef,
1111
} from 'react';
1212
import {
1313
Animated,
14-
View,
14+
Keyboard,
1515
PanResponder,
16+
Platform,
1617
StyleSheet,
17-
type LayoutChangeEvent,
1818
useWindowDimensions,
19-
Keyboard,
20-
Platform,
19+
View,
20+
type LayoutChangeEvent,
2121
} from 'react-native';
2222
import {
2323
DEFAULT_ANIMATION,
@@ -26,23 +26,23 @@ import {
2626
DEFAULT_HEIGHT,
2727
DEFAULT_OPEN_ANIMATION_DURATION,
2828
} from '../../constant';
29-
import DefaultHandleBar from '../defaultHandleBar';
30-
import Container from '../container';
31-
import normalizeHeight from '../../utils/normalizeHeight';
32-
import convertHeight from '../../utils/convertHeight';
33-
import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents';
3429
import useAnimatedValue from '../../hooks/useAnimatedValue';
30+
import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose';
31+
import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents';
32+
import convertHeight from '../../utils/convertHeight';
33+
import normalizeHeight from '../../utils/normalizeHeight';
34+
import separatePaddingStyles from '../../utils/separatePaddingStyles';
3535
import Backdrop from '../backdrop';
36+
import Container from '../container';
37+
import DefaultHandleBar from '../defaultHandleBar';
3638
import {
37-
type BottomSheetProps,
38-
type ToValue,
3939
ANIMATIONS,
40-
type BottomSheetMethods,
4140
CUSTOM_BACKDROP_POSITIONS,
4241
type BOTTOMSHEET,
42+
type BottomSheetMethods,
43+
type BottomSheetProps,
44+
type ToValue,
4345
} from './types.d';
44-
import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose';
45-
import separatePaddingStyles from '../../utils/separatePaddingStyles';
4646

4747
/**
4848
* Main bottom sheet component
@@ -106,8 +106,10 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
106106

107107
const contentWrapperRef = useRef<ComponentRef<typeof Animated.View>>(null);
108108

109-
/** cached _nativeTag property of content container */
110-
const cachedContentWrapperNativeTag = useRef<number | undefined>(undefined);
109+
/** cached unique identifier of content container */
110+
const cachedContentWrapperId = useRef<
111+
{ field: string; value: unknown } | undefined
112+
>(undefined);
111113

112114
// here we separate all padding that may be applied via contentContainerStyle prop,
113115
// these paddings will be applied to the `View` diretly wrapping `ChildNodes` in content container.
@@ -120,20 +122,27 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
120122
);
121123

122124
// Animation utility
123-
const Animators = useMemo(
124-
() => ({
125-
_slideEasingFn(value: number) {
126-
return value === 1 ? 1 : 1 - Math.pow(2, -10 * value);
127-
},
128-
_springEasingFn(value: number) {
129-
const c4 = (2 * Math.PI) / 2.5;
130-
return value === 0
131-
? 0
132-
: value === 1
133-
? 1
134-
: Math.pow(2, -9 * value) * Math.sin((value * 4.5 - 0.75) * c4) +
135-
1;
136-
},
125+
const Animators = useMemo(() => {
126+
const _slideEasingFn = (value: number) => {
127+
return value === 1 ? 1 : 1 - Math.pow(2, -10 * value);
128+
};
129+
const _springEasingFn = (value: number) => {
130+
const decay = 9;
131+
const multiplier = 4.5;
132+
const divisor = 2.3;
133+
134+
const c4 = (2 * Math.PI) / divisor;
135+
136+
return value === 0
137+
? 0
138+
: value === 1
139+
? 1
140+
: Math.pow(2, -decay * value) *
141+
Math.sin((value * multiplier - 0.75) * c4) +
142+
1;
143+
};
144+
145+
return {
137146
animateContainerHeight(toValue: ToValue, duration: number = 0) {
138147
return Animated.timing(_animatedContainerHeight, {
139148
toValue: toValue,
@@ -162,19 +171,18 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
162171
customEasingFunction && typeof customEasingFunction === 'function'
163172
? customEasingFunction
164173
: animationType === ANIMATIONS.SLIDE
165-
? this._slideEasingFn
166-
: this._springEasingFn,
174+
? _slideEasingFn
175+
: _springEasingFn,
167176
});
168177
},
169-
}),
170-
[
171-
animationType,
172-
customEasingFunction,
173-
_animatedContainerHeight,
174-
_animatedBackdropMaskOpacity,
175-
_animatedHeight,
176-
]
177-
);
178+
};
179+
}, [
180+
animationType,
181+
customEasingFunction,
182+
_animatedContainerHeight,
183+
_animatedBackdropMaskOpacity,
184+
_animatedHeight,
185+
]);
178186

179187
const interpolatedOpacity = useMemo(
180188
() =>
@@ -241,21 +249,16 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
241249
if (view === 'handlebar' && disableDragHandlePanning) return null;
242250
if (view === 'contentwrapper' && disableBodyPanning) return null;
243251
return PanResponder.create({
244-
onMoveShouldSetPanResponder: (evt) => {
245-
/**
246-
* `FiberNode._nativeTag` is stable across renders so we use it to determine
247-
* whether content container or it's child should respond to touch move gesture.
248-
*
249-
* The logic is, when content container is laid out, we extract it's _nativeTag property and cache it
250-
* So later when a move gesture event occurs within it, we compare the cached _nativeTag with the _nativeTag of
251-
* the event target's _nativeTag, if they match, then content container should respond, else its children should.
252-
* Also, when the target is the handle bar, we le it handle geture unless panning is disabled through props
253-
*/
254-
return view === 'handlebar'
255-
? true
256-
: cachedContentWrapperNativeTag.current ===
257-
// @ts-expect-error
258-
evt?.target?._nativeTag;
252+
onMoveShouldSetPanResponderCapture: (evt) => {
253+
if (view === 'handlebar') return true;
254+
const cached = cachedContentWrapperId.current;
255+
if (!cached) return false; // this signature alone should fix issue #34
256+
return (
257+
// @ts-expect-error _private field access
258+
cached?.value === evt?.target?.[cached?.field] ||
259+
// @ts-expect-error _private field access
260+
cached?.value === evt?.currentTarget?.[cached?.field]
261+
);
259262
},
260263
onPanResponderMove: (_, gestureState) => {
261264
if (gestureState.dy > 0) {
@@ -304,17 +307,65 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
304307
/* eslint-enable react/no-unstable-nested-components, react-native/no-inline-styles */
305308

306309
/**
307-
* Extracts and caches the _nativeTag property of ContentWrapper
310+
* Extracts and caches either `_nativeTag` or `__nativeTag` or `__internalInstanceHandle` or `_internalFiberInstanceHandleDEV`
311+
* reference of the `ContentWrapper` component based on which is available. Either will do for
312+
* identifying the content wrapper in PanResponder
308313
*/
309-
let extractNativeTag = useCallback(({ target }: LayoutChangeEvent) => {
310-
const tag =
311-
Platform.OS === 'web'
312-
? undefined
313-
: // @ts-expect-error
314-
target?._nativeTag;
315-
if (!cachedContentWrapperNativeTag.current)
316-
cachedContentWrapperNativeTag.current = tag;
317-
}, []);
314+
const cacheElementReference = useCallback(
315+
({ currentTarget, nativeEvent }: LayoutChangeEvent) => {
316+
const fabricInstanceHandleKey = '__internalInstanceHandle';
317+
// @ts-expect-error `Fabric` renderer's instance handle reference/pointer
318+
const fabricInstanceHandle = currentTarget?.[fabricInstanceHandleKey];
319+
320+
const oldNativeTagKey = '_nativeTag';
321+
// @ts-expect-error `Paper` renderer's native tag number
322+
const oldNativeTag = currentTarget?.[oldNativeTagKey];
323+
324+
const newNativeTagKey = '__nativeTag';
325+
// @ts-expect-error `Fabric` renderer's native tag number
326+
const newNativeTag = currentTarget?.[newNativeTagKey];
327+
328+
const paperInstanceHandleKey = '_internalFiberInstanceHandleDEV';
329+
// @ts-expect-error `Paper` renderer's instance handle equivalent
330+
const paperInstanceHandle = currentTarget?.[paperInstanceHandleKey];
331+
332+
if (!cachedContentWrapperId.current) {
333+
if (fabricInstanceHandle)
334+
cachedContentWrapperId.current = {
335+
field: fabricInstanceHandleKey,
336+
value: fabricInstanceHandle,
337+
};
338+
else if (oldNativeTag)
339+
cachedContentWrapperId.current = {
340+
field: oldNativeTagKey,
341+
value: oldNativeTag,
342+
};
343+
else if (newNativeTag)
344+
cachedContentWrapperId.current = {
345+
field: newNativeTagKey,
346+
value: newNativeTag,
347+
};
348+
else if (paperInstanceHandle)
349+
cachedContentWrapperId.current = {
350+
field: paperInstanceHandleKey,
351+
value: paperInstanceHandle,
352+
};
353+
// Check known stable keys for web if none of above exists
354+
else if (Platform.OS === 'web') {
355+
const responderKey = '__reactResponderId';
356+
// @ts-expect-error `.target` is untyped
357+
const responderId = nativeEvent?.target?.[responderKey];
358+
if (responderId) {
359+
cachedContentWrapperId.current = {
360+
field: responderKey,
361+
value: responderId,
362+
};
363+
}
364+
} else cachedContentWrapperId.current = undefined;
365+
}
366+
},
367+
[]
368+
);
318369

319370
/**
320371
* Expands the bottom sheet.
@@ -341,20 +392,31 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
341392
};
342393

343394
const closeBottomSheet = () => {
344-
// 1. fade backdrop
345-
// 2. if using fade animation, close container, set content wrapper height to 0.
346-
// else animate content container height & container height to 0, in sequence
347-
Animators.animateBackdropMaskOpacity(0, closeDuration).start((anim) => {
348-
if (anim.finished) {
349-
if (animationType === ANIMATIONS.FADE) {
395+
if (animationType === ANIMATIONS.FADE) {
396+
// For fade, sheet opacity is tied to the backdrop, so we wait for the
397+
// backdrop fade to complete before snapping the container shut.
398+
Animators.animateBackdropMaskOpacity(0, closeDuration).start((anim) => {
399+
if (anim.finished) {
350400
Animators.animateContainerHeight(0).start();
351401
_animatedHeight.setValue(0);
352-
} else {
353-
Animators.animateHeight(0, closeDuration).start();
354-
Animators.animateContainerHeight(0).start();
355402
}
356-
}
357-
});
403+
});
404+
} else if (animationType === ANIMATIONS.SLIDE) {
405+
// Run backdrop fade and height slide-out in parallel so flick-to-close
406+
// doesn't pause mid-flight waiting for the (faster) backdrop fade.
407+
// Snap the outer container to 0 only after the sheet has
408+
// finished sliding out so the slide animation isn't clipped.
409+
Animators.animateBackdropMaskOpacity(0, closeDuration).start();
410+
Animators.animateHeight(0, closeDuration).start((anim) => {
411+
if (anim.finished) Animators.animateContainerHeight(0).start();
412+
});
413+
} else {
414+
Animators.animateBackdropMaskOpacity(0, closeDuration).start();
415+
// `animateHeight` and `animateContainerHeight` below need to run in parallel
416+
// else there might be a noticeable flicker of sheet content
417+
Animators.animateHeight(0, closeDuration).start();
418+
Animators.animateContainerHeight(0).start();
419+
}
358420
setSheetOpen(false);
359421
keyboardHandler?.removeKeyboardListeners();
360422
Keyboard.dismiss();
@@ -484,11 +546,21 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
484546
<Animated.View
485547
ref={contentWrapperRef}
486548
key={'BottomSheetContentContainer'}
487-
onLayout={extractNativeTag}
549+
onLayout={cacheElementReference}
488550
/* Merge external and internal styles carefully and orderly */
489551
style={[
490552
!modal ? materialStyles.contentContainerShadow : false,
491553
materialStyles.contentContainer,
554+
// Apply default top-corner radii only when the user hasn't
555+
// supplied a `borderRadius` shorthand since RN's render layer keeps
556+
// individual corner properties over the shorthand, so leaving
557+
// them in would silently override the user's value.
558+
!(
559+
sepStyles?.otherStyles &&
560+
'borderRadius' in sepStyles.otherStyles
561+
)
562+
? materialStyles.contentContainerTopRadius
563+
: false,
492564
// we apply styles other than padding here
493565
sepStyles?.otherStyles,
494566
{
@@ -502,8 +574,11 @@ const BottomSheet = forwardRef<BottomSheetMethods, BottomSheetProps>(
502574
<PolymorphicHandleBar />
503575

504576
<View
505-
// we apply padding styles here to not affect drag handle above
506-
style={sepStyles?.paddingStyles}
577+
// we apply padding styles here to not affect drag handle above.
578+
// `flex: 1` lets this fill the remaining space below the drag
579+
// handle so children sized with `flex` or percentage heights
580+
// render against the actual available area (issue #36).
581+
style={[materialStyles.contentBody, sepStyles?.paddingStyles]}
507582
>
508583
{ChildNodes}
509584
</View>
@@ -522,9 +597,14 @@ const materialStyles = StyleSheet.create({
522597
backgroundColor: '#F7F2FA',
523598
width: '100%',
524599
overflow: 'hidden',
600+
},
601+
contentContainerTopRadius: {
525602
borderTopLeftRadius: 28,
526603
borderTopRightRadius: 28,
527604
},
605+
contentBody: {
606+
flex: 1,
607+
},
528608
contentContainerShadow:
529609
Platform.OS === 'android'
530610
? {

src/hooks/useHandleAndroidBackButtonClose/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useEffect, useRef } from 'react';
2-
import { BackHandler, type NativeEventSubscription } from 'react-native';
2+
import {
3+
BackHandler,
4+
Platform,
5+
type NativeEventSubscription,
6+
} from 'react-native';
37
import type { UseHandleAndroidBackButtonClose } from './types.d';
48

59
/**
@@ -15,8 +19,9 @@ const useHandleAndroidBackButtonClose: UseHandleAndroidBackButtonClose = (
1519
closeSheet,
1620
sheetOpen = false
1721
) => {
18-
const handler = useRef<NativeEventSubscription | null>(null);
22+
const handler = useRef<NativeEventSubscription | undefined>(undefined);
1923
useEffect(() => {
24+
if (Platform.OS !== 'android') return;
2025
handler.current = BackHandler.addEventListener('hardwareBackPress', () => {
2126
if (sheetOpen) {
2227
if (shouldClose) {

0 commit comments

Comments
 (0)