@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 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` 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.
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
  }
@@ -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;AA6BlD,eAAeA,mBAAmB,CAACC,GAAG,CAAO,eAAe,CAAC","ignoreList":[]}
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":[]}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import { processColor } from 'react-native';
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.
@@ -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","addPressListener","listener","subscription","onItemPress","remove","id","find","button"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAEA,SAASA,YAAY,QAAQ,cAAc;AAE3C,OAAOC,mBAAmB,MAAM,0BAAuB;AASvD;AACA,OAAO,MAAMC,WAAW,GAAGD,mBAAmB,IAAI,IAAI;AAEtD,IAAIE,OAAwB,GAAG,EAAE;;AAEjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,IAAIA,CAACC,KAAoB,EAAEC,OAAuB,GAAG,CAAC,CAAC,EAAE;EACvE,IAAI,CAACL,mBAAmB,EAAE;IACxB;EACF;EACA,IAAI,CAACI,KAAK,CAACE,IAAI,CAACC,QAAQ,CAAC,EAAE;IACzBC,IAAI,CAAC,CAAC;IACN;EACF;EACAN,OAAO,GAAGE,KAAK,CAACK,MAAM,CAACF,QAAQ,CAAC;EAChC,MAAMG,SAAS,GAAGX,YAAY,CAACM,OAAO,CAACK,SAAS,CAAC;EACjDV,mBAAmB,CAACG,IAAI,CACtBC,KAAK,CAACO,GAAG,CAAEC,IAAI,IACbL,QAAQ,CAACK,IAAI,CAAC,GACV;IACE,GAAGA,IAAI;IACPC,kBAAkB,EAAED,IAAI,CAACC,kBAAkB,IAAID,IAAI,CAACE,KAAK;IACzDC,OAAO,EAAEC,SAAS;IAClBC,IAAI,EAAE;EACR,CAAC,GACDL,IACN,CAAC,EACD,OAAOF,SAAS,KAAK,QAAQ,GAAG;IAAEA;EAAU,CAAC,GAAG,CAAC,CACnD,CAAC;AACH;;AAEA;AACA,OAAO,SAASF,IAAIA,CAAA,EAAG;EACrBN,OAAO,GAAG,EAAE;EACZF,mBAAmB,EAAEQ,IAAI,CAAC,CAAC;AAC7B;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASU,gBAAgBA,CAACC,QAA8B,EAAE;EAC/D,MAAMC,YAAY,GAAGpB,mBAAmB,EAAEqB,WAAW,CAACF,QAAQ,CAAC;EAC/D,OAAO,MAAMC,YAAY,EAAEE,MAAM,CAAC,CAAC;AACrC;AAEA,SAASf,QAAQA,CAACK,IAAiB,EAAyB;EAC1D,OAAOA,IAAI,CAACK,IAAI,KAAKD,SAAS,IAAIJ,IAAI,CAACK,IAAI,KAAK,QAAQ;AAC1D;AAEAjB,mBAAmB,EAAEqB,WAAW,CAAEE,EAAE,IAClCrB,OAAO,CAACsB,IAAI,CAAEC,MAAM,IAAKA,MAAM,CAACF,EAAE,KAAKA,EAAE,CAAC,EAAER,OAAO,GAAG,CACxD,CAAC","ignoreList":[]}
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`. `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":[]}
@@ -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;AAM1E,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,QAuBtE;AAED,yBAAyB;AACzB,wBAAgB,IAAI,SAGnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,0BAG9D"}
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`. `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.2",
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 { processColor } from 'react-native';
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`. `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
  }