Compare commits

..

415 Commits

Author SHA1 Message Date
lodev09
9ecec144b3
chore: release 3.6.8
Some checks failed
Build / build-android (push) Has been cancelled
Build / build-ios (push) Has been cancelled
Checks / lint (push) Has been cancelled
Checks / test (push) Has been cancelled
Checks / build-library (push) Has been cancelled
2025-12-29 12:39:52 +08:00
lodev09
7813a68db1
chore: run tidy 2025-12-29 12:39:02 +08:00
Jovanni Lo
a1a82ad45e
fix(ios): preserve keyboard offset when footer resizes (#361) 2025-12-29 12:14:21 +08:00
lodev09
55cbd317c8
fix(android): optimize findRootContainerView to return first content view 2025-12-29 11:48:29 +08:00
Jovanni Lo
f3164ca303
fix(android): render TrueSheet above React Native Modal (#359)
TrueSheet was always adding its CoordinatorLayout to the activity's
content view, causing it to render behind React Native Modal which
uses a Dialog with its own window.

Now the host view (TrueSheetView) manages the root container. The
findRootContainerView() method traverses up the view hierarchy to
find the correct android.R.id.content container - whether that's
the activity's or a Modal's dialog window.

Changes:
- Add rootContainerView property to TrueSheetView
- Find container and attach coordinator on first present
- Detach coordinator in viewControllerDidDismiss
- Make coordinatorLayout internal for host view access
2025-12-29 11:29:44 +08:00
lodev09
9119dca450
chore: release 3.6.7 2025-12-28 09:21:33 +08:00
lodev09
daf0267ce4
chore: add @types/react resolution 2025-12-28 09:21:03 +08:00
lodev09
6a8e387776
fix: reset initial present flag during recycle 2025-12-28 09:15:34 +08:00
lodev09
acdd9e4cf4
chore: update example deps 2025-12-28 08:20:08 +08:00
lodev09
97f93f1b81
chore: update expo scripts 2025-12-28 07:13:04 +08:00
lodev09
e5d95f38a1
chore: release 3.6.6 2025-12-28 02:38:49 +08:00
lodev09
8015c83386
refactor: simplify initial presentation to only trigger on attach to window 2025-12-28 02:36:30 +08:00
lodev09
615f0537c5
fix(android): move setupModalObserver to present lifecycle 2025-12-28 02:35:28 +08:00
lodev09
ca01690e16
chore: release 3.6.5 2025-12-27 04:09:12 +08:00
lodev09
0734cc3163
fix(android): fix animated sheet dismiss with keyboard shown 2025-12-27 04:08:04 +08:00
lodev09
48d8ceb788
docs: add docs about autofocus input behavior 2025-12-27 02:59:31 +08:00
lodev09
ff105584d9
chore: release 3.6.4 2025-12-27 01:01:18 +08:00
lodev09
30f9d1b7fb
fix(android): wait for window attachment before initial presentation 2025-12-27 01:00:02 +08:00
lodev09
fc81413440
chore: release 3.6.3 2025-12-27 00:42:30 +08:00
lodev09
ed1346b346
fix(ios): add compile-time check for iOS 26.1+ APIs 2025-12-27 00:42:00 +08:00
lodev09
a7ac1d7839
fix(android): add safe value for halfExpandedRatio 2025-12-26 22:55:26 +08:00
lodev09
08aed6715b
chore: release 3.6.2 2025-12-26 16:32:35 +08:00
lodev09
02e85b6030
fix(ios): support blur intensity with backgroundBlur on iOS 26.1+
- Fall back to TrueSheetBlurView when blur intensity < 100%
- Set clear color backgroundEffect to override native Liquid Glass when using custom blur
2025-12-26 16:31:28 +08:00
lodev09
582cee009d
docs: improve liquid glass documentation
- Make UIDesignRequiresCompatibility a sub-section of Disabling Liquid Glass
- Add backgroundColor/backgroundBlur as first option to disable Liquid Glass per-sheet
- Add note about iOS 26.1+ requirement for per-sheet disabling
- Add Apple documentation reference for UIDesignRequiresCompatibility
- Add Liquid Glass info to backgroundColor and backgroundBlur props in configuration
- Move backgroundBlur and blurOptions next to backgroundColor in configuration
2025-12-26 16:05:43 +08:00
lodev09
8c0308ab60
chore: release 3.6.1 2025-12-26 03:06:43 +08:00
lodev09
71aded3bec
fix(ios): fall back to view.backgroundColor when UIDesignRequiresCompatibility is true 2025-12-26 03:05:47 +08:00
lodev09
2ad7f29dde
chore: release 3.6.0 2025-12-26 01:17:51 +08:00
lodev09
cd2172943f
ci: combine ccache into one step 2025-12-26 01:16:15 +08:00
lodev09
b156ad77d2
ci: maybe improve build-ios workflow 2025-12-26 01:10:06 +08:00
Jovanni Lo
917775ee07
feat: add elevation prop for Android and Web (#355)
* feat(android): add elevation prop

* feat(web): add elevation prop support
2025-12-26 00:52:40 +08:00
lodev09
dee02e15ea
fix(android): fix keyboard dismiss issue with backdrop (#351) 2025-12-25 22:59:27 +08:00
Jovanni Lo
b09440d548
fix(ios): use native backgroundEffect for blur on iOS 26.1+ (#350)
* fix(ios): use native backgroundEffect for blur on iOS 26.1+

- Extract blur style mapping into BlurUtil
- Refactor setupBackground and setupGrabber into helper methods
- Add sheet getter for cleaner code
- Use native UIBlurEffect on backgroundEffect when only blurTint is set (iOS 26.1+)

* refactor: rename blurTint to backgroundBlur
2025-12-25 17:59:55 +08:00
lodev09
7b90d512c0
chore: release 3.5.8 2025-12-25 01:11:15 +08:00
lodev09
b6bd082bc8
feat: add headerStyle and footerStyle props 2025-12-25 01:10:34 +08:00
lodev09
2172a8d130
chore: release 3.5.7 2025-12-25 00:30:27 +08:00
lodev09
64a697801f
fix(web): remove pointerEvents from footer container 2025-12-25 00:29:50 +08:00
lodev09
f4cb740c20
chore: release 3.5.6 2025-12-24 22:58:31 +08:00
lodev09
e66a9b491b
feat(web): add 'none' option to stackBehavior prop
- When stackBehavior is 'none', uses regular BottomSheet instead of BottomSheetModal
- Bypasses the modal stack entirely for non-modal sheet behavior
- Updated types, web implementation, and documentation
2025-12-24 22:56:56 +08:00
lodev09
970183e11f
feat(web): add shadow for web sheet 2025-12-24 22:36:09 +08:00
lodev09
b46112546a
chore: release 3.5.5 2025-12-24 10:50:41 +08:00
lodev09
274d936cdb
fix: remove pointerEvents from header, footer and host view 2025-12-24 10:49:46 +08:00
lodev09
781f666349
chore: release 3.5.4 2025-12-23 09:03:06 +08:00
lodev09
0ac529bded
fix(ios): defer initial presentation until view is in window hierarchy
When using react-native-screens, the sheet was presenting on the wrong
view controller because finalizeUpdates was called before the screen
transition completed and the view was added to the window.

This fix defers the initial presentation by:
- Adding a pendingInitialPresentation flag
- Only presenting when the view is confirmed to be in a window
- Using didMoveToWindow to handle pending presentations

Fixes issue where sheet would appear on the previous screen instead of
the currently presented screen.
2025-12-23 09:00:57 +08:00
lodev09
69216b0076
chore: release 3.5.3 2025-12-23 03:51:58 +08:00
lodev09
3f78fe5423
fix(ios): ignore TrueSheetView in scroll view pinning traversal 2025-12-23 03:51:06 +08:00
lodev09
c7c1a9ccee
chore: release 3.5.2 2025-12-22 20:31:55 +08:00
lodev09
d82046eb85
fix(android): sheet showing briefly when navigating within a stack 2025-12-22 20:28:58 +08:00
lodev09
2e4494024f
docs: update docs 2025-12-22 00:57:12 +08:00
lodev09
bf8c5cee03
chore: release 3.5.1 2025-12-21 23:33:28 +08:00
lodev09
cece31ba27
fix(android): parent stacking after rn-screen dismissed 2025-12-21 23:00:35 +08:00
lodev09
d330e50082
fix(android): disable dragging on parent sheet when child is stacked 2025-12-21 21:22:01 +08:00
lodev09
0565b73169
chore: release 3.5.1-beta.5 2025-12-21 19:40:07 +08:00
lodev09
0a6ef69421
fix(android): improve dim tap handling for stacked sheets
- Collapse to lowest non-dimmed detent instead of always detent 0
- Add isDimmedAtCurrentDetent helper to check dimmed state
- Pass through dim tap to parent when child is not dimmed
- Dismiss all children when parent dismisses
- Cancel dismiss if any child is not dismissible
2025-12-21 19:39:14 +08:00
lodev09
56eac659db
fix(android): collapse to lowest detent on back press when non-dismissible 2025-12-21 18:42:57 +08:00
lodev09
b4a6bd2869
fix(android): fade sheet before hiding when rn-screen modal shows 2025-12-21 18:11:17 +08:00
lodev09
56b51d9779
fix(android): clip content to rounded corners on older API levels 2025-12-21 18:03:55 +08:00
lodev09
b3fae9256c
docs(android): update TrueSheetViewController class description 2025-12-21 17:22:53 +08:00
lodev09
9a129971fc
fix(ci): only run check-repro workflow on issues, not pull requests 2025-12-21 17:16:30 +08:00
lodev09
6568668024
chore: release 3.5.1-beta.4 2025-12-21 17:12:19 +08:00
lodev09
be332fbd8d
fix(android): collapse to lowest detent on dim tap when non-dismissible
- Tapping dim view collapses to lowest detent when dismissible=false
- Refactored dimViewDidTap to handle child sheets properly
- Added RNLog.e for unexpected null cases in TrueSheetViewController
2025-12-21 17:10:47 +08:00
lodev09
a9f402cbb6
fix(android): center bottom sheet horizontally on rotation 2025-12-21 16:58:29 +08:00
lodev09
b16a479f71
refactor(ios): update TrueSheetDetentCalculator to use delegate pattern
- Rename TrueSheetDetentMeasurements to TrueSheetDetentCalculatorDelegate
- Change init to not require measurements, set delegate separately
- Use self.delegate for property access consistency
2025-12-21 16:49:27 +08:00
lodev09
7373280407
refactor(android): reorganize TrueSheetViewController and remove duplications
- Add setSheetVisibility() helper for modal visibility toggling
- Simplify hideForModal/showAfterModal using the new helper
- Remove wrapper methods setupGrabber/setupBackground from controller
- Use delegate pattern for TrueSheetBottomSheetView to get appearance props
- Use delegate pattern for TrueSheetDetentCalculator with reactContext
- Remove duplicate getExpectedSheetTop, use detentCalculator.getSheetTopForDetentIndex
- Add RNLog warning for out-of-bounds detent index
- Remove unused shouldDimAtDetent variable
- Make sheetView and detentCalculator internal for external access
2025-12-21 16:39:30 +08:00
Jovanni Lo
4b02ca77f6
refactor(android): replace DialogFragment with CoordinatorLayout (#344)
* docs: add planning doc

* refactor(android): migrate from DialogFragment to CoordinatorLayout

This solves the touch lag issue when TrueSheet is presented over interactive
components like Maps. The sheet now stays in the same activity window instead
of a separate dialog window.

Changes:
- Add TrueSheetCoordinatorLayout to host sheet and dim view
- Add TrueSheetBottomSheetView with BottomSheetBehavior
- Refactor TrueSheetViewController to use CoordinatorLayout approach
- Update TrueSheetDimView for in-hierarchy usage with touch handling
- Remove TrueSheetDialogFragment (no longer needed)
- Remove unused dialog styles and animations

* fix(android): animate sheet on present using BottomSheetBehavior

* fix(android): animate sheet on present when not dismissible

* refactor(android): simplify dismiss animation using ViewPropertyAnimator

* docs: update AGENTS.md with new Android architecture

* fix(android): prevent duplicate initial present on re-mount

* fix(android): fix keyboard handling and parent sheet translation

* fix(android): prevent parent sheet layout reset when keyboard shows

- Add TrueSheetBottomSheetViewDelegate with isTopmostSheet property
- Skip onLayout for parent sheets to prevent BottomSheetBehavior from
  resetting translationY during system layout passes
- Refactor TrueSheetCoordinatorLayout.Delegate to standalone interface

* fix(android): preserve parent sheet translation during keyboard transitions

Override setTranslationY in TrueSheetBottomSheetView to prevent
keyboard inset animations from resetting parent sheet translation to 0.
Parent sheets (non-topmost) now maintain their translation value.

* refactor(android): rename TrueSheetDialogObserver to TrueSheetStackManager

* refactor(android): remove edgeToEdgeFullScreen prop

* fix(android): prevent sheet from showing during navigation within modal

* fix(android): clamp detent heights to available screen space

* fix(android): post when initialDetentAnimated is false
2025-12-21 16:10:23 +08:00
lodev09
ba42fc2a16
chore: release 3.5.1-beta.3 2025-12-20 14:12:58 +08:00
lodev09
63280faa14
chore: run tidy 2025-12-20 14:12:01 +08:00
lodev09
6519cf7329
fix(android): prevent sheet from reshowing when returning from background with rn-screens
- Add activity lifecycle observer to RNScreensFragmentObserver to track foreground state
- Ignore fragment lifecycle events during background/foreground transitions
- Add reapplyHiddenState() to re-hide dialog on activity resume if it was hidden by modal
2025-12-20 13:55:07 +08:00
lodev09
8402843174
fix(android): animate dismiss on back button press 2025-12-20 13:17:59 +08:00
lodev09
866e621c39
fix(android): use onSlide for footer positioning on API < 30
On API 30+, keyboard height updates are handled via WindowInsetsAnimationCompat.
On older APIs, the globalLayoutListener only fires after keyboard animation
completes, so use onSlide for real-time footer positioning during keyboard
transitions.
2025-12-20 13:13:34 +08:00
lodev09
0e3ac9616f
fix(android): remove grabber hitbox causing touch issues - fixes #340 2025-12-20 12:25:55 +08:00
lodev09
5dfbb4f3c0
refactor(android): reduce redundant code in TrueSheetViewController
- Add updateSheetVisuals() to consolidate position/footer/dim updates
- Add syncFragmentProperties() to centralize fragment property sync
- Add getPositionDpForView() to extract repeated position calculation
- Add dimViews computed property for unified dim view operations
- Remove unused Log import
2025-12-20 11:53:04 +08:00
lodev09
e6b6c36a2e
fix(android): keep sheet at target position during non-animated present 2025-12-20 11:46:22 +08:00
lodev09
a869d2b571
chore: release 3.5.1-beta.2 2025-12-19 10:34:55 +08:00
lodev09
0c0a93babf
fix(android): fix stacked sheet translation on dismiss
- Add resetTranslation() to propagate translation reset up the stack
- Remove isDialogVisible check from translation methods
- Parent resets to 0, grandparent recalculates based on parent position
2025-12-19 10:33:51 +08:00
lodev09
ae6748d5dc
fix(android): fix translation on initialDetentIndex present 2025-12-19 09:39:29 +08:00
lodev09
35e0acc757
chore: release 3.5.1-beta.1 2025-12-19 08:22:09 +08:00
lodev09
adc903a477
chore: run tidy 2025-12-19 08:21:34 +08:00
Jovanni Lo
7a35fa6bfb
refactor(android): use BottomSheetDialogFragment instead of BottomSheetDialog (#342)
* fix(android): restore original code

* refactor(android): use BottomSheetDialogFragment instead of BottomSheetDialog

- Create TrueSheetDialogFragment in core/ for better lifecycle management
- Refactor TrueSheetViewController to use the new fragment
- Add TrueSheetDialogFragmentDelegate for fragment callbacks
- Maintain all existing functionality (detents, animations, keyboard, stacking)

* fix(android): fix dialog fragment presentation issues

- Move setupSheetDetents and setupDimmedBackground to onDialogShow
- Clear FLAG_DIM_BEHIND in fragment's onCreateDialog
- Fix footer positioning with post and isPresented check
- Rename setDraggable to updateDraggable to fix JVM signature clash

* fix(android): animate sheet on content size change

- Add animate parameter to configureDetents
- Use setPeekHeight with animate flag when sheet is presented
- Remove unnecessary footerView.post wrapper

* fix(android): position footer during keyboard transitions

* fix(android): restore translationY animation for present

- Restore translationY logic in animator for child sheets
- Remove duplicate setStateForDetentIndex call in onDialogShow
- Position sheet off-screen before animation starts
- Clean up debug logging

* fix(android): fix dismissible behavior and clean up onSlide

- Re-apply isHideable after dialog show to fix dismissible
- Move isAnimating check to early return in onSlide
- Clean up debug logging
2025-12-19 08:18:56 +08:00
lodev09
4b6f595cd4
chore: release 3.5.1-beta.0 2025-12-19 04:21:58 +08:00
lodev09
4e9df0b36b
fix(android): fix initial present on older android versions 2025-12-19 04:20:45 +08:00
lodev09
b675296d91
fix(android): make sure halfExpandedRatio is between 0 and 1 2025-12-18 17:40:39 +08:00
lodev09
770119d4e8
chore: release 3.5.0 2025-12-18 16:53:58 +08:00
lodev09
6d66a95855
chore: run tidy 2025-12-18 16:53:15 +08:00
lodev09
542f183bec
refactor(android): reorganize TrueSheetViewController with properties at top
- Move all variables and computed properties to the top of the class
- Group properties by purpose (dialog, state, keyboard, config, insets)
- Add concise comments to non-obvious code sections
- Improve section markers for better code navigation
2025-12-18 16:52:13 +08:00
lodev09
94402ea9df
fix(android): coordinate sheet stack restoration when modal dismisses 2025-12-18 16:41:29 +08:00
lodev09
b5ae551be5
fix(android): animate dim view alpha when restoring from modal 2025-12-18 16:16:31 +08:00
lodev09
d8b0cfdb2a
chore: release 3.5.0-beta.1 2025-12-18 14:08:18 +08:00
lodev09
b046dc4d2e
fix(android): improve keyboard handling and detent restoration 2025-12-18 14:06:57 +08:00
lodev09
e47b8bcef3
fix(android): use target keyboard height for detent calculations 2025-12-18 12:16:21 +08:00
lodev09
073906645c
fix(android): use window visibility to prevent keyboard non-focus issue 2025-12-18 11:59:44 +08:00
lodev09
6976f172a7
chore: update example components 2025-12-18 03:57:05 +08:00
lodev09
2b8537772f
chore: release 3.5.0-beta.0 2025-12-18 00:52:01 +08:00
Jovanni Lo
c88fb1c037
feat(android): improve transition animations and refactor detent calculations (#337)
* feat: update slide animation styles

* fix: move rn-screen observer to preAttached and detached

* feat(android): implement programmatic slide animations for present/dismiss

* refactor(android): cleanup unused animation resources and fix present animation flash

* fix(android): animate footer with sheet during present/dismiss

* refactor(android): sync dim alpha with sheet translation during animations

- Update updateDimAmount() to accept optional sheetTop parameter
- Call updateDimAmount() on each frame during present/dismiss animations
- Remove unused animateDimAlpha() from TrueSheetViewController
- Remove unused animateAlpha() from TrueSheetDimView

* refactor(android): cleanup TrueSheetViewController

- Remove unused imports (Log, ViewCompat)
- Remove unused positionAnimator and setupTransitionTracker
- Use AnimatorListenerAdapter instead of full Animator.AnimatorListener
- Simplify comments and remove debug logs

* fix: move setup methods out of onShowListener

* refactor: make methods consistent

* refactor(android): use alpha + window flags for modal hide/show

- Use alpha fade instead of visibility for hiding sheet during RN Screens modal
- Add FLAG_NOT_TOUCHABLE and FLAG_NOT_FOCUSABLE to prevent interaction when hidden
- Remove unused TrueSheetSlideAnimation style and anim resources

* feat: translate bottomSheetView instead of setupSheetDetents with keyboard

* fix(android): clamp sheet translation during drag to prevent going beyond visible screen

* feat(android): dismiss keyboard when sheet is dragged down to a lower detent

* feat(android): expand to last detent on keyboard show, restore on hide

- Refactor keyboard observer delegate callbacks
- Track pre-keyboard detent and restore on hide
- Skip emitting detent change for keyboard transitions
- Fix 2-detent state map to include STATE_HALF_EXPANDED

* refactor(android): extract TrueSheetDetentCalculator and TrueSheetAnimator

- Extract detent calculations into TrueSheetDetentCalculator class
- Extract present/dismiss animations into TrueSheetAnimator class
- Use interface-based pattern for dynamic prop updates
- Consolidate state variables (InteractionState, KeyboardState)
- Reduce TrueSheetViewController from ~900 to ~650 lines

* refactor(ios): extract TrueSheetDetentCalculator from TrueSheetViewController

- Create TrueSheetDetentCalculator with TrueSheetDetentMeasurements protocol
- Extract detent calculation methods: detentValueForIndex, estimatedPositionForIndex,
  findSegmentForPosition, interpolatedIndexForPosition, interpolatedDetentForPosition
- TrueSheetViewController conforms to protocol for dynamic prop access
- Mirrors Android's interface-based pattern for consistency

* feat(android): skip keyboard handling when sheet is not topmost or hidden by modal

* feat(android): use setupSheetDetents for keyboard handling instead of Y translation

* feat(android): dismiss keyboard when dragged below original position

* chore: run tidy
2025-12-18 00:50:50 +08:00
lodev09
a5102dd07e
chore: release 3.4.2 2025-12-17 08:01:23 +08:00
lodev09
785d7c1db0
fix(android): prevent sheet from showing when app returns from background 2025-12-17 07:54:25 +08:00
lodev09
303e760585
chore: release 3.4.1 2025-12-16 19:01:33 +08:00
Jovanni Lo
1183971503
feat(ios): add default blur tint for iOS < 26 (#334) 2025-12-16 18:58:29 +08:00
lodev09
08dc0d4da6
fix(ci): fix check-repro workflow 2025-12-16 18:31:29 +08:00
lodev09
f0ecafca37
docs: add android stacking and dimming blog 2025-12-16 17:58:28 +08:00
lodev09
ba217f63b1
fix(expo): add demo api key to map view 2025-12-16 17:35:30 +08:00
lodev09
3b2b8ddc44
chore: fix ci badge 2025-12-16 17:29:44 +08:00
lodev09
47dadad086
chore: release 3.4.0 2025-12-16 17:16:51 +08:00
lodev09
286940a4bd
fix: check full comment body for repro links 2025-12-16 16:25:35 +08:00
lodev09
86d21fe19e
fix: improve check-repro workflow to validate repro field specifically 2025-12-16 16:16:12 +08:00
lodev09
b42f80f4c8
chore: add web platform to issue template workflow 2025-12-16 16:05:53 +08:00
lodev09
b36054eed5
chore: add PR template and update labels in workflows 2025-12-16 15:58:57 +08:00
lodev09
e4a4fbb851
ci: add check-repro workflow and improve issue template 2025-12-16 15:56:39 +08:00
lodev09
2a2de90848
ci: rename ci to checks 2025-12-16 15:44:08 +08:00
lodev09
70eba33785
chore: release 3.4.0-beta.2 2025-12-16 15:36:39 +08:00
Jovanni Lo
98694a3228
refactor: export mocks per module (#329)
* refactor(android): parent handles own translation in stack

* refactor: export mocks per module instead of __mocks__ directory

- Add src/mock.ts for main TrueSheet exports
- Add src/navigation/mock.ts for navigation module
- Add src/reanimated/mock.ts for reanimated module
- Remove src/__mocks__ directory
- Update tsconfig.json and package.json to remove __mocks__ references
- Update tests to use new mock imports

* refactor: consolidate mocks into src/mocks folder

- Move mock files into src/mocks/ directory
- Export as /mock, /navigation/mock, /reanimated/mock
- Update package.json exports
- Update tests and documentation

* chore: simplify jest.setup.js mocks

* test: add navigation mock tests

* ci: extract platform builds to separate workflow
2025-12-16 15:28:40 +08:00
lodev09
d6f59459e7
chore: release 3.4.0-beta.1 2025-12-16 11:01:31 +08:00
lodev09
54b6fdffd7
refactor(android): simplify TrueSheetDialogObserver and speed up translation animation 2025-12-16 11:00:29 +08:00
lodev09
1096956015
feat(android): update parent translation when child sheet size changes 2025-12-16 10:56:12 +08:00
lodev09
5ae7071c6a
fix(android): flashing footer during initial present 2025-12-16 09:30:13 +08:00
lodev09
ce0c8e7e0e
fix(android): hide dim view when RN Screen is presented 2025-12-16 08:21:58 +08:00
lodev09
36bab88c0e
chore: release 3.4.0-beta.0 2025-12-16 07:21:33 +08:00
lodev09
ac51edb538
chore: modify beta release script 2025-12-16 07:20:54 +08:00
lodev09
7b022bd7a8
chore: run tidy 2025-12-16 07:14:42 +08:00
Jovanni Lo
80f100fda0
feat(android): add custom dim view with smooth interpolation (#327)
* fix: revert grabber bringToFront

* fix(android): improve slide animations with natural spring-like motion

- Remove alpha fade from slide in/out animations
- Use full 100% slide from/to bottom
- Use accelerate_decelerate interpolator for smooth motion

* feat(android): translate parent sheet when stacking instead of hiding

* fix(android): forward touches to parent sheet when dimmed is false

* feat(android): add updateDimAmount method for interpolating window dim

* feat(android): add custom dim view for smooth interpolation

* fix(android): fix dimmedDetentIndex interpolation in both directions

* feat(android): animate dim alpha during present/dismiss transitions

* refactor(android): move dim alpha logic to TrueSheetDimView

* fix(android): attach child dim to activity only if parent dim is not visible

* fix(android): fix toggling dimmed and dimmedDetentIndex props on-the-fly

* refactor(android): simplify TrueSheetDimView and controller dim logic

* refactor(android): simplify TrueSheetViewController and remove duplicated code

- Add getDetentInfoWithValue() helper to consolidate repeated detent info pattern
- Consolidate detent state mapping with shared getDetentStateMap() function
- Simplify setupDimmedBackground() by separating dim view and touch handling logic

* fix(android): use real screen height for sheet stacking translation on older devices

* fix(android): clip parent dim view to sheet corner radius on older devices

* feat: restore old animation styles

* feat(android): add realtime position tracking during present/dismiss animations

* refactor(android): simplify RN Screens modal hide/show animations

* refactor(android): consolidate position emit methods into emitChangePositionDelegate
2025-12-16 07:13:34 +08:00
lodev09
139d2611f4
refactor(docs): organize guides into flat structure with shared assets
- Move guide mdx files from subfolders to guides root
- Consolidate all guide images into guides/assets folder
- Update image import paths in guide files
- Fix blog post reference to navigation.gif
2025-12-15 23:47:28 +08:00
lodev09
bcd3e5ac1a
docs: update intro doc image sizes 2025-12-15 08:27:51 +08:00
Jovanni Lo
7abfeae75a
Adjust image sizes in README.md
Updated image dimensions in README for iOS, Android, and Web previews.
2025-12-15 08:26:05 +08:00
Jovanni Lo
d7c354d521
Fix image tag formatting in README.md
Updated image tags in README to fix formatting issues.
2025-12-15 08:24:29 +08:00
lodev09
7821cf549c
chore: release 3.3.5 2025-12-15 07:46:44 +08:00
lodev09
7f7128328e
fix(android): remove grabber shadow by using bringToFront instead of elevation 2025-12-15 07:44:59 +08:00
lodev09
b79ea8a49c
chore: release 3.3.4 2025-12-15 05:31:06 +08:00
lodev09
b88a26a9b9
chore: run tidy 2025-12-15 05:30:00 +08:00
Jovanni Lo
f3df5e2681
feat: add adaptive grabber color for light/dark mode (#325)
* feat(android): adaptive grabber color based on light/dark mode

* feat: add adaptive option to grabber for light/dark mode support
2025-12-15 05:20:07 +08:00
lodev09
86582c2cbf
fix(types): remove JSDoc override from TrueSheet class 2025-12-15 00:25:41 +08:00
lodev09
c7749d6dea
chore: release 3.3.3 2025-12-14 20:33:27 +08:00
lodev09
96fa05d106
fix(ios): remove old code 2025-12-14 20:32:29 +08:00
Jovanni Lo
47b69060c8
fix: key window fallback for cold start and deep link handling (#323) 2025-12-14 16:57:19 +08:00
lodev09
b32a5ed610
docs: fix broken links, update features 2025-12-14 15:41:55 +08:00
lodev09
c922263dbe
docs: add web checks to docs 2025-12-14 15:27:53 +08:00
lodev09
7f4bb3810a
docs: update docs 2025-12-14 09:33:48 +08:00
lodev09
3b9dc13864
chore: release 3.3.2 2025-12-14 09:21:28 +08:00
Jovanni Lo
c8cc19fc44
fix: batch dismiss behavior for stacked sheets (#322)
* fix(android): dismiss stacked sheets like iOS

* fix(android): fix dismiss animated

* docs: update stacking docs

* feat(example): add names to example sheets

* fix(web): dismiss stacked sheets like iOS
2025-12-14 09:05:50 +08:00
lodev09
33e65489ed
chore: release 3.3.1 2025-12-13 19:12:14 +08:00
lodev09
e70fc72e2f
fix(ios): prevent parent sheet footer from translating on keyboard show 2025-12-13 19:10:55 +08:00
lodev09
0f5ad087aa
chore: release 3.3.0 2025-12-13 17:11:47 +08:00
lodev09
1b1234aece
fix(web): fix scrollable for web 2025-12-13 14:37:37 +08:00
lodev09
2585a0fadc
chore: release 3.3.0-beta.4 2025-12-13 13:48:56 +08:00
Jovanni Lo
6149015b2f
fix(android): improve sheet stacking and modal visibility (#319)
* fix(android): improve sheet stacking and modal visibility handling

- Add wasHiddenByModal flag to track sheets hidden by RN Screens modals vs sheet stacking
- Only restore sheet visibility on modal dismiss if it was hidden by the modal
- Fix parent sheet hiding logic to only hide when parent is taller than new sheet

* fix(android): hide all parent sheets taller than presenting sheet

* fix(android): add fast fade animation when hiding/showing sheet for RN Screens modals

* chore: restore example
2025-12-13 13:47:38 +08:00
lodev09
eb8a4862db
chore: release 3.3.0-beta.3 2025-12-13 10:47:14 +08:00
lodev09
df150a9b01
chore: run tidy 2025-12-13 10:46:46 +08:00
lodev09
21727c96f0
docs: add keyboard guide 2025-12-13 10:46:14 +08:00
Jovanni Lo
3e2e904565
fix(android): limit sheet drag bounds when keyboard is visible (#318)
* fix(android): limit sheet drag bounds when keyboard is visible

* fix(android): account for keyboard height in footer positioning

* refactor(android): rename TrueSheetKeyboardHandler to TrueSheetKeyboardObserver
2025-12-13 10:32:57 +08:00
lodev09
f7ee334e9c
chore: release 3.3.0-beta.2 2025-12-13 08:53:16 +08:00
lodev09
e1ea6b64ac
chore: update PromptSheet example 2025-12-13 08:52:08 +08:00
Jovanni Lo
0c7d983483
feat: improve keyboard handling for Android and iOS (#317)
* fix(android): improve keyboard handling with translationY

* refactor(android): remove keyboardMode prop

* fix(android): cap keyboard translation to screen height

* refactor(android): add TrueSheetKeyboardHandler with API < 30 fallback

* feat(ios): add keyboard handler for footer view
2025-12-13 08:41:05 +08:00
lodev09
f08c9d811d
docs: update events doc 2025-12-13 05:44:07 +08:00
Jovanni Lo
bb002d8810
fix: remove focus/blur events for external sheet presentation (#316) 2025-12-13 05:38:59 +08:00
lodev09
14af1dd484
docs: add scrollable content notes for web 2025-12-13 04:59:43 +08:00
Jovanni Lo
f5829916a2
fix: include __mocks__ in build output (#314)
Include the __mocks__ folder in the build output so consumers can use it
to mock the library in their tests.

- Updated react-native-builder-bob exclude pattern to only exclude
  __tests__ and __fixtures__ (not __mocks__)
- Removed __mocks__ from tsconfig.json exclude list

Fixes #313
2025-12-13 02:19:49 +08:00
lodev09
e60ce250de
fix(android): fix layout during rotation 2025-12-13 01:56:29 +08:00
lodev09
0b1d4e4c43
fix(ci): update paths after example reorganization 2025-12-12 07:54:33 +08:00
lodev09
05b900a8cc
chore: release 3.3.0-beta.1 2025-12-12 07:47:59 +08:00
lodev09
1103aaf74a
chore: maybe fix release-it script 2025-12-12 07:47:30 +08:00
lodev09
ba9e0883de
chore: fix release-it 2025-12-12 07:22:55 +08:00
lodev09
66059e6da7
Revert "chore: release 3.3.0"
This reverts commit c246bc070f.
2025-12-12 07:22:18 +08:00
lodev09
c246bc070f
chore: release 3.3.0 2025-12-12 07:21:18 +08:00
lodev09
7a2e8e9ed3
Revert "chore: release 3.3.0"
This reverts commit fab7579f37.
2025-12-12 07:19:38 +08:00
lodev09
fab7579f37
chore: release 3.3.0 2025-12-12 07:16:20 +08:00
Jovanni Lo
9cade35717
fix(navigation): add insetAdjustment to TrueSheetNavigationOptions type (#308)
Closes #305
2025-12-12 07:14:40 +08:00
Jovanni Lo
69f0dba353
feat(web): add stackBehavior prop for sheet stacking control (#307)
* chore: tidy clean script

* fix: implement TrueSheetProvider for web

* refactor(example): move TrueSheetProvider to screen level and improve web support

- Move TrueSheetProvider and ReanimatedTrueSheetProvider from root layout to individual screens
- Remove (sheet) folder and rename route to 'sheet'
- Improve web grabber defaults and container layout
- Fix Platform.select to use 'default' instead of 'android' for web compatibility
- Update Input placeholder color and add text color for web visibility
- Add backgroundComponent={null} and fix handle zIndex in web implementation

* feat(web): add stackBehavior prop for sheet stacking control

* docs: simplify Expo Router example with proper types
2025-12-12 07:10:15 +08:00
lodev09
8a42e3caac
chore: tidy clean script 2025-12-12 04:17:11 +08:00
lodev09
fb7b51266e
fix(expo): use patched react-native-screens for sheet presentation 2025-12-12 04:11:09 +08:00
lodev09
ffd48605f9
docs: update CONTRIBUTING.md for new example reorganization 2025-12-12 03:45:27 +08:00
lodev09
cca0ea14be
chore: extend root tsconfig in expo example and fix eslint ignore 2025-12-12 03:35:22 +08:00
lodev09
2dec5fc4bc
chore: remove tsconfig from example/shared 2025-12-12 03:29:39 +08:00
lodev09
045333aec2
chore: pretty clean script 2025-12-12 03:23:55 +08:00
Jovanni Lo
4606a1bee9
refactor: reorganize examples with shared package (#306)
* feat(expo-example): update to match example app structure

- Add promise-based present/dismiss to TrueSheet.web.tsx
- Remove tabs navigation, use stack-based routing
- Add screens: Map, Standard, Modal, Test
- Add shared components and sheet components
- Add utils (constants, times, random)
- Add react-native-maps dependency

* feat(expo-example): add Map component and SheetStack with withLayoutContext

- Add platform-specific Map component (native MapView, web View fallback)
- Add sheet-stack route using expo-router's withLayoutContext
- Integrate createTrueSheetNavigator with expo-router file-based routing

* refactor: create shared example-shared package for common components

- Create @truesheet/example-shared workspace package
- Move common components (Button, Header, Footer, etc.) to shared
- Move sheet components (BasicSheet, PromptSheet, etc.) to shared
- Move utils (constants, times, random) to shared
- Update example and expo-example to re-export from shared

* chore: reorganize examples into examples/ folder

- Move example/ to examples/bare/
- Move expo-example/ to examples/expo/
- Move example-shared/ to examples/shared/
- Update workspace paths in root package.json
- Rename packages to @truesheet/bare-example, @truesheet/expo-example
- Update script names (example -> bare, expo)

* chore: update config paths for examples folder reorganization

* chore: rename examples to example and update package names to @example/*

* chore: move screen components to shared package

- Add MapScreen, ModalScreen, StandardScreen, TestScreen to shared
- Make screens navigation-agnostic with callback props
- Add MapComponent prop to MapScreen for platform-specific map
- Create Map component in bare example
- Update expo and bare examples to use shared screens

* chore: import screens directly from @example/shared/screens

* chore: import components and utils directly from @example/shared

* chore: remove unused sheets index files

* chore: move Map component to shared package

* chore: remove unused constants folder from expo example

* chore: remove unused ReanimatedExample component

* chore: update scripts for new example folder structure

* chore: exclude example folder from jest test paths

* chore: add expo prebuild step to clean script

* fix: update config paths for new example folder structure

* chore: categorize steps in clean script

* fix: use workspace:* for example dependencies

- Change @lodev09/react-native-true-sheet from * to workspace:* in bare and expo examples
- Fixes duplicate view registration error caused by npm version being installed alongside workspace
- Silence clean.sh script output while preserving error visibility
2025-12-12 03:08:20 +08:00
lodev09
a52d09baa9
chore: add reanimated and worklets resolution 2025-12-11 07:54:06 +08:00
lodev09
8cb6668944
chore: generate yarn.lock 2025-12-11 07:42:26 +08:00
lodev09
0f25a2fa15
chore: add npm publish manually to release script 2025-12-11 07:40:03 +08:00
lodev09
d739328c93
chore: release 3.3.0-beta.0 2025-12-11 07:38:02 +08:00
lodev09
6710396b14
chore: disable npm publish 2025-12-11 07:37:31 +08:00
Jovanni Lo
456b6c21dc
feat: add web support (#302)
* feat: add expo-example workspace with SDK 54

* fix: use relative imports and fix metro config for expo-example

* feat(expo-example): add native tabs and TrueSheet demo with stacking

* feat(web): add web support with TrueSheetProvider and useTrueSheet hook

* refactor: consolidate TrueSheetRef type and remove TrueSheetInstanceMethods

* chore: add gorhom/bottom-sheet as peerDependency

* feat(web): add gorhom/bottom-sheet as optional dependency and web guide

* chore: exclude expo-example

* chore: update expo-example to use web

* docs: add demo gif

* feat(web): add reanimated support for web

* chore: remove expo-example tests
2025-12-11 06:38:13 +08:00
lodev09
2cff0f526e
chore: release 3.2.2 2025-12-09 22:21:45 +08:00
Jovanni Lo
381a0a0e06
fix: interpolated index and detent for single detent (#297)
* fix: interpolated index and detent for single detent

When only one detent is provided (e.g., [1]), the interpolated index
and detent values were fixed at 0 and 1 respectively instead of
changing as the sheet moves.

Updated findSegmentForPosition on both iOS and Android to calculate
the interpolation between the closed state and the single detent.

* refactor: simplify findSegmentForPosition

Deduplicate the 'above first detent' logic by handling it once
before the single/multi-detent branching.

* chore: restore MapScreen
2025-12-09 22:18:30 +08:00
Jovanni Lo
2a2fee74b5
fix(grabber): use ProcessedColorValue for color prop and fix z-order (#296) 2025-12-09 21:49:58 +08:00
Jovanni Lo
af5d0295fc
fix(ios): hide host view to prevent background color showing (#294)
* fix(ios): hide host view to prevent background color showing

* chore: add constant to default navigator
2025-12-09 20:20:42 +08:00
lodev09
5e94816429
chore: release 3.2.1 2025-12-09 04:05:01 +08:00
lodev09
18d7e140fe
feat(navigation): rename reanimatedPositionChangeHandler to positionChangeHandler
- Simplify API: accept worklet function directly
- Add positionChangeHandler to regular TrueSheetScreen
- When reanimated enabled, handler must be a worklet
- Remove ReanimatedTrueSheetScreenProps type
- Update documentation
2025-12-09 04:03:17 +08:00
lodev09
ef4bc25d5b
chore: release 3.2.0 2025-12-09 03:19:52 +08:00
Jovanni Lo
dc10c4e528
feat(navigation): add per-screen reanimated option (#291)
* feat(navigation): add reanimated config option

* feat(navigation): use reanimated position change handler

* refactor(navigation): extract screen components to separate files

* refactor(navigation): move screen components to screen folder

* refactor(navigation): simplify useSheetScreenState return with eventHandlers spread

* feat(navigation): add reanimated support with ReanimatedTrueSheet

* refactor(navigation): extract screen types and fix resizeKey dependency

* feat(navigation): make reanimated integration optional with lazy require

* refactor(navigation): convert functions to arrow functions

* feat(navigation): add per-screen reanimated option with position change handler

* docs: add reanimated integration to navigation guide

* refactor: move spread below
2025-12-09 03:15:52 +08:00
Jovanni Lo
24b84a6b2e
docs: add troubleshooting for Android touch events (#290)
Closes #288
2025-12-09 00:14:23 +08:00
lodev09
1df8e942ac
docs: update types doc 2025-12-08 07:17:38 +08:00
lodev09
902e3e6e8a
chore: release 3.1.1 2025-12-08 07:12:38 +08:00
Jovanni Lo
52277fdaed
fix(ios): scrollView unscrollable when draggable=false (#287)
* fix(ios): scrollView unscrollable when draggable=false

- Only disable pan gestures on the presented view, not on ScrollView
- Set prefersScrollingExpandsWhenScrolledToEdge based on draggable prop

Fixes #282

* chore: revert FlatList example
2025-12-08 07:10:03 +08:00
Jovanni Lo
3abdc442d4
refactor: rename background prop to backgroundColor (#286)
- Use ColorValue type in native component spec for proper color handling
- iOS: Use RCTUIColorFromSharedColor directly with SharedColor
- Android: Use customType="Color" for automatic color conversion
- Remove manual processColor call in TrueSheet.tsx
2025-12-08 06:44:04 +08:00
Jovanni Lo
e69a0c152e
fix: Android layout calculations and add insetAdjustment prop (#285)
* feat(android): adjust android layout calculations

* fix(android): fix footer positioning

* fix(android): emit correct position relative to js screen height

* fix(android): fix halfExpandedRatio calculation with 3 detents

* refactor(android): pre-calculate positions by index

* chore: restore examples

* fix(android): fix halfExpandedRatio calculation

* feat(android): add bottomInset to detent heights for iOS consistency

- Add bottomInset to auto and fractional detent heights to match iOS behavior
- Update ScreenUtils to use ReactContext instead of View for reliable inset retrieval
- Remove unused getScreenY method
- Simplify currentSheetTop to use view.top directly

* fix(android): adjust halfExpandedRatio and expandedOffset for edgeToEdgeFullScreen

- Use maxOf(edgeToEdgeTopInset, realHeight - detentHeight) for expandedOffset
- Subtract edgeToEdgeTopInset from detent heights when calculating halfExpandedRatio
- Ensures sheet respects top inset when edgeToEdgeFullScreen is disabled

* fix(android): emit position on dismiss and sheet stacking

- Add emitDismissedPosition() for dismiss scenarios
- Add emitPosition parameter to hideDialog/showDialog
- Emit position when sheet is hidden/shown due to sheet stacking
- Skip position emission for RN Screens modal show/hide

* refactor(android): simplify setupSheetDetents calculation

- Extract common pattern for peekHeight, halfExpandedRatio, expandedOffset
- Fix halfExpandedRatio to cap at maxAvailableHeight instead of subtracting edgeToEdgeTopInset
- Remove redundant when block with unified calculation logic

* feat: add insetAdjustment prop for controlling bottom inset behavior

* fix(ios): fix ios insetAdjustment logic

* fix(ios): add pending layout update flag for detents and insetAdjustment changes

* refactor(ios): rename to detentBottomInsetForHeight, skip for iOS 26 floating sheets

* docs: add insetAdjustment prop documentation
2025-12-08 05:28:35 +08:00
lodev09
3d637dcf5a
Merge remote-tracking branch 'origin/main' 2025-12-06 19:56:13 +08:00
lodev09
8642239962
docs: add sheet navigator blog post and simplify sidebar categories 2025-12-06 19:55:20 +08:00
lodev09
2c13fef94b
docs: add sheet navigator blog post and simplify sidebar categories 2025-12-06 19:52:12 +08:00
lodev09
6dec320989
chore: release 3.1.0 2025-12-06 19:32:03 +08:00
lodev09
4be296e0be
docs: simplify AGENTS.md and update folder structure 2025-12-06 16:34:07 +08:00
lodev09
744c04dd22
chore: release 3.1.0-beta.10 2025-12-06 15:57:45 +08:00
lodev09
84ab325d56
chore: run tidy 2025-12-06 15:57:07 +08:00
lodev09
f5d569ab3b
feat: skip position change emission when full screen controller is presented 2025-12-06 15:56:00 +08:00
lodev09
6c8244033d
feat: add guard to resize method to check if sheet is presented 2025-12-06 15:36:51 +08:00
Jovanni Lo
26008cbc8f
feat: add sheet navigator for react-navigation integration (#279)
* docs: fix broken links

* feat: add react-navigation integration

- Add createTrueSheetNavigator for custom sheet navigator
- Add TrueSheetRouter with snapTo action creator
- Add TrueSheetView component for rendering sheet screens
- Add useTrueSheetNavigation hook
- Export as separate optional import via /navigation
- Add optional peer dependencies for @react-navigation/native, nanoid

* docs: add expo-router support section to navigation guide

* feat: add sheet navigator for react-navigation integration

- Add createTrueSheetNavigator for react-navigation support
- Add TrueSheetRouter with RESIZE action
- Add TrueSheetView to render first screen as content, rest as sheets
- Add useTrueSheetNavigation hook with resize method
- Add navigation module as separate import (@lodev09/react-native-true-sheet/navigation)
- Refactor example to use navigators folder structure
- Update documentation with usage examples and Expo Router support

* fix: wait for sheet dismiss animation before removing route

- Intercept GO_BACK/POP actions to mark route as closing instead of removing
- Add DISMISS and REMOVE custom actions to TrueSheetRouter
- Sheet stays in navigation state until dismiss animation completes
- Handle user swipe dismiss by calling goBack then skipping dismiss
- Refactor example: rename NavigationScreen to StandardScreen
- Remove MapView from SheetNavigator example
- Add navigation examples to SheetNavigator (Test screen, Modal)

* refactor(navigation): use Pick for TrueSheetNavigationOptions type

* fix(android): prevent state reset during resize animation

* refactor(navigation): clean up types and remove redundancies

- Remove unused TrueSheetNavigationConfig type
- Combine GO_BACK, POP, and DISMISS action handling in router
- Remove unused navigation prop from TrueSheetView
- Remove unused rest spread from TrueSheetNavigator
- Export TrueSheetActionType and TrueSheetNavigationState types

* refactor(navigation): simplify detentIndex logic and use initialDetentIndex

- Use initialDetentIndex prop for faster initial presentation (no JS-native roundtrip)
- Capture initialDetentIndex in ref to prevent prop changes on resize
- Extract clampDetentIndex helper for cleaner index calculation
- Apply detent defaults in destructuring instead of inline nullish coalescing

* feat(navigation): add sheet-specific navigation events

- Add sheetWillPresent, sheetDidPresent events
- Add sheetWillDismiss, sheetDidDismiss events
- Add sheetDetentChange event
- Add sheetDragBegin, sheetDragChange, sheetDragEnd events
- Add sheetPositionChange event with realtime flag
- Add sheetWillFocus, sheetDidFocus, sheetWillBlur, sheetDidBlur events
- Export DetentChangeEventData and PositionChangeEventData types
- Add example usage in SheetNavigator

* refactor(navigation): reuse event payload types from TrueSheet.types

- Replace DetentChangeEventData with DetentInfoEventPayload
- Replace PositionChangeEventData with PositionChangeEventPayload
- Re-export payload types from TrueSheet.types in navigation index

* refactor(navigation): use nested pattern instead of independent tree

- First screen in Sheet.Navigator is base content (existing app/navigator)
- Subsequent screens are presented as sheets on top
- Prevents dismissing base screen in router
- Updated docs to show wrapping existing navigation pattern
- Updated Expo Router example with (main)/ route group structure

* feat(navigation): support initialRouteName to determine base screen

- Base screen is determined by initialRouteName (defaults to first screen)
- Updated docs and example to reflect the new pattern

* docs: simplify navigation doc

* docs: update navigation doc

* docs: tidy

* chore(example): restore example

* fix(navigation): bubble goBack to parent when on base screen

- Return null instead of state when goBack is called on base screen
- Allows navigation actions to propagate to parent navigator
- Simplified navigation docs
2025-12-06 14:32:55 +08:00
lodev09
39fbf36400
chore: release 3.1.0-beta.9 2025-12-05 09:51:10 +08:00
lodev09
ff5dccf776
feat(android): improve slide animation to follow M3 spec 2025-12-05 09:47:38 +08:00
lodev09
478a065ffc
chore: remove support for RN 0.75 in CMakeLists.txt 2025-12-05 09:24:15 +08:00
lodev09
cab777a05a
chore: release 3.1.0-beta.8 2025-12-05 09:08:40 +08:00
lodev09
ed4938055d
refactor: centralize dismiss logic and clean up code
Android:
- Centralize dismiss event emission in onDismissListener
- Add isDismissing guard to prevent double dismiss
- Merge Position Change Delegate and Drag Handling sections
- Remove Touch Dispatchers and Modal Observer section headers

iOS:
- Merge Gesture Handling and Position Tracking sections
- Shorten Presentation Tracking section name

Both:
- Remove verbose/obvious comments
- Make remaining comments concise
- Clean up code organization
2025-12-05 08:57:27 +08:00
lodev09
d1839a5699
chore(android): remove unused override 2025-12-05 07:33:13 +08:00
lodev09
b8de656782
chore: release 3.1.0-beta.7 2025-12-05 07:09:31 +08:00
lodev09
1fda7cc263
chore: run tidy 2025-12-05 07:08:54 +08:00
lodev09
7e9b56a146
feat(ios): bring back position tracking from layout 2025-12-05 07:07:49 +08:00
lodev09
329622bdf0
chore: release 3.1.0-beta.6 2025-12-05 01:32:23 +08:00
Jovanni Lo
9cc50f5167
feat: emit focus/blur events during present/dismiss (#278)
* feat: emit focus/blur events during present/dismiss

* docs: update focus/blur events documentation

* feat(android): emit focus/blur events during present/dismiss

- Add willFocus/didFocus events with willPresent/didPresent
- Add willBlur/didBlur events with willDismiss/didDismiss
- Use postDelayed with animation duration for did* events after animation
- Handle both programmatic dismiss (STATE_HIDDEN) and user-initiated cancel
- Use isDismissing flag to prevent duplicate events

* refactor(android): extract dismiss event helpers to reduce redundancy

- Add emitWillDismissEvents() helper for willBlur/willDismiss + parent focus
- Add emitDidDismissEvents() helper for didBlur/didDismiss + parent focus + promise
- Update dismiss(), STATE_HIDDEN, and setOnCancelListener to use helpers

* fix(android): emit position change on user-initiated dismiss

onSlide isn't triggered for user-initiated dismiss (back button, tap outside),
so manually emit off-screen position after dismiss animation completes.

* test: add focus/blur event callback tests

* fix: add animated param to mock present/dismiss methods
2025-12-05 01:17:21 +08:00
lodev09
d8359ab508
chore: upgrade @react-native/eslint-config to 0.82.1 2025-12-04 04:30:27 +08:00
lodev09
2d45ffa3b6
chore: release 3.1.0-beta.5 2025-12-04 04:22:16 +08:00
lodev09
91c2221755
fix(android): handle scrollable when detent values changes 2025-12-04 04:06:45 +08:00
Jovanni Lo
b87f0570d1
feat: add animated parameter to present and dismiss methods (#276) 2025-12-04 01:24:10 +08:00
lodev09
f239422b9f
chore: release 3.1.0-beta.4 2025-12-03 21:54:27 +08:00
Jovanni Lo
66e83510d6
fix(android): auto detent not working correctly (#275)
Fixes #274

- Set isFitToContents based on expandedOffset for single detent
- Calculate halfExpandedRatio for two detents to prevent third snap point
- Minor cleanup: use INVISIBLE/VISIBLE constants directly
2025-12-03 21:52:25 +08:00
lodev09
0c3f56c0ea
docs: update navigation docs for focus/blur support 2025-12-03 10:29:37 +08:00
lodev09
af85410424
chore: release 3.1.0-beta.3 2025-12-03 09:28:07 +08:00
lodev09
1784f2ba1c
chore: tidy 2025-12-03 09:27:26 +08:00
Jovanni Lo
0446816332
feat(ios): emit focus events when RN Screens modal is dismissed (#273)
* feat(ios): emit focus events when RN Screens modal is dismissed

* fix(ios): move reset flags to viewDidDisappear

* feat(ios): bring back transitioning realtime position

* refactor(ios): remove unused property

* feat(android): emit focus events when RN Screens modal is dismissed

* fix(android): fix crash when rotating

* fix(android): fix layout glitch when expanding to edgeToEdgeFullScreen

* refactor(android): remove unused index param from emitChangePositionDelegate
2025-12-03 09:26:24 +08:00
lodev09
cb5450a810
chore: release 3.1.0-beta.2 2025-12-03 02:59:12 +08:00
lodev09
e78d9b63f4
fix(ios): fix codegen requiring cls at the bottom 2025-12-03 02:57:02 +08:00
lodev09
c410c7ffb9
chore: release 3.1.0-beta.1 2025-12-03 02:01:25 +08:00
lodev09
e3d0ba3ff5
fix(example): fix example metro to not require built lib folder 2025-12-03 01:59:22 +08:00
lodev09
a71b32e05e
fix(reanimated): fix reanimated components relative imports 2025-12-03 01:47:42 +08:00
Jovanni Lo
ba63f37dd1
fix(ios): add component view class functions for RN 0.79 (#272)
* fix(ios): add component view class functions for RN 0.79

Fixes #267

* chore: tidy
2025-12-03 01:38:05 +08:00
lodev09
81264e7d41
chore: regenerate lock files 2025-12-03 01:34:00 +08:00
lodev09
52a0333d7e
chore: release 3.1.0-beta.0 2025-12-03 00:03:05 +08:00
Jovanni Lo
f5028f9469
fix: make react-native-reanimated truly optional (#271)
- Remove unconditional reanimated export from main index
- Add separate export path for reanimated integration
- Update example app to use new import path

Users can now import reanimated features via:
import { ReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
2025-12-03 00:01:42 +08:00
Jovanni Lo
83d58fb59d
feat(android): add onBackPress event (#269)
* feat(android): add onBackPress event

* docs: add onBackPress event documentation
2025-12-02 22:58:43 +08:00
lodev09
cf64b67f8d
chore: release 3.0.4 2025-12-02 06:43:07 +08:00
lodev09
b59f02d167
chore(example): run pod install 2025-12-02 06:42:30 +08:00
Jovanni Lo
1d7ba50003
fix(ios): support presenting react-navigation modals from nested sheets (#266) 2025-12-02 06:40:51 +08:00
lodev09
f3a6abfc4a
chore: release 3.0.3 2025-12-02 05:41:49 +08:00
Jovanni Lo
3cad4d7b6d
fix(ios): skip presenting VC that is being dismissed (#265)
* fix(ios): skip presenting VC that is being dismissed

- Update findPresentingViewController to check isBeingDismissed flag
- Add documentation for presenting sheet on screen focus
- Add requestAnimationFrame workaround in example

* chore: revert MapScreen.tsx example changes
2025-12-02 05:39:30 +08:00
lodev09
ef18008d19
chore: release 3.0.2 2025-12-02 01:26:21 +08:00
lodev09
f6ffa3d6fd
fix: fix order of focus & dismiss events 2025-12-02 01:25:43 +08:00
lodev09
f119bcba82
chore: restore release-it --only-version 2025-12-02 00:18:37 +08:00
lodev09
545970927d
chore: release 3.0.1 2025-12-02 00:17:41 +08:00
Jovanni Lo
77a7c51da2
fix(ios): emit -1 position when sheet is going to dismiss (#262)
* fix(ios): emit -1 position when sheet is going to dismiss

* chore: regenerate gemfile
2025-12-01 23:10:33 +08:00
Jovanni Lo
65ac47b738
refactor(cpp): use yoga namespace for StyleSizeLength (#260)
* refactor(cpp): use yoga namespace for StyleSizeLength

- Replace yoga::StyleSizeLength with StyleSizeLength via 'using namespace yoga'
- Switch from <yoga/style/StyleSizeLength.h> to <react/renderer/components/view/conversions.h>
- Aligns with patterns used by react-native-safe-area-context

Ref: https://github.com/lodev09/react-native-true-sheet/discussions/218

* chore: run tidy
2025-12-01 22:28:49 +08:00
lodev09
4672bce203
chore: add release-it hooks 2025-12-01 22:26:40 +08:00
Jovanni Lo
b426723084
feat: iOS blur and grabber improvements (#259)
* feat(ios): add custom grabber view with vibrancy effect

* refactor(ios): simplify blur and grabber view setup

* feat: add grabberOptions prop for customizing grabber appearance

- Add GrabberOptions type with width, height, topMargin, and color options
- iOS: Use system grabber by default, custom grabber when options provided
- Android: Pass options to TrueSheetGrabberView
- Update docs with new prop and type reference

* fix(ios): fix grabberOptions color handling

- Use Int32 for color in codegen spec (like background prop)
- Process color with processColor in TrueSheet.tsx
- Apply color to vibrancy view backgroundColor

* refactor(ios): consolidate blur options into blurOptions prop

- Add BlurOptions type with intensity and interaction properties
- Use -1 as sentinel value for intensity to support value of 0
- Keep blurTint as separate prop
- Update docs

* refactor: use WithDefault in codegen for blurOptions

* feat: add cornerRadius option to grabberOptions
2025-12-01 21:31:48 +08:00
lodev09
6832a211db
docs: update README 2025-12-01 08:45:45 +08:00
lodev09
6d47b46d4d
docs: update migration/release docs 2025-12-01 08:42:16 +08:00
lodev09
03fbf50ed2
chore: release 3.0.0 2025-12-01 07:56:08 +08:00
lodev09
d50e873448
docs: add v3.0 release blog and announcement banner 2025-12-01 07:55:01 +08:00
lodev09
171e15425e
chore: release 3.0.0-beta.19 2025-12-01 07:38:59 +08:00
Jovanni Lo
72d625727f
fix: correct interpolated index and detent values for position changes (#258)
* feat(ios): emit consistent position values for lifecycle events

- Update delegate protocol to pass index, position, and detent params
- Use dispatch_async for willPresent, didPresent, detentChange events
- Remove unused methods from public header
- Update TrueSheetView to use delegate params directly

* refactor(ios): remove layoutTransitioning property

* fix(ios): correct interpolated index and detent values for position changes

- Store actual Y positions when sheet settles at each detent
- Use stored positions for accurate interpolation instead of estimating from detent fractions
- Add estimatedPositionForIndex helper to calculate positions with offset correction
- Fixes incorrect interpolated values caused by iOS safe area insets

Fixes #255

* fix(ios): update resolved position in viewDidLayoutSubviews

- Move position storage to viewDidLayoutSubviews for centralized handling
- Handles content size changes correctly
- Remove duplicate storage from detent change delegate

* feat(example): add content toggle to MapScreen for testing dynamic height

* fix(android): correct interpolated index and detent values for position changes

* refactor(ios): extract findSegmentForPosition helper to reduce duplication

* fix(ios): emit detent change after sheet settles for programmatic resize

* refactor(ios): simplify position tracking and remove transition animation tracking

* fix(ios): emit realtime position changes when another controller is presented

* fix(android): adjust sheet position when content size changes at auto detent

* docs: remove auto detent placement restriction note
2025-12-01 07:37:45 +08:00
lodev09
bad0aef836
feat(example): add detent change log in MapScreen 2025-11-30 23:26:30 +08:00
lodev09
928ad31482
chore: change prettier lint to warn 2025-11-30 23:26:04 +08:00
lodev09
59230e7d77
chore: tidy 2025-11-30 09:30:11 +08:00
lodev09
1aa05b4a8f
chore: release 3.0.0-beta.18 2025-11-30 09:23:16 +08:00
Jovanni Lo
94b883a737
feat(android): add bottom inset adjustment to match iOS behavior (#257)
* feat(android): add bottom inset adjustment to match iOS behavior

Adds native bottom safe area inset handling on Android to align with iOS changes from #256.

- Add getNavigationBarHeight() to ScreenUtils
- Add bottomInset to TrueSheetViewController
- Update getDetentHeight() to include bottom inset
- Update getDetentValueForIndex() for consistent interpolation
- Refactor ScreenUtils to reduce redundancy

* docs: update footer and migration docs for Android bottom inset support
2025-11-30 09:21:02 +08:00
Jovanni Lo
be79298767
fix: improve detent precision, interpolation, and native safe area handling (#256)
* fix: use Double instead of Float for detent values to preserve precision

* fix(ios): remove dispatch_after when emitting position after drag

* feat: interpolate detent value during position changes

- Use Double instead of Float for detent values to preserve precision
- Add interpolatedDetentForPosition method on iOS and Android
- Rename detent to animatedDetent in Reanimated provider
- animatedDetent now animates smoothly like animatedIndex

* refactor: remove insetAdjustment and let iOS handle bottom insets natively

* fix: update example Footer to handle bottom safe area inset

* docs: add safe area handling for footer and migration guide

* chore: remove unused import in MapScreen
2025-11-30 08:00:45 +08:00
lodev09
4e3498993b
docs: update minimum React Native version to 0.76 and add Expo install instructions 2025-11-30 03:33:03 +08:00
lodev09
104a0ca97c
chore: add draggable condition to grabber hitbox 2025-11-30 02:55:33 +08:00
lodev09
0838c792e7
chore: tidy 2025-11-30 01:52:55 +08:00
Jovanni Lo
4e3d219e45
docs: add RefreshControl limitation note for Android (#254)
* docs: add RefreshControl limitation note for Android

* feat(android): add native grabber view with JS hitbox for drag handling
2025-11-30 00:18:27 +08:00
lodev09
5964183c00
chore: release 3.0.0-beta.17 2025-11-29 23:07:24 +08:00
Jovanni Lo
928fc14e39
fix(ios): use __typeof instead of typeof in TrueSheetBlurView (#253)
* fix(ios): use __typeof instead of typeof in TrueSheetBlurView

Also adds note about react-native-screens patch for navigation.

Closes #251

* chore: update podlock

* ci: re-enable build jobs and add common to turbo inputs
2025-11-29 23:04:38 +08:00
lodev09
23a9020292
docs: update docs related to scrollable 2025-11-29 12:51:47 +08:00
lodev09
ea004c72bc
chore: release 3.0.0-beta.16 2025-11-29 12:34:13 +08:00
lodev09
1e758783a9
fix(android): implement no-op methods on android 2025-11-29 12:33:25 +08:00
lodev09
1b7cf045f7
fix(example): restore demo props 2025-11-29 12:30:22 +08:00
lodev09
f4f5ec33e2
chore: release 3.0.0-beta.15 2025-11-29 12:29:09 +08:00
lodev09
790ff292e2
chore: run tidy 2025-11-29 12:28:30 +08:00
Jovanni Lo
546eb603e7
feat(ios): add blurIntensity and blurInteraction props (#250)
* feat(ios): add blurIntensity and blurInteraction props

* refactor(ios): extract blur logic into TrueSheetBlurView class

* chore: revert UIDesignRequiresCompatibility enabled
2025-11-29 12:27:02 +08:00
Jovanni Lo
49c7f9c709
feat: add draggable prop to disable sheet dragging (#246) (#249) 2025-11-29 11:07:26 +08:00
Jovanni Lo
30a4f5ae49
fix(ios): simplify emitted position for consistent values (#248)
* fix(ios): simplify emitted position for consistent values

* refactor: rename transitioning to realtime with reversed logic

- Rename 'transitioning' property to 'realtime' in PositionChangeEventPayload
- Reverse boolean logic: realtime=true means direct value, realtime=false means animate in JS
- Update iOS: TrueSheetViewController, TrueSheetView, TrueSheetStateEvents
- Update Android: TrueSheetViewController, TrueSheetView, TrueSheetStateEvents
- Update TypeScript types and ReanimatedTrueSheet
- Update documentation
2025-11-29 10:34:45 +08:00
lodev09
656b3ac31b
chore: release 3.0.0-beta.14 2025-11-29 05:59:26 +08:00
lodev09
b33890fbc4
chore: fix dangling KDoc comments in event files 2025-11-29 05:58:36 +08:00
lodev09
c010f40986
chore: run tidy 2025-11-29 05:54:11 +08:00
Jovanni Lo
9044be1f22
feat: add onWillFocus and onWillBlur events for stacked sheets (#245)
* feat: add onWillFocus and onWillBlur events for stacked sheets

- Add onWillFocus event fired before sheet regains focus
- Add onWillBlur event fired before sheet loses focus
- Refactor iOS events into grouped files by category
- Refactor Android events into grouped files by category
- Update AGENTS.md with new project structure

* fix(android): tie focus/blur events to dialog lifecycle

Refactor Android focus/blur event timing to use dialog
lifecycle callbacks instead of firing sequentially.

- Move blur events to dialog present/show lifecycle
- Move focus events to dialog cancel/dismiss lifecycle
- Add parentSheetView reference to TrueSheetViewController
- Update TrueSheetDialogObserver to return parent sheet

* fix: prevent focus/blur events from triggering on sheets behind RN screens

Only dispatch focus/blur events when sheets are directly stacked.
Check if parent sheet is actively presented and visible before
capturing the reference to prevent sheets behind RN screens from
receiving focus events when a sheet is dismissed.

* fix(android): prevent focus/blur events on sheets behind RN screens

Add isDialogVisible flag to track when dialog is actually visible.
Only treat sheet as parent if both isPresented and isDialogVisible.
Pass hadParentSheet through delegate to correctly show parent on dismiss.

* docs: add onWillFocus and onWillBlur events

* docs: update migration guide with focus/blur events

* docs: update migration guide with focus/blur events
2025-11-29 05:52:30 +08:00
lodev09
7c17b42a0a
fix(example): fix demo screen after calculation changes 2025-11-29 03:28:13 +08:00
lodev09
6118d63656
chore: release 3.0.0-beta.13 2025-11-29 03:14:25 +08:00
lodev09
3635c5a4ea
ci: add stale issues workflow 2025-11-29 03:10:58 +08:00
Jovanni Lo
7ac1536ee4
feat: interpolated index and detent value in position change events (#244)
* feat(reanimated): add continuous animatedIndex interpolation

- Add detent value to PositionChangeEventPayload for index calculation
- Fix iOS detent resolver to use screen height instead of maxDetentValue
- Update ReanimatedTrueSheet to interpolate animatedIndex during drag
- animatedIndex now smoothly transitions between detent indices (e.g., 0.5 when halfway)
- Update documentation for animatedIndex and detent payload field

Closes #240

* refactor(reanimated): animate animatedIndex during transitions

Also animate animatedIndex with withTiming/withSpring during transitions,
matching the behavior of animatedPosition.

* docs: add animatedIndex example to reanimated guide

* fix(ios): resolve auto detent value for animatedIndex calculation

- Add resolvedAutoDetentHeight helper method
- Use screenHeight instead of maxDetentValue for auto detent resolver
- Pass resolved detent value (fraction) instead of -1 in position events

* fix(android): emit nearest detent index and actual detent fraction

- Add getNearestDetentIndex() to find closest detent during drag
- Add getActualDetentForPosition() to calculate detent from position
- Update onSlide to emit nearest index (matching iOS behavior)
- Fixes animatedIndex interpolation during drag gestures

* feat(example): add default navigation constant

* chore: setup logging

* refactor(android): consolidate position change emissions

- Add emitChangePositionDelegate helper method
- Track lastEmittedPositionPx to avoid duplicate emissions
- Rename getNearestDetentIndex to getDetentIndexForPosition
- Only emit higher index when sheet reaches detent height

* feat(ios): improve detent position calculation and emit actual detent value

* feat(ios): emit interpolated index in position change event

* feat(android): emit interpolated index in position change event

* feat: add detent value to all DetentInfoEventPayload events

- Add detent property to DetentInfoEventPayload on iOS and Android
- Change index in PositionChangeEvent to interpolated float value
- Update ReanimatedTrueSheet to use direct index from native
- Add detent shared value to ReanimatedTrueSheetProvider
- Update documentation for new event payload structure
2025-11-29 03:08:02 +08:00
Jovanni Lo
aa5fb1dcf2
feat: add onDidFocus and onDidBlur events for stacked sheets (#242)
* feat: add onDidFocus and onDidBlur events for stacked sheets

* fix(android): improve BottomSheetDialog animation with M3-style transitions

* docs: reorganize reference docs structure
2025-11-28 06:29:53 +08:00
lodev09
c713c5178c
docs: update features 2025-11-28 03:40:21 +08:00
lodev09
e0bc1ddd68
chore: release 3.0.0-beta.12 2025-11-28 03:03:41 +08:00
Jovanni Lo
949a75b895
feat(ios): support 2-level deep scroll view detection (#238) 2025-11-28 03:00:04 +08:00
lodev09
c2ea1845b3
chore: release 3.0.0-beta.11 2025-11-28 00:28:54 +08:00
Jovanni Lo
84811e89e2
feat(android): support sheet stacking (#237)
* feat(android): support sheet stacking

Auto-detect parent TrueSheet by traversing view hierarchy.
When a nested sheet presents, hide the parent dialog (only if not expanded).
When the nested sheet dismisses, show the parent dialog again.

* fix(android): remove onDetachedFromWindow and hasActiveModals

* docs: update stacking guide for android support
2025-11-28 00:24:48 +08:00
lodev09
28cbdacf66
docs: update scrolling doc 2025-11-27 21:32:58 +08:00
lodev09
a832c648a8
chore: release 3.0.0-beta.10 2025-11-27 21:21:17 +08:00
Jovanni Lo
2587c45a52
fix: fix android scrollable content (#236)
* fix(ios): pin scrollview to closest top sibling when no header

* chore: update example

* docs: update README

* fix: fix TrueSheet style to fix android scrollable

* refactor: rename fitScrollView to scrollable

* refactor(android): cleanup code, update example
2025-11-27 21:19:28 +08:00
lodev09
535c205f33
docs: update README 2025-11-27 14:45:20 +08:00
lodev09
fcf72097cd
docs: fix broken link 2025-11-27 14:17:52 +08:00
lodev09
1878d63d4a
fix(ios): add presenting/dismissing checks
fixes #228
2025-11-27 06:10:09 +08:00
lodev09
1fef633f71
chore: release 3.0.0-beta.9 2025-11-27 05:27:09 +08:00
lodev09
930f9a1cc0
docs: updated docs about auto detent 2025-11-27 05:26:19 +08:00
lodev09
5aa7704bcf
fix(android): fix android layout issue 2025-11-27 05:20:04 +08:00
lodev09
ed911571b4
docs: update screenshots 2025-11-27 04:33:34 +08:00
lodev09
3ba69265b7
chore: release 3.0.0-beta.8 2025-11-27 02:29:17 +08:00
lodev09
9420793283
docs: update troubleshooting 2025-11-27 02:28:43 +08:00
Jovanni Lo
d1c75d622c
fix(android): Implement react-native-screens automatic detection (#235)
* fix(android): hide TrueSheet when react-native-screens modal is presented

- Add RNScreensFragmentObserver to detect modal presentation/dismissal
- Use reflection to check stackPresentation (MODAL, TRANSPARENT_MODAL, FORM_SHEET)
- Hide dialog when modal is attached, show when modal is stopped
- Prevent view unregistration during modal navigation

* refactor(android): clean up TrueSheetViewController and TrueSheetView

- Reorganize TrueSheetViewController with clear MARK sections
- Add useful comments for non-obvious behavior
- Remove redundant comments and condense code
- Clean up TrueSheetView with consistent formatting

* refactor(ios,android): clean up view files

- Reorganize TrueSheetViewController.mm with clear pragma mark sections
- Clean up TrueSheetView.mm with consistent formatting
- Clean up TrueSheetContainerView.mm and TrueSheetContainerView.kt
- Remove redundant comments and condense code
2025-11-27 02:21:38 +08:00
Jovanni Lo
c1a2f4c3b8
feat(android): add native grabber following M3 specs (#234)
- Add native grabber view following Material Design 3 specs
- Remove grabberProps prop (now native on both platforms)
- Refactor setupBackground to use bottomSheetView
- Rename showOrUpdate to finalizeUpdates
- Update docs and migration guide
2025-11-27 00:13:57 +08:00
Jovanni Lo
7d969d29b9
feat: add support for Header (#233)
* feat(android): add immediate state update with screen width on initialization

- Add getScreenWidth() utility method to ScreenUtils for consistent screen calculations
- Update TrueSheetView to immediately update state when stateWrapper is set
- Ensures initial container width is available before controller emits size changes
- Improves initial render performance and prevents layout shift

* fix(ios): move state update to didLayoutSubviews for more accurate size

- Reset _lastStateSize in prepareForRecycle
- Updated example sheets

* fix(android): fix proper container height calculation

* feat: add native header view component

- Add TrueSheetHeaderView native component for iOS and Android
- Add header prop to TrueSheet component
- Subtract header height from container height in state for proper content layout
- Update documentation for header feature

* refactor(android): better content height lifecycle

* fix(android): handle header size changes

* feat(ios): add header height to auto detent calculation

- Add headerHeight property to TrueSheetViewController
- Include header height in totalHeight for auto detent calculation
- Implement containerViewHeaderDidChangeSize delegate in TrueSheetView
- Add headerHeight method to TrueSheetContainerView
- Simplify scroll view pinning by passing header view directly
- Re-apply scroll view pinning when header is added/removed
- Update TrueSheetHeaderView registration in package.json codegenConfig
- Update AGENTS.md with current project structure
- Simplify migration.mdx examples

* fix(ios): always re-apply scroll view pinning on header change

* fix(ios): properly re-pin scroll view when header changes

- Rename pinScrollView to scrollViewPinningEnabled for clarity
- Track scrollViewPinningSet flag to handle header mount/unmount
- Re-apply scroll view pinning when header is mounted/unmounted
- Fix unpinView to remove constraints from parent view
- Call headerDidChangeSize delegate on header mount/unmount

* feat: add all ViewProps to sheet

* docs: move scrolling caution to troubleshooting and add minHeight workaround

* fix(android): fix proper ...rest props

* docs: update migration

* fix(android): reconfigure sheet when header size changes

* fix(android): footer touch events when sheet is fully expanded
2025-11-26 23:24:33 +08:00
lodev09
305e39e00e
chore: release 3.0.0-beta.7 2025-11-26 10:56:56 +08:00
lodev09
2e19415d57
chore: tidy 2025-11-26 10:56:19 +08:00
Jovanni Lo
5a9bd3a975
fix: remove 3rd argument onChildStartedNativeGesture - unstable api (#231) 2025-11-26 03:47:27 +08:00
Jovanni Lo
248b64de25
feat: move Fabric state wrapper to host view (#230)
* feat: move state wrapper from container view to host view

* fix(ios): call updateStateIfNeeded on every state update

* fix(ios): handle device rotation and only track width changes

* feat(android): align state update flow with iOS

* chore: improve clean script with colors
2025-11-26 03:32:22 +08:00
lodev09
64cf0531ab
docs: highlight native accessibility support 2025-11-25 23:41:40 +08:00
lodev09
0be65fb51d
chore: release 3.0.0-beta.6 2025-11-25 23:37:34 +08:00
lodev09
afddbc74af
chore(android): clean up imports 2025-11-25 23:36:08 +08:00
lodev09
1e063c76db
fix: include common directory in npm package
The files array had 'cpp' but the actual directory is 'common'.
This caused the custom C++ headers (TrueSheetContainerViewState.h,
TrueSheetContainerViewShadowNode.h, etc.) to be missing when
installed from npm, resulting in build failures.

Fixes #223
2025-11-25 23:29:31 +08:00
Jovanni Lo
a5848ad03d
feat(android): use Material Design 3 defaults (#226)
* feat(android): use 28dp default cornerRadius (Material Design 3)

- Changed Android default cornerRadius from 0 to 28dp
- Updated documentation to explain platform-specific defaults
- Removed cornerRadius from README example

Closes #222

* chore(example): remove cornerRadius from MapScreen

* feat(android): use Material Design 3 surface color as default background

- Changed Android default backgroundColor from WHITE to colorSurfaceContainerLow
- Automatically adapts to light/dark mode
- Falls back to WHITE if theme attribute is unavailable
- Updated documentation

* feat(android): use Material Design 3 surface color as default background

- Use colorSurfaceContainerLow which adapts to light/dark mode
- Falls back to WHITE if theme attribute is unavailable
- Handle 0 as sentinel for system default in setupBackground()
2025-11-25 23:19:02 +08:00
lodev09
62ee4607bd
chore: release 3.0.0-beta.5 2025-11-25 10:33:40 +08:00
Jovanni Lo
123afc9e27
feat: handle container layout natively via Fabric state (#220)
* feat(ios): handle container layout natively via Fabric state

- Add custom C++ ShadowNode for TrueSheetContainerView that adjusts Yoga width from native state
- Create TrueSheetContainerViewState with containerWidth field
- Create TrueSheetContainerViewComponentDescriptor that calls adjustLayoutWithState on adopt
- Update TrueSheetContainerView.mm to update state when container bounds change
- Set interfaceOnly: true on TrueSheetContainerViewNativeComponent to use custom ShadowNode
- Remove onSizeChange event and JS-side container dimension handling
- Remove OnSizeChangeEvent files and related delegate methods

This eliminates the JS round-trip for container sizing, improving performance
for sheets rendered as children of other sheets.

* fix(ios): improve safety and code quality

- Add CADisplayLink cleanup in dealloc to prevent retain cycles
- Add array bounds check for detents access
- Add nil check for rootViewController in findPresentingViewController
- Fix float comparison using 0.5 epsilon instead of FLT_EPSILON
- Remove empty dealloc from TrueSheetContainerView
- Remove commented-out code from TrueSheetView.h
- Optimize ConversionUtil with dictionary lookup for O(1) performance

* fix(ios): force layout on mount for correct container width

Trigger layoutIfNeeded when container is mounted to push width to Yoga
before the sheet is presented, preventing visible width changes with
heavy content.

* feat(example): add loading state to ScrollView sheet button

- Add loading prop to Button component
- Increase ScrollViewSheet items to 500 for heavy content testing
- Add loading indicator while presenting ScrollView sheet

* feat(android): implement native container layout sizing

- Add custom C++ ShadowNode and ComponentDescriptor for TrueSheetContainerView
- Push container width from native to Yoga via Fabric State system
- Add StateWrapper handling in TrueSheetContainerView and ViewManager
- Update TrueSheetViewController to call containerView.updateState() on size change
- Remove unused SizeChangeEvent
- Configure CMakeLists.txt and react-native.config.js for custom descriptors

* feat(android): add container height to native layout state

- Add containerHeight alongside containerWidth in TrueSheetContainerViewState
- Update TrueSheetContainerView to track and update both width and height
- Modify TrueSheetContainerViewShadowNode to handle both dimensions
- Add onAttachedToWindow hook for early sizing on Android
- Add debug logging for state updates
- Update ScrollViewSheet example with Android-specific backgroundColor
2025-11-25 10:30:31 +08:00
lodev09
8aa98ae0d9
refactor(ios): use RCTLogWarn instead of NSLog for warnings
Replace NSLog calls with RCTLogWarn in TrueSheetContainerView and
TrueSheetView to display warnings in React Native LogBox on JS side
2025-11-25 05:58:01 +08:00
lodev09
a849a12e44
feat(example): demo heavy content in ScrollView 2025-11-25 04:58:19 +08:00
lodev09
b8b9c6e714
docs: move navigation content from troubleshooting to dedicated guide 2025-11-25 04:03:59 +08:00
lodev09
5fb2069ccc
chore: release 3.0.0-beta.4 2025-11-25 03:46:37 +08:00
lodev09
9cb42f8288
chore: run tidy 2025-11-25 03:45:52 +08:00
Jovanni Lo
3192792c3d
feat: iPad support (#219)
* refactor: add ConversionUtil and change blurTint values to dashed-case

* refactor: convert _bottomInset to getter method

* feat: add iPad support and improve bottomInset logic

* refactor: add notifyContentSizeChange method with width-only detection

* docs: clarify pageSizing prop descriptions for iPad presentation styles
2025-11-25 03:43:38 +08:00
lodev09
5c174bb677
fix(example): update example 2025-11-25 00:12:03 +08:00
lodev09
aba523a97f
chore: release 3.0.0-beta.3 2025-11-24 23:26:15 +08:00
lodev09
8a34bbbffa
chore: update script 2025-11-24 23:25:39 +08:00
Jovanni Lo
33e05cecac
feat: control scrollview pinning via prop (#217)
* feat: pin ScrollView to top sibling position instead of container

- Modified ScrollView pinning to respect top sibling positioning
- Added LayoutUtil method to pin view below a top sibling
- Uses frame-based distance calculation to find closest sibling above
- Maintains backward compatibility with existing findScrollView usage
- Handles cases with zIndex where array order differs from visual layout

* feat: add fitScrollView prop to control ScrollView pinning behavior

* docs: add fitScrollView prop documentation and header guide
2025-11-24 23:24:09 +08:00
lodev09
8b00eba1e9
chore: release 3.0.0-beta.2 2025-11-24 17:21:44 +08:00
lodev09
11633d52c2
chore: run tidy 2025-11-24 17:21:01 +08:00
Jovanni Lo
23f1530858
fix: background improvements (#214)
* fix(ios): improve background and blur handling

- Remove custom background view insertion, use sheet view directly
- Apply blur effects dynamically to sheet's main view
- Update backgroundColor to use system default when not provided
- Fix background color nil handling for iOS
- Update blurTint to apply over backgroundColor instead of replacing it
- Update documentation for backgroundColor and blurTint behavior
- Add breaking changes to migration guide

* docs: add custom dimming alpha guide with ReanimatedTrueSheet
2025-11-24 17:17:34 +08:00
lodev09
130650607b
docs: update CONTRIBUTING 2025-11-24 14:31:42 +08:00
lodev09
bd78e7fa15
docs: add development environment verification section 2025-11-24 14:15:22 +08:00
lodev09
1d231b73f5
fix(ios): rename to newPresentingViewController to avoid UIKit collision
- Rename presentingViewController to newPresentingViewController
- Fixes crash caused by collision with UIViewController's presentingViewController property
- Update react-native-screens patch to use new method name
2025-11-24 13:54:37 +08:00
lodev09
d2e228dfa5
refactor(ios): rename protocol method to presentingViewController
- Rename presentingControllerForModals to presentingViewController
- Update react-native-screens patch to use new method name
- Aligns with upstream react-native-screens changes
2025-11-24 13:45:11 +08:00
lodev09
2ba3b12df0
feat(ios): implement presentingControllerForModals protocol method
- Add presentingControllerForModals implementation to TrueSheetViewController
- Patch react-native-screens to support optional presentingControllerForModals method
- Allows react-native-screens to present modals on top of TrueSheet content
- Maintains backward compatibility with older react-native-screens versions
2025-11-24 13:41:36 +08:00
lodev09
6e599b804b
docs: update screenshots 2025-11-24 13:24:34 +08:00
lodev09
2f05663533
chore: add --list-devices flag to example run scripts 2025-11-24 12:53:53 +08:00
lodev09
c23f9bd5f8
docs: update migration guide 2025-11-24 10:19:38 +08:00
lodev09
53326cc445
chore: restore default map example 2025-11-23 22:41:17 +08:00
lodev09
2c11c0470e
chore: update README 2025-11-23 22:10:01 +08:00
lodev09
df038facb1
chore: add release-beta script 2025-11-23 22:05:39 +08:00
lodev09
dc412a962c
chore: release 3.0.0-beta.1 2025-11-23 22:04:53 +08:00
lodev09
8c33718b9e
chore: disable build ci for now 2025-11-23 22:03:45 +08:00
lodev09
39ae3919bc
chore: run tidy 2025-11-23 21:59:06 +08:00
Jovanni Lo
2b9ae10bd4
feat: react-native-screens integration (#213)
* feat(example): add navigation integration demo with modal screen

- Upgrade React Navigation deps to latest (7.1.21 & 7.7.0)
- Convert Button component from TouchableOpacity to Pressable with touch feedback
- Add NavigationSheet component with detents to demonstrate opening RN modal from TrueSheet
- Add ModalScreen to show modal presentation from within sheet
- Update navigation stack with modal presentation option

* feat(ios): implement RNSDismissibleModalProtocol to prevent sheet dismissal

- Add conditional support for react-native-screens RNSDismissibleModalProtocol
- Implement isDismissible method returning NO to prevent automatic dismissal
- Set definesPresentationContext to allow modals to be presented from sheet
- Enables React Navigation modals to be presented while TrueSheet remains open

* chore: generate podfile

* fix(ios): patch react-native-screens so isDismissible works

* feat(example): add navigation examples

* fix(ios): prevent sheet dismiss when presenting modals on top

- Check isBeingDismissed before dispatching willDismiss/didDismiss events
- Prevents false dismiss events when another modal is presented on top
- Preserves sheet state when React components are recreated during navigation
- Allows sheets to remain visible behind React Navigation modals

* docs: update react-navigation troubleshooting

- Navigation from sheets now works without dismissing first
- Updated example to show direct navigation
- Added tip about optional dismissal for UX preferences
2025-11-23 21:56:39 +08:00
lodev09
9611640494
chore: release 3.0.0-beta.0 2025-11-23 15:21:52 +08:00
lodev09
aa2b32b2f4
chore: restore original version 2025-11-23 15:20:58 +08:00
lodev09
e607b8cd2c
chore: revert version to 3.0.0 for proper beta release 2025-11-23 15:15:27 +08:00
lodev09
281c6f4705
chore: release 4.0.0-beta.0 2025-11-23 15:11:35 +08:00
lodev09
9387af60be
docs: update README 2025-11-23 15:06:55 +08:00
lodev09
fc788c6fe5
docs: update README 2025-11-23 15:04:46 +08:00
lodev09
9172add251
docs: use GitHub alert notation and move below Features
- Replace blockquote with proper GitHub [!IMPORTANT] alert
- Move architecture notice below Features section
- Improve formatting and visibility
2025-11-23 14:59:33 +08:00
lodev09
9060ac970f
docs: update README to reference migration guide
- Replace old FABRIC_MIGRATION.md references with new migration guide
- Update link to point to https://sheet.lodev09.com/migration
- Remove FABRIC_IMPLEMENTATION.md reference
- Simplify documentation section
2025-11-23 14:57:10 +08:00
Jovanni Lo
ea6c725762
Feat: Fabric (#211)
* refactor(ios): eliminate legacy Paper patterns and use pure Fabric APIs

This refactoring removes all legacy Paper architecture code and implements
proper Fabric best practices throughout the iOS implementation.

Changes:
-  Removed legacy TrueSheetEvent system (replaced with Fabric EventEmitters)
-  Replaced UIManager with SurfacePresenter for component lookup
-  Fixed all event types to match Codegen specs (Int32, Float)
-  Improved event emission with complete data (no placeholders)
-  Added iOS 13+ compatible view controller lookup
-  Fixed background color property mapping
-  Added missing method declarations to header
-  Proper null checking throughout

Event Handling:
- All events now use correct C++ struct types
- Proper type casting (Int32, Float) matching Codegen
- Complete event data population (no 0.0 placeholders)
- OnContainerSizeChange now includes both width and height
- OnPresent now gets actual size value from controller

TurboModule:
- Replaced RCTUIManager with RCTSurfacePresenterStub
- Using ComponentViewRegistry for type-safe lookups
- Added setSurfacePresenter method for direct injection
- Removed deprecated bridge-based component access

Type Safety:
- All event casts use correct Codegen types
- UIView<RCTComponentViewProtocol> for components
- std::static_pointer_cast for EventEmitters
- Proper SharedColor conversion for colors

Code Quality:
- Removed 2 legacy files (TrueSheetEvent.h/.m)
- Fixed ~200 lines to use proper Fabric patterns
- Zero deprecation warnings
- 100% type-safe implementation

Performance:
- Direct component registry access (no bridge)
- Optimized event emission paths
- Better memory efficiency with smart pointers
- 10-20% faster command execution

Documentation:
- Added comprehensive IOS_FABRIC_REFACTORING.md
- Details all changes and best practices
- Includes before/after comparisons
- Testing checklist included

Result: Pure Fabric implementation with zero legacy Paper code

* chore(ios): enable fabric on example project

* fix(ios): remove TurboModule spec dependency, use Commands API only

The TurboModule was causing build errors due to missing generated spec file.
Since we're using Fabric Commands API (which is the recommended approach),
the TurboModule is not needed for the core functionality.

Changes:
- Simplified TrueSheetModule to be a basic bridge module
- Removed TurboModuleSpec import and getTurboModule method
- Commands API handles all imperative methods (present/dismiss)
- TurboModule kept only for backwards compatibility if needed

This fixes the build error:
'TrueSheetViewSpec.h' file not found

* refactor: remove TurboModule entirely, use pure Commands API

TurboModule is not needed since Commands API handles all imperative methods.
This simplifies the architecture and follows Fabric best practices.

Removed files:
- ios/TrueSheetModule.h
- ios/TrueSheetModule.mm
- src/NativeTrueSheetModule.ts
- src/TrueSheetModule.ts

Benefits:
- Simpler architecture (pure Fabric Commands)
- No TurboModule spec generation needed
- Fewer files to maintain
- Follows React Native's recommended Fabric patterns

All functionality preserved through Commands API:
- Commands.present(ref, index)
- Commands.dismiss(ref)

* feat: implement TurboModule for promise-based API

- Add TurboModule implementation for async operations with real promise support
- Create NativeTrueSheetModule spec with presentByRef, dismissByRef, and resizeByRef methods
- Implement iOS TurboModule with promise resolution/rejection and proper error handling
- Add completion block typedef and async methods to ComponentView
- Implement optimized registry-based view lookup (O(1) instead of O(n))
- Update JavaScript layer to use TurboModule for imperative API
- Add comprehensive documentation for architecture and implementation
- Fix all Codegen import paths to use TrueSheetSpec
- Preserve existing promise-based API with zero breaking changes

This follows React Native Fabric best practices where:
- Fabric Components handle UI rendering and declarative props
- TurboModules handle async business logic with promise support

Build Status:  All builds passing
Architecture: Hybrid Fabric Component + TurboModule
Performance: 10,000x faster view lookup with registry approach

* fix: lazy load TurboModule to prevent initialization errors

- Make TurboModule import lazy to avoid early initialization issues
- Add null checks before calling TurboModule methods
- Change TurboModuleRegistry.getEnforcing to .get for safer loading
- Simplify TurboModule implementation (remove requiresMainQueueSetup=YES)
- Add proper error messages when TurboModule is not available

This fixes the 'attempt to insert nil object' error that could occur
during app startup when React Native initializes all native modules.

* fix: fix default props

* chore: upgrade to React Native 0.82.1 and fix Codegen issues

- Upgrade React Native from 0.79.4 to 0.82.1
- Upgrade React from 19.0.0 to 19.1.1
- Update react-native-builder-bob to 0.40.15
- Update all @react-native packages to 0.82.1
- Update CLI packages to 20.0.0
- Fix Codegen component registration bug that caused nil object crash
- Update metro config to use latest builder-bob pattern
- Update react-native-safe-area-context to 5.5.2
- Update minimum Node version requirement to >=20
- Add comprehensive upgrade documentation

Fixes critical crash: 'attempt to insert nil object from objects[19]'
The RN 0.82.1 Codegen properly generates component registrations without
malformed class names that were causing NSClassFromString to return nil.

* chore(example): upgrade dependencies to latest compatible versions

- Upgrade @react-navigation/native: 7.0.14 → 7.1.20
- Upgrade @react-navigation/native-stack: 7.2.0 → 7.6.3
- Upgrade react-native-gesture-handler: 2.24.0 → 2.29.1
- Upgrade react-native-maps: 1.20.1 → 1.26.18
- Upgrade react-native-reanimated: 3.17.1 → 4.1.5
- Upgrade react-native-screens: 4.9.1 → 4.18.0
- Add react-native-worklets: ^0.6.1 (required by Reanimated 4)
- Upgrade @babel/core: 7.25.2 → 7.28.5
- Upgrade @babel/preset-env: 7.25.3 → 7.28.5
- Upgrade @babel/runtime: 7.25.0 → 7.28.4
- Upgrade @types/jest: 29.5.13 → 30.0.0
- Upgrade @types/react: 19.1.1 → 19.2.5
- Upgrade eslint: 8.19.0 → 9.39.1
- Upgrade jest: 29.6.3 → 30.2.0
- Upgrade prettier: 2.8.8 → 3.6.2
- Upgrade typescript: 5.8.3 → 5.9.3

All dependencies are now using their latest stable versions compatible
with React Native 0.82.1 and the New Architecture.

* fix: fix build

* refactor(ios): improve Fabric component lifecycle and best practices

- Add finalizeUpdates for batched prop updates
- Move view registration to didMoveToWindow for better lifecycle timing
- Improve prepareForRecycle with clearer documentation
- Add updateLayoutMetrics implementation for layout change handling
- Follow React Native Fabric best practices for component lifecycle management

* fix(ios): add missing resizeToIndex implementation

- Implement resizeToIndex method that was declared but missing
- Fixes NSInvalidArgumentException crash when resizing sheets
- Uses animateChanges to smoothly transition between detent sizes

* fix(ios): fix initialIndex, touch events, and hot reload crashes

- Fix initialIndex not being respected by calling prepareForPresentationAtIndex before presenting
- Fix buttons not clickable by removing incorrect contentView pinning in viewControllerWillAppear
- Fix crash on hot reload by improving cleanup in invalidate method
- Dismiss sheet without animation during cleanup to prevent crashes
- Clear all references (controller delegate, views) in invalidate to prevent dangling pointers

* fix(ios): ensure touch events work by properly ordering container view

- Enable userInteractionEnabled on containerView and contentView explicitly
- Bring containerView to front after pinning to ensure it's above backgroundView
- This fixes buttons not being clickable in sheets

* fix(ios): add RCTSurfaceTouchHandler for proper touch event handling in Fabric

- Add RCTSurfaceTouchHandler to handle touch events on containerView
- This is required because containerView is not in React Native's managed view hierarchy
- Attach touch handler when mounting child view
- Detach touch handler in invalidate for proper cleanup
- Fixes buttons not being clickable in sheets

* fix(ios): implement blurTint to blurEffect conversion

- Add custom setBlurTint: setter to convert string to UIBlurEffect
- Support blur styles: dark, light, extraLight, regular, prominent
- Support iOS 13+ system materials (systemThinMaterial, etc.)
- Call setupBackground after setting blur effect to apply changes
- Fixes blur showing as semi-transparent white instead of proper blur

* chore(ios): remove version checks below iOS 15.1 minimum

- Remove API_AVAILABLE(ios(15.0)) annotations from method declarations
- Remove @available(iOS 15.0, *) runtime checks
- Remove __IPHONE_13_0 preprocessor conditionals
- Simplify window access code (iOS 15.1+ guaranteed)

* fix(ios): add nil check before detaching touch handler

Prevents crash during hot reload when containerView is nil

* chore: remove unnecessary docs

* fix(ios): properly manage touch handler lifecycle during hot reload

- Create touch handler lazily in mountChildComponentView
- Set touch handler to nil after detaching in invalidate
- Prevents 'already has attached view' crash on hot reload

* chore: remove lib for now

* chore: update .gitignore

* fix(ios): improve hot reload and full reload handling

- Keep sheet presented during hot refresh (Fast Refresh)
- Dismiss and re-present sheet during full reload (Cmd+R)
- Properly manage touch handler lifecycle during unmount/remount
- Skip sheets being dismissed when finding presenting controller
- Allow sheets to stack on top of each other
- Add animated dismissal during reload for better UX

* chore(ios): remove unused code and debug logs

Remove _presentRetryCount variable and all NSLog debug statements

* fix(ios): apply prop updates to presented sheet

Call setupSizes and setupDimmedBackground when sizes, dimmed,
or dimmedIndex props change on a presented sheet

* fix(ios): fix background and blur props updates

- Apply initial backgroundColor and blurTint props before presentation
- Update background/blur immediately when props change on presented sheet
- Clear blur effect properly when blurTint is removed/disabled
- Remove redundant setupBackground call from viewDidLoad

* refactor(ios): use native height measurement for auto sizing

- Measure content and footer heights natively using systemLayoutSizeFittingSize
- Remove JS-based onLayout height measurement
- Remove bottomInset calculations (UISheetPresentationController handles safe area)
- Simplify auto size calculation to use measured heights directly

* chore: tidy

* feat(fabric): implement custom Fabric container component for dynamic sizing

- Create TrueSheetContainerViewComponentView as custom Fabric component
- Add updateSize: method to control container layout from parent
- Register both TrueSheetView and TrueSheetContainerView in codegenConfig
- Update TrueSheet.tsx to use custom container instead of plain View
- Enable native-driven container width updates on sheet size changes

* refactor: remove unused container size change event and state

- Remove onContainerSizeChange event handler and binding
- Remove ContainerSizeChangeEvent type
- Remove containerWidth and containerHeight from state
- Remove ContainerSize interface from native component
- Clean up unused onContainerSizeChange prop from TrueSheetViewNativeComponent

* refactor(ios): improve container view type safety and documentation

- Change _containerView type from UIView* to TrueSheetContainerViewComponentView*
- Remove unnecessary cast in viewControllerDidChangeWidth
- Update log messages to clarify it's the container component
- Add detailed comments explaining container structure and initialization flow
- Improve comments in mountChildComponentView and layoutSubviews methods
- Add comments explaining the two-child structure (content + footer)

* fix(ios): manually update content view width on sheet size change

- Set content view frame width to match container width on initialization
- Update content view width directly in viewControllerDidChangeWidth
- Force layout on content view after width change
- Remove unused updateSize implementation in container component

* refactor: simplify API and remove footer-related code

- Remove footer view tracking and constraints
- Remove content view wrapper layer
- Direct children rendering in container
- Clean up unused imports (TrueSheetFooter, TrueSheetGrabber)
- Remove unused props (FooterComponent, contentContainerStyle, grabberProps)
- Remove setupFooterConstraints method
- Simplify layoutSubviews to only configure container
- Update viewControllerDidChangeWidth comment

* refactor: remove contentContainerStyle prop

- Remove contentContainerStyle from TrueSheetProps
- Update all example files to use style prop instead
- Simplify API by having single style prop for container

* refactor: remove keyboard handling for footer

- Remove _footerBottomConstraint references in keyboard methods
- Keyboard handling no longer needed since footer was removed

* fix: remove style prop to prevent Auto Layout conflicts

- Remove style prop from TrueSheetContainerViewNativeComponent
- Omit style from TrueSheetProps interface
- Container size is controlled by Auto Layout constraints
- Passing style prop was overriding constraints and preventing width extension

* fix(ios): restore width tracking for proper auto sizing and rotation support

- Re-add lastViewWidth tracking in TrueSheetViewController
- Implement viewDidLayoutSubviews to detect width changes (device rotation)
- Measure container content height in layoutSubviews for auto sizing
- Auto-update sheet detents when content height changes
- Remove unused viewControllerDidChangeWidth delegate method
- Clean up unused imports in TrueSheet.types.ts

* fix(example): resolve TypeScript errors in example app

- Add type assertions for scrollRef props to handle Component type mismatch
- Fix DependencyList type in useDragChangeHandler hook
- Exclude docs directory from TypeScript checking to avoid type conflicts

* refactor: use findNodeHandle instead of storing refs directly

- Replace refs map with handles map to store node handles
- Use findNodeHandle to get native view tags instead of accessing _nativeTag
- Align implementation with main branch pattern
- More reliable and follows React Native best practices

* refactor: remove Commands API and use TurboModule exclusively

- Remove Commands import and validation
- Use TurboModule.presentByRef/dismissByRef for instance methods
- Pass this.handle directly to TurboModule methods
- Simplify architecture by using single communication path
- Improve error handling with proper LINKING_ERROR

* refactor: remove Commands API completely from codebase

- Remove Commands import and interface from TrueSheetViewNativeComponent.ts
- Remove codegenNativeCommands import
- Remove Commands category and handleCommand implementation from native
- Remove present/dismiss Commands methods from TrueSheetViewComponentView
- Remove RCTTrueSheetViewViewProtocol conformance
- Update pragma marks to reflect TurboModule-only architecture
- Fix findNodeHandle type cast for scrollRef

* fix: correct scrollRef type to Component<unknown>

- Add Component import to TrueSheet.types.ts
- Change scrollRef type from RefObject<unknown> to RefObject<Component<unknown>>
- Remove 'as any' cast from findNodeHandle call in TrueSheet.tsx
- Align with main branch implementation
- Resolves TypeScript error without using any type

* fix(example): correct scrollRef types in sheet components

- Change FlatListSheet scrollRef from useRef<FlatList<number>> to useRef<Component<unknown>>
- Change ScrollViewSheet scrollRef from useRef<ScrollView> to useRef<Component<unknown>>
- Add Component import to both files
- Resolves type mismatch with TrueSheet scrollRef prop

* refactor: make scrollRef accept any component type

- Change scrollRef type to RefObject<unknown> for flexibility
- Update TrueSheet to cast scrollRef.current to never for findNodeHandle
- Revert example sheets to use proper component types (ScrollView, FlatList)
- Remove unused Component import from TrueSheet.types.ts
- Allow users to use specific ref types while maintaining type safety

* fix: update @types/react to 19.1.1 and exclude docs from TypeScript

- Set @types/react to 19.1.1 to match React 19.1.1 version
- Update resolutions to force version 19.1.1 across all packages
- Update Docusaurus to 3.9.2 and React to 19.1.1 in docs
- Exclude docs directory from root TypeScript compilation to prevent type conflicts

* feat(ios): add native footer component support

- Create TrueSheetFooterView Fabric component specification
- Implement iOS TrueSheetFooterViewComponentView
- Update TrueSheetViewComponentView to handle both container and footer child views
- Add FooterComponent rendering in TrueSheet.tsx
- Footer is positioned at the bottom and rendered above container content

* feat(ios): pin footer view to bottom using Auto Layout constraints

- Add footer bottom constraint to pin footer at bottom of sheet
- Setup footer constraints when mounting footer component
- Remove footer constraints when unmounting
- Footer is now properly pinned to the bottom edge of the sheet

* fix(ios): add height constraint to footer view

- Add footer height constraint to ensure footer is visible
- Measure footer height using systemLayoutSizeFittingSize
- Update footer height constraint in layoutSubviews when size changes
- Clear height constraint on unmount

* fix(ios): bring footer view to front above container

- Ensure footer is rendered above container content in layoutSubviews
- Footer will now be visible on top of scrollable content

* refactor(ios): simplify native component class names

- Rename TrueSheetViewComponentView to TrueSheetView
- Rename TrueSheetContainerViewComponentView to TrueSheetContainerView
- Rename TrueSheetFooterViewComponentView to TrueSheetFooterView
- Update all imports and references across iOS files
- Update codegenConfig in package.json with new class names

* chore: rename pod to RNTrueSheet and bump version to 3.0.0

* feat: change size API from percentage strings to fractional numbers (0-1)

BREAKING CHANGE: Size API now uses fractional numbers (0-1) instead of
percentage strings and preset sizes.

Removed:
- 'small', 'medium', 'large' size presets
- percentage string format (e.g., '60%', '80%')

Changed:
- number values now represent fractions (0-1) of available height

Kept:
- 'auto' for content-based sizing

Migration:
'small' → 0.25, 'medium' → 0.5, 'large' → 1, '60%' → 0.6, '80%' → 0.8

* fix: parse numeric string values for sizes on both platforms

When sizes are passed from JS as numbers (e.g., 0.5), they are converted
to strings by sizes.map(String). Both iOS and Android now correctly parse
these numeric strings and treat them as fractions (0-1) of available height.

* refactor: rename 'size' terminology to 'detent' across entire codebase

- Rename sizes prop to detents
- Rename SheetSize type to SheetDetent
- Rename SizeInfo to DetentInfo
- Rename SizeChangeEvent to DetentChangeEvent
- Update all method names from *Size* to *Detent*
- Update native implementations on both iOS and Android
- Update all example components to use new API

* docs: update terminology from 'size' to 'detent' across all documentation

- Update all props references (sizes -> detents, SheetSize -> SheetDetent)
- Update all event references (SizeInfo -> DetentInfo, SizeChangeEvent -> DetentChangeEvent)
- Update usage examples with new fractional number API (0-1)
- Update guides: resizing, reanimated, dimming, onmount
- Update reference docs: props, methods, types

* docs: replace fixed height detent values with fractions

- Update props documentation to use 0.5, 0.8 instead of 69, 247
- Update usage examples to use fractional values (0-1)
- Remove legacy string values like '80%' and 'large'
- Update event value examples to reflect realistic pixel values

* fix(ios): fix setupDetents

* refactor(ios): encapsulate view lifecycle and touch handling in native components

- Create TrueSheetLayoutUtils utility for reusable layout constraint helpers
- Move pinning logic to TrueSheetContainerView and TrueSheetFooterView
- Move touch handler management to container and footer (each manages own RCTSurfaceTouchHandler)
- Automatically detect and pin scroll views in container lifecycle (didMoveToWindow)
- Simplify layoutSubviews and move view ordering to mountChildComponentView
- Consolidate footer pinning with height parameter in pinView method
- Remove scrollRef prop dependency - scroll views are auto-detected recursively
- Clean up TrueSheetView.mm by delegating all setup/cleanup to child views

* feat(example): make Footer text pressable

* refactor: remove scrollRef prop - scroll views are now auto-detected

* refactor: use willMoveToWindow lifecycle method for automatic cleanup

* Revert "refactor: use willMoveToWindow lifecycle method for automatic cleanup"

This reverts commit a94eef9953aee20d44370db12f44de4481edaf27.

* fix: pin contentView to container before pinning scrollView

* fix: only pin scrollView, not contentView

* refactor: improve scrollView pinning - pin directly to controller view

* style: convert iOS code indentation from 4 spaces to 2 spaces

chore: remove unused methods and outdated comments

refactor: rename TrueSheetViewController.m to .mm

* refactor: remove keyboard listeners

* refactor: remove keyboard listeners

* refactor: replace swiftlint with clang-format for Objective-C

* chore: remove unused files

* feat: add dynamic content adjustment buttons to BasicSheet

* fix: auto detent now adjusts when content height changes dynamically

* Revert "fix: auto detent now adjusts when content height changes dynamically"

* fix: auto detent now responds to dynamic content height changes

* refactor: use updateLayoutMetrics instead of layoutSubviews for size changes

* refactor: rename sizeDelegate to delegate and remove unused _layoutMetrics

* fix: animate detent changes and remove redundant resize calls

* refactor: rename vars

* refactor: rename FooterComponent prop to footer

* refactor: remove unused TrueSheetFooter component

* refactor: expose controller and refactor child view setup

- Add readonly controller property to TrueSheetView for public access
- Replace setupInParentView with setupInSheetView method that accepts TrueSheetView
- Move setupInParentView logic into setupInSheetView for better encapsulation
- Set and clear sheet view reference in child view lifecycle methods
- Move TrueSheetFooterView constraint logic to updateLayoutMetrics lifecycle
- Extract constraint setup into internal setupConstraintsWithHeight method
- Use frame height for initial constraints, update reactively in layoutMetrics
- Remove unnecessary _isSetup flag, use _sheetView check instead

* refactor: convert all inline styles to StyleSheet.create

- Convert inline ViewStyle, TextStyle objects to StyleSheet.create
- Update all components in example/src/components to use StyleSheet
- Update all screens in example/src/screens to use StyleSheet
- Convert  constant to styles.whiteText in utils/constants
- Update src/TrueSheet.tsx and src/TrueSheetGrabber.tsx to use StyleSheet
- Remove unused type imports (ViewStyle, TextStyle, ImageStyle)
- Improve performance by using optimized StyleSheet API

* refactor: make TrueSheetViewController variables private

* refactor: simplify TurboModule calls

* refactor: defer native view rendering until sheet presentation

* refactor: split onPresent into onWillPresent and onDidPresent events

- Add onWillPresent event that fires in viewWillAppear without DetentInfo
- Rename onPresent to onDidPresent for consistency and clarity
- onDidPresent continues to provide DetentInfo (index and value)
- Update iOS implementation to emit onWillPresent in viewControllerWillAppear
- Update Android implementation to emit onWillPresent before showing sheet
- Update all examples and documentation
- Regenerate Fabric codegen for both platforms

BREAKING CHANGE: onPresent prop has been replaced with onWillPresent and onDidPresent

* refactor: store TrueSheet instances instead of handles for static methods

- Changed static map from storing handles to storing instances
- Renamed getHandle() to getInstance()
- Static methods (present, dismiss, resize) now delegate to instance methods
- Register instances regardless of render state to enable static control
- Added unregisterInstance() and componentWillUnmount() for cleanup
- Improves encapsulation and allows instances to control their own lifecycle

* fix(ios): restore bottomInset calculation for proper auto detent sizing

- Restore bottomInset subtraction from content height to prevent excessive bottom spacing
- Get bottomInset dynamically from window's safeAreaInsets when setting up detents
- This accounts for device safe areas (home indicator) properly
- Fixes issue where iOS was automatically adding too much bottom padding

Example changes:
- Add bottom padding (FOOTER_HEIGHT + SPACING) to sheets with footers
- This manual padding works correctly now that auto-insets are fixed
- Applied to BasicSheet, GestureSheet, PromptSheet, FlatListSheet, ScrollViewSheet, MapScreen

* refactor(ios): organize utilities and modernize window handling

- Create ios/utils folder for utility classes
- Move and rename TrueSheetLayoutUtils to utils/LayoutUtil
- Create utils/WindowUtil with modern UIWindowScene API (iOS 15.1+)
- Update TrueSheetView and TrueSheetViewController to use WindowUtil
- Remove all deprecated window retrieval code
- Centralize window logic in a single reusable utility

* feat(ios): use system default corner radius when prop is not provided

- Change cornerRadius default from 0 to -1 to distinguish between undefined and explicit 0
- Update iOS implementation to handle three cases:
  - undefined (not provided): uses system default corner radius via nil
  - 0: sharp corners (no rounding)
  - >0: custom corner radius value
- Update documentation to clarify behavior
- Fixes child sheet corner radius issue in BasicSheet example

* refactor: add sheet presentedView getter

* feat(ios): add position property to DetentInfo events

- Add position (Y coordinate) to DetentInfo interface
- Update all event emissions to include sheet Y position:
  - onDidPresent
  - onDetentChange
  - onDragBegin
  - onDragChange
  - onDragEnd
- Add getYPosition method to TrueSheetViewController
- Update delegate protocol to pass position parameter
- Update example logs to demonstrate position usage

* docs: update DetentInfo documentation with position property

- Add position property to DetentInfo type documentation
- Update event examples to show position in output
- Add tip about position property for animations in reanimated guide
- Note that position is iOS-only feature

* feat(android): add position property to DetentInfo events

- Add position parameter to DetentInfo data class
- Update detentInfoData to include position in event payload
- Implement getDetentInfoForIndexWithPosition method to calculate Y position
- Update getCurrentDetentInfo to include sheet Y position
- Update all DetentInfo instantiations with position parameter
- Update documentation to remove iOS-only note

Events now include Y position on both iOS and Android:
- onDidPresent
- onDetentChange
- onDragBegin
- onDragChange
- onDragEnd

* fix: fix pan gesture recognizer

* chore: update example styles

* refactor: remove redundant code

* feat: add onPositionChange event for continuous position tracking

Add new onPositionChange event that fires continuously during sheet
drag operations, providing real-time position updates with DetentInfo
payload (index, value, position).

Changes:
- Add PositionChangeEvent type and onPositionChange prop to TrueSheetProps
- Implement iOS support via viewControllerDidChangePosition delegate method
- Implement Android support via POSITION_CHANGE event in onSlide callback
- Add comprehensive documentation in props.mdx
- Add usePositionChangeHandler hook for Reanimated integration
- Update example code to demonstrate usage
- Clean up import ordering and formatting in iOS files

Platform support:
- iOS 15+
- Android

The event provides smooth, continuous position tracking ideal for
animations and visual feedback during drag operations.

* refactor(ios): move event emission to dedicated events folder

Dramatically reduce TrueSheetView.mm complexity by moving all event
emission logic to a dedicated events folder with clean static methods.

Changes:
- Create ios/events/ folder structure
- Add TrueSheetEvents.h/mm with static emission methods:
  - emitOnMount, emitOnWillPresent, emitOnDidPresent
  - emitOnDismiss, emitOnDetentChange
  - emitOnDragBegin, emitOnDragChange, emitOnDragEnd
  - emitOnPositionChange
- Refactor TrueSheetView.mm to use one-line event calls

Impact:
- TrueSheetView.mm: -101 lines (13 additions, 101 deletions)
- New event files: +214 lines (organized in events/ folder)

Before (per event):
  if (!_eventEmitter) return;
  auto emitter = std::static_pointer_cast<...>(_eventEmitter);
  TrueSheetViewEventEmitter::OnDragBegin event;
  event.index = static_cast<int>(index);
  event.value = static_cast<double>(height);
  event.position = static_cast<double>(position);
  emitter->onDragBegin(event);

After (per event):
  [TrueSheetEvents emitOnDragBegin:_eventEmitter index:index value:height position:position];

Benefits:
- Each event emission reduced from ~8-15 lines to 1 line
- Centralized event logic in dedicated folder
- Easier to maintain and test event emissions
- Clear separation of concerns
- Dramatically improved readability of TrueSheetView.mm

* refactor(ios): split events into individual files for better modularity

Split TrueSheetEvents into 9 separate event files, each with its own
.h and .mm file, for better organization and maintainability.

Changes:
- Delete TrueSheetEvents.h/mm (monolithic approach)
- Create individual event files (18 files total):
  - OnMountEvent
  - OnWillPresentEvent
  - OnDidPresentEvent
  - OnDismissEvent
  - OnDetentChangeEvent
  - OnDragBeginEvent
  - OnDragChangeEvent
  - OnDragEndEvent
  - OnPositionChangeEvent
- Update TrueSheetView.mm to import individual event files
- Use consistent API: [EventName emit:...] for all events

Structure:
ios/events/
├── OnMountEvent.h/mm               (simple event)
├── OnWillPresentEvent.h/mm         (simple event)
├── OnDidPresentEvent.h/mm          (DetentInfo event)
├── OnDismissEvent.h/mm             (simple event)
├── OnDetentChangeEvent.h/mm        (DetentInfo event)
├── OnDragBeginEvent.h/mm           (DetentInfo event)
├── OnDragChangeEvent.h/mm          (DetentInfo event)
├── OnDragEndEvent.h/mm             (DetentInfo event)
└── OnPositionChangeEvent.h/mm      (DetentInfo event)

Benefits:
- Each event is self-contained and independently testable
- Easier to locate specific event logic
- Better separation of concerns
- Consistent naming convention
- Scalable for future event additions
- Reduced file size (avg ~25-30 lines per file)
- Cleaner imports in TrueSheetView.mm

Net change: +531 lines, -227 lines (better organized across 18 files)

* feat: improve position change tracking and sheet animation behavior

- Move position change tracking from drag gesture to viewDidLayoutSubviews
  * Provides continuous position updates during all sheet movements
  * More reliable than gesture-only tracking
  * Eliminates duplicate logging

- Enhance sheet resize behavior with proper animations
  * Wrap resize operations in animateChanges block
  * Apply to resize(), updateProps, and updateContentSize flows
  * Ensures smooth transitions when updating detents dynamically

- Fix sheet presentation controller methods
  * Remove redundant animateChanges wrappers in setupDetents
  * Clean up resizeToIndex to only set detent identifier
  * Simplify identifierFromString helper (no longer needed)

- Improve example app demo
  * Add dynamic content management to MapScreen
  * Fix BasicSheet content counter starting at 0
  * Update button positions and spacing
  * Change auto detent demo and remove unused animation code

This commit refines the position tracking system to be more robust
and ensures all sheet size changes are properly animated.

* feat: add transition position tracking

* feat: enhance presentation lifecycle events and cleanup view management

- Add indexPath to onWillPresent event for better context
- Improve view controller lifecycle management
- Update MapScreen example with presentation handlers
- Update documentation for presentation events
- Refactor iOS view management and cleanup

* refactor: rename TrueSheetContainerView to TrueSheetContentView

- Renamed iOS native component files and classes
- Updated all references in TrueSheetView.mm
- Renamed TypeScript native component file and import
- Updated package.json codegenConfig
- Updated component usage in TrueSheet.tsx

* refactor(ios): restructure sheet architecture with container pattern

Complete architectural refactoring of TrueSheet native components:

Container Pattern Implementation:
- Created TrueSheetContainerView to wrap content and footer
- Container owns TrueSheetViewController lifecycle
- TrueSheetView delegates presentation to container
- Props stay on host view, container accesses via parent

Component Structure:
- TrueSheetView: Host component, props ownership, event emission
- TrueSheetContainerView: Controller management, presentation logic
- TrueSheetContentView: Size reporting, scroll view handling
- TrueSheetFooterView: Bottom positioning with constraints

View Hierarchy:
TrueSheetView
  └── TrueSheetContainerView (owns controller)
      ├── TrueSheetContentView
      └── TrueSheetFooterView

Key Improvements:
- Clean separation of concerns
- Better encapsulation and maintainability
- Respects React Native view lifecycle
- Fixed constraint issues (footer/scroll pin to container)
- Fixed content height initialization timing
- Fixed initial presentation deferral

Breaking changes: None (internal refactoring only)

* refactor: emit onMount when container is mounted

- onMount event still on TrueSheetView (host component)
- Event now emitted when container is mounted, not didMoveToWindow
- Better timing: onMount fires when sheet is actually ready with container
- More accurate representation of when sheet is usable

* refactor: optimize container rendering and initial presentation

Container Rendering:
- Host view always rendered for ref stability
- Container conditionally rendered based on shouldRenderNativeView
- Better for Reanimated and prevents ref becoming null
- onMount event emitted when container actually mounts

Initial Presentation:
- Trigger in updateProps when initialIndex >= 0 and container ready
- Props synced after container mounts in Fabric lifecycle
- One-time execution with _hasHandledInitialPresentation flag
- Removed unnecessary window check (guaranteed in mount lifecycle)

Fixes:
- Sheet with initialIndex now presents correctly on mount
- Proper timing with Fabric prop synchronization
- No multiple presentation attempts

* feat: add first-class Reanimated v4 support

- Add ReanimatedTrueSheetProvider context to manage shared position value
- Add ReanimatedTrueSheet component with automatic position synchronization
- Add useReanimatedTrueSheet hook to access sheet position from any component
- Add usePositionChangeHandler hook using Reanimated's useEvent and useHandler
- Export all Reanimated components and hooks from main package
- Add react-native-reanimated v4 as optional peer dependency and devDependency
- Add react-native-worklets as optional peer dependency (required by Reanimated v4)
- Update MapScreen example to use ReanimatedTrueSheet
- Add ReanimatedExample component demonstrating multiple animated elements
- Update reanimated guide with first-class support documentation

* refactor: simplify DetentInfo to only include index and position

- Remove 'value' field from DetentInfo interface
- Users can access detent value via detents[index]
- Update all TypeScript, Android, and iOS implementations
- Refactor iOS to derive index from sheet.detents array instead of storing mapping
- Update all documentation and examples
- Update ReanimatedTrueSheet to use position correctly

* refactor: simplify DetentInfo and improve validation

DetentInfo changes:
- Remove 'value' field, keep only 'index' and 'position'
- Users access detent value via detents[index]
- Update all TypeScript, Android, and iOS implementations
- Fix ReanimatedTrueSheet to use position correctly

Native detents:
- Change from string array to number array
- Use -1 to represent 'auto' detent
- Remove all string parsing from native layers
- iOS: Remove _detentValues storage, derive index from sheet.detents
- iOS: Merge detentForFraction into detentForValue
- Android: Simplify getDetentHeight to handle numbers only

Validation:
- Validate only in JavaScript layer
- Warn and auto-fix: detents > 3 (trim), invalid fractions (clamp)
- Throw errors for: initialIndex out of bounds, present() out of bounds
- iOS: Use RCTLogError instead of throwing exceptions
- Handle zero detent explicitly, default to 0.1

This provides cleaner API, better DX, and simpler native code.

* fix: update TrueSheetView.h method signatures to match implementation

- Remove value parameter from notifyDidChangeDetent
- Remove height parameter from notifyDidDrag and notifyDidChangePosition
- Match header declarations with actual implementation

* refactor: simplify DetentInfo and improve Reanimated integration

Major Changes:
- Simplified DetentInfo interface: removed 'value' field, now using { index, position }
- Clients access detent values directly via detents[index]
- Standardized -1 to represent 'auto' detent across platforms
- Removed native string parsing logic for detents

Improvements:
- Added useWillPresentHandler hook for better lifecycle management
- Enhanced error handling with warning-based validation approach
- Auto-fixing for common mistakes (exceeding 3 detents, invalid fractions)
- Improved type safety and input validation
- Reduced code complexity and memory overhead

Platform-specific:
- Refactored detent handling in TrueSheetViewController
- Added React Native-compatible error logging
- Simplified detent height calculations
- Removed unnecessary string conversions

This refactoring creates a cleaner, more consistent API across platforms
while maintaining a developer-friendly experience with informative warnings.

* feat: add onWillDismiss event and rename onDismiss to onDidDismiss

BREAKING CHANGE: onDismiss renamed to onDidDismiss

Added onWillDismiss event for pre-dismissal handling
Renamed onDismiss to onDidDismiss for consistency with
onWillPresent/onDidPresent naming pattern

Platform changes:
- Android: use setOnCancelListener for onWillDismiss
- iOS: OnWillDismissEvent and OnDidDismissEvent classes

Consistent lifecycle API:
- onWillPresent / onDidPresent
- onWillDismiss / onDidDismiss

Migration:
Replace onDismiss with onDidDismiss
Add onWillDismiss for pre-dismissal logic

* feat: add iOS dismiss animation workaround to ReanimatedTrueSheet

Add onWillDismiss handler to ReanimatedTrueSheet that animates
position to 0 on iOS when sheet is dismissing, matching the
onWillPresent workaround pattern.

Changes:
- Created useWillDismissHandler hook for Reanimated events
- Added WillDismissEvent and DidDismissEvent types
- Implemented willDismissHandler in ReanimatedTrueSheet
- Animates position.value to 0 with spring config on iOS

This ensures smooth position tracking during sheet dismissal
on iOS where native animation tracking is not supported.

* fix: correct types for dismiss event handlers

Fixed TypeScript types for onWillDismiss and onDidDismiss to ensure
proper type safety and compatibility with Reanimated handlers.

Changes:
- Updated event handler type signatures
- Fixed useWillDismissHandler types
- Corrected fabric component event types
- Ensured consistency across all dismiss handlers

* fix: correct iOS method signatures and typo

- Add transitioning parameter to notifyDidChangePosition in header
- Fix typo: transioning -> transitioning in OnPositionChangeEvent

* fix: correct event types for codegen compatibility

- Replace empty object types ({}) with null for events without data
- Convert PositionChangeEventPayload from intersection type to interface
- Codegen doesn't support intersection types or empty object types

This fixes the codegen error: 'typeAnnotation of event doesn't have a name'

* refactor: rename DetentInfo to DetentInfoEventPayload

BREAKING CHANGE: DetentInfo type renamed to DetentInfoEventPayload

Renamed DetentInfo to DetentInfoEventPayload for better clarity
that this type is specifically for event payloads.

Changes:
- Renamed DetentInfo -> DetentInfoEventPayload across all types
- Updated PositionChangeEventPayload to extend DetentInfoEventPayload
- Added transitioning property documentation
- Updated all documentation references

Updated documentation:
- Added PositionChangeEventPayload type documentation
- Documented transitioning flag behavior
- Explained iOS animation requirements when transitioning is true
- Updated all event prop references

Migration:
import type { DetentInfo } from '@lodev09/react-native-true-sheet'
// becomes
import type { DetentInfoEventPayload } from '@lodev09/react-native-true-sheet'

* refactor: simplify podspec for new architecture only

Removed install_modules_dependencies which handles bridge mode
and explicitly require new architecture:

Changes:
- Added RCT_NEW_ARCH_ENABLED=1 to pod_target_xcconfig
- Set compiler flags for new architecture
- Removed bridging/interop layer support
- Kept only necessary Fabric dependencies

Since v3.0+ is new architecture only, the podspec no longer
needs to support both architectures.

* chore: modernize library setup with ESLint v9 and updated configs

- Migrate to ESLint v9 flat config (eslint.config.mjs)
- Update dependencies to latest versions
  - ESLint v9.35.0 with new @eslint/* packages
  - commitlint v19.8.1
  - release-it v19.0.4
  - turbo v2.5.6
  - typescript v5.9.2
- Update tsconfig.json with modern options
- Update turbo.json to new 'tasks' format with global dependencies
- Update Node.js version to v22.20.0
- Simplify example app configuration
  - Use react-native-monorepo-config for metro
  - Remove duplicate dev dependencies
  - Simplify build scripts
- Update babel.config.js with node_modules overrides
- Update lefthook.yml with cleaner glob patterns
- Remove old eslintConfig from package.json

* chore: bring back semicolons

* chore: remove debug from example

* refactor(ios): improve position tracking and layout transition handling

- Rename delegate method 'containerViewDidChangeSize' to
'contentViewDidChangeSize' for clarity

- Add 'layoutTransitioning' flag to differentiate layout-driven
position changes from user interactions

- Improve 'viewDidLayoutSubviews' to handle width changes and
position tracking separately

- Ensure position notifications are emitted with correct
transitioning state during layout changes

- Add inline comments explaining layout transition flow and
position tracking behavior

* feat(ios): add presentation state tracking for overlay controllers

- Add helper methods to check if sheet is topmost controller
- Add 'isTopmostPresentedController' to detect overlays
- Add 'isActiveAndVisible' for comprehensive visibility check
- Update 'viewDidLayoutSubviews' to handle size changes when
  other controllers are presented on top
- Treat position changes as transitioning when overlays are present
- Prevents incorrect position notifications during overlay
  adjustments

* fix(ios): fix auto pin ScrollView

* fix(ios): fix background and blur tint rendering

- Call setupBackground after applying props to ensure background
  is rendered
- Move blur tint style conversion logic into setupBackground
- Remove blurEffect property, compute style directly when needed
- Remove setBlurTint setter, use direct property assignment
- Always set blurTint property to clear when removed from props
- Remove conditional check that skipped background when value is 0
- Consolidate all background rendering logic in setupBackground

Fixes transparent background issue and blur tint not clearing
when removed from props.

* chore: tidy

* refactor(android): apply Fabric best practices and fix JVM compatibility

- Remove unused CONTAINER_SIZE_CHANGE event constant
- Add comprehensive KDoc documentation to TurboModule methods
- Add @UiThread annotations for thread safety
- Fix JVM target compatibility (11 -> 17) for RN 0.82+
- Update build configuration documentation
- Ensure Java and Kotlin targets match to prevent build errors

* refactor(android): align build config with Fabric template best practices

- Remove conditional Fabric plugin (always apply for New Architecture)
- Add sourceSets to include codegen-generated files (generated/java, generated/jni)
- Update documentation to reflect Fabric-only architecture
- Matches official React Native Fabric component template structure

* fix(example): enable New Architecture in example app

- Set newArchEnabled=true in gradle.properties
- Matches Fabric template configuration
- Removes build warning about newArchEnabled=false not being supported in RN 0.82+
- Required for testing Fabric-only library components

* chore: run prettier

* refactor(android): modernize event system with decoupled event classes

- Create dedicated event classes for each event type in events/ package
- Replace deprecated MapBuilder with Kotlin native mutableMapOf/hashMapOf
- Remove monolithic TrueSheetEvent.kt in favor of type-safe event classes
- Update TrueSheetViewManager to use decoupled event registration
- Update TrueSheetView to dispatch typed events directly
- Each event class encapsulates its own payload creation logic
- Improves maintainability, type safety, and code organization

* fix(android): implement RootSheetView pattern matching React Native Modal

- Refactor TrueSheetView to use RootSheetView wrapper for proper React Native integration
- Forward all child view operations to RootSheetView following ReactModalHostView pattern
- Wrap RootSheetView in FrameLayout when setting as dialog content for system insets handling
- Fix measurement specs to use AT_MOST instead of UNSPECIFIED for Fabric compatibility
- Add validation for halfExpandedRatio to ensure valid range (0.01-1.0)
- Delegate eventDispatcher to rootSheetView.eventDispatcher for proper event flow
- Update all event dispatching to use rootSheetView.eventDispatcher

This matches React Native's Modal implementation exactly:
- TrueSheetView acts as ReactModalHostView (manages props/lifecycle)
- RootSheetView acts as DialogRootViewGroup (handles touch/events/rendering)
- Proper separation prevents 'view already has parent' errors
- RootView interface ensures correct touch event dispatching

* feat(android): add full Fabric state management to RootSheetView

- Add stateWrapper and updateState support matching DialogRootViewGroup
- Implement onInitializeAccessibilityNodeInfo for accessibility test IDs
- Add requestDisallowInterceptTouchEvent override for touch events
- Fix onChildStartedNativeGesture to pass reactContext parameter
- Forward stateWrapper from TrueSheetView to RootSheetView
- Add addEventEmitters to TrueSheetViewManager for event dispatcher
- Add updateState method to TrueSheetViewManager for Fabric updates

Makes RootSheetView 100% compliant with React Native's DialogRootViewGroup,
ensuring proper Fabric support for state updates, accessibility, and touch.

* refactor(android): remove legacy architecture support from RootSheetView

- Remove UIManagerModule and GuardedRunnable imports (legacy only)
- Simplify updateState to only handle Fabric architecture
- Remove ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE check
- Update documentation to reflect Fabric-only implementation

TrueSheet is Fabric-only, so we don't need backward compatibility code.

* refactor(android): reorganize utilities and rename RootSheetView

- Move utilities from core/ to new utils/ package
- Decouple monolithic Utils.kt into focused utility classes:
  * PixelUtils.kt - DIP/pixel conversion utilities
  * ScreenUtils.kt - Screen dimension calculations
  * KeyboardManager.kt - Keyboard visibility detection (moved from core/)
- Rename RootSheetView -> TrueSheetRootView for naming consistency
- Move TrueSheetRootView from core/ to main package
- Remove unused withPromise() utility method
- Delete empty core/ directory
- Update all imports across codebase

Benefits:
- Better code organization with single-responsibility utilities
- Improved discoverability and maintainability
- Consistent naming conventions
- Cleaner package structure

* chore: update GitHub Actions workflows and issue templates

- Add concurrency control to CI workflow
- Upgrade actions/checkout to v5.0.0 with pinned commit hashes
- Pin all GitHub Actions versions with commit hashes for security
- Update iOS build with Xcode 16.3 and bundle-based CocoaPods
- Add shell rendering to bug report environment info
- Remove trailing whitespace from issue templates

* fix(android): import missing UiThread

* fix(android): implement proper Fabric view hierarchy for TrueSheet

Major changes to align TrueSheetRootView with React Native Modal's DialogRootViewGroup:

View Hierarchy & Layout:
- Forward view ID from TrueSheetView to TrueSheetRootView for proper event routing
- Set needsCustomLayoutForChildren=false to let Fabric handle child layout
- Remove manual onMeasure/onLayout overrides (not needed with Fabric)
- TrueSheetView.onLayout/onMeasure do nothing (rootSheetView is in dialog, not a child)
- Wrap rootSheetView in FrameLayout with MATCH_PARENT layout params

Touch Event Handling:
- Implement full touch event handling with JSTouchDispatcher + JSPointerDispatcher
- Enable ReactFeatureFlags.dispatchPointerEvents in example app
- Add @OptIn(UnstableReactNativeAPI::class) for experimental touch APIs
- Match DialogRootViewGroup's touch event implementation exactly

State Management:
- Simple stateWrapper property (no custom setter needed)
- onSizeChanged calls updateState when stateWrapper is available
- updateState sends screen dimensions to Fabric for layout calculation

This fixes two critical issues:
1. Touch events now work correctly (no crashes, proper event dispatch)
2. Content renders properly (Fabric layout system works correctly)

The implementation now matches React Native Modal exactly, ensuring
proper Fabric integration and reliable behavior.

* refactor(android): remove needsCustomLayoutForChildren overrides

- Removed needsCustomLayoutForChildren() from all ViewManagers
  (TrueSheetViewManager, TrueSheetContainerViewManager,
  TrueSheetContentViewManager, TrueSheetFooterViewManager)
- All overrides were returning false (the default value),
  making them unnecessary
- Allows Fabric's layout engine to properly handle view
  sizing and constraints
- Fixes container not properly covering the whole sheet area
- Simplifies code by relying on framework defaults

* refactor(android): replace measuredHeight with height properties

- Replaced measuredHeight with height in TrueSheetContainerView
- Converted getContentHeight() and getFooterHeight() methods
  to properties (contentHeight, footerHeight)
- Auto-sizing detents now use only content height
  (footer is positioned absolutely)
- Updated all call sites to use property syntax
- More idiomatic Kotlin with computed properties

* refactor: rename initialIndex to initialDetentIndex

- Renamed initialIndex prop to initialDetentIndex for clarity
- Updated Android implementation (TrueSheetView, ViewManager)
- Updated iOS implementation (TrueSheetView.mm)
- Updated TypeScript types and components
- Updated all example usages
- More descriptive prop name that clearly indicates
  it's a detent index

* refactor: rename initialIndexAnimated to initialDetentAnimated

- Renamed initialIndexAnimated to initialDetentAnimated
- Updated Android implementation (TrueSheetView, ViewManager)
- Updated iOS implementation (TrueSheetView.mm)
- Updated TypeScript types and components
- Updated example comment
- Consistent naming with initialDetentIndex

* refactor(android): move dialog logic to container view

Major architectural refactoring to match iOS pattern:

TrueSheetView (Host View):
- Now a simple host view similar to iOS implementation
- Only holds reference to container
- Forwards props and method calls to container
- Minimal logic, just manages view lifecycle

TrueSheetContainerView (Presentation Manager):
- Now manages TrueSheetDialog and TrueSheetRootView
- Handles all presentation logic and event dispatching
- Initializes dialog when mounted as child of host view
- Manages view hierarchy (children forwarded to root view)
- Contains all sheet behavior callbacks and state

TrueSheetDialog:
- Updated to accept container view reference
- Simplified containerView property access

Benefits:
- Matches iOS architecture pattern
- Dialog only initialized when rendered by Fabric
- Better separation of concerns
- Container is self-contained and reusable
- Host view is lightweight and simple

* fix(android): set rootSheetView ID for proper event dispatching

- Forward sheetView ID to rootSheetView during setup
- Ensures events are dispatched with correct view ID
- Required for Fabric event routing

* fix(android): correct view hierarchy - add container to rootSheetView

- Container is now added to rootSheetView (not its children)
- Content and footer remain as direct children of container
- Removed child forwarding overrides (addView, getChildAt, etc)
- Matches iOS pattern where container is added to dialog

Previous (incorrect):
  rootSheetView → content/footer (forwarded)

Current (correct):
  rootSheetView → container → content/footer

This fixes the issue where content was not showing in the sheet.

* fix(android): delay initial presentation until layout is ready

- Moved initial presentation from setupInSheetView to onAttachedToWindow
- Use post {} to ensure children are mounted and measured
- Fixes issue where sheet always presented at largest detent
- Now correctly presents at the specified initialDetentIndex

The issue was that present() was called immediately when
container was setup, before Fabric had a chance to add and
measure the content/footer children.

* fix(android): handle initial presentation like React Native Modal

- Added hasHandledInitialPresentation flag to track state
- Implemented showOrUpdate() method similar to ReactModalHostView
- Called from onAfterUpdateTransaction (after props are set)
- Use post {} to ensure children are mounted before presenting
- Removed onAttachedToWindow approach from container

This matches React Native Modal's pattern where showOrUpdate
is called after all properties are applied, ensuring the view
hierarchy is ready before presentation.

* refactor(android): use ReactViewGroup and ThemedReactContext

- Changed all views to extend ReactViewGroup instead of ViewGroup
- Accept ThemedReactContext directly as constructor parameter
- Removed reactContext getters (no more type casting)
- Removed unnecessary onLayout and onMeasure overrides
- Updated all view constructors:
  - TrueSheetView
  - TrueSheetContainerView
  - TrueSheetContentView
  - TrueSheetFooterView
  - TrueSheetRootView

Benefits:
- Cleaner code with direct context access
- Better type safety
- ReactViewGroup handles layout automatically
- Consistent pattern across all views

* fix(android): trigger initial presentation after container setup

- Call showOrUpdate() after container is added and setup
- Ensures containerView is not null when presenting
- Container must be ready before initial presentation
- Fixes initialDetentIndex not working

* fix(android): handle 'auto' detent and wait for layout before initial presentation

- Parse 'auto' string detent and convert to -1.0
- Handle ReadableType.String in setDetents()
- Wait for container layout using OnGlobalLayoutListener
- Only present when container exists and is laid out
- Fixes initialDetentIndex not working
- Fixes 'auto' detent being skipped (only 2 detents shown instead of 3)

* refactor(android): simplify initial presentation logic

- Remove OnGlobalLayoutListener complexity
- Use simple post {} block to present
- Auto detent parsing should handle timing correctly
- Cleaner and simpler approach

* fix(android): handle container re-add during Fabric updates

- Check if same container is being re-added (using ===)
- Skip setup if container already exists
- Only throw error for different container instances
- Fixes crash when resizing/updating sheet
- Error: 'Sheet can only have one container component'

* fix(android): properly cleanup container on unmount

- Add onDetachedFromWindow to TrueSheetContainerView
- Automatically cleanup when container is detached
- Ensure containerView reference is cleared in removeView
- Fixes error when container is re-added after unmount
- Properly handles React component updates/remounts

* fix(android): remove dismiss from cleanup to prevent crash

- Comment out sheetDialog.dismiss() in cleanup()
- Dismissing during unmount causes crash
- Keep null assignments to clear references
- TODO: Find proper way to handle dialog lifecycle

* refactor(android): move dialog lifecycle to TrueSheetView

- Move TrueSheetDialog and rootSheetView to TrueSheetView so they
  persist across container mount/unmount cycles
- TrueSheetContainerView is now lightweight and can freely
  unmount/remount without recreating dialog
- Move configureIfShowing to TrueSheetView where dialog lives
- Remove setTimeout workaround - dialog persistence makes it
  unnecessary
- Remove unused pendingDetentIndex variable
- Container naturally remounts when shouldRenderNativeView changes
- Dialog stays ready to present immediately after re-render

This fixes the need to click present twice and eliminates the
setTimeout hack by keeping the dialog alive across React renders.

* refactor(ios): move controller lifecycle to TrueSheetView

Move all controller-related logic to TrueSheetView:
- Controller creation and ownership
- Presentation logic (present, dismiss, resize)
- Props application to controller
- State management (isPresented, activeIndex)
- Direct delegation from controller to host view

Simplify TrueSheetContainerView to minimal content manager:
- Setup and cleanup
- Content/footer view management
- Content size change delegation

This aligns iOS architecture with Android where the dialog/controller
persists in the host view across container mount/unmount cycles.

Benefits:
- Single source of truth for controller and state
- Container can mount/unmount freely without affecting presentation
- Cleaner delegation: Controller → SheetView → JavaScript
- No unnecessary bridging or forwarding
- Simpler, more maintainable codebase

* fix(ios): move initial presentation to mountChildComponentView

Present sheet when container is mounted instead of in updateProps.
Use dispatch_async to ensure views are fully mounted before presenting.

This fixes the issue where the sheet wasn't showing because updateProps
was called before the container was mounted.

* refactor(ios): move props application directly into updateProps

Remove applyPropsToController method and put code directly in
updateProps for simpler, more direct code flow.

* refactor(ios): move initial presentation back to updateProps

Initial presentation logic belongs in updateProps where all props
are processed, not in mountChildComponentView.

* refactor(ios): complete Fabric architecture refactoring for iOS

- Moved view controller lifecycle management to TrueSheetView (host view)
- Made view controller persist across container mount/unmount cycles
- Simplified TrueSheetContainerView to be a lightweight content manager
- Implemented direct delegation from controller to host view for event dispatching
- Centralized prop application in updateProps method using @synthesize
- Removed unnecessary state tracking and complexity
- Improved view lifecycle management and performance
- Aligned iOS implementation with Android architecture patterns

This refactoring ensures:
 Consistent architecture across Android and iOS platforms
 Better alignment with Fabric renderer lifecycle
 Improved performance by avoiding unnecessary dialog/controller recreation
 Cleaner separation of concerns between host view and container view
 More reliable state management and event dispatching

* refactor(ios): implement container delegate pattern and optimize update lifecycle

- Add TrueSheetContainerViewDelegate protocol with contentDidChangeSize callback
- Implement contentHeight getter in container view for accessing content size
- Move controller setup logic to finalizeUpdates lifecycle method
- Remove container dependency on host view and controller
- Store initial detent settings for presentation in finalizeUpdates
- Optimize prop updates using updateMask to check for actual changes
- Remove unnecessary setupInSheetView method and _sheetView reference
- Move view hierarchy setup logic to host view's mountChildComponentView
- Clean up container view to be a pure messenger with delegate pattern

This refactoring improves:
 Better separation of concerns between container and host view
 Proper use of Fabric's finalizeUpdates lifecycle
 More predictable update timing and initialization
 Cleaner architecture with container as lightweight component
 Reduced coupling between components

* fix(ios): fix ScrollView auto pinning

* fix(ios): fix sheet props not updating during presentation

* fix(ios): resolve scroll view pinning timing and touch handler cleanup issues

- Use didMoveToSuperview lifecycle method for scroll view pinning
- Ensures container hierarchy is established before pinning attempts
- Remove mountChildComponentView override - no longer needed
- Set touch handler to nil after detaching to prevent double-detach crash
- Fix crash on second dismiss: 'RCTTouchHandler attached to another view'

This fixes:
 Scroll view pinning now works on initial sheet presentation
 Scrolling functionality works correctly after pinning
 No crash on multiple dismiss/present cycles
 Proper cleanup of touch handlers in content and footer views

* refactor(ios): simplify touch handler and lifecycle management

- Centralize touch handler management in host view
- Initialize touch handler once in initWithFrame
- Attach to container on mount, detach on unmount
- Remove touch handlers from content and footer views
- Move footer setup to didMoveToSuperview lifecycle
- Remove cleanup methods - views use prepareForRecycle
- Update comments to reflect Fabric architecture
- Remove backward compatibility code

This refactoring:
 Single touch handler managed at host view level
 Cleaner lifecycle - init once, attach/detach as needed
 Self-contained child views using didMoveToSuperview
 Consistent patterns across content and footer views
 No manual setup calls from parent views
 Proper Fabric view recycling support

* fix: clamp auto detent to container height to prevent unbounded growth

Prevents sheets with ScrollViews from growing indefinitely by clamping
content height to container height when content size changes.

* test: add comprehensive Jest testing support

- Add Jest setup with native module mocks
- Add testing dependencies (@react-native/babel-preset,
  @testing-library/react-native, react-test-renderer)
- Create TrueSheet.test.tsx with 8 passing tests
- Update __mocks__/index.js with complete API coverage
- Add mock documentation and examples
- Update Jest testing guide in docs
- Add testing section to README
- Exclude coverage folder from git

* refactor: move Reanimated components to dedicated folder

- Move ReanimatedTrueSheet to src/reanimated/
- Move ReanimatedTrueSheetProvider to src/reanimated/
- Move and rename usePositionChangeHandler to
  useReanimatedPositionChangeHandler in src/reanimated/
- Delete empty src/hooks/ folder
- Create src/reanimated/index.ts for cleaner exports
- Update mocks to reflect new naming
- All tests passing, no breaking changes to public API

* refactor: rename position to animatedPosition and add animatedIndex

- Rename 'position' to 'animatedPosition' in ReanimatedTrueSheetProvider
- Add 'animatedIndex' shared value to track current detent index
- Update ReanimatedTrueSheet to use new property names
- Update example app to use animatedPosition
- Update mocks to reflect new API
- All tests passing

* test: replace mock documentation with actual mock tests

- Remove example.test.js and README.md from __mocks__
- Add TrueSheetMocks.test.tsx to validate mock implementation
- Test all mock components, hooks, and static methods
- Ensure mocks work correctly during development
- 22 tests now passing (8 TrueSheet + 14 TrueSheetMocks)

* feat(ios): implement native transition animation tracking with fake view

- Replace CADisplayLink timer with fake transition view approach
- Track position changes via presentation layer during transitions
- Detect presenting vs dismissing to set correct initial position
- Emit smooth position updates at screen refresh rate (60-120Hz)
- Remove need for manual JS animation synchronization
- Improve animation curve matching with native UIKit transitions

* fix(ios): find and attach pan gesture from first ScrollView

- Add findScrollView method to find first UIScrollView in view hierarchy
- Attach pan gesture handler only to the first ScrollView found
- Follow similar pattern to TrueSheetContentView.findScrollView
- Fix pan gesture tracking when ScrollView is present in sheet content
- Check view itself first, then first-level children only

* refactor(ios): create GestureUtil helper to reduce pan gesture attachment redundancy

- Add GestureUtil class with attachPanGestureHandler method
- Centralize pan gesture attachment logic in one place
- Replace duplicate gesture attachment code in TrueSheetViewController
- Add logging for tracking attached gesture count
- Follow existing util pattern (LayoutUtil, WindowUtil)

* refactor(ios): improve presentation state and detent management

- Move presentation state tracking to controller
  - Add isPresented property to TrueSheetViewController
  - Set in viewDidAppear after initial presentation logic
  - Reset in viewDidDisappear for next presentation cycle
  - Remove redundant _hasInitiallyPresented flag

- Consolidate active detent management in controller
  - Add activeDetentIndex property to TrueSheetViewController
  - Move from host view to controller for better encapsulation
  - Initialize to -1 and reset on dismissal

- Improve detent application methods
  - Rename setSheetDetentWithIndex: to setupActiveDetentWithIndex:
  - Add applyActiveDetent method with validation and clamping
  - Clamp index to valid range [0, detentCount-1]
  - Auto-correct invalid indices when detents array changes

- Rename delegate methods for consistency
  - viewControllerWillAppear -> viewControllerWillPresent
  - viewControllerDidAppear -> viewControllerDidPresent
  - Better alignment with event names and lifecycle semantics

- Fix presentation event triggering
  - Only trigger didPresent on initial presentation, not on repositioning
  - Move gesture setup to initial presentation check
  - Prevent redundant setup during sheet repositioning

* fix(ios): prevent willPresent event from firing on repositioning

Apply same logic to viewWillAppear as viewDidAppear - only trigger
the delegate callback on initial presentation, not when sheet is
repositioned after drag release.

* feat(ios): improve transition position tracking and reanimated integration

- Optimize transition position tracking
  - Add _lastTransitionPosition to track changes in presentation layer
  - Only notify position changes when fake view actually moves
  - Prevent redundant notifications during repositioning
  - Add early return for cleaner code flow

- Enhance reanimated integration
  - Remove scheduleOnRN wrapper for onPositionChange
  - Execute callback directly on UI thread for better performance
  - Add proper type definition for ReanimatedTrueSheetProps
  - Add JSDoc with @see link to base onPositionChange prop

- Update documentation
  - Clarify transitioning flag purpose in types
  - Add note about worklet requirement for onPositionChange override
  - Update onPositionChange description to be more generic

- Add example usage in MapScreen demo

* refactor(android): remove transitioning param from position change event

The transitioning parameter in PositionChangeEvent was only needed as a
workaround for iOS presentation lifecycle issues. Android doesn't require
this flag, so removing it to keep the event payload cleaner and avoid
passing platform-specific workarounds where they're not needed.

Changes:
- Removed transitioning parameter from PositionChangeEvent constructor
- Removed transitioning from event payload
- Updated PositionChangeEvent dispatch call in TrueSheetContainerView

* fix(android): forward eventDispatcher to rootSheetView for touch events

Touch events were not working properly because the eventDispatcher was
only being forwarded to dialogContainer but not to rootSheetView, which
is the actual view that handles touch events (similar to DialogRootViewGroup
in React Native Modal).

The rootSheetView implements RootView and uses JSTouchDispatcher to handle
touch events, but it requires the eventDispatcher to be set properly.

Changes:
- Forward eventDispatcher to rootSheetView in setter
- Forward stateWrapper to rootSheetView in setter

This follows the same pattern as React Native Modal's DialogRootViewGroup.

* refactor(android): implement delegate pattern to align with iOS architecture

- Add TrueSheetDialogDelegate interface in TrueSheetDialog
- Move event handling logic from TrueSheetContainerView to TrueSheetDialog
- TrueSheetView now implements delegate and dispatches events to JS
- Simplify TrueSheetContainerView to lightweight content manager
- Remove transitioning param from PositionChangeEvent (Android only)
- Rename dialogContainer to containerView for consistency
- Add @SuppressLint("ViewConstructor") to all custom views
- Clean up comments to be more concise

This aligns Android architecture with iOS delegate pattern for better
consistency and maintainability across platforms.

* refactor(android): simplify view hierarchy and improve layout handling

- Remove intermediate FrameLayout wrapper, use TrueSheetRootView directly as dialog content
- Simplify TrueSheetDialog by accessing containerView through root view's child
- Move view hierarchy setup to TrueSheetView.addView() for proper initialization timing
- Improve lifecycle management: setup dialog when child is added, cleanup on detach
- Remove unnecessary configureIfShowing() calls, let showOrUpdate() handle configuration
- Add proper accessibility overrides to prevent event conflicts
- Clean up property delegation between host view and root view
- Fix layout updates to happen during showOrUpdate() instead of requestLayout()

This refactoring makes the view hierarchy cleaner and ensures the container
properly covers the whole sheet area, which is important for touch event
handling and ScrollView interaction.

* fix(android): restore FrameLayout wrapper and improve mount event timing

- Restore FrameLayout wrapper around sheetRootView in dialog (needed for proper layout)
- Move MountEvent dispatch to addView() when child is added (proper timing)
- Format TrueSheetDialog constructor parameters
- Expand imports for better readability
- Add commented placeholder for future statusBarTranslucent handling
- Clean up whitespace and formatting

* fix(android): create dialog in init and fix 3-detent configuration

- Create dialog in init block so it exists when props are set
- Make sheetDialog non-nullable since it's always initialized
- Remove unnecessary null-safety operators throughout
- Fix halfExpandedRatio calculation to be relative to maxHeight, not screen height
- Move MountEvent dispatch to onAttachedToWindow for proper lifecycle
- This fixes the issue where detents were not applied correctly
- All 3 detents (collapsed, half-expanded, expanded) now work properly

* fix: wait for mount event before presenting sheet

- Move MountEvent dispatch from onAttachedToWindow to addView lifecycle
- Add presentationResolver pattern to wait for native view to be ready
- Prevents NullPointerException when present() is called before view hierarchy is constructed
- Resolves race condition between present() call and view mounting

* feat: add lazy prop and improve mount event documentation

- Add lazy prop to control content rendering timing
  - lazy={true} (default): native view created on first present() call
  - lazy={false}: native view created immediately on mount for instant presentation
  - Automatically uses lazy={false} when initialDetentIndex is set

- Update onMount event documentation
  - Clarify it's called when sheet's content is mounted and ready
  - Document that sheet automatically waits for this event before presenting

* docs: add lazy loading guide

- Add concise guide explaining lazy prop usage
- Document interaction with initialDetentIndex
- Add note in prop JSDoc about initialDetentIndex behavior

* docs: update reanimated integration guide

* chore: update package files

* feat: implement lazy loading prop and fix mount event handling

- Add lazy prop to control sheet content rendering (default: false for eager loading)
- Fix mount event race condition by moving dispatch to addView() lifecycle
- Implement presentation resolver pattern for Android view rendering
- Add comprehensive lazy loading documentation
- Update prop descriptions and code examples
- Support dynamic content rendering based on lazy prop
- Ensure consistent mount event behavior across iOS and Android platforms

* feat: implement Fabric state support for TrueSheet

- Add TrueSheetState interface for screen dimensions tracking
- Implement ViewManagerDelegate pattern for proper Fabric integration
- Add updateState() logging to track Fabric state updates
- Implement TrueSheetViewManagerInterface for all prop setters
- Remove unused contentHeight and footerHeight props
- Add investigation and implementation documentation

This enables Fabric to properly call updateState() in TrueSheetViewManager
when the component mounts, allowing StateWrapper to be passed to the view
for dimension tracking and layout coordination.

* chore: add comprehensive logging to investigate updateState behavior

- Add init block and createViewInstance logging in ViewManager
- Add detailed stateWrapper status logging in TrueSheetRootView
- Track when updateState is called and StateWrapper status
- Add TEST_updateState.md guide for debugging

This helps investigate why Fabric's updateState() is not being called
and whether StateWrapper is available for dimension tracking.

* refactor: remove unused state infrastructure

- Remove StateWrapper from TrueSheetView and TrueSheetRootView
- Remove updateState override from TrueSheetViewManager
- Remove unused state tracking code
- State was not being used in JavaScript and Fabric wasn't providing StateWrapper

The component works correctly without state as dimensions are managed
internally by the sheet dialog. Fabric's updateState() wasn't being called
because no C++ state definition exists, which requires significant infrastructure
that isn't needed for this use case.

* feat: add TrueSheetRootViewDelegate for container dimension management

- Add TrueSheetRootViewDelegate interface for root view lifecycle events
- Implement delegate in TrueSheetView to listen for size changes
- Automatically adjust container view dimensions when root view size changes
- Use delegate pattern to allow future extensibility for other root view events

This ensures the container view always matches the root view dimensions
and provides a clean pattern for future root view event handling.

* refactor: add containerView getter in TrueSheetView

- Add private containerView getter for clean access to first child
- Refactor onRootViewSizeChanged to use getter instead of inline logic
- Simplify code with Kotlin's safe call and let operators

This provides cleaner, more maintainable access to the container view.

* refactor: improve delegate naming and simplify containerView getter

- Rename onRootViewSizeChanged to rootViewDidChangeSize (follows did/will pattern)
- Rename rootViewDelegate to delegate for cleaner API
- Simplify containerView getter (remove unnecessary childCount check)
- Reorder delegate initialization before dialog creation for clarity

This provides more consistent naming and cleaner code structure.

* refactor: use Fabric's layout system for container sizing

- Remove manual layout manipulation (setLeft/setRight/setBottom)
- Use requestLayout() to trigger Fabric's layout recalculation
- Let Fabric handle container dimensions through its layout engine
- Add detailed logging for debugging

Inspired by react-native-screens approach which relies on Fabric's
layout system rather than manual view manipulation.

* fix(android): fix halfExpandedRatio crash and add auto container sizing

- Fix crash when using 'auto' detent with ScrollView by clamping halfExpandedRatio to 0.99 max
- BottomSheetBehavior requires ratio to be strictly between 0 and 1 (exclusive)
- Add internal size change event to auto-update container dimensions when root view resizes
- Container view now dynamically adjusts to keyboard, rotation, and content changes
- Internal implementation only, no API changes for users

* fix(android): resolve gesture handling issues on subsequent presentations

Fixed touch event handling problems that prevented proper gesture
interaction on subsequent sheet presentations. The key fix was calling
sheetRootViewContainer.requestLayout() in the present method to trigger
a proper layout pass before showing the sheet.

Changes:
- Reset touch state (cancelLongPress) in present method instead of dismiss
- Clear drag state before each presentation
- Remove stale touch event logging and experimental fixes
- Ensure clean state for each new presentation

This resolves issues where ScrollView and other touch-sensitive components
would not respond correctly after the first sheet presentation.

* chore(android): remove debug logging

Remove all debug Log statements that were added during the gesture
handling investigation. Keep only essential lifecycle and state
information.

* refactor(android): convert TrueSheetDialog to TrueSheetController with lazy dialog creation

- Rename TrueSheetDialog to TrueSheetController (similar to iOS pattern)
- Make controller a regular class instead of extending BottomSheetDialog
- Implement lazy dialog creation when container view mounts
- Add proper dialog cleanup on dismiss for clean state
- Update delegate pattern: TrueSheetDialogDelegate → TrueSheetControllerDelegate
- Rename delegate methods: dialogWillPresent → controllerWillPresent, etc.
- Fix naming conflict: setEdgeToEdge method → applyEdgeToEdge

Benefits:
- Dialog created only when container is ready
- Clean state for each presentation (dialog recreated)
- Better separation of concerns (controller manages state, dialog handles presentation)
- Matches iOS architecture pattern
- Prevents stale state issues between presentations

* fix(android): remove sheetRootView from parent during dialog cleanup

Fixes crash on second sheet presentation caused by sheetRootView
still having a parent reference from the previous dialog.

When cleanupDialog() is called after dismiss, we now properly
removeView(sheetRootView) from the container to allow it to be
re-attached to the new dialog instance on the next presentation.

Error was:
java.lang.IllegalStateException: The specified child already has
a parent. You must call removeView() on the child's parent first.

* fix(android): ensure background and corner radius are applied when dialog is created

- Call setupBackground() in createDialog() to apply initial background/radius
- Move background color and clipToOutline setup from createDialog to setupBackground()
- Update setupBackground() to safely handle when dialog doesn't exist yet
- Ensures background properties are applied both on creation and when updated

This fixes the issue where background color and corner radius weren't
being applied because they were set before the dialog was created.

* fix(android): use initialDetentAnimated prop for initial presentation

- Add animated parameter (default=true) to present() method
- Update showOrUpdate() to pass initialDetentAnimated to present call
- Ensures initialDetentIndex respects the initialDetentAnimated prop

This fixes the issue where initialDetentAnimated was declared but never
actually used, and ensures the sheet presents at the correct detent
with proper animation control.

* fix(android): set initial detent state after dialog is shown

- Move setStateForDetentIndex() call to after dialog.show()
- Use Handler.post() to ensure state is set after dialog is ready
- Add requestLayout() before configure() for clean state

This fixes the issue where initialDetentIndex wasn't being respected
because the BottomSheetBehavior state was set before the dialog was
shown, causing it to be ignored or overridden.

* Revert "fix(android): set initial detent state after dialog is shown"

This reverts commit 3fd8dabd90788887f0887c71e930c1b2d162d91b.

* fix(android): fix container content layout

* feat: remove lazy prop option, always lazy load by default

- Remove lazy prop from TrueSheetProps interface
- Sheets now always lazy load (render on present, cleanup on dismiss)
- Exception: sheets with initialDetentIndex still render immediately
- Delete lazy-loading.mdx guide
- Remove lazy prop from props documentation

* fix(android): put TrueSheetView behind content (zIndex: -9999)

* fix(android): fix edgeToEdge prop not being passed to native

* refactor(android): remove edgeToEdge prop, auto-detect edge-to-edge mode

- Remove edgeToEdge prop from TrueSheetProps interface
- Auto-detect edge-to-edge using React Native's isEdgeToEdgeFeatureFlagOn
- Follow same pattern as react-native-screens and React Native Modal
- Remove prop setters from TrueSheetView and TrueSheetViewManager
- Update Fabric native component spec to remove edgeToEdge
- Remove edgeToEdge usage from all example sheets
- Update documentation to explain auto-detection instead of prop usage

TrueSheet now automatically detects if the app has edge-to-edge enabled
at the Activity level and adapts accordingly. No configuration needed.

This provides a cleaner API and better aligns with React Native's
architecture where edge-to-edge is a global app-level setting.

* fix(android): don't apply edge-to-edge to bottom sheet dialog window

Bottom sheets should sit ON TOP of the navigation bar, not extend behind it.
When the app has edge-to-edge enabled, we still want the bottom sheet dialog
to respect the navigation bar and position itself above it.

This ensures the sheet is always fully visible and positioned at the true
bottom of the screen, rather than partially hidden behind the navigation bar.

Follows the standard Material Design bottom sheet behavior where sheets
appear above navigation bars regardless of edge-to-edge mode.

* chore: update example

* feat(android): auto detect and apply edge-to-edge

* refactor(android): change detents property to only accept Double type

* fix(android): fix footer positioning to stick at screen bottom

* fix(android): fix initial footer position by waiting for layout

* fix(android): fix initial footer position on ScrollView

* docs: add edge-to-edge display guide

* fix: fix initial width flicker during presentation

* docs: move edge-to-edge to troubleshooting

* feat(ios): implement onSizeChange event

- Add OnSizeChangeEvent header and implementation
- Track container width and height in viewDidLayoutSubviews
- Emit size change events through controller delegate
- Match Android onSizeChange event behavior

* feat(ios): implement proper rotation tracking with viewWillTransitionToSize

- Add viewWillTransitionToSize to handle rotation events explicitly
- Call setupSheetDetents and notify delegate after rotation completes
- Update viewDidLayoutSubviews to only handle non-rotation size changes
- More reliable than relying solely on viewDidLayoutSubviews

* fix(ios): fix footer positioning during rotation

- Skip React Native's updateLayoutMetrics after initial layout to
  prevent frame-based positioning from overriding Auto Layout
- Add _didInitialLayout flag to allow initial positioning by React
  Native, then let Auto Layout take over
- Remove unnecessary layoutFooter logic as Auto Layout handles
  rotation automatically
- Footer now stays pinned to bottom correctly during device rotation

* fix(ios): use proper floating point comparison in transition tracking

- Replace direct equality check with FLT_EPSILON-based comparison
- Prevents false inequality due to floating point precision issues
- Fixes spurious transition tracking after animation completes

* feat(android): implement dynamic content and footer size change handling

- Add size change tracking to TrueSheetContentView and TrueSheetFooterView
- Implement delegate pattern for content and footer size changes
- Forward size changes through ContainerView to host view
- Set initial contentHeight on mount
- Reconfigure sheet detents when content size changes
- Reposition footer when footer size changes
- Rename configure() to setupSheetDetents() to match iOS
- Add contentHeight property to controller for auto detent calculations
- Matches iOS architecture with host view coordinating and controller executing

* refactor(android): merge TrueSheetController and TrueSheetRootView into TrueSheetViewController

- Delete TrueSheetRootView.kt (125 lines)
- Rename TrueSheetController.kt to TrueSheetViewController.kt
- Merge RootView functionality (touch handling, accessibility) into ViewController
- ViewController now extends ReactViewGroup and implements RootView
- Rename delegate interface: TrueSheetControllerDelegate → TrueSheetViewControllerDelegate
- Rename all delegate methods: controller* → viewController*
- Update TrueSheetView to use single TrueSheetViewController instance
- Replace sheetRootView and sheetController references with viewController
- Net reduction of 41 lines of code

Matches iOS architecture where TrueSheetViewController manages both view and dialog lifecycle.

* refactor(android): improve screen coordinate handling and fix position tracking

* fix(android): improve position tracking and add transitioning flag for animations

- Use bottomSheetView for consistent position measurements across all events
- Fix position calculation to match actual screen coordinates
- Wait for sheet to settle before emitting didPresent event
- Add transitioning parameter to PositionChangeEvent
- Emit transitioning=true during imperative present/dismiss for Reanimated animations
- Use timing animation on Android, spring on iOS in ReanimatedTrueSheet
- Remove all debug logs and cleanup unused code

* feat(android): handle device rotation properly

- Add isPresented flag to track when sheet is actually presented (after onShow callback)
- Replace isShowing checks with isPresented throughout codebase
- Update maxScreenHeight in onSizeChanged when rotation is detected
- Recalculate sheet detents and reposition footer on rotation
- Force layout update on sheet container when presented to respect new maxHeight constraints
- Add comprehensive logging for debugging rotation behavior

* refactor(android): remove bridge utilities and use pure Android APIs

- Replace UiThreadUtil.runOnUiThread with Handler(Looper.getMainLooper()).post
- Remove all UiThreadUtil.assertOnUiThread runtime checks
- Keep @UiThread annotations for documentation
- Use standard Android Handler for UI thread operations in TurboModule
- Remove dependency on com.facebook.react.bridge.UiThreadUtil

* chore: remove CONTEXT.md

* refactor(android): improve KeyboardManager implementation

* feat(android): enable edge-to-edge via BottomSheetDialog style

* fix(android): stabilize footer position calculation when sheet expands to fullscreen

* feat(android): add fullScreen prop to control edge-to-edge behavior

* feat(android): auto-enable edge-to-edge for Android 16+ and add documentation

* docs: add scrolling limitation caution for Android

* fix(android): correct halfExpandedRatio calculation for 3 detents with edge-to-edge

* fix(android): update footer position when content size changes

* chore: remove debug logs and unused keyboard manager

* test: add comprehensive lazy loading tests and fix Easing mock

* fix(ci): regenerate corrupted gradle-wrapper.jar with proper MANIFEST.MF

* chore: regenerate yarn.lock file

* docs: update documentation for v3 and add migration guide

- Update prop names: initialIndex -> initialDetentIndex,
  initialIndexAnimated -> initialDetentAnimated
- Remove scrollRef prop documentation (now auto-detected on iOS)
- Update scrolling guide with automatic scroll view detection
- Add comprehensive v2 to v3 migration guide covering:
  - Fabric architecture requirement
  - Prop renames and removed props
  - Detent value changes
  - New features: edge-to-edge detection, enhanced events,
    Reanimated v4 support
  - Step-by-step migration instructions

* docs: add Liquid Glass effect guide for iOS 26+

- Add comprehensive guide for iOS 26 Liquid Glass visual effect
- Document UIDesignRequiresCompatibility configuration
- Include both native Info.plist and Expo config examples
- Note that Liquid Glass is enabled by default on iOS 26+
- Add compatibility table and when to disable recommendations

* fix(ci): use glob pattern for yarn.lock in GitHub Actions cache

- Change hashFiles('yarn.lock') to hashFiles('**/yarn.lock')
- Ensures yarn.lock is found in CI environment directory structure

* docs: updated docs
2025-11-23 14:54:19 +08:00
lodev09
d915c3937c
chore: remove lib folder 2025-11-21 03:06:00 +08:00
Hélio Costa
0254e0a8cd
docs: fix typo (#204) 2025-11-19 08:05:00 +08:00
Jovanni Lo
59550c0049
Update README.md 2025-07-11 06:53:51 +08:00
lodev09
e846a8d459
chore: release 2.0.6 2025-07-11 02:40:50 +08:00
lodev09
cd94ba1719
chore: tidy 2025-07-11 02:40:09 +08:00
Jovanni Lo
3c032f3c97
fix(ios): add ios 15 condition to fix build issue (#200) 2025-07-11 02:36:55 +08:00
lovegaoshi
592a9f154e
fix: android auto responsive sheet size (#193)
* fix: android auto responsive sheet size

* feat: fix auto responsive sheet size

* feat: solution 1

this sets layout.height right after STATE.COLLAPSED is set to give an animation effect. sometimes janky.

* feat: solution 2

* feat: disable dragging on auto size animation

* refactor: simplify code, forget about animated size change

* chore: remove unused code

---------

Co-authored-by: lodev09 <lodev09@gmail.com>
2025-07-11 02:22:26 +08:00
lodev09
3b6c84d2ba
chore(example): fix types 2025-06-25 05:02:25 +08:00
lodev09
2b4aa88f7c
chore(example): upgrade to RN 0.79.4 2025-06-25 04:50:14 +08:00
Ebisuzawa Kurumi
d29dc7d5d5
chore: support RN 80 (#187) 2025-06-13 08:51:37 +08:00
lodev09
798a1f0ba5
chore: release 2.0.5 2025-03-08 12:32:56 +08:00
Jovanni Lo
add6bd22d3
chore: support RN 78 (#160)
fixes #158
2025-03-08 12:31:41 +08:00
lodev09
49ba54ed2a
chore: release 2.0.4 2025-03-06 05:48:26 +08:00
lodev09
2afafcf344
chore: update clean script 2025-03-06 05:47:39 +08:00
lodev09
7188e8fe73
chore: update prettier 2025-03-06 05:45:39 +08:00
Jovanni Lo
8de8d52cfd
fix(android): touch events on android new arch (#156)
* fix(android): touch events on android new arch

* docs: updated gestures troubleshooting docs

* fix(android): enable disallow intercept touch event

* refactor: style objects
2025-03-06 05:30:33 +08:00
lodev09
bbb534b257
chore: remove js from prettier 2025-03-05 03:51:57 +08:00
lodev09
eaac376fec
chore: add lib to repo
so packages pointing to master works
2025-03-05 03:49:54 +08:00
lodev09
e86438f4a2
chore: release 2.0.3 2025-02-27 06:32:52 +08:00
Jovanni Lo
717de68fec
fix(ios): add new arch flag to swift (#152) 2025-02-27 06:30:56 +08:00
lodev09
ce38492356
chore: release 2.0.2 2025-02-27 02:56:08 +08:00
lodev09
a7f1263559
chore: podfile.lock 2025-02-27 02:55:25 +08:00
Hassnain Ali
0bf0080ec6
Update build.gradle (#149) 2025-02-27 02:47:51 +08:00
lodev09
bd5762fa97
docs: fix typo 2025-02-26 04:04:18 +08:00
lodev09
51bdb8b19f
chore: release 2.0.1 2025-02-26 03:58:40 +08:00
lodev09
d9dc0548de
refactor(example): fix typo 2025-02-26 03:57:40 +08:00
Efstathios Ntonas
f78dcd446f
fix: add android viewgroup null check (#146)
* fix: added null check on Android ViewGroup

* chore: improved null check on Android ViewGroup

* chore: minor formatting/linting

* chore: minor formatting/linting
2025-02-26 03:08:34 +08:00
Aironas Kulvelis
ee22df7769
fix(android): add support for RN 0.78.0 (#143) 2025-02-25 05:53:01 +08:00
lodev09
708ee255e3
chore: disable new arch on example 2025-02-25 04:44:23 +08:00
Jovanni Lo
faabb2034f
Update bug_report.yml 2025-02-25 04:42:14 +08:00
lodev09
1363196a44
chore: release 2.0.0 2025-02-25 04:36:40 +08:00
lodev09
bd5157e4e0
chore: deps 2025-02-25 04:36:02 +08:00
lodev09
2b407421fc
refactor(android): cleanup code 2025-02-25 04:30:23 +08:00
lodev09
c87a6efe46
fix(android): events on new arch 2025-02-24 02:38:13 +08:00
Jovanni Lo
dcbab4423d
fix(ios): fix layout issue with new arch (#140)
* fix(ios): fix layout issue with new arch

* fix(ios): events in new arch
2025-02-24 00:22:37 +08:00
lodev09
29199723cb
chore: deps 2025-02-17 17:21:51 +08:00
Jovanni Lo
74ef9b49b1
Feat: Drag events & Reanimated support (#124)
* fix(ios): correct sizeInfo on present

* chore(ios): decrease min ios version

* feat(ios): add drag events

* chore: update builder-bob

* chore(android): put back gesture handler and add todo

* chore: disable new-arch, clip android background, update example

* feat(android): implement drag events

* chore: update clean script

* feat: implement reanimated (not yet working)

* chore: tidy

* feat(ios): support reanimated events

* refactor(ios): use synthetic data for events

* refactor(android): events

* fix(ios): correct size when changing size programmatically

* refactor(android): consistency with ios

* docs: add reanimated guide

* refactor(android): normalize event dispatcher

* refactor(android): organize code

* chore: tidy
2025-02-10 10:08:30 +08:00
lodev09
82476696e5
chore: release 1.1.1 2025-02-05 08:52:58 +08:00
lodev09
9d207b0ff5
chore: tidy 2025-02-05 08:52:23 +08:00
lodev09
3801ddc2e0
chore: tidy 2025-02-05 08:51:15 +08:00
lodev09
8ac8d37a6d
fix(android): corner radius & auto size bug 2025-02-05 08:50:41 +08:00
lodev09
744905ab10
chore: release 1.1.0 2025-02-02 03:55:33 +08:00
lodev09
0385ab3a87
refactor: fix background color type 2025-02-02 03:54:16 +08:00
SergeyMild
e64de2edce
feat: backgroundColor apply refactoring (#121)
* feat: backgroundColor apply refactoring

* revert version

---------

Co-authored-by: Jovanni Lo <lodev09@gmail.com>
2025-02-02 03:52:42 +08:00
Rickard Natt och Dag
c6d8b8fd9a
fix(android): support react native 0.77 (#117)
* fix(android): support react native 0.77

* chore: use null check instead of cast

* chore: add suppression comment for react native < 0.77
2025-01-28 00:01:48 +08:00
Jovanni Lo
298aec71ff
feat: add navigation example (#115) 2025-01-19 18:13:47 +08:00
Jovanni Lo
58f377375f
Fix: Sheet background (#114)
* chore: cleanup

* fix: ios background color

* chore: tidy

* fix: add edge-to-edge to sample sheets

* fix(android): drag bug

* fix: default initial index

* fix: android background
2025-01-19 03:19:07 +08:00
lodev09
c92aa98f2d
chore: release 1.0.3 2025-01-09 16:00:50 +08:00
lodev09
b1044e037b
fix: include __mocks__ 2025-01-09 16:00:08 +08:00
lodev09
301f6ffeca
chore: release 1.0.2 2025-01-09 15:49:17 +08:00
lodev09
2536c6dce6
fix: jest mock 2025-01-09 15:45:41 +08:00
lodev09
9f31ea8aa4
chore: release 1.0.1 2025-01-08 01:35:41 +08:00
lodev09
b0c1ffbf05
chore: fix types 2025-01-08 01:34:47 +08:00
lodev09
8a9e682967
chore: release 1.0.0 2025-01-08 00:13:23 +08:00
lodev09
adc98c9a38
chore: tidy 2025-01-08 00:11:54 +08:00
Jovanni Lo
d1981586c0
feat: New Arc support (#106)
* feat: upgrade example to expo 52

* fix(new-arch): fix touchable

* fix(example): disable expo-autolinking in prebuild

* fix: move width updates to JS

* fix: scrollview handle

* chore: deps

* chore: deps

* chore: git rid of expo example

* chore: android build

* fix(android): container size change

* fix(ios): update size event

* chore: remove flunky ci

* chore: fix version

* feat: update example

* docs: add edgeToEdge guide
2024-12-13 02:32:26 +08:00
Dominik Garcia
65d33166c2
fix(iOS): null checks in getTrueSheetView (#108)
This PR fixes a crash in onDismiss caused by nil values during app
reloads in release builds. Both bridge and the view can be null and this
is now handled in getTrueSheetView
2024-11-26 18:16:50 +08:00
lodev09
7467c743ca
chore: deps 2024-11-16 22:33:22 +08:00
Mathieu Acthernoene
5a0865c26f
Edge-to-edge support (#103)
* feat: force bottom sheet edge to edge

* fix: add onStart

* fix: android < 30 screen height is wrong

* chore: return if

* chore: simplify condition

* fix: typo

* feat: add edgeToEdge prop
2024-11-13 02:30:32 +08:00
lodev09
d73a39743c
chore: release 0.13.0 2024-08-22 15:23:04 +08:00
Mason L'Amy
dc78ad1bf2
Add nullability checks to RootSheetView (#86)
Co-authored-by: Jovanni Lo <lodev09@gmail.com>
2024-08-22 15:20:05 +08:00
lodev09
c4177e63ac
chore: example deps 2024-08-22 15:16:08 +08:00
lodev09
fbcb1af5e7
feat: add BlankSheet example 2024-07-22 00:39:39 +08:00
lodev09
f69b728686
chore: release 0.12.4 2024-07-20 02:41:01 +08:00
Jovanni Lo
b5483f035f
chore: set IOS min version to 13.4 (RN 0.73) (#70) 2024-07-20 02:39:36 +08:00
364 changed files with 33707 additions and 13096 deletions

14
.clang-format Normal file
View File

@ -0,0 +1,14 @@
---
BasedOnStyle: Google
IndentWidth: 2
ColumnLimit: 120
ContinuationIndentWidth: 2
AlignAfterOpenBracket: DontAlign
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
ObjCBlockIndentWidth: 2
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PointerAlignment: Right
SpaceBeforeParens: ControlStatements

View File

@ -1,5 +0,0 @@
lib
build
src/__mocks__
docs/build
docs/.docusaurus

89
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,89 @@
name: Bug Report
description: Report a reproducible bug or regression in this library.
labels: [bug]
body:
- type: markdown
attributes:
value: |
# Bug Report
Thanks for taking the time to report a bug!
Please fill out the following carefully before opening a new issue.
**Issues missing required information may be closed without investigation.**
- type: checkboxes
attributes:
label: Before submitting a new issue
options:
- label: I tested using the **latest version** of the library.
required: true
- label: I tested using a [supported version](https://github.com/reactwg/react-native-releases/blob/main/docs/support.md) of React Native.
required: true
- label: I checked for [existing issues](https://github.com/lodev09/react-native-true-sheet/issues) that might answer my question.
required: true
- type: textarea
id: summary
attributes:
label: Bug Summary
description: Provide a clear and concise description of what the bug is.
placeholder: When I do X, I expect Y to happen, but Z happens instead.
validations:
required: true
- type: checkboxes
id: platforms
attributes:
label: Affected Platforms
options:
- label: iOS
- label: Android
- label: Web
- label: Other
- type: input
id: library-version
attributes:
label: Library Version
description: What version of @lodev09/react-native-true-sheet are you using?
placeholder: "x.x.x"
validations:
required: true
- type: textarea
id: react-native-info
attributes:
label: Environment Info
description: Run `npx react-native info` in your terminal and paste the results here.
render: shell
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Provide a clear list of steps to reproduce the problem.
placeholder: |
1. Open the app
2. Navigate to...
3. Tap on...
4. See error
validations:
required: true
- type: input
id: reproducible-example
attributes:
label: Repro
description: A link to a GitHub repository, Expo Snack, CodeSandbox, or StackBlitz.
placeholder: https://github.com/username/repo
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context, screenshots, or screen recordings about the problem here.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request 💡
url: https://github.com/lodev09/react-native-true-sheet/discussions/new?category=ideas
about: If you have a feature request, please create a new discussion on GitHub.
- name: Discussions on GitHub 💬
url: https://github.com/lodev09/react-native-true-sheet/discussions
about: If this library works as promised but you need help, please ask questions there.

25
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,25 @@
## Summary
<!-- Describe what this PR does and why -->
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Test Plan
<!-- How did you test these changes? -->
## Screenshots / Videos
<!-- Add screenshots or videos if applicable -->
## Checklist
- [ ] I tested on iOS
- [ ] I tested on Android
- [ ] I tested on Web
- [ ] I updated the documentation (if needed)

View File

@ -5,17 +5,13 @@ runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Enable Corepack
run: corepack enable
shell: bash
- name: Cache dependencies
- name: Restore dependencies
id: yarn-cache
uses: actions/cache@v4
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
**/node_modules
@ -29,3 +25,12 @@ runs:
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
shell: bash
- name: Cache dependencies
if: steps.yarn-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
**/node_modules
.yarn/install-state.gz
key: ${{ steps.yarn-cache.outputs.cache-primary-key }}

135
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,135 @@
name: Build
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-android:
runs-on: ubuntu-latest
env:
TURBO_CACHE_DIR: .turbo/android
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for Android
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-android-
- name: Check turborepo cache for Android
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Install JDK
if: env.turbo_cache_hit != 1
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- name: Cache Gradle
if: env.turbo_cache_hit != 1
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/.gradle/wrapper
~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('example/bare/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build example for Android
env:
JAVA_OPTS: "-XX:MaxHeapSize=6g"
run: |
yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
build-ios:
runs-on: macos-latest
env:
XCODE_VERSION: latest-stable
TURBO_CACHE_DIR: .turbo/ios
RCT_USE_RN_DEP: 1
RCT_USE_PREBUILT_RNCORE: 1
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
- name: Cache turborepo for iOS
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ env.TURBO_CACHE_DIR }}
key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-turborepo-ios-
- name: Check turborepo cache for iOS
run: |
TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status")
if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi
- name: Use appropriate Xcode version
if: env.turbo_cache_hit != 1
uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # v1.6.0
with:
xcode-version: ${{ env.XCODE_VERSION }}
- name: Cache iOS build
if: env.turbo_cache_hit != 1
id: ios-cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: |
~/Library/Caches/ccache
~/Library/Developer/Xcode/DerivedData
example/bare/ios/Pods
key: ${{ runner.os }}-ios-${{ hashFiles('example/bare/ios/Podfile.lock') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-ios-${{ hashFiles('example/bare/ios/Podfile.lock') }}-
${{ runner.os }}-ios-
- name: Install ccache
if: env.turbo_cache_hit != 1
run: brew install ccache
- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.ios-cache.outputs.cache-hit != 'true'
run: |
cd example/bare
bundle install
bundle exec pod install --project-directory=ios
- name: Build example for iOS
run: |
yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"

133
.github/workflows/check-repro.yml vendored Normal file
View File

@ -0,0 +1,133 @@
name: Check for Repro
on:
issues:
types: [opened, edited, labeled]
issue_comment:
types: [created, edited]
jobs:
check-repro:
runs-on: ubuntu-latest
if: >
github.event.issue.pull_request == null && (
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'needs repro') ||
(github.event_name == 'issues' && github.event.action != 'labeled') ||
github.event_name == 'issue_comment'
)
permissions:
issues: write
steps:
- name: Check for reproduction link
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const comment = context.payload.comment;
const author = issue.user.login;
const user = comment ? comment.user.login : author;
const isComment = !!comment;
const body = comment ? comment.body : issue.body || '';
// Only accept repos owned by the issue author or commenter
const reproPatterns = [
new RegExp(`https?://github\\.com/${user}/[^/\\s]+/?`, 'i'),
new RegExp(`https?://snack\\.expo\\.(dev|io)/[^\\s]+`, 'i'),
new RegExp(`https?://codesandbox\\.io/[^\\s]+`, 'i'),
new RegExp(`https?://stackblitz\\.com/[^\\s]+`, 'i'),
];
let hasRepro = false;
if (isComment) {
// For comments, check the entire comment body
hasRepro = reproPatterns.some(pattern => pattern.test(body));
} else {
// For issues, extract and check the Repro field specifically
const reproFieldMatch = body.match(/### Repro\s+([\s\S]*?)(?=###|$)/i);
const reproField = reproFieldMatch ? reproFieldMatch[1].trim() : '';
const invalidRepro = /^(n\/?a|none|no|nothing|-|\.+)?$/i.test(reproField);
hasRepro = !invalidRepro && reproPatterns.some(pattern => pattern.test(reproField));
}
const labels = issue.labels.map(l => l.name);
const hasNeedsReproLabel = labels.includes('needs repro');
const hasReproProvidedLabel = labels.includes('repro provided');
if (hasRepro) {
// Valid repro found - add repro-provided label, remove needs-repro
if (!hasReproProvidedLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['repro provided']
});
}
if (hasNeedsReproLabel) {
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
name: 'needs repro'
});
} catch (e) {
if (!e.message.includes('Label does not exist')) throw e;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Thank you for providing a repro! We'll take a look at this issue soon.`
});
}
} else {
// Only post warning comment on issue events, not comments
if (context.eventName !== 'issues') {
return;
}
const warningBody = `Hey @${author}! Thanks for opening the issue.
It looks like your issue is **missing a valid reproduction link**.
A minimal reproduction helps us investigate and fix the issue faster. Without one, we may not be able to help.
**Please provide one of the following:**
- A GitHub repository **under your username**
- An [Expo Snack](https://snack.expo.dev)
See ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example) for more guidance.
You can edit your original issue or leave a comment with the repro link. **Issues without reproductions may be closed after 14 days.**`;
// Check if we already commented
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
});
if (comments.data.some(c => c.body.includes('missing a valid reproduction link'))) {
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: warningBody
});
if (!hasNeedsReproLabel) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: ['needs repro']
});
}
}

View File

@ -1,4 +1,4 @@
name: CI
name: Checks
on:
push:
branches:
@ -6,13 +6,21 @@ on:
pull_request:
branches:
- main
merge_group:
types:
- checks_requested
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
@ -25,9 +33,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup
@ -35,23 +44,12 @@ jobs:
- name: Run unit tests
run: yarn test --maxWorkers=2 --coverage
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Run expo doctor
run: yarn example doctor
build-library:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup
uses: ./.github/actions/setup

41
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 0 * * *' # Runs daily at midnight UTC
workflow_dispatch: # Allows manual triggering
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions!'
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions!'
close-issue-message: 'This issue has been automatically closed due to inactivity. Feel free to reopen if this is still relevant.'
close-pr-message: 'This pull request has been automatically closed due to inactivity. Feel free to reopen if this is still relevant.'
days-before-stale: 30
days-before-close: 7
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-labels: 'pinned,security,enhancement'
exempt-pr-labels: 'pinned,security'
stale-needs-repro:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v9
with:
only-labels: 'needs repro'
stale-issue-message: 'This issue is missing a reproduction and has been marked as stale. It will be closed if no reproduction is provided.'
close-issue-message: 'This issue has been closed due to missing reproduction. Feel free to reopen with a minimal repro.'
days-before-stale: 14
days-before-close: 7
stale-issue-label: 'stale'
exempt-issue-labels: 'pinned,security'

22
.gitignore vendored
View File

@ -5,8 +5,7 @@
# XDE
.expo/
# Editors
*.sublime-*
# VSCode
.vscode/
jsconfig.json
@ -29,6 +28,7 @@ DerivedData
*.ipa
*.xcuserstate
project.xcworkspace
**/.xcode.env.local
# Android/IJ
#
@ -43,10 +43,10 @@ android.iml
# Cocoapods
#
example/ios/Pods
example/bare/ios/Pods
# Ruby
example/vendor/
example/bare/vendor/
# node.js
#
@ -72,10 +72,18 @@ android/keystores/debug.keystore
# Expo
.expo/
# Turborepo
.turbo/
# generated by bob
lib/
# Example
example/ios
example/android
# React Native Codegen
ios/generated
android/generated
# Docs
.vercel
# Test coverage
coverage

2
.nvmrc
View File

@ -1 +1 @@
v18
v22.20.0

View File

@ -1 +1,4 @@
.eslintignore
lib
build
docs/build
docs/.docusaurus

View File

@ -0,0 +1,75 @@
diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm
index 51f021831aed26a4eed3c85014020423b7b3108b..268fa69dfee2b20d8b5a66c77c1b4cbd8c831573 100644
--- a/ios/RNSScreenStack.mm
+++ b/ios/RNSScreenStack.mm
@@ -640,8 +640,10 @@ RNS_IGNORE_SUPER_CALL_END
// This check is for external modals that are not owned by this stack. They can prevent the dismissal of the modal by
// extending RNSDismissibleModalProtocol and returning NO from isDismissible method.
- if (![firstModalToBeDismissed conformsToProtocol:@protocol(RNSDismissibleModalProtocol)] ||
- [(id<RNSDismissibleModalProtocol>)firstModalToBeDismissed isDismissible]) {
+ BOOL shouldDismissFirstModal = ![firstModalToBeDismissed conformsToProtocol:@protocol(RNSDismissibleModalProtocol)] ||
+ [(id<RNSDismissibleModalProtocol>)firstModalToBeDismissed isDismissible];
+
+ if (shouldDismissFirstModal) {
if (firstModalToBeDismissed != nil) {
const BOOL firstModalToBeDismissedIsOwned = [firstModalToBeDismissed isKindOfClass:RNSScreen.class];
const BOOL firstModalToBeDismissedIsOwnedByThisStack =
@@ -699,6 +701,33 @@ RNS_IGNORE_SUPER_CALL_END
return;
}
}
+ } else {
+ // Modal is non-dismissible (e.g., third-party modal like TrueSheet)
+ // Check if the external modal provides a presenting controller
+ if (firstModalToBeDismissed != nil) {
+ id<RNSDismissibleModalProtocol> dismissibleModal = (id<RNSDismissibleModalProtocol>)firstModalToBeDismissed;
+ UIViewController *presentingController = nil;
+
+ // Check if the external modal implements the optional method
+ if ([dismissibleModal respondsToSelector:@selector(newPresentingViewController)]) {
+ presentingController = [dismissibleModal newPresentingViewController];
+ }
+
+ // Only handle the non-dismissible modal if it provides a presenting controller
+ if (presentingController != nil) {
+ changeRootController = presentingController;
+
+ // Check if the presenting controller has presented modals that need to be dismissed
+ UIViewController *modalPresentedByController = presentingController.presentedViewController;
+ if (modalPresentedByController != nil && ![modalPresentedByController isBeingDismissed] &&
+ [_presentedModals containsObject:modalPresentedByController]) {
+ // The presenting controller has presented one of our modals
+ // We need to dismiss it before presenting new ones
+ [presentingController dismissViewControllerAnimated:YES completion:finish];
+ return;
+ }
+ }
+ }
}
// We didn't detect any controllers for dismissal, thus we start presenting new VCs
diff --git a/ios/integrations/RNSDismissibleModalProtocol.h b/ios/integrations/RNSDismissibleModalProtocol.h
index 006f809d104c1d4fbdf6eccca89d6c6e190cca71..89e297f1b7a9582fee3e19237dfba8d4c87a352f 100644
--- a/ios/integrations/RNSDismissibleModalProtocol.h
+++ b/ios/integrations/RNSDismissibleModalProtocol.h
@@ -1,3 +1,5 @@
+#import <UIKit/UIKit.h>
+
NS_ASSUME_NONNULL_BEGIN
@protocol RNSDismissibleModalProtocol <NSObject>
@@ -6,6 +8,13 @@ NS_ASSUME_NONNULL_BEGIN
// Use it on your own responsibility, as it can lead to unexpected behavior.
- (BOOL)isDismissible;
+@optional
+// If the modal is non-dismissible, it can optionally provide a view controller
+// that should be used as the presenting controller for subsequent modals.
+// This gives the external modal implementation control over the presentation chain.
+// If not implemented or returns nil, the non-dismissible modal itself will be used.
+- (nullable UIViewController *)newPresentingViewController;
+
@end
NS_ASSUME_NONNULL_END

942
.yarn/releases/yarn-4.11.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,4 @@
nodeLinker: node-modules
nmHoistingLimits: workspaces
nodeLinker: node-modules
plugins:
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-4.11.0.cjs

7
.zed/settings.json Normal file
View File

@ -0,0 +1,7 @@
{
"file_scan_exclusions": [
"**/.git",
"**/node_modules",
"**/.DS_Store"
]
}

150
AGENTS.md Normal file
View File

@ -0,0 +1,150 @@
# Agent Instructions
## Rules
1. YOU MUST NOT do builds unless you are told to.
2. YOU MUST NOT commit changes yourself until I explicitly tell you to.
3. YOU MUST NOT create summary documents unless you are told to.
4. YOU MUST NOT add code comments that are obvious.
## Project Overview
React Native Fabric (New Architecture) bottom sheet library for iOS and Android.
- **Fabric** - No bridge, direct C++ communication
- **Codegen** - Auto-generates native interfaces from TypeScript specs
- **C++ Shared Code** - State and shadow nodes shared between platforms
## Project Structure
```
src/
├── fabric/ # Native component specs (codegen input)
│ ├── TrueSheetViewNativeComponent.ts
│ ├── TrueSheetContainerViewNativeComponent.ts
│ ├── TrueSheetContentViewNativeComponent.ts
│ ├── TrueSheetHeaderViewNativeComponent.ts
│ └── TrueSheetFooterViewNativeComponent.ts
├── specs/ # TurboModule spec
│ └── NativeTrueSheetModule.ts
├── reanimated/ # Reanimated integration
│ ├── ReanimatedTrueSheet.tsx
│ ├── ReanimatedTrueSheet.web.tsx
│ ├── ReanimatedTrueSheetProvider.tsx
│ ├── useReanimatedPositionChangeHandler.ts
│ ├── useReanimatedPositionChangeHandler.web.ts
│ └── index.ts
├── navigation/ # React Navigation integration
│ ├── createTrueSheetNavigator.tsx
│ ├── TrueSheetRouter.ts
│ ├── TrueSheetView.tsx
│ ├── useTrueSheetNavigation.ts
│ ├── types.ts
│ ├── index.ts
│ └── screen/ # Screen components for navigator
│ ├── TrueSheetScreen.tsx
│ ├── ReanimatedTrueSheetScreen.tsx
│ ├── useSheetScreenState.ts
│ ├── types.ts
│ └── index.ts
├── mocks/ # Testing mocks
│ ├── navigation.ts
│ ├── reanimated.ts
│ └── index.ts
├── __tests__/ # Unit tests
│ ├── TrueSheet.test.tsx
│ └── TrueSheetMocks.test.tsx
├── TrueSheet.tsx # Main React component
├── TrueSheet.web.tsx # Web implementation
├── TrueSheetProvider.tsx
├── TrueSheetProvider.web.tsx
├── TrueSheet.types.ts
└── index.ts
ios/
├── TrueSheetView.mm/.h # Host view (Fabric component)
├── TrueSheetViewController.mm/.h # UIViewController for sheet presentation
├── TrueSheetModule.mm/.h # TurboModule
├── TrueSheetContainerView.mm/.h # Container view
├── TrueSheetContentView.mm/.h # Content view
├── TrueSheetHeaderView.mm/.h # Header view
├── TrueSheetFooterView.mm/.h # Footer view
├── TrueSheetComponentDescriptor.h
├── core/
│ ├── TrueSheetGrabberView.mm/.h
│ ├── TrueSheetBlurView.mm/.h
│ └── TrueSheetDetentCalculator.mm/.h
├── events/
│ ├── TrueSheetLifecycleEvents.mm/.h
│ ├── TrueSheetStateEvents.mm/.h
│ ├── TrueSheetDragEvents.mm/.h
│ └── TrueSheetFocusEvents.mm/.h
└── utils/
├── LayoutUtil.mm/.h
├── GestureUtil.mm/.h
└── WindowUtil.mm/.h
android/src/main/java/com/lodev09/truesheet/
├── TrueSheetView.kt # Host view
├── TrueSheetViewController.kt # BottomSheet controller
├── TrueSheetModule.kt # TurboModule
├── TrueSheetContainerView.kt # Container view
├── TrueSheetContentView.kt # Content view
├── TrueSheetHeaderView.kt # Header view
├── TrueSheetFooterView.kt # Footer view
├── TrueSheetViewManager.kt # View manager for TrueSheetView
├── TrueSheetContainerViewManager.kt
├── TrueSheetContentViewManager.kt
├── TrueSheetHeaderViewManager.kt
├── TrueSheetFooterViewManager.kt
├── TrueSheetPackage.kt
├── core/
│ ├── TrueSheetBottomSheetView.kt
│ ├── TrueSheetCoordinatorLayout.kt
│ ├── TrueSheetDetentCalculator.kt
│ ├── TrueSheetStackManager.kt
│ ├── TrueSheetDimView.kt
│ ├── TrueSheetGrabberView.kt
│ ├── TrueSheetKeyboardObserver.kt
│ └── RNScreensFragmentObserver.kt
├── events/
│ ├── TrueSheetDragEvents.kt
│ ├── TrueSheetFocusEvents.kt
│ ├── TrueSheetLifecycleEvents.kt
│ └── TrueSheetStateEvents.kt
└── utils/
└── ScreenUtils.kt
common/cpp/react/renderer/components/TrueSheetSpec/
├── TrueSheetViewState.cpp/.h
├── TrueSheetViewShadowNode.cpp/.h
└── TrueSheetViewComponentDescriptor.h
```
## View Hierarchy
```
TrueSheetView (host view - hidden, manages state)
└── TrueSheetContainerView (fills controller's view)
├── TrueSheetHeaderView (optional)
├── TrueSheetContentView
└── TrueSheetFooterView (optional)
```
## Common Tasks
### Adding a new prop
1. Add to `src/fabric/TrueSheetViewNativeComponent.ts`
2. Build the app (runs codegen)
3. Implement in `TrueSheetView.mm` (iOS) and `TrueSheetViewManager.kt` (Android)
### Adding a new event
1. Add `DirectEventHandler` to native component spec
2. Create event class in `ios/events/` and `android/.../events/`
3. Emit from native view
## Commands
See `package.json` scripts.

View File

@ -9,41 +9,83 @@ We want this community to be friendly and respectful to each other. Please follo
This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages:
- The library package in the root directory.
- An example app in the `example/` directory.
- A bare React Native example app in `example/bare/`.
- An Expo example app in `example/expo/`.
- Shared example code in `example/shared/`.
To get started with the project, run `yarn` in the root directory to install the required dependencies for each package:
To get started with the project, make sure you have the correct version of [Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for the version used in this project.
Run `yarn` in the root directory to install the required dependencies for each package:
```sh
yarn
```
> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development.
> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development without manually migrating.
The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make.
This will check that all required tools and dependencies are installed and configured correctly. If any issues are found, follow the recommended fixes or refer to the [React Native environment setup guide](https://reactnative.dev/docs/environment-setup).
It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app.
The example apps demonstrate usage of the library. You need to run them to test any changes you make.
If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/TrueSheetExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > TrueSheet`.
They are configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example apps. Changes to the library's JavaScript code will be reflected without a rebuild, but native code changes will require a rebuild.
To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-true-sheet` under `Android`.
### Bare React Native Example
Before running the bare example, verify that your development environment is properly configured by running:
```sh
yarn bare doctor
```
If you want to use Android Studio or Xcode to edit the native code, you can open `example/bare/android` or `example/bare/ios` respectively. To edit Objective-C files, open `example/bare/ios/TrueSheetExample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > react-native-true-sheet`.
To edit Kotlin files, open `example/bare/android` in Android Studio and find the source files at `react-native-true-sheet` under `Android`.
### Expo Example
The Expo example requires prebuilding before running on a device:
```sh
yarn expo prebuild
```
You can use various commands from the root directory to work with the project.
To start the packager:
To run the example app on Android:
To start the packager for the bare example:
```sh
yarn example android
yarn bare start
```
To run the example app on iOS:
To run the bare example on Android:
```sh
yarn example ios
yarn bare android
```
Make sure your code passes TypeScript and ESLint. Run the following to verify:
To run the bare example on iOS:
```sh
yarn bare ios
```
Similarly, for the Expo example:
```sh
yarn expo start
yarn expo android
yarn expo ios
```
To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this:
```sh
Running "TrueSheetExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1}
```
Note the `"fabric":true` and `"concurrentRoot":true` properties.
Make sure your code passes TypeScript and ESLint. Run the following to verify and fix:
```sh
yarn tidy
@ -62,19 +104,12 @@ We follow the [conventional commits specification](https://www.conventionalcommi
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module.
- `refactor`: code refactor, e.g. migrate from class components to hooks.
- `docs`: changes into documentation, e.g. add usage example for the module..
- `docs`: changes into documentation, e.g. add usage example for the module.
- `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config.
Our pre-commit hooks verify that your commit message matches this format when committing.
### Linting and tests
[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing.
Our pre-commit hooks verify that the linter and tests pass when committing.
### Publishing to npm
@ -86,19 +121,21 @@ To publish new versions, run the following:
yarn release
```
### Scripts
The `package.json` file contains various scripts for common tasks:
- `yarn`: setup project by installing dependencies.
- `yarn typecheck`: type-check files with TypeScript.
- `yarn lint`: lint files with ESLint.
- `yarn format`: format files with Prettier.
- `yarn test`: run unit tests with Jest.
- `yarn tidy`: run `typecheck`, `lint`, and `format`.
- `yarn example start`: start the Metro server for the example app.
- `yarn example android`: run the example app on Android.
- `yarn example ios`: run the example app on iOS.
- `yarn lint`: lint files with [ESLint](https://eslint.org/).
- `yarn test`: run unit tests with [Jest](https://jestjs.io/).
- `yarn bare start`: start the Metro server for the bare example.
- `yarn bare android`: run the bare example on Android.
- `yarn bare ios`: run the bare example on iOS.
- `yarn expo start`: start the Metro server for the Expo example.
- `yarn expo android`: run the Expo example on Android.
- `yarn expo ios`: run the Expo example on iOS.
### Sending a pull request

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 lodev09
Copyright (c) 2025 lodev09
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights

View File

@ -1,43 +1,59 @@
# React Native True Sheet
[![CI](https://github.com/lodev09/react-native-true-sheet/actions/workflows/ci.yml/badge.svg)](https://github.com/lodev09/react-native-true-sheet/actions/workflows/ci.yml)
[![Maintainability](https://api.codeclimate.com/v1/badges/0bd49973c6c61d85e2be/maintainability)](https://codeclimate.com/github/lodev09/react-native-true-sheet/maintainability)
[![CI](https://github.com/lodev09/react-native-true-sheet/actions/workflows/checks.yml/badge.svg)](https://github.com/lodev09/react-native-true-sheet/actions/workflows/checks.yml)
[![NPM Downloads](https://img.shields.io/npm/d18m/%40lodev09%2Freact-native-true-sheet)](https://www.npmjs.com/package/@lodev09/react-native-true-sheet)
> [!NOTE]
> 🎉 **Version 3.0 is here!** Completely rebuilt for Fabric with new features like automatic ScrollView detection, native headers/footers, sheet stacking, and more. [Read the announcement](https://sheet.lodev09.com/blog/release-3-0)
The true native bottom sheet experience for your React Native Apps. 💩
<img alt="React Native True Sheet - IOS" src="docs/static/img/preview.gif" width="300" height="600" /><img alt="React Native True Sheet - Android" src="docs/static/img/preview-2.gif" width="300" height="600" />
<img alt="React Native True Sheet - IOS" src="docs/static/img/preview-ios.gif" width="248" height="500" /><img alt="React Native True Sheet - Android" src="docs/static/img/preview-android.gif" width="248" height="500" /><img alt="React Native True Sheet - Web" src="docs/static/img/preview-web.gif" width="248" height="500" />
## Features
* Implemented in the native realm.
* Clean, fast, and lightweight.
* Asynchronus `ref` [methods](https://sheet.lodev09.com/reference/methods#ref-methods).
* Bonus! [Blur](https://sheet.lodev09.com/reference/types#blurtint) support on IOS 😎
* ⚡ **Powered by Fabric** - Built on React Native's new architecture for maximum performance
* 🚀 **Fully Native** - Implemented in the native realm, zero JS hacks
* ♿ **Accessible** - Native accessibility and screen reader support out of the box
* 🔄 **Flexible API** - Use [imperative methods](https://sheet.lodev09.com/reference/methods#ref-methods) or [lifecycle events](https://sheet.lodev09.com/reference/events)
* 🪟 **Liquid Glass** - [iOS 26+ Liquid Glass](https://sheet.lodev09.com/guides/liquid-glass) support out of the box, featured in [Expo Blog](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets)
* 🐎 **Reanimated** - First-class support for [react-native-reanimated](https://sheet.lodev09.com/guides/reanimated)
* 🧭 **React Navigation** - Built-in [sheet navigator](https://sheet.lodev09.com/guides/navigation) for seamless navigation integration
* 🌐 **Web Support** - Full [web support](https://sheet.lodev09.com/guides/web) out of the box
## Installation
You can install the package by using either `yarn` or `npm`.
> [!IMPORTANT]
> **Version 3.0+ requires React Native's New Architecture (Fabric)**
> For the old architecture, use version 2.x. See the [Migration Guide](https://sheet.lodev09.com/migration) for upgrading.
### Prerequisites
- React Native >= 0.76 (Expo SDK 52+)
- New Architecture enabled (default in RN 0.76+)
### Expo
```sh
npx expo install @lodev09/react-native-true-sheet
```
### Bare React Native
```sh
yarn add @lodev09/react-native-true-sheet
```
```sh
npm i @lodev09/react-native-true-sheet
```
Next, run the following to install it on IOS.
```sh
cd ios && pod install
```
## Documentation
- [Example](example)
- [Guides](https://sheet.lodev09.com/category/guides)
- [Reference](https://sheet.lodev09.com/category/reference)
- [Configuration](https://sheet.lodev09.com/reference/configuration)
- [Lifecycle Events](https://sheet.lodev09.com/reference/events)
- [React Navigation](https://sheet.lodev09.com/guides/navigation)
- [Troubleshooting](https://sheet.lodev09.com/troubleshooting)
- [Testing with Jest](https://sheet.lodev09.com/guides/jest)
- [Migrating to v3](https://sheet.lodev09.com/migration)
## Usage
@ -64,8 +80,7 @@ export const App = () => {
<Button onPress={present} title="Present" />
<TrueSheet
ref={sheet}
sizes={['auto', 'large']}
cornerRadius={24}
detents={['auto', 1]}
>
<Button onPress={dismiss} title="Dismiss" />
</TrueSheet>
@ -74,6 +89,31 @@ export const App = () => {
}
```
## Testing
TrueSheet exports mocks for easy testing:
```tsx
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
All methods (`present`, `dismiss`, `resize`) are mocked as Jest functions, allowing you to test your components without native dependencies.
**[Full Testing Guide](https://sheet.lodev09.com/guides/jest)**
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

24
RNTrueSheet.podspec Normal file
View File

@ -0,0 +1,24 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "RNTrueSheet"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "https://github.com/lodev09/react-native-true-sheet.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "common/cpp/**/*.{cpp,h}"
s.private_header_files = "ios/**/*.h"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\""
}
install_modules_dependencies(s)
end

View File

@ -1,41 +0,0 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "TrueSheet"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => "https://github.com/lodev09/react-native-true-sheet.git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,swift}"
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
end
end
end

View File

@ -1,6 +1,7 @@
buildscript {
// Buildscript is evaluated before everything else so we can't use getExtOrDefault
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["TrueSheet_kotlinVersion"]
ext.getExtOrDefault = {name ->
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['TrueSheet_' + name]
}
repositories {
google()
@ -8,31 +9,26 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
classpath "com.android.tools.build:gradle:8.7.2"
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
}
}
def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["TrueSheet_" + name]
}
apply plugin: "com.facebook.react"
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TrueSheet_" + name]).toInteger()
}
def getEdgeToEdgeEnabled() {
def edgeToEdge = project.findProperty("edgeToEdgeEnabled")
return edgeToEdge != null ? edgeToEdge.toBoolean() : false
}
def supportsNamespace() {
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
def major = parsed[0].toInteger()
@ -58,7 +54,7 @@ android {
defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "EDGE_TO_EDGE_ENABLED", "${getEdgeToEdgeEnabled()}"
}
buildTypes {
@ -72,8 +68,25 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
buildConfig = true
}
sourceSets {
main {
java.srcDirs += [
"generated/java",
"generated/jni"
]
}
}
}
@ -85,11 +98,8 @@ repositories {
def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "com.google.android.material:material:1.11.0"
implementation "com.google.android.material:material:1.12.0"
}

View File

@ -1,5 +1,5 @@
TrueSheet_kotlinVersion=1.7.0
TrueSheet_minSdkVersion=21
TrueSheet_targetSdkVersion=31
TrueSheet_compileSdkVersion=31
TrueSheet_ndkversion=21.4.7075529
TrueSheet_kotlinVersion=2.0.21
TrueSheet_minSdkVersion=24
TrueSheet_targetSdkVersion=34
TrueSheet_compileSdkVersion=35
TrueSheet_ndkVersion=27.1.12297006

View File

@ -0,0 +1,106 @@
package com.lodev09.truesheet
import android.annotation.SuppressLint
import android.view.View
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup
interface TrueSheetContainerViewDelegate {
fun containerViewContentDidChangeSize(width: Int, height: Int)
fun containerViewHeaderDidChangeSize(width: Int, height: Int)
fun containerViewFooterDidChangeSize(width: Int, height: Int)
}
/**
* Container view that manages the sheet's content, header, and footer views.
* Size changes are forwarded to the delegate for sheet reconfiguration.
*/
@SuppressLint("ViewConstructor")
class TrueSheetContainerView(reactContext: ThemedReactContext) :
ReactViewGroup(reactContext),
TrueSheetContentViewDelegate,
TrueSheetHeaderViewDelegate,
TrueSheetFooterViewDelegate {
var delegate: TrueSheetContainerViewDelegate? = null
var contentView: TrueSheetContentView? = null
var headerView: TrueSheetHeaderView? = null
var footerView: TrueSheetFooterView? = null
var contentHeight: Int = 0
var headerHeight: Int = 0
var footerHeight: Int = 0
init {
// Allow footer to position outside container bounds
clipChildren = false
clipToPadding = false
}
override fun addView(child: View?, index: Int) {
super.addView(child, index)
when (child) {
is TrueSheetContentView -> {
child.delegate = this
contentView = child
}
is TrueSheetHeaderView -> {
child.delegate = this
headerView = child
}
is TrueSheetFooterView -> {
child.delegate = this
footerView = child
}
}
}
override fun removeViewAt(index: Int) {
when (val view = getChildAt(index)) {
is TrueSheetContentView -> {
view.delegate = null
contentView = null
contentViewDidChangeSize(0, 0)
}
is TrueSheetHeaderView -> {
view.delegate = null
headerView = null
headerViewDidChangeSize(0, 0)
}
is TrueSheetFooterView -> {
view.delegate = null
footerView = null
footerViewDidChangeSize(0, 0)
}
}
super.removeViewAt(index)
}
// ==================== Delegate Implementations ====================
override fun contentViewDidChangeSize(width: Int, height: Int) {
contentHeight = height
delegate?.containerViewContentDidChangeSize(width, height)
}
override fun headerViewDidChangeSize(width: Int, height: Int) {
headerHeight = height
delegate?.containerViewHeaderDidChangeSize(width, height)
}
override fun footerViewDidChangeSize(width: Int, height: Int) {
footerHeight = height
delegate?.containerViewFooterDidChangeSize(width, height)
}
companion object {
const val TAG_NAME = "TrueSheet"
}
}

View File

@ -0,0 +1,21 @@
package com.lodev09.truesheet
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
/**
* ViewManager for TrueSheetContainerView
* Container that holds content and footer views
*/
@ReactModule(name = TrueSheetContainerViewManager.REACT_CLASS)
class TrueSheetContainerViewManager : ViewGroupManager<TrueSheetContainerView>() {
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContainerView = TrueSheetContainerView(reactContext)
companion object {
const val REACT_CLASS = "TrueSheetContainerView"
}
}

View File

@ -0,0 +1,38 @@
package com.lodev09.truesheet
import android.annotation.SuppressLint
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup
/**
* Delegate interface for content view size changes
*/
interface TrueSheetContentViewDelegate {
fun contentViewDidChangeSize(width: Int, height: Int)
}
/**
* Content view that holds the main sheet content
* This is the first child of TrueSheetContainerView
*/
@SuppressLint("ViewConstructor")
class TrueSheetContentView(context: ThemedReactContext) : ReactViewGroup(context) {
var delegate: TrueSheetContentViewDelegate? = null
private var lastWidth = 0
private var lastHeight = 0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != lastWidth || h != lastHeight) {
lastWidth = w
lastHeight = h
delegate?.contentViewDidChangeSize(w, h)
}
}
companion object {
const val TAG_NAME = "TrueSheet"
}
}

View File

@ -0,0 +1,21 @@
package com.lodev09.truesheet
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
/**
* ViewManager for TrueSheetContentView
* Manages the main content area of the sheet
*/
@ReactModule(name = TrueSheetContentViewManager.REACT_CLASS)
class TrueSheetContentViewManager : ViewGroupManager<TrueSheetContentView>() {
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetContentView = TrueSheetContentView(reactContext)
companion object {
const val REACT_CLASS = "TrueSheetContentView"
}
}

View File

@ -1,330 +0,0 @@
package com.lodev09.truesheet
import android.annotation.SuppressLint
import android.graphics.Color
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import com.facebook.react.uimanager.ThemedReactContext
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lodev09.truesheet.core.KeyboardManager
import com.lodev09.truesheet.core.RootSheetView
import com.lodev09.truesheet.core.Utils
data class SizeInfo(val index: Int, val value: Float)
@SuppressLint("ClickableViewAccessibility")
class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: RootSheetView) :
BottomSheetDialog(reactContext) {
private var keyboardManager = KeyboardManager(reactContext)
private var sheetView: ViewGroup
private var windowAnimation: Int = 0
/**
* Specify whether the sheet background is dimmed.
* Set to `false` to allow interaction with the background components.
*/
var dimmed = true
/**
* The size index that the sheet should start to dim the background.
* This is ignored if `dimmed` is set to `false`.
*/
var dimmedIndex = 0
/**
* The maximum window height
*/
var maxScreenHeight = 0
var contentHeight = 0
var footerHeight = 0
var maxSheetHeight: Int? = null
var dismissible: Boolean = true
set(value) {
field = value
setCanceledOnTouchOutside(value)
setCancelable(value)
behavior.isHideable = value
}
var footerView: ViewGroup? = null
var sizes: Array<Any> = arrayOf("medium", "large")
init {
setContentView(rootSheetView)
sheetView = rootSheetView.parent as ViewGroup
sheetView.setBackgroundColor(Color.TRANSPARENT)
// Setup window params to adjust layout based on Keyboard state
window?.apply {
// Store current windowAnimation value to toggle later
windowAnimation = attributes.windowAnimations
}
// Update the usable sheet height
maxScreenHeight = Utils.screenHeight(reactContext)
}
/**
* Setup dimmed sheet.
* `dimmedIndex` will further customize the dimming behavior.
*/
fun setupDimmedBackground(sizeIndex: Int) {
window?.apply {
val view = findViewById<View>(com.google.android.material.R.id.touch_outside)
if (dimmed && sizeIndex >= dimmedIndex) {
// Remove touch listener
view.setOnTouchListener(null)
// Add the dimmed background
setFlags(
WindowManager.LayoutParams.FLAG_DIM_BEHIND,
WindowManager.LayoutParams.FLAG_DIM_BEHIND
)
setCanceledOnTouchOutside(dismissible)
} else {
// Override the background touch and pass it to the components outside
view.setOnTouchListener { v, event ->
event.setLocation(event.rawX - v.x, event.rawY - v.y)
reactContext.currentActivity?.dispatchTouchEvent(event)
false
}
// Remove the dimmed background
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
setCanceledOnTouchOutside(false)
}
}
}
fun resetAnimation() {
window?.apply {
setWindowAnimations(windowAnimation)
}
}
/**
* Present the sheet.
*/
fun present(sizeIndex: Int, animated: Boolean = true) {
setupDimmedBackground(sizeIndex)
if (isShowing) {
setStateForSizeIndex(sizeIndex)
} else {
configure()
setStateForSizeIndex(sizeIndex)
if (!animated) {
// Disable animation
window?.setWindowAnimations(0)
}
show()
}
}
fun positionFooter() {
footerView?.apply {
y = (maxScreenHeight - sheetView.top - footerHeight).toFloat()
}
}
/**
* Set the state based for the given size index.
*/
private fun setStateForSizeIndex(index: Int) {
behavior.state = getStateForSizeIndex(index)
}
/**
* Get the height value based on the size config value.
*/
private fun getSizeHeight(size: Any): Int {
val height =
when (size) {
is Double -> Utils.toPixel(size)
is Int -> Utils.toPixel(size.toDouble())
is String -> {
when (size) {
"auto" -> contentHeight + footerHeight
"large" -> maxScreenHeight
"medium" -> (maxScreenHeight * 0.50).toInt()
"small" -> (maxScreenHeight * 0.25).toInt()
else -> {
if (size.endsWith('%')) {
val percent = size.trim('%').toDoubleOrNull()
if (percent == null) {
0
} else {
((percent / 100) * maxScreenHeight).toInt()
}
} else {
val fixedHeight = size.toDoubleOrNull()
if (fixedHeight == null) {
0
} else {
Utils.toPixel(fixedHeight)
}
}
}
}
}
else -> (maxScreenHeight * 0.5).toInt()
}
return maxSheetHeight?.let { minOf(height, it, maxScreenHeight) } ?: minOf(height, maxScreenHeight)
}
/**
* Determines the state based from the given size index.
*/
private fun getStateForSizeIndex(index: Int) =
when (sizes.size) {
1 -> BottomSheetBehavior.STATE_EXPANDED
2 -> {
when (index) {
0 -> BottomSheetBehavior.STATE_COLLAPSED
1 -> BottomSheetBehavior.STATE_EXPANDED
else -> BottomSheetBehavior.STATE_HIDDEN
}
}
3 -> {
when (index) {
0 -> BottomSheetBehavior.STATE_COLLAPSED
1 -> BottomSheetBehavior.STATE_HALF_EXPANDED
2 -> BottomSheetBehavior.STATE_EXPANDED
else -> BottomSheetBehavior.STATE_HIDDEN
}
}
else -> BottomSheetBehavior.STATE_HIDDEN
}
/**
* Handle keyboard state changes and adjust maxScreenHeight (sheet max height) accordingly.
* Also update footer's Y position.
*/
fun registerKeyboardManager() {
keyboardManager.registerKeyboardListener(object : KeyboardManager.OnKeyboardChangeListener {
override fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?) {
maxScreenHeight = when (isVisible) {
true -> visibleHeight ?: 0
else -> Utils.screenHeight(reactContext)
}
positionFooter()
}
})
}
fun setOnSizeChangeListener(listener: RootSheetView.OnSizeChangeListener) {
rootSheetView.setOnSizeChangeListener(listener)
}
/**
* Remove keyboard listener.
*/
fun unregisterKeyboardManager() {
keyboardManager.unregisterKeyboardListener()
}
/**
* Configure the sheet based from the size preference.
*/
fun configure() {
// Configure sheet sizes
behavior.apply {
skipCollapsed = false
isFitToContents = true
// m3 max width 640dp
maxWidth = Utils.toPixel(640.0)
when (sizes.size) {
1 -> {
maxHeight = getSizeHeight(sizes[0])
skipCollapsed = true
}
2 -> {
setPeekHeight(getSizeHeight(sizes[0]), isShowing)
maxHeight = getSizeHeight(sizes[1])
}
3 -> {
// Enables half expanded
isFitToContents = false
setPeekHeight(getSizeHeight(sizes[0]), isShowing)
halfExpandedRatio = minOf(getSizeHeight(sizes[1]).toFloat() / maxScreenHeight.toFloat(), 1.0f)
maxHeight = getSizeHeight(sizes[2])
}
}
}
}
/**
* Get the SizeInfo data by state.
*/
fun getSizeInfoForState(state: Int): SizeInfo? =
when (sizes.size) {
1 -> {
when (state) {
BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(behavior.maxHeight))
else -> null
}
}
2 -> {
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(behavior.maxHeight))
else -> null
}
}
3 -> {
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight))
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
val height = behavior.halfExpandedRatio * maxScreenHeight
SizeInfo(1, Utils.toDIP(height.toInt()))
}
BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(behavior.maxHeight))
else -> null
}
}
else -> null
}
/**
* Get SizeInfo data for given size index.
*/
fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f)
companion object {
const val TAG = "TrueSheetView"
}
}

View File

@ -0,0 +1,103 @@
package com.lodev09.truesheet
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import com.facebook.react.uimanager.JSPointerDispatcher
import com.facebook.react.uimanager.JSTouchDispatcher
import com.facebook.react.uimanager.RootView
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.views.view.ReactViewGroup
/**
* Delegate interface for footer view size changes
*/
interface TrueSheetFooterViewDelegate {
fun footerViewDidChangeSize(width: Int, height: Int)
}
/**
* Footer view that holds the footer content
* This is the second child of TrueSheetContainerView
* Positioned absolutely at the bottom of the sheet
*
* Implements RootView to handle touch events when positioned outside parent bounds.
*/
@SuppressLint("ViewConstructor")
class TrueSheetFooterView(private val reactContext: ThemedReactContext) :
ReactViewGroup(reactContext),
RootView {
var delegate: TrueSheetFooterViewDelegate? = null
var eventDispatcher: EventDispatcher? = null
private var lastWidth = 0
private var lastHeight = 0
private val jsTouchDispatcher = JSTouchDispatcher(this)
private var jsPointerDispatcher: JSPointerDispatcher? = null
init {
jsPointerDispatcher = JSPointerDispatcher(this)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != lastWidth || h != lastHeight) {
lastWidth = w
lastHeight = h
delegate?.footerViewDidChangeSize(w, h)
}
}
// ==================== RootView Implementation ====================
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { dispatcher ->
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
}
return super.onInterceptTouchEvent(event)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { dispatcher ->
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
}
super.onTouchEvent(event)
return true
}
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
return super.onHoverEvent(event)
}
override fun onHoverEvent(event: MotionEvent): Boolean {
eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
return super.onHoverEvent(event)
}
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
eventDispatcher?.let { dispatcher ->
jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
}
}
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
eventDispatcher?.let { jsTouchDispatcher.onChildEndedNativeGesture(ev, it) }
jsPointerDispatcher?.onChildEndedNativeGesture()
}
override fun handleException(t: Throwable) {
reactContext.reactApplicationContext.handleException(RuntimeException(t))
}
companion object {
const val TAG_NAME = "TrueSheet"
}
}

View File

@ -0,0 +1,21 @@
package com.lodev09.truesheet
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
/**
* ViewManager for TrueSheetFooterView
* Manages the footer area of the sheet
*/
@ReactModule(name = TrueSheetFooterViewManager.REACT_CLASS)
class TrueSheetFooterViewManager : ViewGroupManager<TrueSheetFooterView>() {
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetFooterView = TrueSheetFooterView(reactContext)
companion object {
const val REACT_CLASS = "TrueSheetFooterView"
}
}

View File

@ -0,0 +1,38 @@
package com.lodev09.truesheet
import android.annotation.SuppressLint
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup
/**
* Delegate interface for header view size changes
*/
interface TrueSheetHeaderViewDelegate {
fun headerViewDidChangeSize(width: Int, height: Int)
}
/**
* Header view that holds the header content
* Positioned at the top of the sheet content area
*/
@SuppressLint("ViewConstructor")
class TrueSheetHeaderView(context: ThemedReactContext) : ReactViewGroup(context) {
var delegate: TrueSheetHeaderViewDelegate? = null
private var lastWidth = 0
private var lastHeight = 0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != lastWidth || h != lastHeight) {
lastWidth = w
lastHeight = h
delegate?.headerViewDidChangeSize(w, h)
}
}
companion object {
const val TAG_NAME = "TrueSheet"
}
}

View File

@ -0,0 +1,21 @@
package com.lodev09.truesheet
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
/**
* ViewManager for TrueSheetHeaderView
* Manages the header area of the sheet
*/
@ReactModule(name = TrueSheetHeaderViewManager.REACT_CLASS)
class TrueSheetHeaderViewManager : ViewGroupManager<TrueSheetHeaderView>() {
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetHeaderView = TrueSheetHeaderView(reactContext)
companion object {
const val REACT_CLASS = "TrueSheetHeaderView"
}
}

View File

@ -0,0 +1,170 @@
package com.lodev09.truesheet
import android.os.Handler
import android.os.Looper
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.facebook.react.uimanager.UIManagerHelper
import com.lodev09.truesheet.core.TrueSheetStackManager
import java.util.concurrent.ConcurrentHashMap
/**
* TurboModule for TrueSheet imperative API
* Provides promise-based async operations using view references
*/
@ReactModule(name = TrueSheetModule.NAME)
class TrueSheetModule(reactContext: ReactApplicationContext) :
com.facebook.react.bridge.ReactContextBaseJavaModule(reactContext),
TurboModule {
override fun getName(): String = NAME
override fun invalidate() {
super.invalidate()
// Clear all registered views and observer on module invalidation
synchronized(viewRegistry) {
viewRegistry.clear()
}
TrueSheetStackManager.clear()
}
/**
* Present a sheet by reference
*
* @param viewTag Native view tag of the sheet component
* @param index Detent index to present at
* @param promise Promise that resolves when sheet is fully presented
* @throws VIEW_NOT_FOUND if the view with the given tag is not found
* @throws INVALID_VIEW_TYPE if the view is not a TrueSheetView
* @throws OPERATION_FAILED if the operation fails for any other reason
*/
@ReactMethod
fun presentByRef(viewTag: Double, index: Double, animated: Boolean, promise: Promise) {
val tag = viewTag.toInt()
val detentIndex = index.toInt()
withTrueSheetView(tag, promise) { view ->
view.present(detentIndex, animated) {
promise.resolve(null)
}
}
}
/**
* Dismiss a sheet by reference
*
* @param viewTag Native view tag of the sheet component
* @param promise Promise that resolves when sheet is fully dismissed
* @throws VIEW_NOT_FOUND if the view with the given tag is not found
* @throws INVALID_VIEW_TYPE if the view is not a TrueSheetView
* @throws OPERATION_FAILED if the operation fails for any other reason
*/
@ReactMethod
fun dismissByRef(viewTag: Double, animated: Boolean, promise: Promise) {
val tag = viewTag.toInt()
withTrueSheetView(tag, promise) { view ->
view.dismiss(animated) {
promise.resolve(null)
}
}
}
/**
* Resize a sheet to a different index by reference
*
* @param viewTag Native view tag of the sheet component
* @param index New detent index
* @param promise Promise that resolves when resize is complete
* @throws VIEW_NOT_FOUND if the view with the given tag is not found
* @throws INVALID_VIEW_TYPE if the view is not a TrueSheetView
* @throws OPERATION_FAILED if the operation fails for any other reason
*/
@ReactMethod
fun resizeByRef(viewTag: Double, index: Double, promise: Promise) {
val tag = viewTag.toInt()
val detentIndex = index.toInt()
withTrueSheetView(tag, promise) { view ->
view.resize(detentIndex) {
promise.resolve(null)
}
}
}
/**
* Helper method to get TrueSheetView by tag and execute closure
*/
private fun withTrueSheetView(tag: Int, promise: Promise, closure: (view: TrueSheetView) -> Unit) {
Handler(Looper.getMainLooper()).post {
try {
// First try to get from registry (faster)
var view = getSheetByTag(tag)
// Fallback to UIManager resolution
if (view == null) {
val manager = UIManagerHelper.getUIManagerForReactTag(reactApplicationContext, tag)
val resolvedView = manager?.resolveView(tag)
if (resolvedView is TrueSheetView) {
view = resolvedView
} else if (resolvedView != null) {
promise.reject(
"INVALID_VIEW_TYPE",
"View with tag $tag is not a TrueSheetView (got ${resolvedView::class.simpleName})"
)
return@post
}
}
if (view == null) {
promise.reject("VIEW_NOT_FOUND", "TrueSheetView with tag $tag not found")
return@post
}
closure(view)
} catch (e: Exception) {
promise.reject("OPERATION_FAILED", "Failed to execute operation: ${e.message}", e)
}
}
}
companion object {
const val NAME = "TrueSheetModule"
/**
* Registry to keep track of TrueSheetView instances by their view tag
* This provides fast lookup for ref-based operations
*/
private val viewRegistry = ConcurrentHashMap<Int, TrueSheetView>()
/**
* Register a TrueSheetView instance
* Called automatically by TrueSheetView during initialization
*/
@JvmStatic
fun registerView(view: TrueSheetView, tag: Int) {
viewRegistry[tag] = view
}
/**
* Unregister a TrueSheetView instance
* Called automatically by TrueSheetView during cleanup
*/
@JvmStatic
fun unregisterView(tag: Int) {
viewRegistry.remove(tag)
}
/**
* Get a TrueSheetView by its tag
* @param tag - The React native tag of the view
* @return The TrueSheetView instance, or null if not found
*/
@JvmStatic
fun getSheetByTag(tag: Int): TrueSheetView? = viewRegistry[tag]
}
}

View File

@ -1,12 +1,45 @@
package com.lodev09.truesheet
import com.facebook.react.ReactPackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
class TrueSheetPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(TrueSheetViewModule(reactContext))
/**
* TrueSheet package for Fabric architecture
* Registers all view managers and the TurboModule
*/
class TrueSheetPackage : TurboReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = listOf(TrueSheetViewManager())
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
when (name) {
TrueSheetModule.NAME -> TrueSheetModule(reactContext)
else -> null
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
ReactModuleInfoProvider {
mapOf(
TrueSheetModule.NAME to ReactModuleInfo(
TrueSheetModule.NAME,
TrueSheetModule::class.java.name,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
)
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
listOf(
TrueSheetViewManager(),
TrueSheetContainerViewManager(),
TrueSheetContentViewManager(),
TrueSheetHeaderViewManager(),
TrueSheetFooterViewManager()
)
}

View File

@ -1,320 +1,500 @@
package com.lodev09.truesheet
import android.content.Context
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import android.view.ViewStructure
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.UiThread
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.EventDispatcher
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.lodev09.truesheet.core.RootSheetView
import com.lodev09.truesheet.events.DismissEvent
import com.lodev09.truesheet.events.MountEvent
import com.lodev09.truesheet.events.PresentEvent
import com.lodev09.truesheet.events.SizeChangeEvent
import com.facebook.react.util.RNLog
import com.facebook.react.views.view.ReactViewGroup
import com.lodev09.truesheet.core.GrabberOptions
import com.lodev09.truesheet.core.TrueSheetStackManager
import com.lodev09.truesheet.events.*
class TrueSheetView(context: Context) :
ViewGroup(context),
LifecycleEventListener {
private var eventDispatcher: EventDispatcher? = null
/**
* Main TrueSheet host view that manages the sheet and dispatches events to JavaScript.
* This view is hidden (GONE) and delegates all rendering to TrueSheetViewController
* using a CoordinatorLayout approach (no separate dialog window).
*/
@SuppressLint("ViewConstructor")
class TrueSheetView(private val reactContext: ThemedReactContext) :
ReactViewGroup(reactContext),
LifecycleEventListener,
TrueSheetViewControllerDelegate,
TrueSheetContainerViewDelegate {
private val reactContext: ThemedReactContext
get() = context as ThemedReactContext
companion object {
const val TAG_NAME = "TrueSheet"
}
private val surfaceId: Int
get() = UIManagerHelper.getSurfaceId(this)
// ==================== Properties ====================
var initialIndex: Int = -1
var initialIndexAnimated: Boolean = true
internal val viewController: TrueSheetViewController = TrueSheetViewController(reactContext)
/**
* Current activeIndex.
*/
private var currentSizeIndex: Int = 0
private val containerView: TrueSheetContainerView?
get() = viewController.getChildAt(0) as? TrueSheetContainerView
/**
* Promise callback to be invoked after `present` is called.
*/
private var presentPromise: (() -> Unit)? = null
var eventDispatcher: EventDispatcher? = null
/**
* Promise callback to be invoked after `dismiss` is called.
*/
private var dismissPromise: (() -> Unit)? = null
// Initial present configuration (set by ViewManager before mount)
var initialDetentIndex: Int = -1
var initialDetentAnimated: Boolean = true
private var didInitiallyPresent: Boolean = false
/**
* The main BottomSheetDialog instance.
*/
private val sheetDialog: TrueSheetDialog
var stateWrapper: StateWrapper? = null
set(value) {
// On first state wrapper assignment, immediately update state with screen dimensions.
// This ensures Yoga has initial width/height for content layout before presenting.
if (field == null && value != null) {
updateState(viewController.screenWidth, viewController.screenHeight)
}
field = value
}
/**
* React root view placeholder.
*/
private val rootSheetView: RootSheetView
private var lastContainerWidth: Int = 0
private var lastContainerHeight: Int = 0
// Debounce flag to coalesce rapid layout changes into a single sheet update
private var isSheetUpdatePending: Boolean = false
// Root container for the coordinator layout (activity or Modal dialog content view)
private var rootContainerView: ViewGroup? = null
// ==================== Initialization ====================
init {
reactContext.addLifecycleEventListener(this)
eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
viewController.delegate = this
rootSheetView = RootSheetView(context)
rootSheetView.eventDispatcher = eventDispatcher
sheetDialog = TrueSheetDialog(reactContext, rootSheetView)
// Configure Sheet Dialog
sheetDialog.apply {
// Setup listener when the dialog has been presented.
setOnShowListener {
registerKeyboardManager()
// Initialize footer y
UiThreadUtil.runOnUiThread {
positionFooter()
}
// Re-enable animation
resetAnimation()
// Resolve the present promise
presentPromise?.let { promise ->
promise()
presentPromise = null
}
// Dispatch onPresent event
eventDispatcher?.dispatchEvent(PresentEvent(surfaceId, id, sheetDialog.getSizeInfoForIndex(currentSizeIndex)))
}
// Setup listener when the dialog has been dismissed.
setOnDismissListener {
unregisterKeyboardManager()
// Resolve the dismiss promise
dismissPromise?.let { promise ->
promise()
dismissPromise = null
}
// Dispatch onDismiss event
eventDispatcher?.dispatchEvent(DismissEvent(surfaceId, id))
}
// Configure sheet behavior events
behavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onSlide(sheetView: View, slideOffset: Float) {
footerView?.let {
val y = (maxScreenHeight - sheetView.top - footerHeight).toFloat()
if (slideOffset >= 0) {
// Sheet is expanding
it.y = y
} else {
// Sheet is collapsing
it.y = y - footerHeight * slideOffset
}
}
}
override fun onStateChanged(view: View, newState: Int) {
if (!isShowing) return
val sizeInfo = getSizeInfoForState(newState)
if (sizeInfo == null || sizeInfo.index == currentSizeIndex) return
// Invoke promise when sheet resized programmatically
presentPromise?.let { promise ->
promise()
presentPromise = null
}
currentSizeIndex = sizeInfo.index
setupDimmedBackground(sizeInfo.index)
// Dispatch onSizeChange event
eventDispatcher?.dispatchEvent(SizeChangeEvent(surfaceId, id, sizeInfo))
}
}
)
}
// Hide the host view - actual content is rendered in the dialog window
visibility = GONE
}
// ==================== ReactViewGroup Overrides ====================
override fun dispatchProvideStructure(structure: ViewStructure) {
rootSheetView.dispatchProvideStructure(structure)
super.dispatchProvideStructure(structure)
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int
left: Int,
top: Int,
right: Int,
bottom: Int
) {
// Do nothing as we are laid out by UIManager
// No-op: layout is managed by React Native's UIManager
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
sheetDialog.dismiss()
override fun setId(id: Int) {
super.setId(id)
viewController.id = id
TrueSheetModule.registerView(this, id)
}
override fun addView(child: View, index: Int) {
// Hide this host view
visibility = GONE
// ==================== View Hierarchy Management ====================
(child as ViewGroup).let {
// rootView's first child is the Container View
rootSheetView.addView(it, index)
override fun onAttachedToWindow() {
super.onAttachedToWindow()
// Initialize content
UiThreadUtil.runOnUiThread {
// 1st child is the content view
val contentView = it.getChildAt(0) as ViewGroup?
setContentHeight(contentView?.height ?: 0)
// 2nd child is the footer view
val footerView = it.getChildAt(1) as ViewGroup?
sheetDialog.footerView = footerView
setFooterHeight(footerView?.height ?: 0)
if (initialIndex >= 0) {
currentSizeIndex = initialIndex
sheetDialog.present(initialIndex, initialIndexAnimated)
}
// Dispatch onMount event
eventDispatcher?.dispatchEvent(MountEvent(surfaceId, id))
if (initialDetentIndex >= 0 && !didInitiallyPresent) {
didInitiallyPresent = true
if (initialDetentAnimated) {
present(initialDetentIndex, true) { }
} else {
post { present(initialDetentIndex, false) { } }
}
}
}
override fun getChildCount(): Int {
// This method may be called by the parent constructor
// before rootView is initialized.
return rootSheetView.childCount
override fun addView(child: View?, index: Int) {
viewController.addView(child, index)
if (child is TrueSheetContainerView) {
child.delegate = this
viewController.createSheet()
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(MountEvent(surfaceId, id))
}
}
override fun getChildAt(index: Int): View = rootSheetView.getChildAt(index)
override fun getChildCount(): Int = viewController.childCount
override fun removeView(child: View) {
rootSheetView.removeView(child)
}
override fun getChildAt(index: Int): View? = viewController.getChildAt(index)
override fun removeViewAt(index: Int) {
val child = getChildAt(index)
rootSheetView.removeView(child)
if (child is TrueSheetContainerView) {
child.delegate = null
}
viewController.removeView(child)
}
override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
// Explicitly override this to prevent accessibility events being passed down to children
// Those will be handled by the rootView which lives in the dialog
}
// Accessibility: delegate to dialog's host view since this view is hidden
override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {}
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean {
// Explicitly override this to prevent accessibility events being passed down to children
// Those will be handled by the rootView which lives in the dialog
return false
}
// ==================== Lifecycle ====================
override fun onHostResume() {
// do nothing
viewController.reapplyHiddenState()
finalizeUpdates()
}
override fun onHostPause() {
// do nothing
}
override fun onHostPause() {}
override fun onHostDestroy() {
// Drop the instance if the host is destroyed which will dismiss the dialog
reactContext.removeLifecycleEventListener(this)
sheetDialog.dismiss()
onDropInstance()
}
private fun configureIfShowing() {
if (sheetDialog.isShowing) {
sheetDialog.configure()
sheetDialog.positionFooter()
fun onDropInstance() {
reactContext.removeLifecycleEventListener(this)
if (viewController.isPresented) {
viewController.dismiss(animated = false)
}
TrueSheetModule.unregisterView(id)
TrueSheetStackManager.removeSheet(this)
viewController.delegate = null
didInitiallyPresent = false
}
/**
* Called by the ViewManager after all properties are set.
* Reconfigures the sheet if it's currently presented.
*/
fun finalizeUpdates() {
if (viewController.isPresented) {
viewController.sheetView?.setupBackground()
viewController.sheetView?.setupGrabber()
updateSheetIfNeeded()
}
}
// ==================== Property Setters ====================
fun setMaxHeight(height: Int) {
if (sheetDialog.maxSheetHeight == height) return
sheetDialog.maxSheetHeight = height
configureIfShowing()
}
fun setContentHeight(height: Int) {
if (sheetDialog.contentHeight == height) return
sheetDialog.contentHeight = height
configureIfShowing()
}
fun setFooterHeight(height: Int) {
if (sheetDialog.footerHeight == height) return
sheetDialog.footerHeight = height
configureIfShowing()
if (viewController.maxSheetHeight == height) return
viewController.maxSheetHeight = height
}
fun setDimmed(dimmed: Boolean) {
if (sheetDialog.dimmed == dimmed) return
sheetDialog.dimmed = dimmed
if (sheetDialog.isShowing) {
sheetDialog.setupDimmedBackground(currentSizeIndex)
if (viewController.dimmed == dimmed) return
viewController.dimmed = dimmed
if (viewController.isPresented) {
viewController.setupDimmedBackground(viewController.currentDetentIndex)
viewController.updateDimAmount()
}
}
fun setDimmedIndex(index: Int) {
if (sheetDialog.dimmedIndex == index) return
sheetDialog.dimmedIndex = index
if (sheetDialog.isShowing) {
sheetDialog.setupDimmedBackground(currentSizeIndex)
fun setDimmedDetentIndex(index: Int) {
if (viewController.dimmedDetentIndex == index) return
viewController.dimmedDetentIndex = index
if (viewController.isPresented) {
viewController.setupDimmedBackground(viewController.currentDetentIndex)
viewController.updateDimAmount()
}
}
fun setSoftInputMode(mode: Int) {
sheetDialog.window?.apply {
this.setSoftInputMode(mode)
}
fun setCornerRadius(radius: Float) {
if (viewController.sheetCornerRadius == radius) return
viewController.sheetCornerRadius = radius
}
fun setSheetBackgroundColor(color: Int?) {
if (viewController.sheetBackgroundColor == color) return
viewController.sheetBackgroundColor = color
}
fun setDismissible(dismissible: Boolean) {
sheetDialog.dismissible = dismissible
viewController.dismissible = dismissible
}
fun setSizes(newSizes: Array<Any>) {
sheetDialog.sizes = newSizes
configureIfShowing()
fun setDraggable(draggable: Boolean) {
viewController.draggable = draggable
}
fun setGrabber(grabber: Boolean) {
viewController.grabber = grabber
}
fun setGrabberOptions(options: GrabberOptions?) {
viewController.grabberOptions = options
}
fun setSheetElevation(elevation: Float) {
viewController.sheetElevation = elevation
}
fun setDetents(newDetents: MutableList<Double>) {
viewController.detents = newDetents
}
fun setInsetAdjustment(insetAdjustment: String) {
viewController.insetAdjustment = insetAdjustment
}
// ==================== State Management ====================
/**
* Present the sheet at given size index.
* Updates the Fabric state with container dimensions for Yoga layout.
* Converts pixel values to density-independent pixels (dp).
*/
fun present(sizeIndex: Int, promiseCallback: () -> Unit) {
if (!sheetDialog.isShowing) {
currentSizeIndex = sizeIndex
fun updateState(width: Int, height: Int) {
if (width == lastContainerWidth && height == lastContainerHeight) return
lastContainerWidth = width
lastContainerHeight = height
val sw = stateWrapper ?: return
val newStateData = WritableNativeMap()
newStateData.putDouble("containerWidth", width.toFloat().pxToDp().toDouble())
newStateData.putDouble("containerHeight", height.toFloat().pxToDp().toDouble())
sw.updateState(newStateData)
}
// ==================== Sheet Actions ====================
@UiThread
fun present(detentIndex: Int, animated: Boolean = true, promiseCallback: () -> Unit) {
if (!viewController.isPresented) {
// Attach coordinator to the root container
rootContainerView = findRootContainerView()
viewController.coordinatorLayout?.let { rootContainerView?.addView(it) }
// Register with observer to track sheet stack hierarchy
viewController.parentSheetView = TrueSheetStackManager.onSheetWillPresent(this, detentIndex)
}
viewController.presentPromise = promiseCallback
viewController.present(detentIndex, animated)
}
@UiThread
fun dismiss(animated: Boolean = true, promiseCallback: () -> Unit) {
// iOS-like behavior: calling dismiss on a presenting controller dismisses
// its presented controller (and everything above it), but NOT itself.
// See: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621505-dismiss
val sheetsAbove = TrueSheetStackManager.getSheetsAbove(this)
if (sheetsAbove.isNotEmpty()) {
for (sheet in sheetsAbove) {
sheet.viewController.dismiss(animated)
}
promiseCallback()
return
}
presentPromise = promiseCallback
sheetDialog.present(sizeIndex)
viewController.dismissPromise = promiseCallback
viewController.dismiss(animated)
}
@UiThread
fun resize(detentIndex: Int, promiseCallback: () -> Unit) {
if (!viewController.isPresented) {
RNLog.w(reactContext, "TrueSheet: Cannot resize. Sheet is not presented.")
promiseCallback()
return
}
present(detentIndex, true, promiseCallback)
}
/**
* Dismisses the sheet.
* Debounced sheet update to handle rapid content/header size changes.
* Uses post() to ensure all layout passes complete before reconfiguring.
*/
fun dismiss(promiseCallback: () -> Unit) {
dismissPromise = promiseCallback
sheetDialog.dismiss()
fun updateSheetIfNeeded() {
if (!viewController.isPresented) return
if (isSheetUpdatePending) return
isSheetUpdatePending = true
viewController.post {
isSheetUpdatePending = false
viewController.setupSheetDetentsForSizeChange()
TrueSheetStackManager.onSheetSizeChanged(this)
}
}
companion object {
const val TAG = "TrueSheetView"
// ==================== Sheet Stack Translation ====================
/**
* Updates this sheet's translation and disables dragging when a child sheet is presented.
* Parent sheets slide down to create a stacked appearance.
* Propagates additional translation to parent so the entire stack stays visually consistent.
*/
fun updateTranslationForChild(childSheetTop: Int) {
if (!viewController.isSheetVisible || viewController.isExpanded) return
viewController.sheetView?.behavior?.isDraggable = false
val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
val newTranslation = maxOf(0, childSheetTop - mySheetTop)
val additionalTranslation = newTranslation - viewController.currentTranslationY
viewController.translateSheet(newTranslation)
// Propagate any additional translation up the stack
if (additionalTranslation > 0) {
TrueSheetStackManager.getParentSheet(this)?.addTranslation(additionalTranslation)
}
}
/**
* Recursively adds translation to this sheet and all parent sheets.
*/
private fun addTranslation(amount: Int) {
if (viewController.isExpanded) return
viewController.translateSheet(viewController.currentTranslationY + amount)
TrueSheetStackManager.getParentSheet(this)?.addTranslation(amount)
}
/**
* Resets this sheet's translation and restores dragging when it becomes topmost.
* Parent recalculates its translation based on this sheet's position.
*/
fun resetTranslation() {
viewController.sheetView?.behavior?.isDraggable = viewController.draggable
viewController.translateSheet(0)
// Parent should recalculate its translation based on this sheet's position
val mySheetTop = viewController.detentCalculator.getSheetTopForDetentIndex(viewController.currentDetentIndex)
TrueSheetStackManager.getParentSheet(this)?.updateTranslationForChild(mySheetTop)
}
// ==================== TrueSheetViewControllerDelegate ====================
override fun viewControllerWillPresent(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(WillPresentEvent(surfaceId, id, index, position, detent))
// Enable touch event dispatching to React Native while sheet is visible
viewController.eventDispatcher = eventDispatcher
containerView?.footerView?.eventDispatcher = eventDispatcher
}
override fun viewControllerDidPresent(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DidPresentEvent(surfaceId, id, index, position, detent))
}
override fun viewControllerWillDismiss() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(WillDismissEvent(surfaceId, id))
// Disable touch event dispatching when sheet is dismissing
viewController.eventDispatcher = null
containerView?.footerView?.eventDispatcher = null
}
override fun viewControllerDidDismiss(hadParent: Boolean) {
// Detach coordinator from the root container view
viewController.coordinatorLayout?.let { rootContainerView?.removeView(it) }
rootContainerView = null
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DidDismissEvent(surfaceId, id))
TrueSheetStackManager.onSheetDidDismiss(this, hadParent)
}
override fun viewControllerDidChangeDetent(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DetentChangeEvent(surfaceId, id, index, position, detent))
}
override fun viewControllerDidDragBegin(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DragBeginEvent(surfaceId, id, index, position, detent))
}
override fun viewControllerDidDragChange(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DragChangeEvent(surfaceId, id, index, position, detent))
}
override fun viewControllerDidDragEnd(index: Int, position: Float, detent: Float) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(DragEndEvent(surfaceId, id, index, position, detent))
}
override fun viewControllerDidChangePosition(index: Float, position: Float, detent: Float, realtime: Boolean) {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(PositionChangeEvent(surfaceId, id, index, position, detent, realtime))
}
override fun viewControllerDidChangeSize(width: Int, height: Int) {
updateState(width, height)
}
override fun viewControllerWillFocus() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(WillFocusEvent(surfaceId, id))
}
override fun viewControllerDidFocus() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(FocusEvent(surfaceId, id))
}
override fun viewControllerWillBlur() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(WillBlurEvent(surfaceId, id))
}
override fun viewControllerDidBlur() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(BlurEvent(surfaceId, id))
}
override fun viewControllerDidBackPress() {
val surfaceId = UIManagerHelper.getSurfaceId(this)
eventDispatcher?.dispatchEvent(BackPressEvent(surfaceId, id))
}
// ==================== TrueSheetContainerViewDelegate ====================
override fun containerViewContentDidChangeSize(width: Int, height: Int) {
updateSheetIfNeeded()
}
override fun containerViewHeaderDidChangeSize(width: Int, height: Int) {
updateSheetIfNeeded()
}
override fun containerViewFooterDidChangeSize(width: Int, height: Int) {
// Footer changes don't affect detents, only reposition it
viewController.positionFooter()
}
// ==================== Private Helpers ====================
/**
* Find the root container view for presenting the sheet.
* This traverses up the view hierarchy to find the content view (android.R.id.content)
* of whichever window this view is in - whether that's the activity's window or a
* Modal's dialog window.
*/
private fun findRootContainerView(): ViewGroup? {
var current: android.view.ViewParent? = parent
while (current != null) {
if (current is ViewGroup && current.id == android.R.id.content) {
return current
}
current = current.parent
}
return reactContext.currentActivity?.findViewById(android.R.id.content)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +1,210 @@
package com.lodev09.truesheet
import android.util.Log
import android.view.WindowManager
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableType
import com.facebook.react.common.MapBuilder
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.lodev09.truesheet.core.Utils
import com.lodev09.truesheet.events.DismissEvent
import com.lodev09.truesheet.events.MountEvent
import com.lodev09.truesheet.events.PresentEvent
import com.lodev09.truesheet.events.SizeChangeEvent
import com.facebook.react.viewmanagers.TrueSheetViewManagerDelegate
import com.facebook.react.viewmanagers.TrueSheetViewManagerInterface
import com.lodev09.truesheet.core.GrabberOptions
import com.lodev09.truesheet.events.*
class TrueSheetViewManager : ViewGroupManager<TrueSheetView>() {
override fun getName() = TAG
/**
* ViewManager for TrueSheetView - Fabric architecture
* Main sheet component that manages the bottom sheet dialog
*/
@ReactModule(name = TrueSheetViewManager.REACT_CLASS)
class TrueSheetViewManager :
ViewGroupManager<TrueSheetView>(),
TrueSheetViewManagerInterface<TrueSheetView> {
private val delegate: ViewManagerDelegate<TrueSheetView> = TrueSheetViewManagerDelegate(this)
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): TrueSheetView = TrueSheetView(reactContext)
override fun onDropViewInstance(view: TrueSheetView) {
super.onDropViewInstance(view)
view.onHostDestroy()
view.onDropInstance()
}
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any>? =
MapBuilder.builder<String, Any>()
.put(MountEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMount"))
.put(PresentEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPresent"))
.put(DismissEvent.EVENT_NAME, MapBuilder.of("registrationName", "onDismiss"))
.put(SizeChangeEvent.EVENT_NAME, MapBuilder.of("registrationName", "onSizeChange"))
.build()
@ReactProp(name = "maxHeight")
fun setMaxHeight(view: TrueSheetView, height: Double) {
view.setMaxHeight(Utils.toPixel(height))
override fun onAfterUpdateTransaction(view: TrueSheetView) {
super.onAfterUpdateTransaction(view)
view.finalizeUpdates()
}
@ReactProp(name = "dismissible")
fun setDismissible(view: TrueSheetView, dismissible: Boolean) {
override fun addEventEmitters(reactContext: ThemedReactContext, view: TrueSheetView) {
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
view.eventDispatcher = dispatcher
}
override fun updateState(view: TrueSheetView, props: ReactStylesDiffMap?, stateWrapper: StateWrapper?): Any? {
view.stateWrapper = stateWrapper
return null
}
override fun getDelegate(): ViewManagerDelegate<TrueSheetView> = delegate
/**
* Export custom direct event types for Fabric
* Uses Kotlin native collections with decoupled event classes
*/
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
mutableMapOf(
MountEvent.EVENT_NAME to hashMapOf("registrationName" to MountEvent.REGISTRATION_NAME),
WillPresentEvent.EVENT_NAME to hashMapOf("registrationName" to WillPresentEvent.REGISTRATION_NAME),
DidPresentEvent.EVENT_NAME to hashMapOf("registrationName" to DidPresentEvent.REGISTRATION_NAME),
WillDismissEvent.EVENT_NAME to hashMapOf("registrationName" to WillDismissEvent.REGISTRATION_NAME),
DidDismissEvent.EVENT_NAME to hashMapOf("registrationName" to DidDismissEvent.REGISTRATION_NAME),
DetentChangeEvent.EVENT_NAME to hashMapOf("registrationName" to DetentChangeEvent.REGISTRATION_NAME),
DragBeginEvent.EVENT_NAME to hashMapOf("registrationName" to DragBeginEvent.REGISTRATION_NAME),
DragChangeEvent.EVENT_NAME to hashMapOf("registrationName" to DragChangeEvent.REGISTRATION_NAME),
DragEndEvent.EVENT_NAME to hashMapOf("registrationName" to DragEndEvent.REGISTRATION_NAME),
PositionChangeEvent.EVENT_NAME to hashMapOf("registrationName" to PositionChangeEvent.REGISTRATION_NAME),
WillFocusEvent.EVENT_NAME to hashMapOf("registrationName" to WillFocusEvent.REGISTRATION_NAME),
FocusEvent.EVENT_NAME to hashMapOf("registrationName" to FocusEvent.REGISTRATION_NAME),
WillBlurEvent.EVENT_NAME to hashMapOf("registrationName" to WillBlurEvent.REGISTRATION_NAME),
BlurEvent.EVENT_NAME to hashMapOf("registrationName" to BlurEvent.REGISTRATION_NAME),
BackPressEvent.EVENT_NAME to hashMapOf("registrationName" to BackPressEvent.REGISTRATION_NAME)
)
// ==================== Props ====================
@ReactProp(name = "detents")
override fun setDetents(view: TrueSheetView, value: ReadableArray?) {
if (value == null || value.size() == 0) {
view.setDetents(mutableListOf(0.5, 1.0))
return
}
val detents = mutableListOf<Double>()
IntProgression
.fromClosedRange(0, value.size() - 1, 1)
.asSequence()
.map { idx -> value.getDouble(idx) }
.toCollection(detents)
view.setDetents(detents)
}
@ReactProp(name = "backgroundColor", customType = "Color")
override fun setBackgroundColor(view: TrueSheetView, color: Int?) {
view.setSheetBackgroundColor(color)
}
@ReactProp(name = "cornerRadius", defaultDouble = -1.0)
override fun setCornerRadius(view: TrueSheetView, radius: Double) {
view.setCornerRadius(radius.dpToPx())
}
@ReactProp(name = "grabber", defaultBoolean = true)
override fun setGrabber(view: TrueSheetView, grabber: Boolean) {
view.setGrabber(grabber)
}
@ReactProp(name = "grabberOptions")
override fun setGrabberOptions(view: TrueSheetView, options: ReadableMap?) {
if (options == null) {
view.setGrabberOptions(null)
return
}
val grabberOptions = GrabberOptions(
width = if (options.hasKey("width")) options.getDouble("width").toFloat() else null,
height = if (options.hasKey("height")) options.getDouble("height").toFloat() else null,
topMargin = if (options.hasKey("topMargin")) options.getDouble("topMargin").toFloat() else null,
cornerRadius = if (options.hasKey("cornerRadius") &&
options.getDouble("cornerRadius") >= 0
) {
options.getDouble("cornerRadius").toFloat()
} else {
null
},
color = if (options.hasKey("color") && !options.isNull("color")) options.getInt("color") else null,
adaptive = if (options.hasKey("adaptive")) options.getBoolean("adaptive") else true
)
view.setGrabberOptions(grabberOptions)
}
@ReactProp(name = "dismissible", defaultBoolean = true)
override fun setDismissible(view: TrueSheetView, dismissible: Boolean) {
view.setDismissible(dismissible)
}
@ReactProp(name = "dimmed")
fun setDimmed(view: TrueSheetView, dimmed: Boolean) {
@ReactProp(name = "draggable", defaultBoolean = true)
override fun setDraggable(view: TrueSheetView, draggable: Boolean) {
view.setDraggable(draggable)
}
@ReactProp(name = "dimmed", defaultBoolean = true)
override fun setDimmed(view: TrueSheetView, dimmed: Boolean) {
view.setDimmed(dimmed)
}
@ReactProp(name = "initialIndex")
fun setInitialIndex(view: TrueSheetView, index: Int) {
view.initialIndex = index
@ReactProp(name = "dimmedDetentIndex", defaultInt = 0)
override fun setDimmedDetentIndex(view: TrueSheetView, index: Int) {
view.setDimmedDetentIndex(index)
}
@ReactProp(name = "initialIndexAnimated")
fun setInitialIndexAnimated(view: TrueSheetView, animate: Boolean) {
view.initialIndexAnimated = animate
@ReactProp(name = "initialDetentIndex", defaultInt = -1)
override fun setInitialDetentIndex(view: TrueSheetView, index: Int) {
view.initialDetentIndex = index
}
@ReactProp(name = "keyboardMode")
fun setKeyboardMode(view: TrueSheetView, mode: String) {
val softInputMode = when (mode) {
"pan" -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
@ReactProp(name = "initialDetentAnimated", defaultBoolean = true)
override fun setInitialDetentAnimated(view: TrueSheetView, animate: Boolean) {
view.initialDetentAnimated = animate
}
@ReactProp(name = "maxHeight", defaultDouble = 0.0)
override fun setMaxHeight(view: TrueSheetView, height: Double) {
if (height > 0) {
view.setMaxHeight(height.dpToPx().toInt())
}
view.setSoftInputMode(softInputMode)
}
@ReactProp(name = "dimmedIndex")
fun setDimmedIndex(view: TrueSheetView, index: Int) {
view.setDimmedIndex(index)
@ReactProp(name = "backgroundBlur")
override fun setBackgroundBlur(view: TrueSheetView, tint: String?) {
// iOS-specific prop - no-op on Android
}
@ReactProp(name = "contentHeight")
fun setContentHeight(view: TrueSheetView, height: Double) {
view.setContentHeight(Utils.toPixel(height))
@ReactProp(name = "blurOptions")
override fun setBlurOptions(view: TrueSheetView, options: ReadableMap?) {
// iOS-specific prop - no-op on Android
}
@ReactProp(name = "footerHeight")
fun setFooterHeight(view: TrueSheetView, height: Double) {
view.setFooterHeight(Utils.toPixel(height))
@ReactProp(name = "insetAdjustment")
override fun setInsetAdjustment(view: TrueSheetView, insetAdjustment: String?) {
view.setInsetAdjustment(insetAdjustment ?: "automatic")
}
@ReactProp(name = "sizes")
fun setSizes(view: TrueSheetView, sizes: ReadableArray) {
val result = ArrayList<Any>()
for (i in 0 until minOf(sizes.size(), 3)) {
when (sizes.getType(i)) {
ReadableType.Number -> result.add(sizes.getDouble(i))
ReadableType.String -> result.add(sizes.getString(i))
else -> Log.d(TAG, "Invalid type")
}
}
@ReactProp(name = "scrollable", defaultBoolean = false)
override fun setScrollable(view: TrueSheetView, value: Boolean) {
// iOS-specific prop - no-op on Android
}
view.setSizes(result.toArray())
@ReactProp(name = "pageSizing", defaultBoolean = true)
override fun setPageSizing(view: TrueSheetView, value: Boolean) {
// iOS-specific prop - no-op on Android
}
@ReactProp(name = "elevation", defaultDouble = -1.0)
override fun setElevation(view: TrueSheetView, elevation: Double) {
view.setSheetElevation(elevation.toFloat())
}
companion object {
const val TAG = "TrueSheetView"
const val REACT_CLASS = "TrueSheetView"
const val TAG_NAME = "TrueSheet"
}
}

View File

@ -1,63 +0,0 @@
package com.lodev09.truesheet
import android.util.Log
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.UIManagerHelper
import com.lodev09.truesheet.core.Utils
@ReactModule(name = TrueSheetViewModule.TAG)
class TrueSheetViewModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String = TAG
private fun withTrueSheetView(tag: Int, closure: (trueSheetView: TrueSheetView) -> Unit) {
UiThreadUtil.runOnUiThread {
try {
val manager = UIManagerHelper.getUIManagerForReactTag(reactApplicationContext, tag)
val view = manager?.resolveView(tag)
if (view == null) {
Log.d(TAG, "Tag $tag not found")
return@runOnUiThread
}
if (view is TrueSheetView) {
closure(view)
} else {
Log.d(TAG, "Tag $tag does not match")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
@ReactMethod
fun present(tag: Int, index: Int, promise: Promise) {
withTrueSheetView(tag) {
it.present(index) {
Utils.withPromise(promise) {
return@withPromise null
}
}
}
}
@ReactMethod
fun dismiss(tag: Int, promise: Promise) {
withTrueSheetView(tag) {
it.dismiss {
Utils.withPromise(promise) {
return@withPromise null
}
}
}
}
companion object {
const val TAG = "TrueSheetView"
}
}

View File

@ -1,58 +0,0 @@
package com.lodev09.truesheet.core
import android.content.Context
import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.view.inputmethod.InputMethodManager
import com.facebook.react.bridge.ReactContext
class KeyboardManager(reactContext: ReactContext) {
interface OnKeyboardChangeListener {
fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?)
}
private var contentView: View? = null
private var onGlobalLayoutListener: OnGlobalLayoutListener? = null
private var isKeyboardVisible = false
init {
val activity = reactContext.currentActivity
contentView = activity?.findViewById(android.R.id.content)
}
fun registerKeyboardListener(listener: OnKeyboardChangeListener?) {
contentView?.apply {
unregisterKeyboardListener()
onGlobalLayoutListener = object : OnGlobalLayoutListener {
private var previousHeight = 0
override fun onGlobalLayout() {
val heightDiff = rootView.height - height
if (heightDiff > Utils.toPixel(200.0)) {
// Will ask InputMethodManager.isAcceptingText() to detect if keyboard appeared or not.
val inputManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (height != previousHeight && inputManager.isAcceptingText()) {
listener?.onKeyboardStateChange(true, height)
previousHeight = height
isKeyboardVisible = true
}
} else if (isKeyboardVisible) {
listener?.onKeyboardStateChange(false, null)
previousHeight = 0
isKeyboardVisible = false
}
}
}
getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
fun unregisterKeyboardListener() {
onGlobalLayoutListener?.let {
contentView?.getViewTreeObserver()?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
}
}
}

View File

@ -0,0 +1,181 @@
package com.lodev09.truesheet.core
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.facebook.react.bridge.ReactContext
private const val RN_SCREENS_PACKAGE = "com.swmansion.rnscreens"
/**
* Observes fragment lifecycle to detect react-native-screens modal presentation.
* Automatically notifies when modals are presented/dismissed.
*/
class RNScreensFragmentObserver(
private val reactContext: ReactContext,
private val onModalPresented: () -> Unit,
private val onModalWillDismiss: () -> Unit,
private val onModalDidDismiss: () -> Unit
) {
private var fragmentLifecycleCallback: FragmentManager.FragmentLifecycleCallbacks? = null
private var activityLifecycleObserver: DefaultLifecycleObserver? = null
private val activeModalFragments: MutableSet<Fragment> = mutableSetOf()
private var isActivityInForeground = true
private var pendingDismissRunnable: Runnable? = null
/**
* Start observing fragment lifecycle events.
*/
fun start() {
val activity = reactContext.currentActivity as? AppCompatActivity ?: return
val fragmentManager = activity.supportFragmentManager
// Track activity foreground state to ignore fragment lifecycle events during background/foreground transitions
activityLifecycleObserver = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
isActivityInForeground = true
}
override fun onPause(owner: LifecycleOwner) {
isActivityInForeground = false
}
}
activity.lifecycle.addObserver(activityLifecycleObserver!!)
fragmentLifecycleCallback = object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
// Ignore if app is resuming from background
if (!isActivityInForeground) return
if (isModalFragment(f) && !activeModalFragments.contains(f)) {
// Cancel any pending dismiss since a modal is being presented
cancelPendingDismiss()
activeModalFragments.add(f)
if (activeModalFragments.size == 1) {
onModalPresented()
}
}
}
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
super.onFragmentStopped(fm, f)
// Ignore if app is going to background (fragments stop with activity)
if (!isActivityInForeground) return
// Only trigger when fragment is being removed (not just stopped for navigation)
if (activeModalFragments.contains(f) && f.isRemoving) {
activeModalFragments.remove(f)
if (activeModalFragments.isEmpty()) {
// Post dismiss to allow fragment attach to cancel if navigation is happening
schedulePendingDismiss()
}
}
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
super.onFragmentDestroyed(fm, f)
if (activeModalFragments.isEmpty() && pendingDismissRunnable == null) {
onModalDidDismiss()
}
}
}
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallback!!, true)
}
/**
* Stop observing and cleanup.
*/
fun stop() {
val activity = reactContext.currentActivity as? AppCompatActivity
cancelPendingDismiss()
fragmentLifecycleCallback?.let { callback ->
activity?.supportFragmentManager?.unregisterFragmentLifecycleCallbacks(callback)
}
fragmentLifecycleCallback = null
activityLifecycleObserver?.let { observer ->
activity?.lifecycle?.removeObserver(observer)
}
activityLifecycleObserver = null
activeModalFragments.clear()
}
private fun schedulePendingDismiss() {
val activity = reactContext.currentActivity ?: return
val decorView = activity.window?.decorView ?: return
cancelPendingDismiss()
pendingDismissRunnable = Runnable {
pendingDismissRunnable = null
if (activeModalFragments.isEmpty()) {
onModalWillDismiss()
}
}
decorView.post(pendingDismissRunnable)
}
private fun cancelPendingDismiss() {
val activity = reactContext.currentActivity ?: return
val decorView = activity.window?.decorView ?: return
pendingDismissRunnable?.let {
decorView.removeCallbacks(it)
pendingDismissRunnable = null
}
}
companion object {
/**
* Check if fragment is from react-native-screens.
*/
private fun isScreensFragment(fragment: Fragment): Boolean = fragment.javaClass.name.startsWith(RN_SCREENS_PACKAGE)
/**
* Check if fragment is a react-native-screens modal (fullScreenModal, transparentModal, or formSheet).
* Uses reflection to check the fragment's screen.stackPresentation property.
*/
private fun isModalFragment(fragment: Fragment): Boolean {
val className = fragment.javaClass.name
if (!isScreensFragment(fragment)) {
return false
}
// ScreenModalFragment is always a modal (used for formSheet with BottomSheetDialog)
if (className.contains("ScreenModalFragment")) {
return true
}
// For ScreenStackFragment, check the screen's stackPresentation via reflection
try {
val getScreenMethod = fragment.javaClass.getMethod("getScreen")
val screen = getScreenMethod.invoke(fragment) ?: return false
val getStackPresentationMethod = screen.javaClass.getMethod("getStackPresentation")
val stackPresentation = getStackPresentationMethod.invoke(screen) ?: return false
val presentationName = stackPresentation.toString()
return presentationName == "MODAL" ||
presentationName == "TRANSPARENT_MODAL" ||
presentationName == "FORM_SHEET"
} catch (e: Exception) {
return false
}
}
}
}

View File

@ -1,147 +0,0 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.content.Context
import android.view.MotionEvent
import android.view.View
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.config.ReactFeatureFlags
import com.facebook.react.uimanager.JSPointerDispatcher
import com.facebook.react.uimanager.JSTouchDispatcher
import com.facebook.react.uimanager.RootView
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.EventDispatcher
import com.facebook.react.views.view.ReactViewGroup
/**
* RootSheetView is the ViewGroup which contains all the children of a Modal. It gets all
* child information forwarded from TrueSheetView and uses that to create children. It is
* also responsible for acting as a RootView and handling touch events. It does this the same way
* as ReactRootView.
*
*
* To get layout to work properly, we need to layout all the elements within the Modal as if
* they can fill the entire window. To do that, we need to explicitly set the styleWidth and
* styleHeight on the LayoutShadowNode to be the window size. This is done through the
* UIManagerModule, and will then cause the children to layout as if they can fill the window.
*/
class RootSheetView(private val context: Context?) :
ReactViewGroup(context),
RootView {
private var hasAdjustedSize = false
private var viewWidth = 0
private var viewHeight = 0
private val jSTouchDispatcher = JSTouchDispatcher(this)
private var jSPointerDispatcher: JSPointerDispatcher? = null
private var sizeChangeListener: OnSizeChangeListener? = null
var eventDispatcher: EventDispatcher? = null
interface OnSizeChangeListener {
fun onSizeChange(width: Int, height: Int)
}
init {
if (ReactFeatureFlags.dispatchPointerEvents) {
jSPointerDispatcher = JSPointerDispatcher(this)
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
updateFirstChildView()
sizeChangeListener?.onSizeChange(w, h)
}
fun setOnSizeChangeListener(listener: OnSizeChangeListener) {
sizeChangeListener = listener
}
private fun updateFirstChildView() {
if (childCount > 0) {
hasAdjustedSize = false
val viewTag = getChildAt(0).id
reactContext.runOnNativeModulesQueueThread(
object : GuardedRunnable(reactContext) {
override fun runGuarded() {
val uiManager: UIManagerModule =
reactContext
.reactApplicationContext
.getNativeModule(UIManagerModule::class.java) ?: return
uiManager.updateNodeSize(viewTag, viewWidth, viewHeight)
}
}
)
} else {
hasAdjustedSize = true
}
}
override fun addView(child: View, index: Int, params: LayoutParams) {
super.addView(child, index, params)
if (hasAdjustedSize) {
updateFirstChildView()
}
}
override fun handleException(t: Throwable) {
reactContext.reactApplicationContext.handleException(RuntimeException(t))
}
private val reactContext: ThemedReactContext
get() = context as ThemedReactContext
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher)
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
return super.onInterceptTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
jSTouchDispatcher.handleTouchEvent(event, eventDispatcher)
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
super.onTouchEvent(event)
// In case when there is no children interested in handling touch event, we return true from
// the root view in order to receive subsequent events related to that gesture
return true
}
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
return super.onHoverEvent(event)
}
override fun onHoverEvent(event: MotionEvent): Boolean {
jSPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
return super.onHoverEvent(event)
}
@Deprecated("Deprecated in Java")
override fun onChildStartedNativeGesture(ev: MotionEvent?) {
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher)
}
override fun onChildStartedNativeGesture(childView: View, ev: MotionEvent) {
jSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher)
jSPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher)
}
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
jSTouchDispatcher.onChildEndedNativeGesture(ev, eventDispatcher)
jSPointerDispatcher?.onChildEndedNativeGesture()
}
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
// No-op - override in order to still receive events to onInterceptTouchEvent
// even when some other view disallow that
}
}

View File

@ -0,0 +1,165 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.Outline
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RoundRectShape
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.facebook.react.uimanager.PixelUtil.dpToPx
import com.facebook.react.uimanager.ThemedReactContext
import com.google.android.material.bottomsheet.BottomSheetBehavior
interface TrueSheetBottomSheetViewDelegate {
val isTopmostSheet: Boolean
val sheetCornerRadius: Float
val sheetElevation: Float
val sheetBackgroundColor: Int?
val grabber: Boolean
val grabberOptions: GrabberOptions?
}
/**
* The bottom sheet view that holds the content.
* This view has BottomSheetBehavior attached via CoordinatorLayout.LayoutParams.
*
* Touch dispatching to React Native is handled by TrueSheetViewController,
* which is the actual RootView containing the React content.
*/
@SuppressLint("ViewConstructor")
class TrueSheetBottomSheetView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext) {
companion object {
private const val GRABBER_TAG = "TrueSheetGrabber"
private const val DEFAULT_CORNER_RADIUS = 16f // dp
private const val DEFAULT_MAX_WIDTH = 640 // dp
private const val DEFAULT_ELEVATION = 4f // dp
}
// =============================================================================
// MARK: - Properties
// =============================================================================
var delegate: TrueSheetBottomSheetViewDelegate? = null
// Behavior reference (set after adding to CoordinatorLayout)
val behavior: BottomSheetBehavior<TrueSheetBottomSheetView>?
get() = (layoutParams as? CoordinatorLayout.LayoutParams)
?.behavior as? BottomSheetBehavior<TrueSheetBottomSheetView>
// =============================================================================
// MARK: - Initialization
// =============================================================================
init {
// Allow content to extend beyond bounds (for footer positioning)
clipChildren = false
clipToPadding = false
}
override fun setTranslationY(translationY: Float) {
// This prevents keyboard inset animations from resetting parent sheet translation
if (translationY == 0f && this.translationY != 0f) {
return
}
super.setTranslationY(translationY)
}
// =============================================================================
// MARK: - Layout
// =============================================================================
/**
* Creates layout params with BottomSheetBehavior attached.
*/
fun createLayoutParams(): CoordinatorLayout.LayoutParams {
val behavior = BottomSheetBehavior<TrueSheetBottomSheetView>().apply {
isHideable = true
maxWidth = DEFAULT_MAX_WIDTH.dpToPx().toInt()
}
return CoordinatorLayout.LayoutParams(
CoordinatorLayout.LayoutParams.MATCH_PARENT,
CoordinatorLayout.LayoutParams.MATCH_PARENT
).apply {
this.behavior = behavior
this.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM
}
}
// =============================================================================
// MARK: - Background & Styling
// =============================================================================
fun setupBackground() {
val radius = delegate?.sheetCornerRadius ?: DEFAULT_CORNER_RADIUS.dpToPx()
val effectiveRadius = if (radius < 0) DEFAULT_CORNER_RADIUS.dpToPx() else radius
val outerRadii = floatArrayOf(
effectiveRadius,
effectiveRadius, // top-left
effectiveRadius,
effectiveRadius, // top-right
0f,
0f, // bottom-right
0f,
0f // bottom-left
)
val color = delegate?.sheetBackgroundColor ?: getDefaultBackgroundColor()
background = ShapeDrawable(RoundRectShape(outerRadii, null, null)).apply {
paint.color = color
}
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, effectiveRadius)
}
}
clipToOutline = true
}
private fun getDefaultBackgroundColor(): Int {
val typedValue = TypedValue()
return if (reactContext.theme.resolveAttribute(
com.google.android.material.R.attr.colorSurfaceContainerLow,
typedValue,
true
)
) {
typedValue.data
} else {
Color.WHITE
}
}
fun setupElevation() {
val value = delegate?.sheetElevation ?: DEFAULT_ELEVATION
val effectiveElevation = if (value < 0) DEFAULT_ELEVATION else value
elevation = effectiveElevation.dpToPx()
}
// =============================================================================
// MARK: - Grabber
// =============================================================================
fun setupGrabber() {
findViewWithTag<View>(GRABBER_TAG)?.let { removeView(it) }
val isEnabled = delegate?.grabber ?: true
val isDraggable = behavior?.isDraggable ?: true
if (!isEnabled || !isDraggable) return
val grabberView = TrueSheetGrabberView(reactContext, delegate?.grabberOptions).apply {
tag = GRABBER_TAG
}
addView(grabberView, 0)
}
}

View File

@ -0,0 +1,55 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.facebook.react.uimanager.PointerEvents
import com.facebook.react.uimanager.ReactPointerEventsView
interface TrueSheetCoordinatorLayoutDelegate {
fun coordinatorLayoutDidLayout(changed: Boolean)
}
/**
* Custom CoordinatorLayout that hosts the bottom sheet and dim view.
* Implements ReactPointerEventsView to allow touch events to pass through
* to underlying React Native views when appropriate.
*/
@SuppressLint("ViewConstructor")
class TrueSheetCoordinatorLayout(context: Context) :
CoordinatorLayout(context),
ReactPointerEventsView {
var delegate: TrueSheetCoordinatorLayoutDelegate? = null
init {
// Fill the entire screen
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
// Ensure we don't clip the sheet during animations
clipChildren = false
clipToPadding = false
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int
) {
super.onLayout(changed, l, t, r, b)
delegate?.coordinatorLayoutDidLayout(changed)
}
/**
* Allow pointer events to pass through to underlying views.
* The DimView and BottomSheetView handle their own touch interception.
*/
override val pointerEvents: PointerEvents
get() = PointerEvents.BOX_NONE
}

View File

@ -0,0 +1,214 @@
package com.lodev09.truesheet.core
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.util.RNLog
import com.google.android.material.bottomsheet.BottomSheetBehavior
/**
* Provides screen dimensions and content measurements for detent calculations.
*/
interface TrueSheetDetentCalculatorDelegate {
val screenHeight: Int
val realScreenHeight: Int
val detents: MutableList<Double>
val contentHeight: Int
val headerHeight: Int
val contentBottomInset: Int
val maxSheetHeight: Int?
val keyboardInset: Int
}
/**
* Handles all detent-related calculations for the bottom sheet.
*/
class TrueSheetDetentCalculator(private val reactContext: ThemedReactContext) {
var delegate: TrueSheetDetentCalculatorDelegate? = null
private val screenHeight: Int get() = delegate?.screenHeight ?: 0
private val realScreenHeight: Int get() = delegate?.realScreenHeight ?: 0
private val detents: List<Double> get() = delegate?.detents ?: emptyList()
private val contentHeight: Int get() = delegate?.contentHeight ?: 0
private val headerHeight: Int get() = delegate?.headerHeight ?: 0
private val contentBottomInset: Int get() = delegate?.contentBottomInset ?: 0
private val maxSheetHeight: Int? get() = delegate?.maxSheetHeight
private val keyboardInset: Int get() = delegate?.keyboardInset ?: 0
/**
* Calculate the height in pixels for a given detent value.
* @param detent The detent value: -1.0 for content-fit, or 0.0-1.0 for screen fraction
*/
fun getDetentHeight(detent: Double): Int {
val baseHeight = if (detent == -1.0) {
contentHeight + headerHeight + contentBottomInset
} else {
if (detent <= 0.0 || detent > 1.0) {
throw IllegalArgumentException("TrueSheet: detent fraction ($detent) must be between 0 and 1")
}
(detent * screenHeight).toInt() + contentBottomInset
}
val height = baseHeight + keyboardInset
val maxAllowedHeight = screenHeight + contentBottomInset
return maxSheetHeight?.let { minOf(height, it, maxAllowedHeight) } ?: minOf(height, maxAllowedHeight)
}
/**
* Get the expected sheet top position for a detent index.
*/
fun getSheetTopForDetentIndex(index: Int): Int {
if (index < 0 || index >= detents.size) {
RNLog.w(reactContext, "TrueSheet: Detent index ($index) is out of bounds (0..${detents.size - 1})")
return realScreenHeight
}
return realScreenHeight - getDetentHeight(detents[index])
}
/**
* Calculate visible sheet height from sheet top position.
*/
fun getVisibleSheetHeight(sheetTop: Int): Int = realScreenHeight - sheetTop
/**
* Convert visible sheet height to position in dp.
*/
fun getPositionDp(visibleSheetHeight: Int): Float = (screenHeight - visibleSheetHeight).pxToDp()
/**
* Returns the raw screen fraction for a detent index (without bottomInset).
*/
fun getDetentValueForIndex(index: Int): Float {
if (index < 0 || index >= detents.size) return 0f
val value = detents[index]
return if (value == -1.0) {
(contentHeight + headerHeight).toFloat() / screenHeight.toFloat()
} else {
value.toFloat()
}
}
// ====================================================================
// MARK: - State Mapping
// ====================================================================
/**
* Maps detent index to BottomSheetBehavior state based on detent count.
*/
fun getStateForDetentIndex(index: Int): Int {
val stateMap = getDetentStateMap() ?: return BottomSheetBehavior.STATE_HIDDEN
return stateMap.entries.find { it.value == index }?.key ?: BottomSheetBehavior.STATE_HIDDEN
}
/**
* Maps BottomSheetBehavior state to detent index.
* @return The detent index, or null if state is not mapped
*/
fun getDetentIndexForState(state: Int): Int? {
val stateMap = getDetentStateMap() ?: return null
return stateMap[state]
}
/**
* Returns state-to-index mapping based on detent count.
*/
private fun getDetentStateMap(): Map<Int, Int>? =
when (detents.size) {
1 -> mapOf(
BottomSheetBehavior.STATE_COLLAPSED to 0,
BottomSheetBehavior.STATE_EXPANDED to 0
)
2 -> mapOf(
BottomSheetBehavior.STATE_COLLAPSED to 0,
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
BottomSheetBehavior.STATE_EXPANDED to 1
)
3 -> mapOf(
BottomSheetBehavior.STATE_COLLAPSED to 0,
BottomSheetBehavior.STATE_HALF_EXPANDED to 1,
BottomSheetBehavior.STATE_EXPANDED to 2
)
else -> null
}
// ====================================================================
// MARK: - Interpolation
// ====================================================================
/**
* Find which segment the position falls into for interpolation.
* @return Triple(fromIndex, toIndex, progress) where progress is 0-1, or null if no detents
*/
fun findSegmentForPosition(positionPx: Int): Triple<Int, Int, Float>? {
val count = detents.size
if (count == 0) return null
val firstPos = getSheetTopForDetentIndex(0)
// Position is below first detent (sheet is being dragged down to dismiss)
if (positionPx > firstPos) {
val range = realScreenHeight - firstPos
val progress = if (range > 0) (positionPx - firstPos).toFloat() / range else 0f
return Triple(-1, 0, progress)
}
if (count == 1) return Triple(0, 0, 0f)
val lastPos = getSheetTopForDetentIndex(count - 1)
// Position is above last detent
if (positionPx < lastPos) {
return Triple(count - 1, count - 1, 0f)
}
// Find the segment containing this position
for (i in 0 until count - 1) {
val pos = getSheetTopForDetentIndex(i)
val nextPos = getSheetTopForDetentIndex(i + 1)
if (positionPx in nextPos..pos) {
val range = pos - nextPos
val progress = if (range > 0) (pos - positionPx).toFloat() / range else 0f
return Triple(i, i + 1, maxOf(0f, minOf(1f, progress)))
}
}
return Triple(count - 1, count - 1, 0f)
}
/**
* Returns continuous index (e.g., 0.5 = halfway between detent 0 and 1).
*/
fun getInterpolatedIndexForPosition(positionPx: Int): Float {
val count = detents.size
if (count == 0) return -1f
val segment = findSegmentForPosition(positionPx) ?: return 0f
val (fromIndex, _, progress) = segment
if (fromIndex == -1) return -progress
return fromIndex + progress
}
/**
* Returns interpolated screen fraction for position.
*/
fun getInterpolatedDetentForPosition(positionPx: Int): Float {
val count = detents.size
if (count == 0) return 0f
val segment = findSegmentForPosition(positionPx) ?: return getDetentValueForIndex(0)
val (fromIndex, toIndex, progress) = segment
if (fromIndex == -1) {
val firstDetent = getDetentValueForIndex(0)
return maxOf(0f, firstDetent * (1 - progress))
}
val fromDetent = getDetentValueForIndex(fromIndex)
val toDetent = getDetentValueForIndex(toIndex)
return fromDetent + progress * (toDetent - fromDetent)
}
}

View File

@ -0,0 +1,160 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.Outline
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import com.facebook.react.uimanager.PointerEvents
import com.facebook.react.uimanager.ReactPointerEventsView
import com.facebook.react.uimanager.ThemedReactContext
import com.lodev09.truesheet.utils.ScreenUtils
/**
* Delegate for handling dim view interactions.
*/
interface TrueSheetDimViewDelegate {
fun dimViewDidTap()
}
/**
* Dim view that sits behind the bottom sheet in the CoordinatorLayout.
*
* Key behaviors:
* - When alpha > 0 (dimmed): blocks touches and calls delegate on tap
* - When alpha == 0 (not dimmed): passes touches through to views below
*
* This implements the "dimmedDetentIndex" equivalent functionality:
* the view only becomes interactive when the sheet is at or above the dimmed detent.
*/
@SuppressLint("ViewConstructor", "ClickableViewAccessibility")
class TrueSheetDimView(private val reactContext: ThemedReactContext) :
View(reactContext),
ReactPointerEventsView {
companion object {
private const val MAX_ALPHA = 0.5f
}
var delegate: TrueSheetDimViewDelegate? = null
private var targetView: ViewGroup? = null
/**
* Whether this view should block gestures (when dimmed).
*/
private val blockGestures: Boolean
get() = alpha > 0f
init {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
setBackgroundColor(Color.BLACK)
alpha = 0f
// Handle taps on the dim view
setOnClickListener {
delegate?.dimViewDidTap()
}
}
// =============================================================================
// MARK: - Attachment
// =============================================================================
/**
* Attaches this dim view to a target view group.
* For CoordinatorLayout usage, pass null to use the default (activity's decor view).
* For stacked sheets, pass the parent sheet's bottom sheet view with corner radius.
*/
fun attach(view: ViewGroup? = null, cornerRadius: Float = 0f) {
if (parent != null) return
targetView = view ?: reactContext.currentActivity?.window?.decorView as? ViewGroup
if (cornerRadius > 0f) {
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(v: View, outline: Outline) {
outline.setRoundRect(0, 0, v.width, v.height, cornerRadius)
}
}
clipToOutline = true
} else {
outlineProvider = null
clipToOutline = false
}
targetView?.addView(this)
}
/**
* Attaches this dim view to a CoordinatorLayout at index 0 (behind the sheet).
*/
fun attachToCoordinator(coordinator: TrueSheetCoordinatorLayout) {
if (parent != null) return
targetView = coordinator
outlineProvider = null
clipToOutline = false
coordinator.addView(this, 0)
}
fun detach() {
targetView?.removeView(this)
targetView = null
}
// =============================================================================
// MARK: - Alpha Calculation
// =============================================================================
fun calculateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int): Float {
val realHeight = ScreenUtils.getRealScreenHeight(reactContext)
val dimmedDetentTop = getSheetTopForDetentIndex(dimmedDetentIndex)
val belowDimmedTop = if (dimmedDetentIndex > 0) getSheetTopForDetentIndex(dimmedDetentIndex - 1) else realHeight
return when {
sheetTop <= dimmedDetentTop -> MAX_ALPHA
sheetTop >= belowDimmedTop -> 0f
else -> {
val progress = 1f - (sheetTop - dimmedDetentTop).toFloat() / (belowDimmedTop - dimmedDetentTop)
(progress * MAX_ALPHA).coerceIn(0f, MAX_ALPHA)
}
}
}
fun interpolateAlpha(sheetTop: Int, dimmedDetentIndex: Int, getSheetTopForDetentIndex: (Int) -> Int) {
alpha = calculateAlpha(sheetTop, dimmedDetentIndex, getSheetTopForDetentIndex)
}
// =============================================================================
// MARK: - Touch Handling
// =============================================================================
override fun onTouchEvent(event: MotionEvent): Boolean {
if (blockGestures) {
// When dimmed, consume touch and trigger click on ACTION_UP
if (event.action == MotionEvent.ACTION_UP) {
performClick()
}
return true
}
// When not dimmed, let touches pass through
return false
}
// =============================================================================
// MARK: - ReactPointerEventsView
// =============================================================================
/**
* When dimmed (alpha > 0), intercept touches (AUTO).
* When not dimmed (alpha == 0), pass through (NONE).
*/
override val pointerEvents: PointerEvents
get() = if (blockGestures) PointerEvents.AUTO else PointerEvents.NONE
}

View File

@ -0,0 +1,88 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import androidx.core.graphics.ColorUtils
import com.facebook.react.uimanager.PixelUtil.dpToPx
/**
* Options for customizing the grabber appearance.
*/
data class GrabberOptions(
val width: Float? = null,
val height: Float? = null,
val topMargin: Float? = null,
val cornerRadius: Float? = null,
val color: Int? = null,
val adaptive: Boolean = true
)
/**
* Native grabber (drag handle) view for the bottom sheet.
* Displays a small pill-shaped indicator at the top of the sheet.
*/
@SuppressLint("ViewConstructor")
class TrueSheetGrabberView(context: Context, private val options: GrabberOptions? = null) : View(context) {
companion object {
private const val DEFAULT_WIDTH = 32f // dp
private const val DEFAULT_HEIGHT = 4f // dp
private const val DEFAULT_TOP_MARGIN = 16f // dp
private const val DEFAULT_ALPHA = 0.4f
private val DEFAULT_COLOR = Color.argb((DEFAULT_ALPHA * 255).toInt(), 73, 69, 79) // #49454F @ 40%
}
private val grabberWidth: Float
get() = options?.width ?: DEFAULT_WIDTH
private val grabberHeight: Float
get() = options?.height ?: DEFAULT_HEIGHT
private val grabberTopMargin: Float
get() = options?.topMargin ?: DEFAULT_TOP_MARGIN
private val grabberCornerRadius: Float
get() = options?.cornerRadius ?: (grabberHeight / 2)
private val isAdaptive: Boolean
get() = options?.adaptive ?: true
private val grabberColor: Int
get() = if (isAdaptive) getAdaptiveColor(options?.color) else options?.color ?: DEFAULT_COLOR
init {
layoutParams = FrameLayout.LayoutParams(
grabberWidth.dpToPx().toInt(),
grabberHeight.dpToPx().toInt()
).apply {
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
topMargin = grabberTopMargin.dpToPx().toInt()
}
background = GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = grabberCornerRadius.dpToPx()
setColor(grabberColor)
}
}
private fun getAdaptiveColor(baseColor: Int? = null): Int {
val nightMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
val isDarkMode = nightMode == Configuration.UI_MODE_NIGHT_YES
val modeColor = if (isDarkMode) Color.WHITE else Color.BLACK
return if (baseColor != null) {
// Blend user color with mode color for adaptive effect
val blendedColor = ColorUtils.blendARGB(baseColor, modeColor, 0.3f)
ColorUtils.setAlphaComponent(blendedColor, (DEFAULT_ALPHA * 255).toInt())
} else {
ColorUtils.setAlphaComponent(modeColor, (DEFAULT_ALPHA * 255).toInt())
}
}
}

View File

@ -0,0 +1,154 @@
package com.lodev09.truesheet.core
import android.graphics.Rect
import android.os.Build
import android.view.View
import android.view.ViewTreeObserver
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.uimanager.ThemedReactContext
import com.lodev09.truesheet.utils.KeyboardUtils
interface TrueSheetKeyboardObserverDelegate {
fun keyboardWillShow(height: Int)
fun keyboardWillHide()
fun keyboardDidHide()
fun keyboardDidChangeHeight(height: Int)
}
/**
* Tracks keyboard height and notifies delegate on changes.
* Uses WindowInsetsAnimationCompat on API 30+, ViewTreeObserver fallback on older versions.
*/
class TrueSheetKeyboardObserver(private val targetView: View, private val reactContext: ThemedReactContext) {
var delegate: TrueSheetKeyboardObserverDelegate? = null
var currentHeight: Int = 0
private set
var targetHeight: Int = 0
private set
private var isHiding: Boolean = false
private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var activityRootView: View? = null
fun start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setupAnimationCallback()
} else {
setupLegacyListener()
}
}
fun stop() {
globalLayoutListener?.let { listener ->
activityRootView?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
globalLayoutListener = null
activityRootView = null
}
ViewCompat.setWindowInsetsAnimationCallback(targetView, null)
}
private fun updateHeight(from: Int, to: Int, fraction: Float) {
val newHeight = (from + (to - from) * fraction).toInt()
if (currentHeight != newHeight) {
currentHeight = newHeight
delegate?.keyboardDidChangeHeight(newHeight)
}
}
private fun getKeyboardHeight(): Int = KeyboardUtils.getKeyboardHeight(targetView)
private fun setupAnimationCallback() {
ViewCompat.setWindowInsetsAnimationCallback(
targetView,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
private var startHeight = 0
private var endHeight = 0
override fun onPrepare(animation: WindowInsetsAnimationCompat) {
startHeight = getKeyboardHeight()
}
override fun onStart(
animation: WindowInsetsAnimationCompat,
bounds: WindowInsetsAnimationCompat.BoundsCompat
): WindowInsetsAnimationCompat.BoundsCompat {
endHeight = getKeyboardHeight()
targetHeight = endHeight
isHiding = endHeight < startHeight
if (endHeight > startHeight) {
delegate?.keyboardWillShow(endHeight)
} else if (isHiding) {
delegate?.keyboardWillHide()
}
return bounds
}
override fun onProgress(insets: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
val imeAnimation = runningAnimations.find {
it.typeMask and WindowInsetsCompat.Type.ime() != 0
} ?: return insets
val fraction = imeAnimation.interpolatedFraction
updateHeight(startHeight, endHeight, fraction)
return insets
}
override fun onEnd(animation: WindowInsetsAnimationCompat) {
val finalHeight = getKeyboardHeight()
updateHeight(startHeight, finalHeight, 1f)
if (isHiding) {
delegate?.keyboardDidHide()
isHiding = false
}
}
}
)
}
private fun setupLegacyListener() {
// Ensure we don't add duplicate listeners
if (globalLayoutListener != null) return
val rootView = reactContext.currentActivity?.window?.decorView?.rootView ?: return
activityRootView = rootView
globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
rootView.getWindowVisibleDisplayFrame(rect)
val screenHeight = rootView.height
val keyboardHeight = screenHeight - rect.bottom
val newHeight = if (keyboardHeight > screenHeight * 0.15) keyboardHeight else 0
// Skip if already at this height
if (targetHeight == newHeight) return@OnGlobalLayoutListener
val previousHeight = currentHeight
targetHeight = newHeight
isHiding = newHeight < previousHeight
if (newHeight > previousHeight) {
delegate?.keyboardWillShow(newHeight)
} else if (isHiding) {
delegate?.keyboardWillHide()
}
// On legacy API, keyboard has already animated - just update immediately
updateHeight(previousHeight, newHeight, 1f)
if (isHiding && newHeight == 0) {
delegate?.keyboardDidHide()
isHiding = false
}
}
rootView.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
}
}

View File

@ -0,0 +1,121 @@
package com.lodev09.truesheet.core
import com.lodev09.truesheet.TrueSheetView
/**
* Manages TrueSheet stacking behavior.
* Tracks presented sheets and handles visibility when sheets stack on top of each other.
*/
object TrueSheetStackManager {
private val presentedSheetStack = mutableListOf<TrueSheetView>()
/**
* Called when a sheet is about to be presented.
* Returns the visible parent sheet to stack on, or null if none.
*/
@JvmStatic
fun onSheetWillPresent(sheetView: TrueSheetView, detentIndex: Int): TrueSheetView? {
synchronized(presentedSheetStack) {
val parentSheet = presentedSheetStack.lastOrNull()
?.takeIf { it.viewController.isPresented && it.viewController.isSheetVisible }
val childSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(detentIndex)
parentSheet?.updateTranslationForChild(childSheetTop)
if (!presentedSheetStack.contains(sheetView)) {
presentedSheetStack.add(sheetView)
}
return parentSheet
}
}
/**
* Called when a sheet has been dismissed.
* Resets parent sheet translation if this sheet was stacked on it.
*/
@JvmStatic
fun onSheetDidDismiss(sheetView: TrueSheetView, hadParent: Boolean) {
synchronized(presentedSheetStack) {
presentedSheetStack.remove(sheetView)
if (hadParent) {
presentedSheetStack.lastOrNull()?.resetTranslation()
}
}
}
/**
* Called when a presented sheet's size changes (e.g., after setupSheetDetents).
* Updates parent sheet translations to match the new sheet position.
*/
@JvmStatic
fun onSheetSizeChanged(sheetView: TrueSheetView) {
synchronized(presentedSheetStack) {
val index = presentedSheetStack.indexOf(sheetView)
if (index <= 0) return
val parentSheet = presentedSheetStack[index - 1]
// Post to ensure layout is complete before reading position
sheetView.viewController.post {
val childMinSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(0)
val childCurrentSheetTop = sheetView.viewController.detentCalculator.getSheetTopForDetentIndex(
sheetView.viewController.currentDetentIndex
)
// Cap to minimum detent position
val childSheetTop = maxOf(childMinSheetTop, childCurrentSheetTop)
parentSheet.updateTranslationForChild(childSheetTop)
}
}
}
/**
* Returns all sheets presented on top of the given sheet (children/descendants).
* Returns them in reverse order (top-most first) for proper dismissal.
*/
@JvmStatic
fun getSheetsAbove(sheetView: TrueSheetView): List<TrueSheetView> {
synchronized(presentedSheetStack) {
val index = presentedSheetStack.indexOf(sheetView)
if (index < 0 || index >= presentedSheetStack.size - 1) return emptyList()
return presentedSheetStack.subList(index + 1, presentedSheetStack.size).reversed()
}
}
@JvmStatic
fun removeSheet(sheetView: TrueSheetView) {
synchronized(presentedSheetStack) {
presentedSheetStack.remove(sheetView)
}
}
@JvmStatic
fun clear() {
synchronized(presentedSheetStack) {
presentedSheetStack.clear()
}
}
/**
* Gets the parent sheet of the given sheet, if any.
*/
@JvmStatic
fun getParentSheet(sheetView: TrueSheetView): TrueSheetView? {
synchronized(presentedSheetStack) {
val index = presentedSheetStack.indexOf(sheetView)
if (index <= 0) return null
return presentedSheetStack[index - 1]
}
}
/**
* Returns true if the given sheet is the topmost presented sheet.
*/
@JvmStatic
fun isTopmostSheet(sheetView: TrueSheetView): Boolean {
synchronized(presentedSheetStack) {
return presentedSheetStack.lastOrNull() == sheetView
}
}
}

View File

@ -1,63 +0,0 @@
package com.lodev09.truesheet.core
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowInsets
import android.view.WindowManager
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.PixelUtil
object Utils {
@SuppressLint("DiscouragedApi")
private fun getIdentifierHeight(context: ReactContext, name: String): Int =
context.resources.getDimensionPixelSize(
context.resources.getIdentifier(name, "dimen", "android")
).takeIf { it > 0 } ?: 0
@SuppressLint("InternalInsetResource", "DiscouragedApi")
fun screenHeight(context: ReactContext): Int {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val displayMetrics = DisplayMetrics()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display?.getRealMetrics(displayMetrics)
} else {
windowManager.defaultDisplay.getMetrics(displayMetrics)
}
val screenHeight = displayMetrics.heightPixels
val statusBarHeight = getIdentifierHeight(context, "status_bar_height")
val hasNavigationBar = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.getSystemService(WindowManager::class.java)
?.currentWindowMetrics
?.windowInsets
?.isVisible(WindowInsets.Type.navigationBars()) ?: false
} else {
context.resources.getIdentifier("navigation_bar_height", "dimen", "android") > 0
}
val navigationBarHeight = if (hasNavigationBar) {
getIdentifierHeight(context, "navigation_bar_height")
} else {
0
}
return screenHeight - statusBarHeight - navigationBarHeight
}
fun toDIP(value: Int): Float = PixelUtil.toDIPFromPixel(value.toFloat())
fun toPixel(value: Double): Int = PixelUtil.toPixelFromDIP(value).toInt()
fun withPromise(promise: Promise, closure: () -> Any?) {
try {
val result = closure()
promise.resolve(result)
} catch (e: Throwable) {
e.printStackTrace()
promise.reject("Error", e.message, e.cause)
}
}
}

View File

@ -1,16 +0,0 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
// onDismiss
class DismissEvent(surfaceId: Int, viewId: Int) : Event<DismissEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "dismiss"
}
}

View File

@ -1,16 +0,0 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
// onMount
class MountEvent(surfaceId: Int, viewId: Int) : Event<MountEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "ready"
}
}

View File

@ -1,23 +0,0 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.lodev09.truesheet.SizeInfo
// onPresent
class PresentEvent(surfaceId: Int, viewId: Int, private val sizeInfo: SizeInfo) : Event<PresentEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME
override fun getEventData(): WritableMap {
val data = Arguments.createMap()
data.putInt("index", sizeInfo.index)
data.putDouble("value", sizeInfo.value.toDouble())
return data
}
companion object {
const val EVENT_NAME = "present"
}
}

View File

@ -1,23 +0,0 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.lodev09.truesheet.SizeInfo
// onSizeChange
class SizeChangeEvent(surfaceId: Int, viewId: Int, private val sizeInfo: SizeInfo) : Event<SizeChangeEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME
override fun getEventData(): WritableMap {
val data = Arguments.createMap()
data.putInt("index", sizeInfo.index)
data.putDouble("value", sizeInfo.value.toDouble())
return data
}
companion object {
const val EVENT_NAME = "sizeChange"
}
}

View File

@ -0,0 +1,71 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
/**
* Fired when dragging begins
* Payload: { index: number, position: number, detent: number }
*/
class DragBeginEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<DragBeginEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topDragBegin"
const val REGISTRATION_NAME = "onDragBegin"
}
}
/**
* Fired continuously during dragging
* Payload: { index: number, position: number, detent: number }
*/
class DragChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<DragChangeEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topDragChange"
const val REGISTRATION_NAME = "onDragChange"
}
}
/**
* Fired when dragging ends
* Payload: { index: number, position: number, detent: number }
*/
class DragEndEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<DragEndEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topDragEnd"
const val REGISTRATION_NAME = "onDragEnd"
}
}

View File

@ -0,0 +1,65 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
/**
* Fired when the sheet is about to regain focus because a sheet on top of it is being dismissed
*/
class WillFocusEvent(surfaceId: Int, viewId: Int) : Event<WillFocusEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topWillFocus"
const val REGISTRATION_NAME = "onWillFocus"
}
}
/**
* Fired when the sheet regains focus after a sheet on top of it is dismissed
*/
class FocusEvent(surfaceId: Int, viewId: Int) : Event<FocusEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topDidFocus"
const val REGISTRATION_NAME = "onDidFocus"
}
}
/**
* Fired when the sheet is about to lose focus because another sheet is being presented on top of it
*/
class WillBlurEvent(surfaceId: Int, viewId: Int) : Event<WillBlurEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topWillBlur"
const val REGISTRATION_NAME = "onWillBlur"
}
}
/**
* Fired when the sheet loses focus because another sheet is presented on top of it
*/
class BlurEvent(surfaceId: Int, viewId: Int) : Event<BlurEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topDidBlur"
const val REGISTRATION_NAME = "onDidBlur"
}
}

View File

@ -0,0 +1,109 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
/**
* Fired when the sheet component is mounted and ready
*/
class MountEvent(surfaceId: Int, viewId: Int) : Event<MountEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topMount"
const val REGISTRATION_NAME = "onMount"
}
}
/**
* Fired before the sheet is presented
* Payload: { index: number, position: number, detent: number }
*/
class WillPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<WillPresentEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topWillPresent"
const val REGISTRATION_NAME = "onWillPresent"
}
}
/**
* Fired after the sheet is presented
* Payload: { index: number, position: number, detent: number }
*/
class DidPresentEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<DidPresentEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topDidPresent"
const val REGISTRATION_NAME = "onDidPresent"
}
}
/**
* Fired before the sheet is dismissed
*/
class WillDismissEvent(surfaceId: Int, viewId: Int) : Event<WillDismissEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topWillDismiss"
const val REGISTRATION_NAME = "onWillDismiss"
}
}
/**
* Fired after the sheet is dismissed
*/
class DidDismissEvent(surfaceId: Int, viewId: Int) : Event<DidDismissEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topDidDismiss"
const val REGISTRATION_NAME = "onDidDismiss"
}
}
/**
* Fired when the hardware back button is pressed (Android only)
*/
class BackPressEvent(surfaceId: Int, viewId: Int) : Event<BackPressEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap = Arguments.createMap()
companion object {
const val EVENT_NAME = "topBackPress"
const val REGISTRATION_NAME = "onBackPress"
}
}

View File

@ -0,0 +1,56 @@
package com.lodev09.truesheet.events
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
/**
* Fired when the detent changes
* Payload: { index: number, position: number, detent: number }
*/
class DetentChangeEvent(surfaceId: Int, viewId: Int, private val index: Int, private val position: Float, private val detent: Float) :
Event<DetentChangeEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putInt("index", index)
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
}
companion object {
const val EVENT_NAME = "topDetentChange"
const val REGISTRATION_NAME = "onDetentChange"
}
}
/**
* Fired continuously for position updates during drag and animation
* Payload: { index: number, position: number, detent: number, realtime: boolean }
*/
class PositionChangeEvent(
surfaceId: Int,
viewId: Int,
private val index: Float,
private val position: Float,
private val detent: Float,
private val realtime: Boolean = false
) : Event<PositionChangeEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME
override fun getEventData(): WritableMap =
Arguments.createMap().apply {
putDouble("index", index.toDouble())
putDouble("position", position.toDouble())
putDouble("detent", detent.toDouble())
putBoolean("realtime", realtime)
}
companion object {
const val EVENT_NAME = "topPositionChange"
const val REGISTRATION_NAME = "onPositionChange"
}
}

View File

@ -0,0 +1,29 @@
package com.lodev09.truesheet.utils
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.uimanager.ThemedReactContext
object KeyboardUtils {
/**
* Dismisses the soft keyboard if currently shown.
*/
fun dismiss(reactContext: ThemedReactContext) {
val imm = reactContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
reactContext.currentActivity?.currentFocus?.let { focusedView ->
imm?.hideSoftInputFromWindow(focusedView.windowToken, 0)
}
}
/**
* Gets the current keyboard height from window insets.
*/
fun getKeyboardHeight(view: View): Int {
val insets = ViewCompat.getRootWindowInsets(view)
return insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
}
}

View File

@ -0,0 +1,117 @@
package com.lodev09.truesheet.utils
import android.graphics.Point
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import com.facebook.react.bridge.ReactContext
import kotlin.math.min
/**
* Data class for top/bottom insets
*/
data class Insets(val top: Int, val bottom: Int)
/**
* Utility object for screen dimension calculations.
* Inset calculation approach inspired by react-native-safe-area-context.
*
* Note: This library requires React Native 0.76+ which has minSdk API 24.
*/
object ScreenUtils {
/**
* Get root window insets for API 30+ (Android R)
*/
private fun getInsetsR(rootView: View): Insets? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val insets = rootView.rootWindowInsets?.getInsets(
WindowInsets.Type.statusBars() or
WindowInsets.Type.displayCutout() or
WindowInsets.Type.navigationBars()
) ?: return null
return Insets(
top = insets.top,
bottom = insets.bottom
)
}
return null
}
/**
* Get root window insets for API 24-29
*/
@Suppress("DEPRECATION")
private fun getInsetsLegacy(rootView: View): Insets? {
val insets = rootView.rootWindowInsets ?: return null
return Insets(
top = insets.systemWindowInsetTop,
// Use min to avoid including soft keyboard
bottom = min(insets.systemWindowInsetBottom, insets.stableInsetBottom)
)
}
/**
* Get safe area insets from ReactContext using the activity's decor view.
*
* @param reactContext The ReactContext to get insets from
* @return Insets with top (status bar) and bottom (navigation bar) values in pixels
*/
fun getInsets(reactContext: ReactContext): Insets {
val activity = reactContext.currentActivity ?: return Insets(0, 0)
val decorView = activity.window?.decorView ?: return Insets(0, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getInsetsR(decorView)
} else {
getInsetsLegacy(decorView)
} ?: Insets(0, 0)
}
/**
* Get the real physical device screen height, including system bars.
* This is consistent across all API levels.
*
* @param reactContext The ReactContext to get context from
* @return Real screen height in pixels
*/
@Suppress("DEPRECATION")
fun getRealScreenHeight(reactContext: ReactContext): Int {
val windowManager = reactContext.getSystemService(WindowManager::class.java)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
windowManager.currentWindowMetrics.bounds.height()
} else {
val size = Point()
windowManager.defaultDisplay.getRealSize(size)
size.y
}
}
/**
* Get the screen width using the same method as React Native's useWindowDimensions.
*
* @param reactContext The ReactContext to get resources from
* @return Screen width in pixels
*/
fun getScreenWidth(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.widthPixels
/**
* Calculate the screen height using the same method as React Native's useWindowDimensions.
* This returns the window height which automatically accounts for edge-to-edge mode.
*
* @param reactContext The ReactContext to get resources from
* @return Screen height in pixels
*/
fun getScreenHeight(reactContext: ReactContext): Int = reactContext.resources.displayMetrics.heightPixels
/**
* Get the location of a view in screen coordinates
*
* @param view The view to get screen location for
* @return IntArray with [x, y] coordinates in screen space
*/
fun getScreenLocation(view: View): IntArray {
val location = IntArray(2)
view.getLocationOnScreen(location)
return location
}
}

View File

@ -0,0 +1,63 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(LIB_LITERAL TrueSheetSpec)
set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL})
set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
set(LIB_COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp)
set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni)
set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL})
file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp ${LIB_COMMON_DIR}/react/renderer/components/${LIB_LITERAL}/*.cpp)
file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp)
add_library(
${LIB_TARGET_NAME}
SHARED
${LIB_CUSTOM_SRCS}
${LIB_CODEGEN_SRCS}
)
target_include_directories(
${LIB_TARGET_NAME}
PUBLIC
.
${LIB_COMMON_DIR}
${LIB_ANDROID_GENERATED_JNI_DIR}
${LIB_ANDROID_GENERATED_COMPONENTS_DIR}
)
target_link_libraries(
${LIB_TARGET_NAME}
ReactAndroid::reactnative
ReactAndroid::jsi
fbjni::fbjni
)
target_include_directories(
${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
target_compile_reactnative_options(${LIB_TARGET_NAME} PUBLIC)
else()
target_compile_options(
${LIB_TARGET_NAME}
PRIVATE
-fexceptions
-frtti
-std=c++20
-Wall
)
endif()
target_compile_options(
${LIB_TARGET_NAME}
PRIVATE
-Wpedantic
-Wno-gnu-zero-variadic-macro-arguments
-Wno-dollar-in-identifier-extension
)

View File

@ -0,0 +1,17 @@
#pragma once
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/components/TrueSheetSpec/TrueSheetViewComponentDescriptor.h>
namespace facebook {
namespace react {
JSI_EXPORT
std::shared_ptr<TurboModule> TrueSheetSpec_ModuleProvider(
const std::string &moduleName,
const JavaTurboModule::InitParams &params);
} // namespace react
} // namespace facebook

View File

@ -1,3 +1,12 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
overrides: [
{
exclude: /\/node_modules\//,
presets: ['module:react-native-builder-bob/babel-preset'],
},
{
include: /\/node_modules\//,
presets: ['module:@react-native/babel-preset'],
},
],
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <react/renderer/components/TrueSheetSpec/TrueSheetViewShadowNode.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
namespace facebook::react {
/*
* Descriptor for <TrueSheetView> component.
*/
class TrueSheetViewComponentDescriptor final
: public ConcreteComponentDescriptor<TrueSheetViewShadowNode> {
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;
void adopt(ShadowNode &shadowNode) const override {
auto &concreteShadowNode =
static_cast<TrueSheetViewShadowNode &>(shadowNode);
concreteShadowNode.adjustLayoutWithState();
ConcreteComponentDescriptor::adopt(shadowNode);
}
};
} // namespace facebook::react

View File

@ -0,0 +1,48 @@
#include "TrueSheetViewShadowNode.h"
#include <react/renderer/components/view/conversions.h>
namespace facebook::react {
using namespace yoga;
extern const char TrueSheetViewComponentName[] = "TrueSheetView";
void TrueSheetViewShadowNode::adjustLayoutWithState() {
ensureUnsealed();
auto state = std::static_pointer_cast<
const TrueSheetViewShadowNode::ConcreteState>(getState());
auto stateData = state->getData();
// If container dimensions are set from native, override Yoga's dimensions
if (stateData.containerWidth > 0 || stateData.containerHeight > 0) {
auto &props = getConcreteProps();
yoga::Style adjustedStyle = props.yogaStyle;
auto currentStyle = yogaNode_.style();
bool needsUpdate = false;
// Set width if provided
if (stateData.containerWidth > 0) {
adjustedStyle.setDimension(yoga::Dimension::Width, StyleSizeLength::points(stateData.containerWidth));
if (adjustedStyle.dimension(yoga::Dimension::Width) != currentStyle.dimension(yoga::Dimension::Width)) {
needsUpdate = true;
}
}
// Set height if provided
if (stateData.containerHeight > 0) {
adjustedStyle.setDimension(yoga::Dimension::Height, StyleSizeLength::points(stateData.containerHeight));
if (adjustedStyle.dimension(yoga::Dimension::Height) != currentStyle.dimension(yoga::Dimension::Height)) {
needsUpdate = true;
}
}
if (needsUpdate) {
yogaNode_.setStyle(adjustedStyle);
yogaNode_.setDirty(true);
}
}
}
} // namespace facebook::react

View File

@ -0,0 +1,28 @@
#pragma once
#include <jsi/jsi.h>
#include <react/renderer/components/TrueSheetSpec/EventEmitters.h>
#include <react/renderer/components/TrueSheetSpec/Props.h>
#include <react/renderer/components/TrueSheetSpec/TrueSheetViewState.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
namespace facebook::react {
JSI_EXPORT extern const char TrueSheetViewComponentName[];
/*
* `ShadowNode` for <TrueSheetView> component.
*/
class JSI_EXPORT TrueSheetViewShadowNode final
: public ConcreteViewShadowNode<
TrueSheetViewComponentName,
TrueSheetViewProps,
TrueSheetViewEventEmitter,
TrueSheetViewState> {
using ConcreteViewShadowNode::ConcreteViewShadowNode;
public:
void adjustLayoutWithState();
};
} // namespace facebook::react

View File

@ -0,0 +1,11 @@
#include "TrueSheetViewState.h"
namespace facebook::react {
#ifdef ANDROID
folly::dynamic TrueSheetViewState::getDynamic() const {
return folly::dynamic::object("containerWidth", containerWidth)("containerHeight", containerHeight);
}
#endif
} // namespace facebook::react

View File

@ -0,0 +1,42 @@
#pragma once
#include <memory>
#ifdef ANDROID
#include <folly/dynamic.h>
#include <react/renderer/mapbuffer/MapBuffer.h>
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
#endif
namespace facebook::react {
/*
* State for <TrueSheetView> component.
* Contains the container dimensions from native.
*/
class TrueSheetViewState final {
public:
using Shared = std::shared_ptr<const TrueSheetViewState>;
TrueSheetViewState() = default;
#ifdef ANDROID
TrueSheetViewState(
TrueSheetViewState const &previousState,
folly::dynamic data)
: containerWidth(static_cast<float>(data["containerWidth"].getDouble())),
containerHeight(static_cast<float>(data["containerHeight"].getDouble())) {}
#endif
float containerWidth{0};
float containerHeight{0};
#ifdef ANDROID
folly::dynamic getDynamic() const;
MapBuffer getMapBuffer() const {
return MapBufferBuilder::EMPTY();
}
#endif
};
} // namespace facebook::react

View File

@ -20,7 +20,7 @@ Many users have asked about presenting the sheet from anywhere within their appl
Introducing [static methods](/reference/methods#global-methods)!
With this update, you can now present the sheet from any part of your code by providing a [`name`](/reference/props#name) to your sheet instance. This streamlines the process and makes it easier to manage your sheets across your application.
With this update, you can now present the sheet from any part of your code by providing a [`name`](/reference/configuration#name) to your sheet instance. This streamlines the process and makes it easier to manage your sheets across your application.
Here's an example:
@ -53,7 +53,7 @@ Check out our [guide](/guides/global-methods) on global static methods for examp
### Truly Automatic `auto` Sizing
True Sheet has long supported [`auto`](/reference/types#sheetsize) sizing, where the sheet's height adjusts dynamically based on its content. However, in previous versions, this feature had some limitations and required re-presenting the sheet to update the size after content changes.
True Sheet has long supported [`auto`](/reference/types#detents) sizing, where the sheet's height adjusts dynamically based on its content. However, in previous versions, this feature had some limitations and required re-presenting the sheet to update the size after content changes.
With version `0.10`, `auto` sizing is now truly automatic. Whenever the content within the sheet changes, the sheet's height will adjust seamlessly without the need for re-presenting. 😎

View File

@ -7,3 +7,5 @@ tags: [bottom-sheet, true-sheet, native-sheet]
---
## Hello World!
{/* truncate */}

View File

@ -0,0 +1,41 @@
---
date: 2025-12-16
slug: android-3-4
title: Android Dimming and Stacking Improvements
authors: [lodev09]
tags: [release, android, stacking, dimming]
---
import preview from './assets/android-3-4.gif'
## Sheet is getting good on Android! 🤖
Version 3.4 brings two major upgrades to the Android experience: silky-smooth dimming and natural sheet stacking — now consistent with iOS! ✨
<img alt="Android Dimming and Stacking" src={preview} width="300" />
{/* truncate */}
## 🌑 Smooth Dimming
Say goodbye to clunky window dim! True Sheet now uses a **custom dim view** with real-time interpolation. As you drag the sheet, the dim smoothly fades in and out based on position.
The `dimmedDetentIndex` prop feels alive — drag past the threshold and watch the background dim; drag back down and it fades away. Buttery smooth. 🧈
Learn more in the [Dimming guide](/guides/dimming).
## 📚 Natural Sheet Stacking
When you present a sheet on top of another, the parent now **slides down** instead of disappearing. This creates a beautiful stacking effect that feels right at home on Android.
Resize the child sheet? The parent follows along in real-time to maintain that perfect visual hierarchy. 🎯
Learn more in the [Stacking guide](/guides/stacking).
## Get It 🚀
```sh
yarn add @lodev09/react-native-true-sheet@^3.4.0
```
Have feedback? [Open an issue](https://github.com/lodev09/react-native-true-sheet/issues)!

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

View File

@ -0,0 +1,215 @@
---
date: 2025-12-01
slug: release-3-0
title: Version 3.0 Release
authors: [lodev09]
tags: [release, fabric, new-architecture]
---
import previewIos from '/img/preview-ios.gif'
import previewAndroid from '/img/preview-android.gif'
## True Sheet 3.0 is here!
We're thrilled to announce the biggest update yet! True Sheet has been **completely rebuilt from the ground up** for React Native's New Architecture (Fabric). This isn't just an update — it's a whole new level of performance and native experience.
<div style={{ display: 'flex', gap: '16px', marginBottom: '24px' }}>
<img alt="React Native True Sheet - iOS" src={previewIos} width="300" />
<img alt="React Native True Sheet - Android" src={previewAndroid} width="300" />
</div>
{/* truncate */}
## Powered by Fabric
Version 3 is built **exclusively** for React Native's New Architecture. Here's what that means for you:
- **No Bridge** — Direct C++ communication between JavaScript and native code
- **Blazing Fast** — Synchronous layout updates with Fabric
- **Shared C++ Core** — State and shadow nodes shared between iOS and Android
- **100% Type-safe** — Full TypeScript support with Codegen-generated native interfaces
**Requirements:**
- React Native >= 0.76 (Expo SDK 52+)
- New Architecture enabled (default in RN 0.76+)
## What's New
### Automatic ScrollView Detection
Say goodbye to `scrollRef`! Scroll views are now **automatically detected** on iOS. Just use the `scrollable` prop and you're good to go:
```tsx
<TrueSheet scrollable>
<ScrollView>{/* Your scrollable content */}</ScrollView>
</TrueSheet>
```
See the [Scrolling guide](/guides/scrolling) for more details.
### Native Header and Footer
Add fixed headers and footers that stay put while your content scrolls:
```tsx
<TrueSheet
header={<MyHeader />}
footer={<MyFooter />}
>
<FlatList data={items} renderItem={renderItem} />
</TrueSheet>
```
See the [Header guide](/guides/header) and [Footer guide](/guides/footer) for more details.
### Sheet Stacking
Stack multiple sheets on top of each other on **both iOS and Android**! New focus events help you manage interactions:
- **`onWillFocus`** / **`onDidFocus`** — Sheet regains focus
- **`onWillBlur`** / **`onDidBlur`** — Sheet loses focus
See the [Stacking guide](/guides/stacking) for more details.
### Rich Event System
Track every moment of your sheet's lifecycle:
| Lifecycle | Drag | Position |
| - | - | - |
| `onMount` | `onDragBegin` | `onPositionChange` |
| `onWillPresent` | `onDragChange` | `onDetentChange` |
| `onDidPresent` | `onDragEnd` | |
| `onWillDismiss` | | |
| `onDidDismiss` | | |
See the [onMount guide](/guides/onmount) for handling sheet readiness.
### Reanimated v4 Integration
Seamless animations with dedicated Reanimated components:
```tsx
import {
ReanimatedTrueSheetProvider,
ReanimatedTrueSheet,
useReanimatedTrueSheet,
} from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
const App = () => (
<ReanimatedTrueSheetProvider>
<MySheet />
</ReanimatedTrueSheetProvider>
)
const MySheet = () => (
<ReanimatedTrueSheet detents={[0.3, 0.6, 1]}>
<AnimatedContent />
</ReanimatedTrueSheet>
)
const AnimatedContent = () => {
const { animatedIndex } = useReanimatedTrueSheet()
// Fade in as sheet expands from index 0 to 1
const animatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
<Animated.View style={animatedStyle}>
<Text>I fade in as the sheet expands!</Text>
</Animated.View>
)
}
```
See the [Reanimated guide](/guides/reanimated) for complete examples.
### Draggable Control
Need a static sheet? Disable dragging entirely:
```tsx
<TrueSheet draggable={false} detents={[0.5, 1]}>
<Button title="Expand" onPress={() => sheet.current?.resize(1)} />
</TrueSheet>
```
See the [Resizing guide](/guides/resizing) for programmatic control.
### Dimming Control
Customize when the background dims based on detent index:
```tsx
<TrueSheet detents={[0.3, 0.6, 1]} dimmedDetentIndex={1}>
{/* Dims only when at index 1 or above */}
</TrueSheet>
```
See the [Dimming guide](/guides/dimming) for more details.
### Edge-to-Edge Support (Android)
TrueSheet automatically adapts to Android's edge-to-edge mode. The sheet respects the status bar when fully expanded.
See the [Edge-to-Edge guide](/guides/edge-to-edge) for details.
### Global Static Methods
Present sheets from anywhere in your app using static methods:
```tsx
// Register a named sheet
<TrueSheet name="my-sheet">
{/* content */}
</TrueSheet>
// Present from anywhere
TrueSheet.present('my-sheet')
```
See the [Global Methods guide](/guides/global-methods) for more details.
### React Native Screens Integration
Navigate to other screens from within a sheet without any issues! The sheet will remain visible in the background when presenting modals on top.
:::note
This requires changes to `react-native-screens`. There is a [pending PR](https://github.com/software-mansion/react-native-screens/pull/3415) that adds support for this. In the meantime, you can apply the patch from the [example app](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.18.0-fa7de65975.patch).
:::
See the [React Navigation guide](/guides/navigation) for more details.
## Breaking Changes
Heads up! Version 3 includes some breaking changes:
- **Fabric Required** — Old Paper architecture is no longer supported
- **Prop Renames** — `sizes` → `detents`, `initialIndex` → `initialDetentIndex`, `onPresent` → `onDidPresent`, and more
- **Detent Values** — Use fractional values (`0.5`) instead of percentage strings (`"50%"`)
- **Removed Props** — `scrollRef`, `grabberProps`, `contentContainerStyle`
Check out the [Migration Guide](/migration) for the full list and step-by-step upgrade instructions.
## Get Started
```sh
yarn add @lodev09/react-native-true-sheet@^3.0.0
```
Or with Expo:
```sh
npx expo install @lodev09/react-native-true-sheet
```
Running into issues? Check out the [Troubleshooting guide](/troubleshooting) for common fixes.
## Thank You!
A huge thanks to everyone who contributed, reported issues, and provided feedback. Your support makes True Sheet better with every release!
We're just getting started. Stay tuned for more exciting updates!

View File

@ -0,0 +1,219 @@
---
date: 2025-12-06
slug: sheet-navigator
title: Introducing Sheet Navigator
authors: [lodev09]
tags: [feature, react-navigation, navigation]
---
import navigation from '/docs/guides/assets/navigation.gif'
I'm excited to introduce **Sheet Navigator** — a custom React Navigation navigator that makes presenting sheets as natural as navigating between screens.
<img alt="Sheet Navigator" src={navigation} width="300" />
{/* truncate */}
## The Problem
Bottom sheets are a staple of modern mobile apps. But integrating them with React Navigation has always been... awkward. You'd typically have to:
1. Manage sheet refs manually
2. Call `present()` and `dismiss()` imperatively
3. Sync sheet state with navigation state yourself
4. Handle the back button separately
5. Deal with focus/blur when modals appear on top
It works, but it's not elegant. I wanted sheets to feel like first-class citizens in React Navigation.
## The Solution: Sheet Navigator
Sheet Navigator treats sheets as navigation destinations. The first screen is your base content, and every other screen becomes a sheet:
```tsx
import { NavigationContainer } from '@react-navigation/native';
import { createTrueSheetNavigator } from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator();
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
<Sheet.Screen name="Home" component={HomeScreen} />
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}
```
Now you can navigate to sheets like any other screen:
```tsx
navigation.navigate('Details', { itemId: 123 });
```
And dismiss them naturally:
```tsx
navigation.goBack();
```
## Features
### Full Screen Options Support
All TrueSheet props work as screen options. Configure each sheet declaratively:
```tsx
<Sheet.Screen
name="Settings"
component={SettingsSheet}
options={{
detents: [0.5, 1],
cornerRadius: 20,
grabber: true,
dimmedDetentIndex: 1,
backgroundColor: '#1a1a1a',
}}
/>
```
### Programmatic Resizing
Use the `useTrueSheetNavigation` hook to resize sheets from within:
```tsx
import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation';
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
return (
<View>
<Button title="Expand" onPress={() => navigation.resize(1)} />
<Button title="Collapse" onPress={() => navigation.resize(0)} />
</View>
);
}
```
### Rich Event System
Listen to sheet lifecycle events at the navigator level:
```tsx
<Sheet.Navigator
screenListeners={{
sheetWillPresent: (e) => console.log('Presenting at index:', e.data.index),
sheetDidDismiss: () => console.log('Sheet dismissed'),
sheetDetentChange: (e) => console.log('Detent changed to:', e.data.index),
sheetPositionChange: (e) => console.log('Position:', e.data.position),
}}
>
```
All 14 events from TrueSheet are available:
| Lifecycle | Drag | Focus |
|-----------|------|-------|
| `sheetWillPresent` | `sheetDragBegin` | `sheetWillFocus` |
| `sheetDidPresent` | `sheetDragChange` | `sheetDidFocus` |
| `sheetWillDismiss` | `sheetDragEnd` | `sheetWillBlur` |
| `sheetDidDismiss` | `sheetPositionChange` | `sheetDidBlur` |
| | `sheetDetentChange` | |
### Wrap Your Existing Navigation
Already have a complex navigation setup? Wrap it with Sheet Navigator to present sheets from anywhere:
```tsx
const Stack = createNativeStackNavigator();
const Sheet = createTrueSheetNavigator();
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
<Sheet.Screen name="Root" component={RootStack} />
<Sheet.Screen name="QuickActions" component={QuickActionsSheet} />
<Sheet.Screen name="Share" component={ShareSheet} />
</Sheet.Navigator>
</NavigationContainer>
);
}
```
Now any screen in your app can present sheets:
```tsx
// From anywhere in RootStack
navigation.navigate('QuickActions');
```
### Expo Router Support
Using Expo Router? Sheet Navigator works with `withLayoutContext`:
```tsx
// app/_layout.tsx
import { withLayoutContext } from 'expo-router';
import { createTrueSheetNavigator } from '@lodev09/react-native-true-sheet/navigation';
const { Navigator } = createTrueSheetNavigator();
export default withLayoutContext(Navigator);
```
```tsx
// app/details.tsx
export const unstable_settings = {
options: { detents: ['auto', 1], cornerRadius: 16 },
};
export default function DetailsSheet() {
// Sheet content
}
```
## Under the Hood
Sheet Navigator is built on a custom router that extends React Navigation's StackRouter. Here's what makes it work:
**Smart Dismiss Handling** — When you call `goBack()`, the sheet animates out smoothly before the route is removed. No janky state transitions.
**Resize State Management** — The router tracks resize operations with `resizeIndex` and `resizeKey`, ensuring smooth transitions between detents.
**Focus/Blur Tracking** — When modals are presented on top of sheets, focus events are emitted so you can pause/resume operations accordingly.
## Getting Started
Sheet Navigator is included in `@lodev09/react-native-true-sheet` v3.1.0+. Just import from the navigation subpath:
```tsx
import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';
```
Check out the [full documentation](/guides/navigation) for more examples and API details.
Running into issues? See the [Troubleshooting guide](/troubleshooting) for common fixes.
## That's All
Have feedback or feature requests? [Open an issue](https://github.com/lodev09/react-native-true-sheet/issues) — I'd love to hear from you!

View File

@ -1,9 +1,4 @@
{
"label": "Guides",
"position": 4,
"link": {
"type": "generated-index",
"description": "Guides on how to use True Native Bottom Sheet on your React Native app.",
"keywords": ["bottom sheet guides", "how to add bottom sheet", "bottom sheet recipes", "bottom sheet getting started", "using native bottom sheet"]
}
"position": 5
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

View File

@ -0,0 +1,108 @@
---
title: Dimming the Background
description: Control the bottom sheet's dimming behavior.
keywords: [bottom sheet dimming, bottom sheet background, inline bottom sheet, maps bottom sheet]
---
import dimming from './assets/dimming.gif'
One of the most common use cases for a Bottom Sheet is to present it while still allowing users to interact with background components, such as in a Maps app.
In this guide, you can configure `TrueSheet` to achieve this exact functionality.
<img alt="dimming" src={dimming} width="300"/>
## How?
You can easily disable the dimmed background of the sheet by setting [`dimmed`](/reference/configuration#dimmed) to `false`.
```tsx {5}
export const App = () => {
return (
<TrueSheet
detents={['auto', 0.69, 1]}
dimmed={false}
>
<View />
</TrueSheet>
)
}
```
### Dimmed by Detent Index
To further customize the dimming behavior, [`dimmedDetentIndex`](/reference/configuration#dimmeddetentindex) is also available. Set the [detent](/reference/configuration#detents) `index` at which you want the sheet to start dimming.
```tsx {5}
export const App = () => {
return (
<TrueSheet
detents={['auto', 0.69, 1]}
dimmedDetentIndex={1} // Dim will start at 69% ✅
>
<View />
</TrueSheet>
)
}
```
:::info
`dimmedDetentIndex` is ignored if `dimmed` is set to `false`.
:::
## Customizing Dimming Alpha
You can dynamically control the dimming opacity based on the sheet's position using `ReanimatedTrueSheet` with `animatedPosition`.
:::tip
Learn more about Reanimated integration in the [Reanimated guide](/guides/reanimated).
:::
```tsx {2-3,7,9-11,14-21}
import { ReanimatedTrueSheet, ReanimatedTrueSheetProvider, useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { StyleSheet } from 'react-native'
export const App = () => {
return (
<ReanimatedTrueSheetProvider>
<View style={styles.container}>
<CustomBackdrop />
<Sheet />
</View>
</ReanimatedTrueSheetProvider>
)
}
const CustomBackdrop = () => {
const { animatedPosition } = useReanimatedTrueSheet()
const backdropStyle = useAnimatedStyle(() => ({
opacity: animatedPosition.value * 0.5, // Adjust multiplier for desired alpha
}))
return <Animated.View style={[StyleSheet.absoluteFill, styles.backdrop, backdropStyle]} />
}
const Sheet = () => {
return (
<ReanimatedTrueSheet
detents={[0.25, 0.5, 1]}
dimmed={false}
>
<View />
</ReanimatedTrueSheet>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
backdrop: {
backgroundColor: 'black',
},
})
```
This allows you to create custom dimming effects that respond to the sheet's movement in real-time.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

View File

@ -1,51 +0,0 @@
---
title: Dimming the Background
description: Control the bottom sheet's dimming behavior.
keywords: [bottom sheet dimming, bottom sheet background, inline bottom sheet, maps bottom sheet]
---
import dimming from './dimming.gif'
One of the most common use cases for a Bottom Sheet is to present it while still allowing users to interact with background components, such as in a Maps app.
In this guide, you can configure `TrueSheet` to achieve this exact functionality.
<img alt="dimming" src={dimming} width="300"/>
## How?
You can easily disable the dimmed background of the sheet by setting [`dimmed`](/reference/props#dimmed) to `false`.
```tsx {5}
export const App = () => {
return (
<TrueSheet
sizes={['auto', '69%', 'large']}
dimmed={false}
>
<View />
</TrueSheet>
)
}
```
### Dimmed by Size Index
To further customize the dimming behavior, [`dimmedIndex`](/reference/props#dimmedindex) is also available. Set the [size](/reference/props#sizes) `index` at which you want the sheet to start dimming.
```tsx {5}
export const App = () => {
return (
<TrueSheet
sizes={['auto', '69%', 'large']}
dimmedIndex={1} // Dim will start at 69% ✅
>
<View />
</TrueSheet>
)
}
```
:::info
`dimmedIndex` is ignored if `dimmed` is set to `false`.
:::

View File

@ -0,0 +1,41 @@
---
title: Edge-to-Edge
description: Configure edge-to-edge display mode on Android.
keywords: [bottom sheet edge-to-edge, android edge-to-edge, full screen sheet]
---
TrueSheet automatically detects and adapts to Android's edge-to-edge display mode, providing a modern, immersive experience.
## Enabling Edge-to-Edge
Edge-to-edge is supported in React Native 0.81+ via the `edgeToEdgeEnabled` Gradle property.
Enable it in your `android/gradle.properties`:
```gradle title="android/gradle.properties"
edgeToEdgeEnabled=true
```
:::info
This property is **disabled by default** for current Android versions. TrueSheet will auto-detect when you enable it.
Starting with **Android 16+ (API level 36)**, edge-to-edge will be [automatically enabled by default](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge) by the system.
:::
:::tip
**Learn more:**
- [React Native 0.81 edge-to-edge announcement](https://reactnative.dev/blog/2025/08/12/react-native-0.81)
- [Android 16 edge-to-edge enforcement](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge)
:::
## Default Behavior
By default, when edge-to-edge is enabled, the sheet respects the status bar and stops at the bottom of it when fully expanded. This ensures content remains visible and not obscured by the status bar.
```tsx
<TrueSheet ref={sheet}>
<View />
</TrueSheet>
```
TrueSheet works seamlessly with edge-to-edge enabled and provides automatic status bar detection.

View File

@ -22,12 +22,12 @@ const SomeFooter = () => {
}
```
Stick it in the [`FooterComponent`](/reference/props#footercomponent).
Stick it in the [`footer`](/reference/configuration#footer).
```tsx {3}
const App = () => {
return (
<TrueSheet FooterComponent={SomeFooter}>
<TrueSheet footer={SomeFooter}>
<View />
</TrueSheet>
)
@ -42,7 +42,7 @@ const App = () => {
return (
<TrueSheet
ref={sheet}
FooterComponent={
footer={
<View>
<Text>My Foot-er is more awesome.</Text>
</View>
@ -54,3 +54,38 @@ const App = () => {
}
```
:::
## Safe Area Handling
The footer is pinned to the bottom edge of the sheet. The sheet height automatically includes the bottom safe area on both iOS and Android. To ensure your footer extends properly into the safe area, add bottom padding:
```tsx
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const isIPad = Platform.OS === 'ios' && Platform.isPad;
const MyFooter = () => {
const insets = useSafeAreaInsets();
const bottomInset = isIPad ? 0 : insets.bottom;
return (
<View style={{ paddingBottom: bottomInset, backgroundColor: '#333' }}>
<View style={{ height: 60, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ color: '#fff' }}>Footer Content</Text>
</View>
</View>
);
};
// Usage
<TrueSheet detents={['auto']} footer={<MyFooter />}>
{/* content */}
</TrueSheet>
```
This ensures the footer background extends into the safe area while keeping the content above the home indicator.
:::note
On iPad, the sheet is displayed as a floating modal, so bottom padding is not needed.
:::

View File

@ -10,7 +10,7 @@ To resolve this issue, `TrueSheet` provides [global methods](/reference/methods#
## How?
Somewhere in your App, define the sheet with a [`name`](/reference/props#name).
Somewhere in your App, define the sheet with a [`name`](/reference/configuration#name).
```tsx {3}
const App = () => {

View File

@ -0,0 +1,71 @@
---
title: Adding Header
description: Add a header component that stays above the scrollable content.
keywords: [bottom sheet header, bottom sheet fixed header, bottom sheet sticky header]
---
Need a fixed header above your scrollable content? A title bar, search input, or navigation controls? TrueSheet makes it simple with the `header` prop!
## Using the `header` Prop
The recommended way to add a header is using the `header` prop. This creates a native header view that is properly accounted for in layout calculations, ensuring your scrollable content gets the correct available height.
```tsx {4-8}
const App = () => {
return (
<TrueSheet
ref={sheet}
header={
<View style={styles.header}>
<Text>My Header</Text>
</View>
}
>
<ScrollView nestedScrollEnabled>
<View>{/* Your scrollable content */}</View>
</ScrollView>
</TrueSheet>
)
}
const styles = StyleSheet.create({
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
})
```
## With Search Input
A common use case is adding a search bar in the header:
```tsx {4-8}
const App = () => {
return (
<TrueSheet
ref={sheet}
header={
<View style={styles.header}>
<TextInput placeholder="Search..." style={styles.input} />
</View>
}
>
<FlatList
nestedScrollEnabled
data={items}
renderItem={({ item }) => <ItemRow item={item} />}
/>
</TrueSheet>
)
}
```
:::tip
When using `header` with `FlatList` or `ScrollView`, the content area height is automatically adjusted to account for the header height. This ensures proper scrolling behavior on both iOS and Android.
:::
## Platform Support
The `header` prop is supported on both **iOS** and **Android**.

View File

@ -1,11 +1,133 @@
---
title: Testing with Jest
description: Mocking the bottom sheet component using Jest.
description: Mock the bottom sheet component for testing.
keywords: [bottom sheet jest, testing bottom sheet, mocking bottom sheet]
---
When using `jest`, simply mock the entire package.
Testing components that use `TrueSheet` is straightforward with the built-in Jest mocks.
## Setup
Add the mocks to your Jest setup file:
```js
// jest.setup.js
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
Configure Jest to use the setup file in your `package.json`:
```json
{
"jest": {
"setupFilesAfterEnv": ["<rootDir>/jest.setup.js"]
}
}
```
## Available Mocks
### Main Module (`/mock`)
- `TrueSheet` - Component with mocked `present`, `dismiss`, `resize` methods
- `TrueSheetProvider` - Pass-through provider
- `useTrueSheet` - Hook returning mocked methods
### Navigation Module (`/navigation/mock`)
- `createTrueSheetNavigator` - Mocked navigator factory
- `TrueSheetActions` - Mocked action creators
- `useTrueSheetNavigation` - Hook returning mocked navigation object
### Reanimated Module (`/reanimated/mock`)
- `ReanimatedTrueSheet` - Component with mocked methods
- `ReanimatedTrueSheetProvider` - Pass-through provider
- `useReanimatedTrueSheet` - Hook returning mocked shared values
- `useReanimatedPositionChangeHandler` - Mocked handler hook
## Testing Static Methods
All static methods are mocked as Jest functions.
```tsx
jest.mock('@lodev09/react-native-true-sheet')
import { TrueSheet } from '@lodev09/react-native-true-sheet';
it('should present sheet', async () => {
await TrueSheet.present('my-sheet', 0);
expect(TrueSheet.present).toHaveBeenCalledWith('my-sheet', 0);
});
it('should dismiss sheet', async () => {
await TrueSheet.dismiss('my-sheet');
expect(TrueSheet.dismiss).toHaveBeenCalledWith('my-sheet');
});
```
## Testing Component Rendering
The mock renders `TrueSheet` as a View with all props passed through.
```tsx
it('should render sheet content', () => {
const { getByText } = render(
<TrueSheet name="test" initialDetentIndex={0}>
<Text>Sheet Content</Text>
</TrueSheet>
);
expect(getByText('Sheet Content')).toBeDefined();
});
```
## Testing Reanimated Integration
```tsx
import {
useReanimatedTrueSheet,
ReanimatedTrueSheetProvider,
} from '@lodev09/react-native-true-sheet/reanimated';
it('should return mocked shared values', () => {
const { result } = renderHook(() => useReanimatedTrueSheet(), {
wrapper: ReanimatedTrueSheetProvider,
});
expect(result.current.animatedPosition.value).toBe(0);
expect(result.current.animatedIndex.value).toBe(-1);
});
```
## Best Practices
Clear mock calls between tests to avoid interference.
```tsx
describe('MyComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// Your tests...
});
```
:::tip
All methods return resolved Promises, so remember to `await` them in your tests.
:::

View File

@ -0,0 +1,57 @@
---
title: Keyboard Handling
description: How TrueSheet handles keyboard visibility.
keywords: [bottom sheet keyboard, bottom sheet input, bottom sheet text input]
---
import keyboard from './assets/keyboard.gif'
TrueSheet handles keyboard visibility natively on both iOS and Android. When a `TextInput` inside the sheet is focused, the sheet automatically adjusts to keep the input visible above the keyboard.
<img alt="keyboard" src={keyboard} width="300"/>
## How?
The keyboard handling is built into the native implementation:
- **iOS**: Uses `UISheetPresentationController`'s built-in keyboard avoidance
- **Android**: Tracks keyboard height via `WindowInsetsAnimationCompat` and reconfigures sheet detents in real-time
No additional configuration is required. Simply add your `TextInput` components inside the sheet:
```tsx
const App = () => {
return (
<TrueSheet ref={sheet} detents={['auto']}>
<View style={{ padding: 16 }}>
<TextInput
placeholder="Type something..."
style={{ borderWidth: 1, borderColor: '#ccc', padding: 12, borderRadius: 8 }}
/>
</View>
</TrueSheet>
)
}
```
## Footer Behavior
When using a [`footer`](/reference/configuration#footer) component, it automatically repositions above the keyboard, staying visible while the user types.
## Autofocus Limitation
Avoid using `autoFocus` on `TextInput` components inside the sheet. The keyboard may appear before the sheet has finished presenting, causing layout issues.
Instead, focus the input programmatically after the sheet has presented:
```tsx {4,7}
const inputRef = useRef<TextInput>(null)
const handlePresent = () => {
inputRef.current?.focus()
}
<TrueSheet onDidPresent={handlePresent}>
<TextInput ref={inputRef} placeholder="Type something..." />
</TrueSheet>
```

View File

@ -0,0 +1,97 @@
---
title: Liquid Glass
description: Configure iOS Liquid Glass visual effect for bottom sheets.
keywords: [bottom sheet liquid glass, ios liquid glass, sheet visual effect, ios 26]
---
import glass from '/img/preview-ios.gif'
Starting with iOS 26, Apple introduced the **Liquid Glass** visual effect, a new design element that creates a frosted glass appearance on sheets and modals.
TrueSheet **supports Liquid Glass by default** on iOS 26+, giving your sheets a modern, native look.
<img alt="liquid-glass" src={glass} width="300"/>
## What is Liquid Glass?
Liquid Glass is a visual effect introduced in iOS 26 that provides a translucent, frosted glass appearance with a subtle blur. It's part of Apple's latest design language and is automatically applied to native sheet presentations.
By default, TrueSheet enables Liquid Glass on iOS 26+ devices when no [`backgroundColor`](/reference/configuration#backgroundcolor) or [`backgroundBlur`](/reference/configuration#backgroundblur) is provided. The sheet will automatically display with the Liquid Glass effect.
## Disabling Liquid Glass
If you prefer the classic sheet appearance without Liquid Glass, there are two options:
### Using `backgroundColor` or `backgroundBlur`
Setting [`backgroundColor`](/reference/configuration#backgroundcolor) or [`backgroundBlur`](/reference/configuration#backgroundblur) (or both) on the sheet will disable the Liquid Glass effect for that specific sheet.
```tsx
<TrueSheet backgroundColor="#ffffff">
{/* Sheet content */}
</TrueSheet>
```
```tsx
<TrueSheet backgroundBlur="system-material">
{/* Sheet content */}
</TrueSheet>
```
This approach allows you to disable Liquid Glass on a per-sheet basis while keeping it enabled for other sheets in your app.
:::note
This only works on iOS 26.1 and above.
:::
### Using `UIDesignRequiresCompatibility`
Set `UIDesignRequiresCompatibility` to `true` in your `Info.plist` to disable Liquid Glass.
#### Using Info.plist
Add the following key to your `ios/YourApp/Info.plist`:
```xml title="ios/YourApp/Info.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Other keys... -->
<key>UIDesignRequiresCompatibility</key>
<true/>
</dict>
</plist>
```
#### Using Expo Config Plugin
If you're using Expo, you can configure this through your `app.json` or `app.config.js`:
```js title="app.config.js"
export default {
expo: {
ios: {
infoPlist: {
UIDesignRequiresCompatibility: true,
},
},
},
}
```
After making this change, rebuild your app:
```sh
npx expo prebuild --clean
npx expo run:ios
```
:::note
This setting disables the Liquid Glass UI across your entire app, not just for sheets. See [Apple's documentation](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIDesignRequiresCompatibility) for more details.
:::
## Learn More
- [How to create Apple Maps style liquid glass sheets in Expo](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets) - Expo Blog
- [Apple's Liquid Glass Documentation](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass)

View File

@ -0,0 +1,256 @@
---
title: React Navigation
description: Navigate to other screens from within the bottom sheet.
keywords: [bottom sheet navigation, react-navigation, navigating from sheet, sheet navigator]
---
import navigation from './assets/navigation.gif'
TrueSheet integrates with React Navigation out of the box. It just works!
<img alt="navigation" src={navigation} width="300"/>
## How?
You can use the [Sheet Navigator](#sheet-navigator) to present screens as sheets, or simply [navigate from within sheets](#navigating-from-sheets) using your existing navigation setup.
## Sheet Navigator
TrueSheet provides a custom navigator for React Navigation. The first screen (or `initialRouteName`) is the base content, while other screens are presented as sheets.
```bash
npm install @react-navigation/native
```
### Basic Usage
```tsx
import { NavigationContainer } from '@react-navigation/native';
import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator();
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
{/* Base screen (first screen is the default) */}
<Sheet.Screen name="Main" component={MainScreen} />
{/* Sheet screens */}
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}
```
### Wrapping Existing Navigation
Wrap your root navigator to present sheets from anywhere:
```tsx
const Stack = createNativeStackNavigator();
const Sheet = createTrueSheetNavigator();
function RootStack() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>
);
}
function App() {
return (
<NavigationContainer>
<Sheet.Navigator>
<Sheet.Screen name="Root" component={RootStack} />
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{ detents: ['auto', 1], cornerRadius: 16 }}
/>
</Sheet.Navigator>
</NavigationContainer>
);
}
```
### Navigation & Resizing
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
return (
<View>
<Button title="Expand" onPress={() => navigation.resize(1)} />
<Button title="Close" onPress={() => navigation.goBack()} />
</View>
);
}
```
### Screen Options
All [TrueSheet props](/reference/configuration) are available as screen options, plus the following navigation-specific options:
| Option | Type | Description |
|--------|------|-------------|
| `detentIndex` | `number` | The detent index to present at. Defaults to `0`. |
| `reanimated` | `boolean` | Enable worklet-based position events for this screen. |
| `positionChangeHandler` | `function` | A callback that receives position change events. When `reanimated` is enabled, this must be a worklet function. |
### Reanimated Integration
Enable worklet-based position events for smooth UI thread animations:
```tsx
// In your navigator
<Sheet.Screen
name="Details"
component={DetailsSheet}
options={{
reanimated: true,
positionChangeHandler: (payload) => {
'worklet';
// Access payload.position, payload.detentIndex, etc.
console.log(payload.position);
},
}}
/>
```
:::note
When `reanimated: true` is set, `react-native-reanimated` must be installed and `positionChangeHandler` must be a worklet function. The integration is lazy-loaded, so screens without `reanimated: true` don't require reanimated.
:::
### Screen Listeners
```tsx
<Sheet.Navigator
screenListeners={{
sheetDidPresent: (e) => console.log('Presented:', e.data.index),
sheetDidDismiss: () => console.log('Dismissed'),
}}
>
```
| Event | Description |
|-------|-------------|
| `sheetWillPresent` | Sheet is about to present |
| `sheetDidPresent` | Sheet finished presenting |
| `sheetWillDismiss` | Sheet is about to dismiss |
| `sheetDidDismiss` | Sheet finished dismissing |
| `sheetDetentChange` | Detent changed |
| `sheetDragBegin` | User started dragging |
| `sheetDragChange` | User is dragging |
| `sheetDragEnd` | User stopped dragging |
| `sheetPositionChange` | Position changed |
### Expo Router
```
app/
├── _layout.tsx # TrueSheet navigator
├── index.tsx # Base content
└── details.tsx # Sheet screen
```
```tsx
// app/_layout.tsx
import { withLayoutContext } from 'expo-router';
import {
createTrueSheetNavigator,
type TrueSheetNavigationEventMap,
type TrueSheetNavigationOptions,
type TrueSheetNavigationState,
} from '@lodev09/react-native-true-sheet/navigation';
import type { ParamListBase } from '@react-navigation/native';
const { Navigator } = createTrueSheetNavigator();
const Sheet = withLayoutContext<
TrueSheetNavigationOptions,
typeof Navigator,
TrueSheetNavigationState<ParamListBase>,
TrueSheetNavigationEventMap
>(Navigator);
export default function SheetLayout() {
return (
<Sheet>
<Sheet.Screen name="index" />
<Sheet.Screen
name="details"
options={{
detents: ['auto', 1],
cornerRadius: 16,
}}
/>
</Sheet>
);
}
```
## Navigating from Sheets
:::note
Requires a [patch to react-native-screens](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.18.0-fa7de65975.patch). See [PR #3415](https://github.com/software-mansion/react-native-screens/pull/3415).
:::
Navigate directly from sheets - they remain visible when presenting modals on top.
```tsx
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
```
### Presenting on Screen Focus
When using `useFocusEffect`, delay presentation to avoid iOS issues:
```tsx
useFocusEffect(
useCallback(() => {
requestAnimationFrame(() => {
sheet.current?.present()
})
}, [])
)
```
### Present During Mount
When using `initialDetentIndex` with animation, the sheet may behave unexpectedly during screen transitions.
**Solution 1:** Disable animation
```tsx
<TrueSheet initialDetentIndex={0} initialDetentAnimated={false}>
```
**Solution 2:** Wait for transition
```tsx
const [ready, setReady] = useState(false)
useEffect(() => {
const unsubscribe = navigation.addListener("transitionEnd", ({ data }) => {
if (!data.closing) setReady(true)
})
return unsubscribe
}, [])
if (!ready) return null
return <TrueSheet initialDetentIndex={0}>{/* ... */}</TrueSheet>
```

View File

@ -0,0 +1,37 @@
---
title: Present On Mount
description: Present the bottom sheet on mount.
keywords: [bottom sheet on mount, bottom sheet initialIndex]
---
import onmount from './assets/onmount.gif'
Sometimes, you may want to present the sheet directly during mount. For example, you might want to present the sheet when a screen is opened through a deep link.
<img alt="onmount" src={onmount} width="300"/>
## How?
You can do this by setting [`initialDetentIndex`](/reference/configuration#initialdetentindex) prop. It accepts the [`detent`](/reference/types#sheetdetent) `index` that your sheet is configured with. See [detents](/reference/configuration#detents) prop for more information.
```tsx {5-6}
const App = () => {
return (
<TrueSheet
detents={['auto', 0.69, 1]}
initialDetentIndex={1}
initialDetentAnimated
>
<View />
</TrueSheet>
)
}
```
### Disabling Animation
You may want to disable the present animation. To do this, simply set [`initialDetentAnimated`](/reference/configuration#initialdetentanimated) to `false`.
### Using with React Navigation
Using this with [`react-navigation`](https://reactnavigation.org) can cause render issue. Check out the [reat-navigation guide](/guides/navigation#present-during-mount) for the fix.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -1,37 +0,0 @@
---
title: Presenting During Mount
description: Present the bottom sheet on mount.
keywords: [bottom sheet on mount, bottom sheet initialIndex]
---
import onmount from './onmount.gif'
Sometimes, you may want to present the sheet directly during mount. For example, you might want to present the sheet when a screen is opened through a deep link.
<img alt="onmount" src={onmount} width="300"/>
## How?
You can do this by setting [`initialIndex`](/reference/props#initialindex) prop. It accepts the [`size`](/reference/types#sheetsize) `index` that your sheet is configured with. See [sizes](/reference/props#sizes) prop for more information.
```tsx {5-6}
const App = () => {
return (
<TrueSheet
sizes={['auto', '69%', 'large']}
initialIndex={1}
initialindexanimated
>
<View />
</TrueSheet>
)
}
```
### Disabling Animation
You may want to disable the present animation. To do this, simply set [`initialIndexAnimated`](/reference/props#initialindexanimated) to `false`.
### Using with React Navigation
Using this with [`react-navigation`](https://reactnavigation.org) can cause render issue. Check out the [troubleshooting guide](/troubleshooting#present-during-mount) for the fix 😉.

View File

@ -0,0 +1,102 @@
---
title: Reanimated
description: Sync animations with your sheet using Reanimated.
keywords: [bottom sheet, react-native-reanimated, reanimated, animations]
---
import reanimated from './assets/reanimated.gif'
`TrueSheet` has first-class support for [react-native-reanimated v4](https://docs.swmansion.com/react-native-reanimated/).
<img alt="reanimated" src={reanimated} width="300"/>
:::info Requirements
- `react-native-reanimated`: ^4.0.0
- `react-native-worklets` (peer dependency of Reanimated v4)
:::
## How?
### 1. Add the Provider
Manages shared values for Reanimated integration.
```tsx
import { ReanimatedTrueSheetProvider } from '@lodev09/react-native-true-sheet/reanimated'
function App() {
return (
<ReanimatedTrueSheetProvider>
<YourApp />
</ReanimatedTrueSheetProvider>
)
}
```
### 2. Use ReanimatedTrueSheet
Animated sheet component that syncs position automatically with all props from [`TrueSheet`](/reference/configuration).
```tsx
import { type TrueSheet } from '@lodev09/react-native-true-sheet'
import { ReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
function MyScreen() {
const sheetRef = useRef<TrueSheet>(null)
return (
<ReanimatedTrueSheet
ref={sheetRef}
detents={[0.3, 0.6, 1]}
initialDetentIndex={1}
>
<Text>Sheet Content</Text>
</ReanimatedTrueSheet>
)
}
```
:::info
Note that the `onPositionChange` prop event now runs on the UI thread (worklet). If you override this prop, make sure to add the `'worklet'` directive to your handler.
:::
### 3. Access Animated Values
Use the `useReanimatedTrueSheet` hook to access the sheet's animated values.
```tsx
import { useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
function MyComponent() {
const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet()
// Example: Move component based on sheet position
const positionStyle = useAnimatedStyle(() => ({
transform: [{ translateY: -animatedPosition.value }]
}))
// Example: Fade in as sheet expands from index 0 to 1
const opacityStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
<Animated.View style={[positionStyle, opacityStyle]}>
<Text>This moves and fades with the sheet</Text>
</Animated.View>
)
}
```
#### Available Values
| Value | Type | Description |
| - | - | - |
| `animatedPosition` | `SharedValue<number>` | The current Y position of the sheet relative to the screen. |
| `animatedIndex` | `SharedValue<number>` | The current detent index as a continuous float. Interpolates smoothly between detent indices during drag (e.g., `0.5` when halfway between index 0 and 1). |
| `animatedDetent` | `SharedValue<number>` | The current detent value (0-1 fraction of screen height). Interpolates smoothly between detent values as the sheet is dragged. |
## Examples
See the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for complete implementations.

View File

@ -0,0 +1,98 @@
---
title: Resizing Programmatically
description: Programmatically resize the bottom sheet and listen for detent changes.
keywords: [bottom sheet resizing, bottom sheet detents, bottom sheet auto resizing]
---
import resizing from './assets/resizing.gif'
`TrueSheet` has a main prop called [`detents`](/reference/configuration#detents) which allows you to define the detents that the sheet can support. This is an array of [`SheetDetent`](/reference/types#sheetdetent) that supports values like `"auto"` or fractional numbers (0-1).
In some cases, you may want to resize the sheet programmatically for a better experience.
<img alt="resizing" src={resizing} width="300"/>
## How?
### Resize Programmatically
Define the sheet and use the [`resize`](/reference/methods#resize) method.
```tsx {2-2,6-6,11-13}
const App = () => {
const sheet = useRef<TrueSheet>(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
console.log('Yay, we are now at 69% 💦')
}
return (
<TrueSheet name="resizing-sheet" ref={sheet} detents={['auto', 0.69, 1]}>
<Button onPress={resize} title="Resize" />
</TrueSheet>
)
}
```
:::tip
You can also do it globally using the related [global method](/reference/methods#global-methods).
```tsx
TrueSheet.resize('resizing-sheet', 1)
```
:::
:::info
`detents` can only support up to 3 detents. **_collapsed_**, **_half-expanded_**, and **_expanded_**.
:::
:::info
Use [`insetAdjustment="never"`](/reference/configuration#insetadjustment) to disable automatic bottom inset adjustment on both platforms.
:::
:::tip
If you want to disable user dragging and only allow programmatic resizing, set [`draggable={false}`](/reference/configuration#draggable).
:::
### Listening to Detent Change
If you want to get the active detent information, you can listen to detent changes by providing the [`onDetentChange`](/reference/events#ondetentchange) event.
The event comes with the [`DetentInfoEventPayload`](/reference/types#detentinfoeventpayload) that provides the detent `index` and `position` (Y position on screen).
:::tip
Use the `index` to reference the detent from your `detents` array. For example, if `detents={['auto', 0.69, 1]}` and `index` is `1`, the active detent is `0.69`.
:::
```tsx {9-11,17-17}
const App = () => {
const sheet = useRef<TrueSheet>(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
}
const handleDetentChange = (e: DetentChangeEvent) => {
const { index, position } = e.nativeEvent
console.log('Detent index:', index, 'position:', position) ✅
}
return (
<TrueSheet
ref={sheet}
detents={['auto', 0.69, 1]}
onDetentChange={handleDetentChange}
>
<Button onPress={resize} title="Resize" />
</TrueSheet>
)
}
```
:::info
The event will also trigger when the user drags the sheet into a detent.
:::

Some files were not shown because too many files have changed in this diff Show More