@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 CHANGED
@@ -1,4 +1,16 @@
1
- # @27appstudio/react-native-bottom-toolbar
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://github.com/27AppStudio) and used in production in [Splitify](https://splitify.cc).
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` and `options` are read each time the toolbar is shown, so passing a new array doesn't resubscribe. To change a toolbar that's already on screen, call `show` again.
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`. `items` and
14
- * `options` are read at show time, so a new array (a language change) needs
15
- * no resubscribe; call `show` to update a toolbar already on screen.
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 = () => show(latest.current.items, latest.current.options);
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
- let arrived = navigation.isFocused() && navigation.getState().index === 0;
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","showLatest","arrived","isFocused","getState","index","offTransitionEnd","addListener","event","data","closing","offFocus","offBlur"],"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,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;EAEnCP,SAAS,CAAC,MAAM;IACd;IACA;IACA,MAAMU,UAAU,GAAGA,CAAA,KAAMP,IAAI,CAACK,MAAM,CAACC,OAAO,CAACH,KAAK,EAAEE,MAAM,CAACC,OAAO,CAACF,OAAO,CAAC;IAC3E;IACA;IACA,IAAII,OAAO,GAAGN,UAAU,CAACO,SAAS,CAAC,CAAC,IAAIP,UAAU,CAACQ,QAAQ,CAAC,CAAC,CAACC,KAAK,KAAK,CAAC;IACzE,IAAIH,OAAO,EAAE;MACXD,UAAU,CAAC,CAAC;IACd;IAEA,MAAMK,gBAAgB,GAAGV,UAAU,CAACW,WAAW,CAC7C,eAAe,EACdC,KAAK,IAAK;MACT,IAAI,CAACA,KAAK,CAACC,IAAI,CAACC,OAAO,EAAE;QACvBR,OAAO,GAAG,IAAI;QACdD,UAAU,CAAC,CAAC;MACd;IACF,CACF,CAAC;IACD,MAAMU,QAAQ,GAAGf,UAAU,CAACW,WAAW,CAAC,OAAO,EAAE,MAAM;MACrD,IAAIL,OAAO,EAAE;QACXD,UAAU,CAAC,CAAC;MACd;IACF,CAAC,CAAC;IACF,MAAMW,OAAO,GAAGhB,UAAU,CAACW,WAAW,CAAC,MAAM,EAAEd,IAAI,CAAC;IAEpD,OAAO,MAAM;MACXa,gBAAgB,CAAC,CAAC;MAClBK,QAAQ,CAAC,CAAC;MACVC,OAAO,CAAC,CAAC;MACTnB,IAAI,CAAC,CAAC;IACR,CAAC;EACH,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;AAClB","ignoreList":[]}
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`. `items` and
12
- * `options` are read at show time, so a new array (a language change) needs
13
- * no resubscribe; call `show` to update a toolbar already on screen.
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;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,yBAAyB,CAAC,aAAa,CAAC,EACpD,KAAK,EAAE,WAAW,EAAE,EACpB,OAAO,CAAC,EAAE,cAAc,QAuCzB"}
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.1.1",
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://github.com/27AppStudio)",
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`. `items` and
17
- * `options` are read at show time, so a new array (a language change) needs
18
- * no resubscribe; call `show` to update a toolbar already on screen.
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 = () => show(latest.current.items, latest.current.options);
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
- let arrived = navigation.isFocused() && navigation.getState().index === 0;
35
- if (arrived) {
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
  }