@27appstudio/react-native-bottom-toolbar 0.1.1 → 0.2.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 +38 -4
- package/lib/module/react-navigation/index.js +29 -8
- package/lib/module/react-navigation/index.js.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 +2 -2
- package/src/react-navigation/index.ts +33 -8
package/README.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/wordmark-on-dark.svg">
|
|
4
|
+
<img src="docs/images/wordmark.svg" width="568" alt="@27appstudio/react-native-bottom-toolbar">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
<p>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@27appstudio/react-native-bottom-toolbar"><img src="https://img.shields.io/npm/v/@27appstudio/react-native-bottom-toolbar?label=npm&labelColor=0B0B0F&color=3340FF" alt="npm version"></a>
|
|
10
|
+
<a href="https://github.com/27AppStudio/react-native-bottom-toolbar/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/27AppStudio/react-native-bottom-toolbar/ci.yml?branch=main&label=ci&labelColor=0B0B0F" alt="CI status"></a>
|
|
11
|
+
<a href="#installation"><img src="https://img.shields.io/badge/platforms-iOS%20%7C%20Android-3340FF?labelColor=0B0B0F" alt="Platforms: iOS and Android"></a>
|
|
12
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3340FF?labelColor=0B0B0F" alt="MIT license"></a>
|
|
13
|
+
</p>
|
|
2
14
|
|
|
3
15
|
The platform's own bottom toolbar for React Native screens:
|
|
4
16
|
|
|
@@ -12,7 +24,7 @@ The platform's own bottom toolbar for React Native screens:
|
|
|
12
24
|
|
|
13
25
|
It's a TurboModule with no views of its own in JS, no Expo modules, and no config plugin.
|
|
14
26
|
|
|
15
|
-
Made by [27AppStudio](https://
|
|
27
|
+
Made by [27AppStudio](https://27appstudio.com) and used in production in [Splitify](https://splitify.cc).
|
|
16
28
|
|
|
17
29
|
## When to use something else
|
|
18
30
|
|
|
@@ -58,7 +70,7 @@ function ExpenseScreen({
|
|
|
58
70
|
}
|
|
59
71
|
```
|
|
60
72
|
|
|
61
|
-
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.
|
|
62
74
|
|
|
63
75
|
### Without React Navigation
|
|
64
76
|
|
|
@@ -112,7 +124,7 @@ Called for every button tap, in addition to that button's own `onPress`.
|
|
|
112
124
|
|
|
113
125
|
### `useBottomToolbar(navigation, items, options?)`
|
|
114
126
|
|
|
115
|
-
Imported from `/react-navigation`. `items`
|
|
127
|
+
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.
|
|
116
128
|
|
|
117
129
|
## Platform notes
|
|
118
130
|
|
|
@@ -129,3 +141,25 @@ Imported from `/react-navigation`. `items` and `options` are read each time the
|
|
|
129
141
|
## License
|
|
130
142
|
|
|
131
143
|
MIT
|
|
144
|
+
|
|
145
|
+
## Made by 27AppStudio
|
|
146
|
+
|
|
147
|
+
<p>
|
|
148
|
+
<a href="https://27appstudio.com">
|
|
149
|
+
<picture>
|
|
150
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/27appstudio-on-dark.svg">
|
|
151
|
+
<img src="docs/images/27appstudio.svg" width="180" alt="27AppStudio">
|
|
152
|
+
</picture>
|
|
153
|
+
</a>
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
An independent French mobile app studio. We design, build and launch iPhone and Android apps, and open-source the native pieces we make along the way, most of them for [Splitify](https://splitify.cc).
|
|
157
|
+
|
|
158
|
+
| Library | What it does |
|
|
159
|
+
| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
160
|
+
| [react-native-alert](https://github.com/27AppStudio/react-native-alert) | Native alerts and text prompts in your app's colors |
|
|
161
|
+
| [react-native-bottom-toolbar](https://github.com/27AppStudio/react-native-bottom-toolbar) | The platform's own bottom toolbar: UIKit on iOS, Material 3 on Android |
|
|
162
|
+
| [react-native-emoji-picker](https://github.com/27AppStudio/react-native-emoji-picker) | A native emoji picker: iOS popover, Material 3 bottom sheet |
|
|
163
|
+
| [rn-ship](https://github.com/27AppStudio/rn-ship) | EAS-style release pipeline for bare React Native apps |
|
|
164
|
+
|
|
165
|
+
Need an app built, or a hand with a React Native one? [27appstudio.com](https://27appstudio.com)
|
|
@@ -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":[]}
|
|
@@ -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.2.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",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"type": "git",
|
|
58
58
|
"url": "git+https://github.com/27AppStudio/react-native-bottom-toolbar.git"
|
|
59
59
|
},
|
|
60
|
-
"author": "27AppStudio <hello@27appstudio.com> (https://
|
|
60
|
+
"author": "27AppStudio <hello@27appstudio.com> (https://27appstudio.com)",
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"bugs": {
|
|
63
63
|
"url": "https://github.com/27AppStudio/react-native-bottom-toolbar/issues"
|
|
@@ -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
|
}
|