react-native-unified-action-sheet 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +25 -113
  3. package/android/build.gradle +28 -1
  4. package/android/src/main/java/com/unifiedactionsheet/ActionSheetOptions.kt +6 -2
  5. package/android/src/main/java/com/unifiedactionsheet/UnifiedActionSheetModule.kt +6 -0
  6. package/android/src/material/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  7. package/android/src/material/java/com/unifiedactionsheet/MaterialBottomSheetPresenter.kt +90 -0
  8. package/android/src/material/res/values/unified_action_sheet_bottom_sheet.xml +22 -0
  9. package/android/src/noMaterial/java/com/unifiedactionsheet/BottomSheetPresenterFactory.kt +10 -0
  10. package/app.plugin.js +1 -0
  11. package/ios/BottomSheetViewController.swift +208 -0
  12. package/ios/UnifiedActionSheet.mm +26 -1
  13. package/ios/UnifiedActionSheetImpl.swift +147 -7
  14. package/lib/commonjs/NativeUnifiedActionSheet.js.map +1 -1
  15. package/lib/commonjs/index.js +18 -1
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/module/NativeUnifiedActionSheet.js.map +1 -1
  18. package/lib/module/index.js +18 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts +3 -0
  21. package/lib/typescript/commonjs/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts +1 -1
  23. package/lib/typescript/commonjs/src/action-sheet-options.interface.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  25. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts +3 -0
  26. package/lib/typescript/module/src/NativeUnifiedActionSheet.d.ts.map +1 -1
  27. package/lib/typescript/module/src/action-sheet-options.interface.d.ts +1 -1
  28. package/lib/typescript/module/src/action-sheet-options.interface.d.ts.map +1 -1
  29. package/lib/typescript/module/src/index.d.ts.map +1 -1
  30. package/package.json +12 -2
  31. package/plugin/withUnifiedActionSheet.js +58 -0
  32. package/src/NativeUnifiedActionSheet.ts +5 -0
  33. package/src/action-sheet-options.interface.ts +1 -1
  34. package/src/index.tsx +35 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0]
11
+
12
+ ### Added
13
+
14
+ - `presentationStyle: 'bottom'`: a native bottom sheet on both platforms, with
15
+ a drag handle and swipe to dismiss. A long list opens part-way and drags up
16
+ to expand. On iOS it is a `UISheetPresentationController` sheet with native
17
+ rows (on iPad, the action sheet's popover instead); on Android, a Material 3
18
+ bottom sheet. `anchor` is ignored for it.
19
+ - Material is opt-in, so apps that don't use the bottom sheet stay free of the
20
+ dependency. Set `unifiedActionSheet.material=true` in
21
+ `android/gradle.properties`. Only then does the library add
22
+ `com.google.android.material` (1.12.0 by default; override it with
23
+ `ext.materialVersion` or `unifiedActionSheet.materialVersion`).
24
+ - An Expo config plugin that writes those properties:
25
+ `plugins: [['react-native-unified-action-sheet', { material: true }]]`, with
26
+ an optional `materialVersion`. `@expo/config-plugins` is an optional peer
27
+ dependency.
28
+
29
+ ### Fixed
30
+
31
+ - On iOS, a sheet, alert or prompt stayed on screen after a JS reload (pressing
32
+ `r` in Metro). It now closes on reload, as it already did on Android, and its
33
+ promise is left unresolved, since it belonged to the discarded runtime.
34
+
35
+ ### Notes
36
+
37
+ - Without the Material opt-in, `'bottom'` on Android falls back to the centered
38
+ dialog, and warns once in development.
39
+ - Prompts do not support `'bottom'`; they stay centered on both platforms.
40
+
10
41
  ## [0.2.0]
11
42
 
12
43
  ### Added
@@ -75,7 +106,8 @@ Initial release.
75
106
  - No `com.google.android.material` dependency, and no runtime dependencies —
76
107
  `react` and `react-native` are peers.
77
108
 
78
- [unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.2.0...HEAD
109
+ [unreleased]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.3.0...HEAD
110
+ [0.3.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.2.0...v0.3.0
79
111
  [0.2.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.1...v0.2.0
80
112
  [0.1.1]: https://github.com/galikvalkin/react-native-unified-action-sheet/compare/v0.1.0...v0.1.1
81
113
  [0.1.0]: https://github.com/galikvalkin/react-native-unified-action-sheet/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,22 +1,16 @@
1
1
  # react-native-unified-action-sheet
2
2
 
3
- Unified action sheet API for React Native: a native `UIAlertController` on iOS, a native `AppCompatDialog` on Android.
3
+ Unified native action sheet for React Native, one API on iOS and Android.
4
4
 
5
- | Android | Android (dark mode) | iOS |
5
+ | | Light | Dark |
6
6
  | :---: | :---: | :---: |
7
- | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/android-demo.gif" width="280" alt="Android demo" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/android-demo-dark-mode.gif" width="280" alt="Android dark-mode demo — following the system dark theme" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/ios-demo.gif" width="280" alt="iOS demo" /> |
7
+ | **Android** | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/android-demo-light-mode.gif" width="280" alt="Android demo in light mode" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/android-demo-dark-mode.gif" width="280" alt="Android demo in dark mode" /> |
8
+ | **iOS** | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/ios-demo-light-mode.gif" width="280" alt="iOS demo in light mode" /> | <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/images/ios-demo-dark-mode.gif" width="280" alt="iOS demo in dark mode" /> |
8
9
 
9
- <p align="center">
10
- <img src="https://raw.githubusercontent.com/galikvalkin/react-native-unified-action-sheet/HEAD/docs/ipad-demo.gif" width="420" alt="iPad demo — the sheet as a popover anchored to the button that opened it" />
11
- <br />
12
- <em>iPad: the same sheet presented as a popover, anchored to the view that opened it.</em>
13
- </p>
14
-
15
- - **Fully native, always on top.** The sheet gets its own platform window, so it renders above your whole view tree — including an already-open [`Modal`](https://reactnative.dev/docs/modal). JS-rendered sheets live *inside* the component tree, where `overflow`, `zIndex`, a transform or an open modal can clip or bury them.
16
- - **One API, native on both platforms**, so the same options behave the same way.
17
- - **Prompts included.** React Native's `Alert.prompt` is iOS-only and silently does nothing on Android; `showPromptWithOptions()` works on both.
18
- - Works on React Native **0.81 (old and new architecture)** through the **latest** release.
19
- - **No extra native dependencies** on either platform, and zero runtime dependencies (`react` / `react-native` peers only).
10
+ - **Fully native, always on top.** The sheet gets its own platform window, so it renders above your whole view tree, including an open [`Modal`](https://reactnative.dev/docs/modal). JS-rendered sheets live inside the component tree, where `overflow`, `zIndex` or a modal can clip or bury them.
11
+ - **One API on both platforms**: action sheets, anchored popovers, bottom sheets and text prompts.
12
+ - **Prompts on Android too.** React Native's `Alert.prompt` is iOS-only; `showPromptWithOptions()` works on both.
13
+ - **No dependencies.** `react` and `react-native` are peers; the Android bottom sheet's Material is opt-in.
20
14
 
21
15
  ## Installation
22
16
 
@@ -26,7 +20,9 @@ npm install react-native-unified-action-sheet
26
20
  yarn add react-native-unified-action-sheet
27
21
  ```
28
22
 
29
- Autolinking handles the rest on both platforms. In Expo apps, use a development build (`npx expo run:ios` / `npx expo run:android`) — custom native modules do not work in **Expo Go**.
23
+ Autolinking handles the rest. In Expo, use a development build (`npx expo run:ios` / `npx expo run:android`); custom native modules do not work in Expo Go.
24
+
25
+ **Optional:** for the Android bottom sheet, set `unifiedActionSheet.material=true` in `android/gradle.properties`, or add the Expo plugin. See [Bottom sheets](docs/bottom-sheet.md).
30
26
 
31
27
  ## Usage
32
28
 
@@ -45,41 +41,19 @@ await showActionSheetWithOptions({
45
41
  });
46
42
  ```
47
43
 
48
- `style` marks the two native roles, which are mutually exclusive; `disabled` is independent of it. `onPress` runs when that button resolves the sheet, and the call also resolves with the tapped index — the two never disagree:
49
-
50
- | | promise resolves with | `onPress` runs |
51
- | --- | --- | --- |
52
- | Tapping a button | its index | that button's |
53
- | Backdrop tap, back button, swipe down | the `'cancel'` button's index, or `-1` if there is none | the cancel button's, if there is one |
54
- | `dismissActionSheet()` | `undefined` | nothing |
55
-
56
- The promise never rejects on its own — only if one of your own `onPress` handlers throws.
57
-
58
- ### Anchored sheets
44
+ The call resolves with the tapped button's index, and that button's `onPress` runs. A backdrop tap or back resolves the `'cancel'` button. The promise never rejects. See [Behavior](docs/behavior.md).
59
45
 
60
- `presentationStyle: 'anchored'` attaches the sheet to a view. Pass the ref itself; the library measures it and sends only the resulting rectangle across:
46
+ ### Presentation styles
61
47
 
62
- ```tsx
63
- const anchorRef = useRef<View>(null);
64
-
65
- <Pressable
66
- ref={anchorRef}
67
- onPress={() =>
68
- showActionSheetWithOptions({
69
- options: [...],
70
- presentationStyle: 'anchored',
71
- anchor: anchorRef,
72
- })
73
- }
74
- />;
75
- ```
76
-
77
- Anything with a `measureInWindow` method works, which is every React Native host component ref. On iOS the anchor only applies on iPad, where the sheet becomes a popover; iPhone always presents from the bottom.
48
+ | `presentationStyle` | iOS | Android |
49
+ | --- | --- | --- |
50
+ | unset | Action sheet from the bottom | Centered dialog |
51
+ | `'centered'` | Centered alert | Centered dialog |
52
+ | `'anchored'` | Popover on iPad, pointing at `anchor` | Popup attached to `anchor` |
53
+ | `'bottom'` | Sheet with a grabber (popover on iPad) | Material bottom sheet ([opt-in](docs/bottom-sheet.md)) |
78
54
 
79
55
  ### Prompts
80
56
 
81
- `showPromptWithOptions()` is the same dialog with a text field:
82
-
83
57
  ```ts
84
58
  import { showPromptWithOptions } from 'react-native-unified-action-sheet';
85
59
 
@@ -95,78 +69,16 @@ const result = await showPromptWithOptions({
95
69
  // { buttonIndex, text }, or undefined after dismissActionSheet().
96
70
  ```
97
71
 
98
- Buttons behave as they do for the sheet, except `onPress` receives the field's
99
- text. That text is whatever was in the field when the prompt closed, including
100
- on a dismissal, so a draft is recoverable. Extra options: `placeholder`,
101
- `defaultValue`, `secureTextEntry` and `keyboardType` (`'default' |
102
- 'email-address' | 'numeric' | 'phone-pad' | 'url'`).
103
-
104
- A prompt is always centered — UIKit has no text field in an action sheet, so
105
- `presentationStyle` and `anchor` do not apply.
106
-
107
72
  ### Dismissing from code
108
73
 
109
- `dismissActionSheet()` closes the most recently opened sheet, `dismissAllActionSheets()` closes every open one. Both are no-ops when nothing is open, and the dismissed sheets resolve with `undefined`.
110
-
111
- ```ts
112
- import {
113
- dismissActionSheet,
114
- dismissAllActionSheets,
115
- } from 'react-native-unified-action-sheet';
116
- ```
117
-
118
- ### Options
119
-
120
- | Option | Type | iOS | Android | Description |
121
- | --- | --- | :---: | :---: | --- |
122
- | `options` | `ActionSheetButtonInterface[]` | ✅ | ✅ | The buttons, in order: `{ label, style?, disabled?, onPress? }`. |
123
- | `style` (per button) | `'cancel' \| 'destructive'?` | ✅ | ✅ | `'cancel'` renders a separated row and resolves on backdrop tap / back; `'destructive'` uses the destructive color. Only the first `'cancel'` counts. |
124
- | `disabled` (per button) | `boolean?` | ✅ | ✅ | Renders the row dimmed and unresponsive to taps. |
125
- | `onPress` (per button) | `() => void?` | ✅ | ✅ | Runs when this button resolves the sheet. |
126
- | `title` | `string?` | ✅ | ✅ | Sheet title. |
127
- | `message` | `string?` | ✅ | ✅ | Secondary text under the title. |
128
- | `presentationStyle` | `'centered' \| 'anchored'` | ✅ | ✅ | `'centered'` is a centered dialog; `'anchored'` attaches to `anchor` (an iPad popover on iOS). **Defaults differ**: Android `'centered'`, iOS the standard action sheet. |
129
- | `anchor` | `ActionSheetAnchorInterface?` | ✅ | ✅ | A ref, or anything with `measureInWindow`. Without a measurable anchor an `'anchored'` sheet falls back to a centered dialog. |
130
- | `anchorAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of an `'anchored'` popup relative to its anchor. `'start'` (default) aligns leading edges, flipping in RTL. |
131
- | `userInterfaceStyle` | `'light' \| 'dark'` | ✅ | ✅ | Forces the appearance; defaults to following the system setting. |
132
- | `tintColor` | `string?` | ✅ | ✅ | Text color of non-destructive buttons. |
133
- | `cancelButtonTintColor` | `string?` | ✅ | ✅ | Text color of the cancel button; overrides `tintColor` for that row. |
134
- | `destructiveColor` | `string?` | ✅ | ✅ | Overrides the destructive row color (Android's palette error color, iOS system red). |
135
- | `buttonTextAlignment` | `'start' \| 'center'` | — | ✅ | Alignment of button labels. Defaults to `'start'`, which follows layout direction. |
136
-
137
- Every options and button type is exported for typing your own wrappers —
138
- `ActionSheetOptionsInterface`, `PromptOptionsInterface`, `PromptResultInterface`
139
- and the interfaces they are composed from.
140
-
141
- ### Platform notes
142
-
143
- - **Which gestures dismiss differs.** On iOS a sheet can only be tapped away if it has a `'cancel'` button, and a `'centered'` one never can — UIKit treats it as strictly modal. Android's centered dialog always cancels on a backdrop tap. Give a sheet a cancel button if you want that gesture everywhere.
144
- - **On iPad, a popover hides the cancel row**, since tapping outside already cancels. The index you receive is unaffected.
145
- - **Sheets stack.** Opening one over another puts it on top, and each resolves its own promise. Opening a sheet over a `Modal` does not dismiss the modal.
146
- - **Light or dark follows the system setting** unless `userInterfaceStyle` forces one, chosen when the sheet opens. It uses its own palette, so it looks the same in any host app.
147
-
148
- ## Testing
149
-
150
- The package ships a Jest mock, so a screen that opens a sheet can be tested without the native module:
74
+ `dismissActionSheet()` closes the most recently opened sheet, and `dismissAllActionSheets()` closes every open one. Dismissed sheets resolve with `undefined`.
151
75
 
152
- ```ts
153
- jest.mock('react-native-unified-action-sheet', () =>
154
- require('react-native-unified-action-sheet/jest')
155
- );
156
- ```
157
-
158
- Every sheet then resolves with no selection. To simulate a tap, queue the index it should resolve with — the pressed button's `onPress` runs as it would for real:
159
-
160
- ```ts
161
- import { setNextButtonIndex } from 'react-native-unified-action-sheet/jest';
162
-
163
- setNextButtonIndex(0);
164
- await openTheSheet();
165
- ```
76
+ ## Documentation
166
77
 
167
- `setNextPromptResult({ buttonIndex, text })` does the same for a prompt. Prefer
168
- these over `mockResolvedValueOnce`, which replaces the implementation and so
169
- skips `onPress`. `dismissActionSheet` and `dismissAllActionSheets` are spies.
78
+ - [API reference](docs/api.md): every option, per platform
79
+ - [Behavior](docs/behavior.md): what each gesture resolves with, and platform differences
80
+ - [Bottom sheets](docs/bottom-sheet.md): both platforms, and enabling Material on Android
81
+ - [Testing](docs/testing.md): the shipped Jest mock
170
82
 
171
83
  ## Compatibility
172
84
 
@@ -2,7 +2,8 @@ buildscript {
2
2
  ext.UnifiedActionSheet = [
3
3
  kotlinVersion: "2.0.21",
4
4
  minSdkVersion: 24,
5
- compileSdkVersion: 36
5
+ compileSdkVersion: 36,
6
+ materialVersion: "1.12.0"
6
7
  ]
7
8
 
8
9
  ext.getExtOrDefault = { prop ->
@@ -31,6 +32,20 @@ apply plugin: "kotlin-android"
31
32
 
32
33
  apply plugin: "com.facebook.react"
33
34
 
35
+ // Material is opt-in, so apps that don't use presentationStyle 'bottom' stay
36
+ // free of the dependency. The app sets unifiedActionSheet.material=true in its
37
+ // gradle.properties (the Expo config plugin writes it); only then is the
38
+ // Material presenter compiled and the dependency added. src/main never
39
+ // references a Material class: it goes through BottomSheetPresenterFactory,
40
+ // which both variant source sets define.
41
+ def materialEnabled = (findProperty("unifiedActionSheet.material") ?: "false").toString().toBoolean()
42
+
43
+ // ext.materialVersion on the root project wins, then the gradle property, then
44
+ // the version this library is tested against.
45
+ def materialVersion = rootProject.ext.has("materialVersion")
46
+ ? rootProject.ext.get("materialVersion")
47
+ : (findProperty("unifiedActionSheet.materialVersion") ?: UnifiedActionSheet.materialVersion)
48
+
34
49
  android {
35
50
  namespace "com.unifiedactionsheet"
36
51
 
@@ -40,6 +55,15 @@ android {
40
55
  minSdkVersion getExtOrDefault("minSdkVersion")
41
56
  }
42
57
 
58
+ sourceSets {
59
+ main {
60
+ java.srcDirs += materialEnabled ? "src/material/java" : "src/noMaterial/java"
61
+ if (materialEnabled) {
62
+ res.srcDirs += "src/material/res"
63
+ }
64
+ }
65
+ }
66
+
43
67
  compileOptions {
44
68
  sourceCompatibility JavaVersion.VERSION_17
45
69
  targetCompatibility JavaVersion.VERSION_17
@@ -48,4 +72,7 @@ android {
48
72
 
49
73
  dependencies {
50
74
  implementation "com.facebook.react:react-android"
75
+ if (materialEnabled) {
76
+ implementation "com.google.android.material:material:${materialVersion}"
77
+ }
51
78
  }
@@ -6,11 +6,15 @@ import com.facebook.react.bridge.ReadableMap
6
6
  internal enum class PresentationStyle {
7
7
  CENTERED,
8
8
  ANCHORED,
9
+ BOTTOM,
9
10
  ;
10
11
 
11
12
  companion object {
12
- fun fromWire(value: String?): PresentationStyle =
13
- if (value == "anchored") ANCHORED else CENTERED
13
+ fun fromWire(value: String?): PresentationStyle = when (value) {
14
+ "anchored" -> ANCHORED
15
+ "bottom" -> BOTTOM
16
+ else -> CENTERED
17
+ }
14
18
  }
15
19
  }
16
20
 
@@ -51,6 +51,9 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
51
51
  }
52
52
  }
53
53
 
54
+ override fun getTypedExportedConstants(): MutableMap<String, Any> =
55
+ mutableMapOf("isMaterialEnabled" to BottomSheetPresenterFactory.isAvailable)
56
+
54
57
  override fun dismissActionSheet() {
55
58
  UiThreadUtil.runOnUiThread {
56
59
  openDialogs.lastOrNull()?.let { dialog ->
@@ -111,6 +114,9 @@ class UnifiedActionSheetModule(reactContext: ReactApplicationContext) :
111
114
  CenteredDialogPresenter
112
115
  }
113
116
  }
117
+ // Without Material compiled in there is no bottom sheet; JS has already
118
+ // warned in development, from the isMaterialEnabled constant.
119
+ PresentationStyle.BOTTOM -> BottomSheetPresenterFactory.create() ?: CenteredDialogPresenter
114
120
  }
115
121
 
116
122
  val dialog = presenter.build(activity, options) { presented, index ->
@@ -0,0 +1,10 @@
1
+ package com.unifiedactionsheet
2
+
3
+ /// Compiled only when the app sets unifiedActionSheet.material=true, which also
4
+ /// adds the Material dependency. The noMaterial source set defines the same
5
+ /// object, so src/main compiles against either.
6
+ internal object BottomSheetPresenterFactory {
7
+ const val isAvailable = true
8
+
9
+ fun create(): SheetPresenter? = MaterialBottomSheetPresenter
10
+ }
@@ -0,0 +1,90 @@
1
+ package com.unifiedactionsheet
2
+
3
+ import android.app.Activity
4
+ import android.app.Dialog
5
+ import android.content.res.ColorStateList
6
+ import android.view.View
7
+ import android.widget.LinearLayout
8
+ import androidx.core.view.ViewCompat
9
+ import androidx.core.view.WindowCompat
10
+ import androidx.core.view.WindowInsetsCompat
11
+ import com.google.android.material.bottomsheet.BottomSheetDialog
12
+ import com.google.android.material.bottomsheet.BottomSheetDragHandleView
13
+ import com.google.android.material.R as MaterialR
14
+
15
+ /// presentationStyle 'bottom': a Material 3 modal bottom sheet. Material's own
16
+ /// behavior does the rest: a long list opens at the peek height and drags up to
17
+ /// expand, a short one opens fully, and swiping it away cancels the dialog, so
18
+ /// it resolves through the same cancel path as a backdrop tap.
19
+ internal object MaterialBottomSheetPresenter : SheetPresenter {
20
+ override fun build(
21
+ activity: Activity,
22
+ options: ActionSheetOptions,
23
+ onSelect: (Dialog, Int) -> Unit,
24
+ ): Dialog {
25
+ val isDark = isDarkAppearance(activity, options.userInterfaceStyle)
26
+ val palette = paletteFor(isDark)
27
+ // A full Material 3 theme rather than an overlay, so the sheet works inside
28
+ // an app whose own theme is plain AppCompat.
29
+ val dialog = BottomSheetDialog(activity, bottomSheetTheme(isDark))
30
+
31
+ val context = dialog.context
32
+ // The cancel row stays the last row, as on the centered dialog. With a long
33
+ // list it starts below the peek height, which Material users expect:
34
+ // swipe down, back and a backdrop tap all resolve the cancel index anyway.
35
+ val content = buildContent(context, options, palette) { index -> onSelect(dialog, index) }
36
+
37
+ val root = LinearLayout(context).apply {
38
+ orientation = LinearLayout.VERTICAL
39
+ addView(
40
+ BottomSheetDragHandleView(context),
41
+ LinearLayout.LayoutParams(
42
+ LinearLayout.LayoutParams.MATCH_PARENT,
43
+ LinearLayout.LayoutParams.WRAP_CONTENT,
44
+ ),
45
+ )
46
+ addView(
47
+ content,
48
+ LinearLayout.LayoutParams(
49
+ LinearLayout.LayoutParams.MATCH_PARENT,
50
+ LinearLayout.LayoutParams.WRAP_CONTENT,
51
+ ),
52
+ )
53
+ }
54
+ dialog.setContentView(root)
55
+ dialog.setCanceledOnTouchOutside(true)
56
+
57
+ // Tint Material's own sheet background instead of replacing it, so its
58
+ // rounded top corners and expansion animation stay intact, while the color
59
+ // matches the palette the centered and anchored styles use.
60
+ dialog.findViewById<View>(MaterialR.id.design_bottom_sheet)?.let { sheet ->
61
+ ViewCompat.setBackgroundTintList(sheet, ColorStateList.valueOf(palette.surface))
62
+ }
63
+
64
+ // The sheet now sits behind the navigation bar, so its buttons or gesture
65
+ // handle need to contrast with the sheet, not with the app underneath.
66
+ dialog.window?.let { window ->
67
+ WindowCompat.getInsetsController(window, window.decorView)
68
+ .isAppearanceLightNavigationBars = !isDark
69
+ }
70
+
71
+ // Edge to edge is for the bottom only. It would also let a long list
72
+ // expand behind the status bar, so cap the sheet just below it (and any
73
+ // display cutout); the rows scroll inside it instead.
74
+ val decor = activity.window.decorView
75
+ val topInset = ViewCompat.getRootWindowInsets(decor)
76
+ ?.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout())
77
+ ?.top ?: 0
78
+ if (decor.height > topInset) {
79
+ dialog.behavior.maxHeight = decor.height - topInset
80
+ }
81
+
82
+ return dialog
83
+ }
84
+
85
+ private fun bottomSheetTheme(isDark: Boolean): Int = if (isDark) {
86
+ R.style.UnifiedActionSheet_BottomSheetDialog_Dark
87
+ } else {
88
+ R.style.UnifiedActionSheet_BottomSheetDialog_Light
89
+ }
90
+ }
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Full Material 3 themes, not overlays: Material components refuse to
4
+ inflate under a non-Material theme, and the host app's may be plain
5
+ AppCompat. userInterfaceStyle picks light or dark explicitly, so neither
6
+ follows DayNight.
7
+
8
+ Edge to edge, so the sheet runs down behind the navigation bar instead
9
+ of stopping above it and leaving a strip of scrim there. Material only
10
+ does this when enableEdgeToEdge is set and the navigation bar is
11
+ translucent; the sheet then pads its content by the bar's inset, so the
12
+ last row stays clear of it. -->
13
+ <style name="UnifiedActionSheet.BottomSheetDialog.Light" parent="Theme.Material3.Light.BottomSheetDialog">
14
+ <item name="enableEdgeToEdge">true</item>
15
+ <item name="android:navigationBarColor">@android:color/transparent</item>
16
+ </style>
17
+
18
+ <style name="UnifiedActionSheet.BottomSheetDialog.Dark" parent="Theme.Material3.Dark.BottomSheetDialog">
19
+ <item name="enableEdgeToEdge">true</item>
20
+ <item name="android:navigationBarColor">@android:color/transparent</item>
21
+ </style>
22
+ </resources>
@@ -0,0 +1,10 @@
1
+ package com.unifiedactionsheet
2
+
3
+ /// Compiled when the app has not set unifiedActionSheet.material=true. There is
4
+ /// no bottom sheet without Material, so the module falls back to the centered
5
+ /// dialog, and JS warns in development (see getTypedExportedConstants).
6
+ internal object BottomSheetPresenterFactory {
7
+ const val isAvailable = false
8
+
9
+ fun create(): SheetPresenter? = null
10
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/withUnifiedActionSheet');