From f4b2020feab6839af0fcb87897ddf7f971cbe174 Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Sun, 17 May 2026 08:02:35 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20chore:=20increase=20co?= =?UTF-8?q?mmit=20footer=20max=20length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commitlint.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/commitlint.config.ts b/commitlint.config.ts index 85a83dd..e2a164d 100644 --- a/commitlint.config.ts +++ b/commitlint.config.ts @@ -39,6 +39,7 @@ const Configuration: UserConfig = { '🧪 test', ], ], + 'footer-max-line-length': [2, 'always', 400], }, }; From 5c47a68df37aef1f2de7dcf13cc68740d5ff9180 Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Sun, 17 May 2026 08:02:47 +0100 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20rewrite=20content=20c?= =?UTF-8?q?ontainer=20unique=20id=20tagging=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix issue #34 by rewriting the content container element ID tagging logic for handling pan gestures. Also, ensure multiple fallbacks for resilience and backward compatibility with React Native’s old `Paper` renderer --- src/components/bottomSheet/index.tsx | 118 +++++++++++++++++---------- 1 file changed, 74 insertions(+), 44 deletions(-) diff --git a/src/components/bottomSheet/index.tsx b/src/components/bottomSheet/index.tsx index 18f150d..6a7bc35 100644 --- a/src/components/bottomSheet/index.tsx +++ b/src/components/bottomSheet/index.tsx @@ -1,23 +1,23 @@ import React, { forwardRef, useCallback, + useEffect, useImperativeHandle, + useLayoutEffect, useMemo, useRef, useState, - useLayoutEffect, - useEffect, type ComponentRef, } from 'react'; import { Animated, - View, + Keyboard, PanResponder, + Platform, StyleSheet, - type LayoutChangeEvent, useWindowDimensions, - Keyboard, - Platform, + View, + type LayoutChangeEvent, } from 'react-native'; import { DEFAULT_ANIMATION, @@ -26,23 +26,23 @@ import { DEFAULT_HEIGHT, DEFAULT_OPEN_ANIMATION_DURATION, } from '../../constant'; -import DefaultHandleBar from '../defaultHandleBar'; -import Container from '../container'; -import normalizeHeight from '../../utils/normalizeHeight'; -import convertHeight from '../../utils/convertHeight'; -import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents'; import useAnimatedValue from '../../hooks/useAnimatedValue'; +import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose'; +import useHandleKeyboardEvents from '../../hooks/useHandleKeyboardEvents'; +import convertHeight from '../../utils/convertHeight'; +import normalizeHeight from '../../utils/normalizeHeight'; +import separatePaddingStyles from '../../utils/separatePaddingStyles'; import Backdrop from '../backdrop'; +import Container from '../container'; +import DefaultHandleBar from '../defaultHandleBar'; import { - type BottomSheetProps, - type ToValue, ANIMATIONS, - type BottomSheetMethods, CUSTOM_BACKDROP_POSITIONS, type BOTTOMSHEET, + type BottomSheetMethods, + type BottomSheetProps, + type ToValue, } from './types.d'; -import useHandleAndroidBackButtonClose from '../../hooks/useHandleAndroidBackButtonClose'; -import separatePaddingStyles from '../../utils/separatePaddingStyles'; /** * Main bottom sheet component @@ -106,8 +106,10 @@ const BottomSheet = forwardRef( const contentWrapperRef = useRef>(null); - /** cached _nativeTag property of content container */ - const cachedContentWrapperNativeTag = useRef(undefined); + /** cached unique identifier of content container */ + const cachedContentWrapperId = useRef< + { field: string; value: unknown } | undefined + >(undefined); // here we separate all padding that may be applied via contentContainerStyle prop, // these paddings will be applied to the `View` diretly wrapping `ChildNodes` in content container. @@ -241,21 +243,12 @@ const BottomSheet = forwardRef( if (view === 'handlebar' && disableDragHandlePanning) return null; if (view === 'contentwrapper' && disableBodyPanning) return null; return PanResponder.create({ - onMoveShouldSetPanResponder: (evt) => { - /** - * `FiberNode._nativeTag` is stable across renders so we use it to determine - * whether content container or it's child should respond to touch move gesture. - * - * The logic is, when content container is laid out, we extract it's _nativeTag property and cache it - * So later when a move gesture event occurs within it, we compare the cached _nativeTag with the _nativeTag of - * the event target's _nativeTag, if they match, then content container should respond, else its children should. - * Also, when the target is the handle bar, we le it handle geture unless panning is disabled through props - */ - return view === 'handlebar' - ? true - : cachedContentWrapperNativeTag.current === - // @ts-expect-error - evt?.target?._nativeTag; + onMoveShouldSetPanResponderCapture: (evt) => { + if (view === 'handlebar') return true; + const cached = cachedContentWrapperId.current; + if (!cached) return false; // this signature alone should fix issue #34 + // @ts-expect-error _private field access + return cached?.value === evt?.target?.[cached?.field]; }, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { @@ -304,17 +297,54 @@ const BottomSheet = forwardRef( /* eslint-enable react/no-unstable-nested-components, react-native/no-inline-styles */ /** - * Extracts and caches the _nativeTag property of ContentWrapper + * Extracts and caches either `_nativeTag` or `__nativeTag` or `__internalInstanceHandle` or `_internalFiberInstanceHandleDEV` + * reference of the `ContentWrapper` component based on which is available. Either will do for + * identifying the content wrapper in PanResponder */ - let extractNativeTag = useCallback(({ target }: LayoutChangeEvent) => { - const tag = - Platform.OS === 'web' - ? undefined - : // @ts-expect-error - target?._nativeTag; - if (!cachedContentWrapperNativeTag.current) - cachedContentWrapperNativeTag.current = tag; - }, []); + const cacheElementReference = useCallback( + ({ currentTarget }: LayoutChangeEvent) => { + const fabricInstanceHandleKey = '__internalInstanceHandle'; + // @ts-expect-error `Fabric` renderer's instance handle reference/pointer + const fabricInstanceHandle = currentTarget?.[fabricInstanceHandleKey]; + + const oldNativeTagKey = '_nativeTag'; + // @ts-expect-error `Paper` renderer's native tag number + const oldNativeTag = currentTarget?.[oldNativeTagKey]; + + const newNativeTagKey = '__nativeTag'; + // @ts-expect-error `Fabric` renderer's native tag number + const newNativeTag = currentTarget?.[newNativeTagKey]; + + const paperInstanceHandleKey = '_internalFiberInstanceHandleDEV'; + // @ts-expect-error `Paper` renderer's instance handle equivalent + const paperInstanceHandle = currentTarget?.[paperInstanceHandleKey]; + + if (!cachedContentWrapperId.current) { + if (fabricInstanceHandle) + cachedContentWrapperId.current = { + field: fabricInstanceHandleKey, + value: fabricInstanceHandle, + }; + else if (oldNativeTag) + cachedContentWrapperId.current = { + field: oldNativeTagKey, + value: oldNativeTag, + }; + else if (newNativeTag) + cachedContentWrapperId.current = { + field: newNativeTagKey, + value: newNativeTag, + }; + else if (paperInstanceHandle) + cachedContentWrapperId.current = { + field: paperInstanceHandleKey, + value: paperInstanceHandle, + }; + else cachedContentWrapperId.current = undefined; + } + }, + [] + ); /** * Expands the bottom sheet. @@ -484,7 +514,7 @@ const BottomSheet = forwardRef( Date: Mon, 18 May 2026 03:43:20 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20fix=20lag=20in=20clos?= =?UTF-8?q?ing=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit on non-fade closing animation, we now run backdrop fade and height slide-out in parallel so flick-to-close doesn't pause mid-flight waiting for the (faster) backdrop fade and snap the outer container to 0 only after the sheet has finished sliding out so the slide animation isn't clipped. Fixes #29 --- src/components/bottomSheet/index.tsx | 43 +++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/components/bottomSheet/index.tsx b/src/components/bottomSheet/index.tsx index 6a7bc35..5bdbda3 100644 --- a/src/components/bottomSheet/index.tsx +++ b/src/components/bottomSheet/index.tsx @@ -128,12 +128,18 @@ const BottomSheet = forwardRef( return value === 1 ? 1 : 1 - Math.pow(2, -10 * value); }, _springEasingFn(value: number) { - const c4 = (2 * Math.PI) / 2.5; + const decay = 9; + const multiplier = 4.5; + const divisor = 2.3; + + const c4 = (2 * Math.PI) / divisor; + return value === 0 ? 0 : value === 1 ? 1 - : Math.pow(2, -9 * value) * Math.sin((value * 4.5 - 0.75) * c4) + + : Math.pow(2, -decay * value) * + Math.sin((value * multiplier - 0.75) * c4) + 1; }, animateContainerHeight(toValue: ToValue, duration: number = 0) { @@ -371,20 +377,31 @@ const BottomSheet = forwardRef( }; const closeBottomSheet = () => { - // 1. fade backdrop - // 2. if using fade animation, close container, set content wrapper height to 0. - // else animate content container height & container height to 0, in sequence - Animators.animateBackdropMaskOpacity(0, closeDuration).start((anim) => { - if (anim.finished) { - if (animationType === ANIMATIONS.FADE) { + if (animationType === ANIMATIONS.FADE) { + // For fade, sheet opacity is tied to the backdrop, so we wait for the + // backdrop fade to complete before snapping the container shut. + Animators.animateBackdropMaskOpacity(0, closeDuration).start((anim) => { + if (anim.finished) { Animators.animateContainerHeight(0).start(); _animatedHeight.setValue(0); - } else { - Animators.animateHeight(0, closeDuration).start(); - Animators.animateContainerHeight(0).start(); } - } - }); + }); + } else if (animationType === ANIMATIONS.SLIDE) { + // Run backdrop fade and height slide-out in parallel so flick-to-close + // doesn't pause mid-flight waiting for the (faster) backdrop fade. + // Snap the outer container to 0 only after the sheet has + // finished sliding out so the slide animation isn't clipped. + Animators.animateBackdropMaskOpacity(0, closeDuration).start(); + Animators.animateHeight(0, closeDuration).start((anim) => { + if (anim.finished) Animators.animateContainerHeight(0).start(); + }); + } else { + Animators.animateBackdropMaskOpacity(0, closeDuration).start(); + // `animateHeight` and `animateContainerHeight` below need to run in parallel + // else there might be a noticeable flicker of sheet content + Animators.animateHeight(0, closeDuration).start(); + Animators.animateContainerHeight(0).start(); + } setSheetOpen(false); keyboardHandler?.removeKeyboardListeners(); Keyboard.dismiss(); From ec75df845ff576b3d22fd4d282e28fbc5d220e20 Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Mon, 18 May 2026 03:55:47 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20add=20platform=20guar?= =?UTF-8?q?d=20to=20backhandler=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make back button handler hook useEffect early-return when platform is not android. Fixes #42 and part of #21 --- src/hooks/useHandleAndroidBackButtonClose/index.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hooks/useHandleAndroidBackButtonClose/index.ts b/src/hooks/useHandleAndroidBackButtonClose/index.ts index d467b44..4d01217 100644 --- a/src/hooks/useHandleAndroidBackButtonClose/index.ts +++ b/src/hooks/useHandleAndroidBackButtonClose/index.ts @@ -1,5 +1,9 @@ import { useEffect, useRef } from 'react'; -import { BackHandler, type NativeEventSubscription } from 'react-native'; +import { + BackHandler, + Platform, + type NativeEventSubscription, +} from 'react-native'; import type { UseHandleAndroidBackButtonClose } from './types.d'; /** @@ -15,8 +19,9 @@ const useHandleAndroidBackButtonClose: UseHandleAndroidBackButtonClose = ( closeSheet, sheetOpen = false ) => { - const handler = useRef(null); + const handler = useRef(undefined); useEffect(() => { + if (Platform.OS !== 'android') return; handler.current = BackHandler.addEventListener('hardwareBackPress', () => { if (sheetOpen) { if (shouldClose) { From c312ba0535f63e7eb6c3c3643a850581bf16c0d9 Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Mon, 18 May 2026 04:37:43 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20add=20ID=20tagging=20?= =?UTF-8?q?logic=20for=20web=20container=20element,=20enabling=20body=20pa?= =?UTF-8?q?nning=20on=20web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/bottomSheet/index.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/bottomSheet/index.tsx b/src/components/bottomSheet/index.tsx index 5bdbda3..d7ba605 100644 --- a/src/components/bottomSheet/index.tsx +++ b/src/components/bottomSheet/index.tsx @@ -253,8 +253,12 @@ const BottomSheet = forwardRef( if (view === 'handlebar') return true; const cached = cachedContentWrapperId.current; if (!cached) return false; // this signature alone should fix issue #34 - // @ts-expect-error _private field access - return cached?.value === evt?.target?.[cached?.field]; + return ( + // @ts-expect-error _private field access + cached?.value === evt?.target?.[cached?.field] || + // @ts-expect-error _private field access + cached?.value === evt?.currentTarget?.[cached?.field] + ); }, onPanResponderMove: (_, gestureState) => { if (gestureState.dy > 0) { @@ -308,7 +312,7 @@ const BottomSheet = forwardRef( * identifying the content wrapper in PanResponder */ const cacheElementReference = useCallback( - ({ currentTarget }: LayoutChangeEvent) => { + ({ currentTarget, nativeEvent }: LayoutChangeEvent) => { const fabricInstanceHandleKey = '__internalInstanceHandle'; // @ts-expect-error `Fabric` renderer's instance handle reference/pointer const fabricInstanceHandle = currentTarget?.[fabricInstanceHandleKey]; @@ -346,7 +350,18 @@ const BottomSheet = forwardRef( field: paperInstanceHandleKey, value: paperInstanceHandle, }; - else cachedContentWrapperId.current = undefined; + // Check known stable keys for web if none of above exists + else if (Platform.OS === 'web') { + const responderKey = '__reactResponderId'; + // @ts-expect-error `.target` is untyped + const responderId = nativeEvent?.target?.[responderKey]; + if (responderId) { + cachedContentWrapperId.current = { + field: responderKey, + value: responderId, + }; + } + } else cachedContentWrapperId.current = undefined; } }, [] From 35c10c977717bf067060a2203477d1011173c626 Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Mon, 18 May 2026 05:32:44 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20make=20child=20conten?= =?UTF-8?q?t=20height=20flexible/full-height=20and=20fix=20custom=20border?= =?UTF-8?q?=20radii=20style=20override?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `flex:1` to child content container `View` to allow full-height content (fixes #36). Also apply default top-corner radii only when the user hasn't supplied a `borderRadius` shorthand (fixes #25) --- src/components/bottomSheet/index.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/bottomSheet/index.tsx b/src/components/bottomSheet/index.tsx index d7ba605..e274186 100644 --- a/src/components/bottomSheet/index.tsx +++ b/src/components/bottomSheet/index.tsx @@ -551,6 +551,16 @@ const BottomSheet = forwardRef( style={[ !modal ? materialStyles.contentContainerShadow : false, materialStyles.contentContainer, + // Apply default top-corner radii only when the user hasn't + // supplied a `borderRadius` shorthand since RN's render layer keeps + // individual corner properties over the shorthand, so leaving + // them in would silently override the user's value. + !( + sepStyles?.otherStyles && + 'borderRadius' in sepStyles.otherStyles + ) + ? materialStyles.contentContainerTopRadius + : false, // we apply styles other than padding here sepStyles?.otherStyles, { @@ -564,8 +574,11 @@ const BottomSheet = forwardRef( {ChildNodes} @@ -584,9 +597,14 @@ const materialStyles = StyleSheet.create({ backgroundColor: '#F7F2FA', width: '100%', overflow: 'hidden', + }, + contentContainerTopRadius: { borderTopLeftRadius: 28, borderTopRightRadius: 28, }, + contentBody: { + flex: 1, + }, contentContainerShadow: Platform.OS === 'android' ? { From 1fb577f258d20db0d20310ccbeeb4bf4f1dcd25c Mon Sep 17 00:00:00 2001 From: Stanley Ugwu Date: Mon, 18 May 2026 06:19:13 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=90=9E=20fix:=20use=20closure-scoped?= =?UTF-8?q?=20easing=20fns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use closure-scoped easing fns instead of `this` to prevent undefined reference when animateHeight is called unbound (fixes #45) --- src/components/bottomSheet/index.tsx | 62 ++++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/bottomSheet/index.tsx b/src/components/bottomSheet/index.tsx index e274186..dd72a0b 100644 --- a/src/components/bottomSheet/index.tsx +++ b/src/components/bottomSheet/index.tsx @@ -122,26 +122,27 @@ const BottomSheet = forwardRef( ); // Animation utility - const Animators = useMemo( - () => ({ - _slideEasingFn(value: number) { - return value === 1 ? 1 : 1 - Math.pow(2, -10 * value); - }, - _springEasingFn(value: number) { - const decay = 9; - const multiplier = 4.5; - const divisor = 2.3; - - const c4 = (2 * Math.PI) / divisor; - - return value === 0 - ? 0 - : value === 1 - ? 1 - : Math.pow(2, -decay * value) * - Math.sin((value * multiplier - 0.75) * c4) + - 1; - }, + const Animators = useMemo(() => { + const _slideEasingFn = (value: number) => { + return value === 1 ? 1 : 1 - Math.pow(2, -10 * value); + }; + const _springEasingFn = (value: number) => { + const decay = 9; + const multiplier = 4.5; + const divisor = 2.3; + + const c4 = (2 * Math.PI) / divisor; + + return value === 0 + ? 0 + : value === 1 + ? 1 + : Math.pow(2, -decay * value) * + Math.sin((value * multiplier - 0.75) * c4) + + 1; + }; + + return { animateContainerHeight(toValue: ToValue, duration: number = 0) { return Animated.timing(_animatedContainerHeight, { toValue: toValue, @@ -170,19 +171,18 @@ const BottomSheet = forwardRef( customEasingFunction && typeof customEasingFunction === 'function' ? customEasingFunction : animationType === ANIMATIONS.SLIDE - ? this._slideEasingFn - : this._springEasingFn, + ? _slideEasingFn + : _springEasingFn, }); }, - }), - [ - animationType, - customEasingFunction, - _animatedContainerHeight, - _animatedBackdropMaskOpacity, - _animatedHeight, - ] - ); + }; + }, [ + animationType, + customEasingFunction, + _animatedContainerHeight, + _animatedBackdropMaskOpacity, + _animatedHeight, + ]); const interpolatedOpacity = useMemo( () =>