@27appstudio/react-native-bottom-toolbar 0.1.2 → 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.
- package/README.md +17 -2
- package/android/src/main/java/com/bottomtoolbar/BottomToolbarModule.kt +20 -0
- package/ios/BottomToolbar.mm +28 -0
- package/lib/module/NativeBottomToolbar.js.map +1 -1
- package/lib/module/index.js +54 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/react-navigation/index.js +29 -8
- package/lib/module/react-navigation/index.js.map +1 -1
- package/lib/typescript/src/NativeBottomToolbar.d.ts +5 -0
- package/lib/typescript/src/NativeBottomToolbar.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/react-navigation/index.d.ts +4 -3
- package/lib/typescript/src/react-navigation/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeBottomToolbar.ts +5 -0
- package/src/index.tsx +52 -1
- package/src/react-navigation/index.ts +33 -8
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ function ExpenseScreen({
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
The hook shows the toolbar once the push has landed (or right away on the stack's first screen), hides it when the screen loses focus, and brings it back when a modal over the screen closes.
|
|
73
|
+
The hook shows the toolbar once the push has landed (or right away on the stack's first screen), hides it when the screen loses focus, and brings it back when a modal over the screen closes. New `items` or `options` update a toolbar already on screen, and an empty array hides it, so memoize them (`useMemo`) rather than building a new array on every render.
|
|
74
74
|
|
|
75
75
|
### Without React Navigation
|
|
76
76
|
|
|
@@ -112,6 +112,21 @@ When `items` contains no space, iOS puts the buttons at the trailing edge. When
|
|
|
112
112
|
|
|
113
113
|
`options.tintColor` sets the icon and title color. iOS defaults to the navigation bar's tint (matching the header buttons); Android defaults to the Material 3 theme.
|
|
114
114
|
|
|
115
|
+
### `getHeight(): Promise<number>` and `useBottomToolbarHeight(): number`
|
|
116
|
+
|
|
117
|
+
The room the toolbar takes at the bottom, in points, to keep a list's last row clear of it (its touches take the whole band, not just the buttons):
|
|
118
|
+
|
|
119
|
+
- iOS: the band the bar adds over the content above the bottom safe area, measured from the screen's safe area (iOS 26 floats the bar inside a full-screen frame, so the bar's own frame says nothing). 0 where the bar doesn't sit under the content, such as a foldable's side column. While hidden, the band last measured, else the bar's own height.
|
|
120
|
+
- Android: the floating button or toolbar's height plus its margin, above the navigation bar.
|
|
121
|
+
|
|
122
|
+
`useBottomToolbarHeight()` keeps it up to date: it measures again once a `show` has settled and when the window changes size (rotation, a foldable opening). It reads 0 until the first answer.
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
const toolbarHeight = useBottomToolbarHeight();
|
|
126
|
+
// ...
|
|
127
|
+
<FlatList ListFooterComponent={<View style={{ height: toolbarHeight }} />} />;
|
|
128
|
+
```
|
|
129
|
+
|
|
115
130
|
### `hide()`
|
|
116
131
|
|
|
117
132
|
### `addPressListener(listener: (id: string) => void): () => void`
|
|
@@ -124,7 +139,7 @@ Called for every button tap, in addition to that button's own `onPress`.
|
|
|
124
139
|
|
|
125
140
|
### `useBottomToolbar(navigation, items, options?)`
|
|
126
141
|
|
|
127
|
-
Imported from `/react-navigation`. `items`
|
|
142
|
+
Imported from `/react-navigation`. New `items` or `options` update the toolbar while the screen is on top (an empty array hides it); they never resubscribe the navigation listeners. Memoize them, or every render sends them to the native side again.
|
|
128
143
|
|
|
129
144
|
## Platform notes
|
|
130
145
|
|
|
@@ -15,6 +15,7 @@ import androidx.core.view.ViewCompat
|
|
|
15
15
|
import androidx.core.view.WindowInsetsCompat
|
|
16
16
|
import androidx.core.view.updateLayoutParams
|
|
17
17
|
import java.lang.ref.WeakReference
|
|
18
|
+
import com.facebook.react.bridge.Promise
|
|
18
19
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
19
20
|
import com.facebook.react.bridge.ReadableArray
|
|
20
21
|
import com.facebook.react.bridge.ReadableMap
|
|
@@ -70,6 +71,23 @@ class BottomToolbarModule(reactContext: ReactApplicationContext) :
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The room at the bottom, in dp, above the navigation bar: the shown view's height
|
|
76
|
+
* plus its margin, else a FAB's (the Material 3 size), which a one-button `show` adds.
|
|
77
|
+
*/
|
|
78
|
+
override fun getHeight(promise: Promise) {
|
|
79
|
+
UiThreadUtil.runOnUiThread {
|
|
80
|
+
val view = shown?.get()
|
|
81
|
+
val density = reactApplicationContext.resources.displayMetrics.density
|
|
82
|
+
val height = if (view != null && view.height > 0) {
|
|
83
|
+
view.height / density + MARGIN_DP
|
|
84
|
+
} else {
|
|
85
|
+
FAB_SIZE_DP + MARGIN_DP
|
|
86
|
+
}
|
|
87
|
+
promise.resolve(height.toDouble())
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
override fun hide() {
|
|
74
92
|
UiThreadUtil.runOnUiThread { removeShown(animated = true) }
|
|
75
93
|
}
|
|
@@ -191,5 +209,7 @@ class BottomToolbarModule(reactContext: ReactApplicationContext) :
|
|
|
191
209
|
private const val EXIT_DURATION = 200L
|
|
192
210
|
private const val ENTER_OFFSET_DP = 24f
|
|
193
211
|
private const val MARGIN_DP = 16f
|
|
212
|
+
// Material 3's regular FAB.
|
|
213
|
+
private const val FAB_SIZE_DP = 56f
|
|
194
214
|
}
|
|
195
215
|
}
|
package/ios/BottomToolbar.mm
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* hides it on leave.
|
|
13
13
|
*/
|
|
14
14
|
@implementation BottomToolbar {
|
|
15
|
+
/** The band last measured while the bar was shown, in points. */
|
|
16
|
+
NSNumber *_measuredHeight;
|
|
15
17
|
__weak UIViewController *_owner;
|
|
16
18
|
/** The navigation bar's tint, as last read undimmed. */
|
|
17
19
|
UIColor *_barTint;
|
|
@@ -137,6 +139,32 @@ static UINavigationController *VisibleNavigationController(void)
|
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
- (void)getHeight:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
143
|
+
{
|
|
144
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
145
|
+
UINavigationController *navigation =
|
|
146
|
+
self->_owner.navigationController ?: VisibleNavigationController();
|
|
147
|
+
UIToolbar *toolbar = navigation.toolbar;
|
|
148
|
+
UIView *top = navigation.topViewController.viewIfLoaded;
|
|
149
|
+
if (toolbar == nil || top == nil) {
|
|
150
|
+
resolve(self->_measuredHeight ?: @0);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!navigation.toolbarHidden) {
|
|
154
|
+
// UIKit adds the bar's band to the screen's bottom safe area: measured there, as
|
|
155
|
+
// the bar's own frame is the whole screen on iOS 26 (it floats inside it). 0 where
|
|
156
|
+
// the bar doesn't sit under the content, e.g. a foldable's side column.
|
|
157
|
+
CGFloat band = top.safeAreaInsets.bottom - navigation.view.safeAreaInsets.bottom;
|
|
158
|
+
self->_measuredHeight = @(MAX(0, band));
|
|
159
|
+
resolve(self->_measuredHeight);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Hidden: the band last measured, else the bar's own height (without iOS 26's gap
|
|
163
|
+
// under it, which only shows once the bar does).
|
|
164
|
+
resolve(self->_measuredHeight ?: @([toolbar sizeThatFits:navigation.view.bounds.size].height));
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
- (void)hide
|
|
141
169
|
{
|
|
142
170
|
// The owner is kept (weakly) for the next `show`: a modal over it hides the toolbar,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeBottomToolbar.ts"],"mappings":";;AAEA,SAASA,mBAAmB,QAAQ,cAAc;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeBottomToolbar.ts"],"mappings":";;AAEA,SAASA,mBAAmB,QAAQ,cAAc;AAkClD,eAAeA,mBAAmB,CAACC,GAAG,CAAO,eAAe,CAAC","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { processColor, useWindowDimensions } from 'react-native';
|
|
4
5
|
import NativeBottomToolbar from "./NativeBottomToolbar.js";
|
|
5
6
|
/** False where the native module is missing: web, Expo Go, an unlinked build. */
|
|
6
7
|
export const isAvailable = NativeBottomToolbar != null;
|
|
@@ -39,14 +40,66 @@ export function show(items, options = {}) {
|
|
|
39
40
|
} : item), typeof tintColor === 'number' ? {
|
|
40
41
|
tintColor
|
|
41
42
|
} : {});
|
|
43
|
+
// Once the bar has slid in (and any push has landed), its band is final.
|
|
44
|
+
setTimeout(refreshHeight, SETTLE_MS);
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
/** Longer than UIKit's toolbar and push animations. */
|
|
48
|
+
const SETTLE_MS = 600;
|
|
49
|
+
|
|
44
50
|
/** Hides the toolbar. */
|
|
45
51
|
export function hide() {
|
|
46
52
|
buttons = [];
|
|
47
53
|
NativeBottomToolbar?.hide();
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The room the toolbar takes at the bottom of the screen, in points: on iOS the
|
|
58
|
+
* band it adds over the content above the bottom safe area (so not the home
|
|
59
|
+
* indicator; 0 where the bar doesn't sit under the content, e.g. a foldable's side
|
|
60
|
+
* column), on Android above the navigation bar. While hidden, the size it last
|
|
61
|
+
* took, so a list can keep that much room under its last row without jumping when
|
|
62
|
+
* the toolbar comes and goes. 0 without the native module.
|
|
63
|
+
*/
|
|
64
|
+
export function getHeight() {
|
|
65
|
+
return NativeBottomToolbar?.getHeight() ?? Promise.resolve(0);
|
|
66
|
+
}
|
|
67
|
+
let lastHeight = 0;
|
|
68
|
+
const heightListeners = new Set();
|
|
69
|
+
|
|
70
|
+
/** Measures the band again and tells `useBottomToolbarHeight` users if it changed. */
|
|
71
|
+
function refreshHeight() {
|
|
72
|
+
getHeight().then(height => {
|
|
73
|
+
if (height !== lastHeight) {
|
|
74
|
+
lastHeight = height;
|
|
75
|
+
heightListeners.forEach(listener => listener(height));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `getHeight`, kept up to date: measured again once a `show` has settled (the band
|
|
82
|
+
* is only exact once the bar is on screen) and when the window changes size (a
|
|
83
|
+
* rotation, a foldable opening). 0 until the first answer.
|
|
84
|
+
*/
|
|
85
|
+
export function useBottomToolbarHeight() {
|
|
86
|
+
const [height, setHeight] = useState(lastHeight);
|
|
87
|
+
const {
|
|
88
|
+
height: windowHeight,
|
|
89
|
+
width: windowWidth
|
|
90
|
+
} = useWindowDimensions();
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
heightListeners.add(setHeight);
|
|
93
|
+
return () => {
|
|
94
|
+
heightListeners.delete(setHeight);
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
refreshHeight();
|
|
99
|
+
}, [windowHeight, windowWidth]);
|
|
100
|
+
return height;
|
|
101
|
+
}
|
|
102
|
+
|
|
50
103
|
/**
|
|
51
104
|
* Calls `listener` with the id of each tapped button, in addition to that
|
|
52
105
|
* button's own `onPress`. Returns the unsubscribe function.
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["processColor","NativeBottomToolbar","isAvailable","buttons","show","items","options","some","isButton","hide","filter","tintColor","map","item","accessibilityLabel","title","onPress","undefined","type","
|
|
1
|
+
{"version":3,"names":["useEffect","useState","processColor","useWindowDimensions","NativeBottomToolbar","isAvailable","buttons","show","items","options","some","isButton","hide","filter","tintColor","map","item","accessibilityLabel","title","onPress","undefined","type","setTimeout","refreshHeight","SETTLE_MS","getHeight","Promise","resolve","lastHeight","heightListeners","Set","then","height","forEach","listener","useBottomToolbarHeight","setHeight","windowHeight","width","windowWidth","add","delete","addPressListener","subscription","onItemPress","remove","id","find","button"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
|
|
@@ -10,9 +10,10 @@ import { hide, show } from "../index.js";
|
|
|
10
10
|
*
|
|
11
11
|
* The toolbar belongs to the whole stack: it is shown once the push has landed
|
|
12
12
|
* (`transitionEnd`), or at once on the stack's first screen; again on
|
|
13
|
-
* returning from a modal (`focus`); and hidden on `blur`.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* returning from a modal (`focus`); and hidden on `blur`. While the screen is
|
|
14
|
+
* on top, new `items` or `options` update the toolbar (an empty array hides
|
|
15
|
+
* it), so memoize them (`useMemo`): a new array on every render would send it
|
|
16
|
+
* to the native side every time.
|
|
16
17
|
*/
|
|
17
18
|
export function useBottomToolbar(navigation, items, options) {
|
|
18
19
|
const latest = useRef({
|
|
@@ -23,24 +24,31 @@ export function useBottomToolbar(navigation, items, options) {
|
|
|
23
24
|
items,
|
|
24
25
|
options
|
|
25
26
|
};
|
|
27
|
+
// Whether the push has landed, so `show` reaches this screen and not the one
|
|
28
|
+
// below; and what was last sent, so an update only goes out when it changed.
|
|
29
|
+
const arrived = useRef(false);
|
|
30
|
+
const shown = useRef(undefined);
|
|
26
31
|
useEffect(() => {
|
|
27
32
|
// Back from a pop, both focus and transitionEnd show: the latter comes after
|
|
28
33
|
// the popped screen's unmount, whose `hide` would otherwise win.
|
|
29
|
-
const showLatest = () =>
|
|
34
|
+
const showLatest = () => {
|
|
35
|
+
shown.current = latest.current;
|
|
36
|
+
show(latest.current.items, latest.current.options);
|
|
37
|
+
};
|
|
30
38
|
// A pushed screen mounts before its push starts, so showing now would put
|
|
31
39
|
// the items on the screen below; the first screen has no push to wait for.
|
|
32
|
-
|
|
33
|
-
if (arrived) {
|
|
40
|
+
arrived.current = navigation.isFocused() && navigation.getState().index === 0;
|
|
41
|
+
if (arrived.current) {
|
|
34
42
|
showLatest();
|
|
35
43
|
}
|
|
36
44
|
const offTransitionEnd = navigation.addListener('transitionEnd', event => {
|
|
37
45
|
if (!event.data.closing) {
|
|
38
|
-
arrived = true;
|
|
46
|
+
arrived.current = true;
|
|
39
47
|
showLatest();
|
|
40
48
|
}
|
|
41
49
|
});
|
|
42
50
|
const offFocus = navigation.addListener('focus', () => {
|
|
43
|
-
if (arrived) {
|
|
51
|
+
if (arrived.current) {
|
|
44
52
|
showLatest();
|
|
45
53
|
}
|
|
46
54
|
});
|
|
@@ -52,5 +60,18 @@ export function useBottomToolbar(navigation, items, options) {
|
|
|
52
60
|
hide();
|
|
53
61
|
};
|
|
54
62
|
}, [navigation]);
|
|
63
|
+
|
|
64
|
+
// New items or options on a screen whose toolbar is up: update it in place.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const last = shown.current;
|
|
67
|
+
if (!arrived.current || !navigation.isFocused() || last && last.items === items && last.options === options) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
shown.current = {
|
|
71
|
+
items,
|
|
72
|
+
options
|
|
73
|
+
};
|
|
74
|
+
show(items, options);
|
|
75
|
+
}, [items, navigation, options]);
|
|
55
76
|
}
|
|
56
77
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["useEffect","useRef","hide","show","useBottomToolbar","navigation","items","options","latest","current","
|
|
1
|
+
{"version":3,"names":["useEffect","useRef","hide","show","useBottomToolbar","navigation","items","options","latest","current","arrived","shown","undefined","showLatest","isFocused","getState","index","offTransitionEnd","addListener","event","data","closing","offFocus","offBlur","last"],"sourceRoot":"../../../src","sources":["react-navigation/index.ts"],"mappings":";;AAIA,SAASA,SAAS,EAAEC,MAAM,QAAQ,OAAO;AAEzC,SAASC,IAAI,EAAEC,IAAI,QAAQ,aAAU;;AAErC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAC9BC,UAAoD,EACpDC,KAAoB,EACpBC,OAAwB,EACxB;EACA,MAAMC,MAAM,GAAGP,MAAM,CAAC;IAAEK,KAAK;IAAEC;EAAQ,CAAC,CAAC;EACzCC,MAAM,CAACC,OAAO,GAAG;IAAEH,KAAK;IAAEC;EAAQ,CAAC;EACnC;EACA;EACA,MAAMG,OAAO,GAAGT,MAAM,CAAC,KAAK,CAAC;EAC7B,MAAMU,KAAK,GAAGV,MAAM,CAElBW,SAAS,CAAC;EAEZZ,SAAS,CAAC,MAAM;IACd;IACA;IACA,MAAMa,UAAU,GAAGA,CAAA,KAAM;MACvBF,KAAK,CAACF,OAAO,GAAGD,MAAM,CAACC,OAAO;MAC9BN,IAAI,CAACK,MAAM,CAACC,OAAO,CAACH,KAAK,EAAEE,MAAM,CAACC,OAAO,CAACF,OAAO,CAAC;IACpD,CAAC;IACD;IACA;IACAG,OAAO,CAACD,OAAO,GACbJ,UAAU,CAACS,SAAS,CAAC,CAAC,IAAIT,UAAU,CAACU,QAAQ,CAAC,CAAC,CAACC,KAAK,KAAK,CAAC;IAC7D,IAAIN,OAAO,CAACD,OAAO,EAAE;MACnBI,UAAU,CAAC,CAAC;IACd;IAEA,MAAMI,gBAAgB,GAAGZ,UAAU,CAACa,WAAW,CAC7C,eAAe,EACdC,KAAK,IAAK;MACT,IAAI,CAACA,KAAK,CAACC,IAAI,CAACC,OAAO,EAAE;QACvBX,OAAO,CAACD,OAAO,GAAG,IAAI;QACtBI,UAAU,CAAC,CAAC;MACd;IACF,CACF,CAAC;IACD,MAAMS,QAAQ,GAAGjB,UAAU,CAACa,WAAW,CAAC,OAAO,EAAE,MAAM;MACrD,IAAIR,OAAO,CAACD,OAAO,EAAE;QACnBI,UAAU,CAAC,CAAC;MACd;IACF,CAAC,CAAC;IACF,MAAMU,OAAO,GAAGlB,UAAU,CAACa,WAAW,CAAC,MAAM,EAAEhB,IAAI,CAAC;IAEpD,OAAO,MAAM;MACXe,gBAAgB,CAAC,CAAC;MAClBK,QAAQ,CAAC,CAAC;MACVC,OAAO,CAAC,CAAC;MACTrB,IAAI,CAAC,CAAC;IACR,CAAC;EACH,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;;EAEhB;EACAL,SAAS,CAAC,MAAM;IACd,MAAMwB,IAAI,GAAGb,KAAK,CAACF,OAAO;IAC1B,IACE,CAACC,OAAO,CAACD,OAAO,IAChB,CAACJ,UAAU,CAACS,SAAS,CAAC,CAAC,IACtBU,IAAI,IAAIA,IAAI,CAAClB,KAAK,KAAKA,KAAK,IAAIkB,IAAI,CAACjB,OAAO,KAAKA,OAAQ,EAC1D;MACA;IACF;IACAI,KAAK,CAACF,OAAO,GAAG;MAAEH,KAAK;MAAEC;IAAQ,CAAC;IAClCJ,IAAI,CAACG,KAAK,EAAEC,OAAO,CAAC;EACtB,CAAC,EAAE,CAACD,KAAK,EAAED,UAAU,EAAEE,OAAO,CAAC,CAAC;AAClC","ignoreList":[]}
|
|
@@ -19,6 +19,11 @@ type NativeToolbarOptions = {
|
|
|
19
19
|
tintColor?: number;
|
|
20
20
|
};
|
|
21
21
|
export interface Spec extends TurboModule {
|
|
22
|
+
/**
|
|
23
|
+
* The room the toolbar takes at the bottom, in points: above the bottom safe area on
|
|
24
|
+
* iOS, above the navigation bar on Android. Its size when shown, even while hidden.
|
|
25
|
+
*/
|
|
26
|
+
getHeight(): Promise<number>;
|
|
22
27
|
hide(): void;
|
|
23
28
|
readonly onItemPress: CodegenTypes.EventEmitter<string>;
|
|
24
29
|
show(items: NativeToolbarItem[], options: NativeToolbarOptions): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeBottomToolbar.d.ts","sourceRoot":"","sources":["../../../src/NativeBottomToolbar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI9D,KAAK,iBAAiB,GAAG;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,KAAK,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;CAC7B,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACvE;;AAED,wBAA8D"}
|
|
1
|
+
{"version":3,"file":"NativeBottomToolbar.d.ts","sourceRoot":"","sources":["../../../src/NativeBottomToolbar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAI9D,KAAK,iBAAiB,GAAG;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,KAAK,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;CAC7B,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7B,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IACxD,IAAI,CAAC,KAAK,EAAE,iBAAiB,EAAE,EAAE,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;CACvE;;AAED,wBAA8D"}
|
|
@@ -20,6 +20,21 @@ export declare const isAvailable: boolean;
|
|
|
20
20
|
export declare function show(items: ToolbarItem[], options?: ToolbarOptions): void;
|
|
21
21
|
/** Hides the toolbar. */
|
|
22
22
|
export declare function hide(): void;
|
|
23
|
+
/**
|
|
24
|
+
* The room the toolbar takes at the bottom of the screen, in points: on iOS the
|
|
25
|
+
* band it adds over the content above the bottom safe area (so not the home
|
|
26
|
+
* indicator; 0 where the bar doesn't sit under the content, e.g. a foldable's side
|
|
27
|
+
* column), on Android above the navigation bar. While hidden, the size it last
|
|
28
|
+
* took, so a list can keep that much room under its last row without jumping when
|
|
29
|
+
* the toolbar comes and goes. 0 without the native module.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getHeight(): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* `getHeight`, kept up to date: measured again once a `show` has settled (the band
|
|
34
|
+
* is only exact once the bar is on screen) and when the window changes size (a
|
|
35
|
+
* rotation, a foldable opening). 0 until the first answer.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useBottomToolbarHeight(): number;
|
|
23
38
|
/**
|
|
24
39
|
* Calls `listener` with the id of each tapped button, in addition to that
|
|
25
40
|
* button's own `onPress`. Returns the unsubscribe function.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,WAAW,EAAE,cAAc,EAAE,MAAM,YAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,WAAW,EAAE,cAAc,EAAE,MAAM,YAAS,CAAC;AAO1E,YAAY,EACV,aAAa,EACb,WAAW,EACX,cAAc,EACd,YAAY,GACb,MAAM,YAAS,CAAC;AAEjB,iFAAiF;AACjF,eAAO,MAAM,WAAW,SAA8B,CAAC;AAIvD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,OAAO,GAAE,cAAmB,QAyBtE;AAKD,yBAAyB;AACzB,wBAAgB,IAAI,SAGnB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3C;AAeD;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAa/C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,0BAG9D"}
|
|
@@ -8,9 +8,10 @@ import type { ToolbarItem, ToolbarOptions } from '../types.js';
|
|
|
8
8
|
*
|
|
9
9
|
* The toolbar belongs to the whole stack: it is shown once the push has landed
|
|
10
10
|
* (`transitionEnd`), or at once on the stack's first screen; again on
|
|
11
|
-
* returning from a modal (`focus`); and hidden on `blur`.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* returning from a modal (`focus`); and hidden on `blur`. While the screen is
|
|
12
|
+
* on top, new `items` or `options` update the toolbar (an empty array hides
|
|
13
|
+
* it), so memoize them (`useMemo`): a new array on every render would send it
|
|
14
|
+
* to the native side every time.
|
|
14
15
|
*/
|
|
15
16
|
export declare function useBottomToolbar(navigation: NativeStackNavigationProp<ParamListBase>, items: ToolbarItem[], options?: ToolbarOptions): void;
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/react-navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAU,CAAC;AAM5D
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/react-navigation/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAU,CAAC;AAM5D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,yBAAyB,CAAC,aAAa,CAAC,EACpD,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,CAAC,EAAE,cAAc,QA+DzB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@27appstudio/react-native-bottom-toolbar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Native bottom toolbar for React Native: UIKit's toolbar on iOS (Liquid Glass on iOS 26) and a Material 3 floating toolbar on Android.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -24,6 +24,11 @@ type NativeToolbarOptions = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export interface Spec extends TurboModule {
|
|
27
|
+
/**
|
|
28
|
+
* The room the toolbar takes at the bottom, in points: above the bottom safe area on
|
|
29
|
+
* iOS, above the navigation bar on Android. Its size when shown, even while hidden.
|
|
30
|
+
*/
|
|
31
|
+
getHeight(): Promise<number>;
|
|
27
32
|
hide(): void;
|
|
28
33
|
readonly onItemPress: CodegenTypes.EventEmitter<string>;
|
|
29
34
|
show(items: NativeToolbarItem[], options: NativeToolbarOptions): void;
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolbarButton, ToolbarItem, ToolbarOptions } from './types';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { processColor, useWindowDimensions } from 'react-native';
|
|
4
5
|
|
|
5
6
|
import NativeBottomToolbar from './NativeBottomToolbar';
|
|
6
7
|
|
|
@@ -54,14 +55,64 @@ export function show(items: ToolbarItem[], options: ToolbarOptions = {}) {
|
|
|
54
55
|
),
|
|
55
56
|
typeof tintColor === 'number' ? { tintColor } : {}
|
|
56
57
|
);
|
|
58
|
+
// Once the bar has slid in (and any push has landed), its band is final.
|
|
59
|
+
setTimeout(refreshHeight, SETTLE_MS);
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/** Longer than UIKit's toolbar and push animations. */
|
|
63
|
+
const SETTLE_MS = 600;
|
|
64
|
+
|
|
59
65
|
/** Hides the toolbar. */
|
|
60
66
|
export function hide() {
|
|
61
67
|
buttons = [];
|
|
62
68
|
NativeBottomToolbar?.hide();
|
|
63
69
|
}
|
|
64
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The room the toolbar takes at the bottom of the screen, in points: on iOS the
|
|
73
|
+
* band it adds over the content above the bottom safe area (so not the home
|
|
74
|
+
* indicator; 0 where the bar doesn't sit under the content, e.g. a foldable's side
|
|
75
|
+
* column), on Android above the navigation bar. While hidden, the size it last
|
|
76
|
+
* took, so a list can keep that much room under its last row without jumping when
|
|
77
|
+
* the toolbar comes and goes. 0 without the native module.
|
|
78
|
+
*/
|
|
79
|
+
export function getHeight(): Promise<number> {
|
|
80
|
+
return NativeBottomToolbar?.getHeight() ?? Promise.resolve(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let lastHeight = 0;
|
|
84
|
+
const heightListeners = new Set<(height: number) => void>();
|
|
85
|
+
|
|
86
|
+
/** Measures the band again and tells `useBottomToolbarHeight` users if it changed. */
|
|
87
|
+
function refreshHeight() {
|
|
88
|
+
getHeight().then((height) => {
|
|
89
|
+
if (height !== lastHeight) {
|
|
90
|
+
lastHeight = height;
|
|
91
|
+
heightListeners.forEach((listener) => listener(height));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `getHeight`, kept up to date: measured again once a `show` has settled (the band
|
|
98
|
+
* is only exact once the bar is on screen) and when the window changes size (a
|
|
99
|
+
* rotation, a foldable opening). 0 until the first answer.
|
|
100
|
+
*/
|
|
101
|
+
export function useBottomToolbarHeight(): number {
|
|
102
|
+
const [height, setHeight] = useState(lastHeight);
|
|
103
|
+
const { height: windowHeight, width: windowWidth } = useWindowDimensions();
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
heightListeners.add(setHeight);
|
|
106
|
+
return () => {
|
|
107
|
+
heightListeners.delete(setHeight);
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
refreshHeight();
|
|
112
|
+
}, [windowHeight, windowWidth]);
|
|
113
|
+
return height;
|
|
114
|
+
}
|
|
115
|
+
|
|
65
116
|
/**
|
|
66
117
|
* Calls `listener` with the id of each tapped button, in addition to that
|
|
67
118
|
* button's own `onPress`. Returns the unsubscribe function.
|
|
@@ -13,9 +13,10 @@ import { hide, show } from '../index';
|
|
|
13
13
|
*
|
|
14
14
|
* The toolbar belongs to the whole stack: it is shown once the push has landed
|
|
15
15
|
* (`transitionEnd`), or at once on the stack's first screen; again on
|
|
16
|
-
* returning from a modal (`focus`); and hidden on `blur`.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* returning from a modal (`focus`); and hidden on `blur`. While the screen is
|
|
17
|
+
* on top, new `items` or `options` update the toolbar (an empty array hides
|
|
18
|
+
* it), so memoize them (`useMemo`): a new array on every render would send it
|
|
19
|
+
* to the native side every time.
|
|
19
20
|
*/
|
|
20
21
|
export function useBottomToolbar(
|
|
21
22
|
navigation: NativeStackNavigationProp<ParamListBase>,
|
|
@@ -24,15 +25,25 @@ export function useBottomToolbar(
|
|
|
24
25
|
) {
|
|
25
26
|
const latest = useRef({ items, options });
|
|
26
27
|
latest.current = { items, options };
|
|
28
|
+
// Whether the push has landed, so `show` reaches this screen and not the one
|
|
29
|
+
// below; and what was last sent, so an update only goes out when it changed.
|
|
30
|
+
const arrived = useRef(false);
|
|
31
|
+
const shown = useRef<
|
|
32
|
+
{ items: ToolbarItem[]; options?: ToolbarOptions } | undefined
|
|
33
|
+
>(undefined);
|
|
27
34
|
|
|
28
35
|
useEffect(() => {
|
|
29
36
|
// Back from a pop, both focus and transitionEnd show: the latter comes after
|
|
30
37
|
// the popped screen's unmount, whose `hide` would otherwise win.
|
|
31
|
-
const showLatest = () =>
|
|
38
|
+
const showLatest = () => {
|
|
39
|
+
shown.current = latest.current;
|
|
40
|
+
show(latest.current.items, latest.current.options);
|
|
41
|
+
};
|
|
32
42
|
// A pushed screen mounts before its push starts, so showing now would put
|
|
33
43
|
// the items on the screen below; the first screen has no push to wait for.
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
arrived.current =
|
|
45
|
+
navigation.isFocused() && navigation.getState().index === 0;
|
|
46
|
+
if (arrived.current) {
|
|
36
47
|
showLatest();
|
|
37
48
|
}
|
|
38
49
|
|
|
@@ -40,13 +51,13 @@ export function useBottomToolbar(
|
|
|
40
51
|
'transitionEnd',
|
|
41
52
|
(event) => {
|
|
42
53
|
if (!event.data.closing) {
|
|
43
|
-
arrived = true;
|
|
54
|
+
arrived.current = true;
|
|
44
55
|
showLatest();
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
);
|
|
48
59
|
const offFocus = navigation.addListener('focus', () => {
|
|
49
|
-
if (arrived) {
|
|
60
|
+
if (arrived.current) {
|
|
50
61
|
showLatest();
|
|
51
62
|
}
|
|
52
63
|
});
|
|
@@ -59,4 +70,18 @@ export function useBottomToolbar(
|
|
|
59
70
|
hide();
|
|
60
71
|
};
|
|
61
72
|
}, [navigation]);
|
|
73
|
+
|
|
74
|
+
// New items or options on a screen whose toolbar is up: update it in place.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const last = shown.current;
|
|
77
|
+
if (
|
|
78
|
+
!arrived.current ||
|
|
79
|
+
!navigation.isFocused() ||
|
|
80
|
+
(last && last.items === items && last.options === options)
|
|
81
|
+
) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
shown.current = { items, options };
|
|
85
|
+
show(items, options);
|
|
86
|
+
}, [items, navigation, options]);
|
|
62
87
|
}
|