react-native-nitro-modal 0.3.0-rc → 0.3.1-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +5 -1
  2. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +73 -9
  3. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +4 -0
  4. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +4 -0
  5. package/ios/HybridNitroModal.swift +5 -1
  6. package/ios/ModalConfig.swift +4 -0
  7. package/ios/SheetView.swift +82 -10
  8. package/lib/module/NitroModal.js +16 -1
  9. package/lib/module/NitroModal.js.map +1 -1
  10. package/lib/module/NitroModalView.web.js +32 -2
  11. package/lib/module/NitroModalView.web.js.map +1 -1
  12. package/lib/module/web/ModalConfig.js +3 -1
  13. package/lib/module/web/ModalConfig.js.map +1 -1
  14. package/lib/module/web/ModalPresenter.js.map +1 -1
  15. package/lib/module/web/SheetPresenter.js +66 -7
  16. package/lib/module/web/SheetPresenter.js.map +1 -1
  17. package/lib/module/web/dom.js +3 -0
  18. package/lib/module/web/dom.js.map +1 -1
  19. package/lib/typescript/src/NitroModal.d.ts +10 -1
  20. package/lib/typescript/src/NitroModal.d.ts.map +1 -1
  21. package/lib/typescript/src/NitroModal.nitro.d.ts +8 -0
  22. package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -1
  23. package/lib/typescript/src/NitroModalView.web.d.ts.map +1 -1
  24. package/lib/typescript/src/web/ModalConfig.d.ts +4 -0
  25. package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -1
  26. package/lib/typescript/src/web/ModalPresenter.d.ts +4 -0
  27. package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -1
  28. package/lib/typescript/src/web/SheetPresenter.d.ts +12 -1
  29. package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -1
  30. package/lib/typescript/src/web/dom.d.ts +2 -0
  31. package/lib/typescript/src/web/dom.d.ts.map +1 -1
  32. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +18 -0
  33. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +4 -0
  34. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +10 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +12 -0
  36. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +15 -0
  37. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +14 -0
  38. package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +12 -0
  39. package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +2 -0
  40. package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +48 -0
  41. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +4 -0
  42. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +4 -0
  43. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +4 -0
  44. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +6 -0
  45. package/nitrogen/generated/shared/json/NitroModalConfig.json +2 -0
  46. package/package.json +1 -1
  47. package/src/NitroModal.nitro.ts +8 -0
  48. package/src/NitroModal.tsx +25 -0
  49. package/src/NitroModalView.web.tsx +26 -1
  50. package/src/web/ModalConfig.ts +6 -0
  51. package/src/web/ModalPresenter.ts +4 -0
  52. package/src/web/SheetPresenter.ts +72 -5
  53. package/src/web/dom.ts +3 -0
@@ -6,7 +6,9 @@ import {
6
6
  findVerticalScroller,
7
7
  isTextInput,
8
8
  prefersReducedMotion,
9
+ REFRESH_INDICATOR_SIZE,
9
10
  suppressNextClick,
11
+ toCSSColor,
10
12
  type Insets,
11
13
  type Size,
12
14
  } from './dom';
@@ -15,7 +17,10 @@ import { ModalPresenter, type PresenterListener } from './ModalPresenter';
15
17
 
16
18
  /** Gap kept above a modal sheet at its tallest. */
17
19
  const TOP_GAP = 10;
18
- /** How far below its lowest detent the sheet must be pulled to refresh. */
20
+ /**
21
+ * How far below its lowest detent the sheet must be pulled to refresh, and
22
+ * where it rests while refreshing.
23
+ */
19
24
  const REFRESH_THRESHOLD = 56;
20
25
  /**
21
26
  * Extra sheet height below the viewport so it stays attached to the bottom
@@ -147,6 +152,9 @@ export class SheetPresenter extends ModalPresenter {
147
152
  private drag: Drag | null = null;
148
153
  /** Vertical velocity of the swipe that requested the dismissal. */
149
154
  private releaseVelocity = 0;
155
+ /** The sheet rests below its lowest detent, the indicator spinning above it. */
156
+ private refreshing = false;
157
+ private refreshSpin: Animation | null = null;
150
158
  /** The sheet's top edge in the viewport. */
151
159
  private readonly top = new AnimatedValue(0, 0.5, (value) =>
152
160
  this.render(value)
@@ -158,6 +166,7 @@ export class SheetPresenter extends ModalPresenter {
158
166
  Math.max(config.initialDetentIndex, 0),
159
167
  this.detents.length - 1
160
168
  );
169
+ this.refreshing = config.refreshing ?? false;
161
170
  }
162
171
 
163
172
  snapToDetent(index: number) {
@@ -199,7 +208,10 @@ export class SheetPresenter extends ModalPresenter {
199
208
  });
200
209
  }
201
210
 
202
- protected configDidChange() {
211
+ protected configDidChange(previous: ModalConfig) {
212
+ if (previous.refreshing !== this.config.refreshing) {
213
+ this.refreshing = this.config.refreshing ?? false;
214
+ }
203
215
  this.selectedIndex = Math.min(this.selectedIndex, this.detents.length - 1);
204
216
  this.layout();
205
217
  this.animateToRest();
@@ -223,6 +235,8 @@ export class SheetPresenter extends ModalPresenter {
223
235
  protected teardown() {
224
236
  super.teardown();
225
237
  this.cancelDrag();
238
+ this.refreshSpin?.cancel();
239
+ this.refreshSpin = null;
226
240
  const surface = this.elements?.surface;
227
241
  surface?.removeEventListener('touchstart', this.handleTouchStart);
228
242
  surface?.removeEventListener('touchmove', this.handleTouchMove);
@@ -281,6 +295,18 @@ export class SheetPresenter extends ModalPresenter {
281
295
  return this.detents.map((_, index) => this.restingTop(index));
282
296
  }
283
297
 
298
+ /** Where the sheet settles on `detents[index]`: held below the lowest detent while refreshing. */
299
+ private settledTop(index: number): number {
300
+ const top = this.restingTop(index);
301
+ if (!this.refreshing || Math.abs(top - Math.max(...this.detentTops)) >= 1)
302
+ return top;
303
+ return top + REFRESH_THRESHOLD;
304
+ }
305
+
306
+ private get settledTops(): number[] {
307
+ return this.detents.map((_, index) => this.settledTop(index));
308
+ }
309
+
284
310
  private layout() {
285
311
  const { surface, content, grabber } = this.elements!;
286
312
  const width = sheetWidth(this.viewport.width, this.config.isInline);
@@ -298,6 +324,10 @@ export class SheetPresenter extends ModalPresenter {
298
324
  content.style.width = `${width}px`;
299
325
  content.style.height = `${this.maximumDetentValue}px`;
300
326
  grabber.style.display = this.config.grabberVisible ? '' : 'none';
327
+ this.elements!.refreshSpinner.style.color =
328
+ this.config.refreshIndicatorColor != null
329
+ ? toCSSColor(this.config.refreshIndicatorColor)
330
+ : 'GrayText';
301
331
 
302
332
  let height = contentHeight(this.config.detents, this.maximumDetentValue);
303
333
  if (this.config.keyboardBehavior === 'resize') {
@@ -311,6 +341,38 @@ export class SheetPresenter extends ModalPresenter {
311
341
  if (!this.elements) return;
312
342
  this.elements.surface.style.transform = `translate3d(0, ${top}px, 0)`;
313
343
  this.elements.backdrop.style.opacity = String(this.backdropAlpha(top));
344
+ this.renderRefreshIndicator(top);
345
+ }
346
+
347
+ /**
348
+ * Centered in the gap a pull uncovers below the lowest detent, fading in
349
+ * as the pull nears the threshold.
350
+ */
351
+ private renderRefreshIndicator(top: number) {
352
+ const { refreshIndicator, refreshSpinner } = this.elements!;
353
+ const visible =
354
+ this.config.refreshing != null &&
355
+ !this.entering &&
356
+ !this.isDismissing &&
357
+ !this.isOffscreen;
358
+ const lowest = Math.max(...this.detentTops);
359
+ const progress = visible
360
+ ? Math.min(Math.max((top - lowest) / REFRESH_THRESHOLD, 0), 1)
361
+ : 0;
362
+ const x = (this.viewport.width - REFRESH_INDICATOR_SIZE) / 2;
363
+ const y = top - (REFRESH_THRESHOLD + REFRESH_INDICATOR_SIZE) / 2;
364
+ refreshIndicator.style.transform = `translate3d(${x}px, ${y}px, 0)`;
365
+ refreshIndicator.style.opacity = String(progress);
366
+ if (progress > 0 && !this.refreshSpin) {
367
+ this.refreshSpin =
368
+ refreshSpinner.animate?.(
369
+ [{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
370
+ { duration: 800, iterations: Infinity }
371
+ ) ?? null;
372
+ } else if (progress === 0 && this.refreshSpin) {
373
+ this.refreshSpin.cancel();
374
+ this.refreshSpin = null;
375
+ }
314
376
  }
315
377
 
316
378
  /** Fully dimmed at every detent; fades out below the lowest one. */
@@ -327,7 +389,7 @@ export class SheetPresenter extends ModalPresenter {
327
389
 
328
390
  private settle(motion: Motion, velocity?: number) {
329
391
  this.top.animateTo(
330
- this.restingTop(this.selectedIndex),
392
+ this.settledTop(this.selectedIndex),
331
393
  this.motion(motion),
332
394
  {
333
395
  velocity,
@@ -401,6 +463,7 @@ export class SheetPresenter extends ModalPresenter {
401
463
  drag.canRefresh =
402
464
  this.config.pullToRefreshEnabled &&
403
465
  !this.config.dismissOnSwipe &&
466
+ !this.refreshing &&
404
467
  isSettled &&
405
468
  Math.abs(drag.rawTop - Math.max(...this.detentTops)) < 1;
406
469
  const { surface } = this.elements!;
@@ -468,7 +531,11 @@ export class SheetPresenter extends ModalPresenter {
468
531
 
469
532
  const velocity = drag.sheetDrove ? this.velocity(drag) : 0;
470
533
  const top = this.top.value;
471
- const tops = this.detentTops;
534
+ if (drag.refreshArmed && this.config.refreshing != null) {
535
+ // Settles on the refreshing position; React confirms or ends it.
536
+ this.refreshing = true;
537
+ }
538
+ const tops = this.settledTops;
472
539
  const lowest = Math.max(...tops);
473
540
  // Where a fling of this velocity would come to rest.
474
541
  const projected = top + velocity * 0.2;
@@ -529,7 +596,7 @@ export class SheetPresenter extends ModalPresenter {
529
596
 
530
597
  /** Rubber-bands above the tallest detent, and below the lowest one when swiping can't dismiss. */
531
598
  private constrained(top: number): number {
532
- const tops = this.detentTops;
599
+ const tops = this.settledTops;
533
600
  const minTop = Math.min(...tops);
534
601
  const maxTop = Math.max(...tops);
535
602
  const dimension = Math.max(this.viewport.height, 1);
package/src/web/dom.ts CHANGED
@@ -17,6 +17,9 @@ export const ZERO_INSETS: Insets = { top: 0, right: 0, bottom: 0, left: 0 };
17
17
  export const canUseDOM =
18
18
  typeof window !== 'undefined' && typeof document !== 'undefined';
19
19
 
20
+ /** Width and height of the pull-to-refresh indicator. */
21
+ export const REFRESH_INDICATOR_SIZE = 24;
22
+
20
23
  /** Converts a color from `processColor` (0xAARRGGBB) to CSS, scaling its alpha by `opacity`. */
21
24
  export function toCSSColor(argb: number, opacity = 1): string {
22
25
  // As an unsigned 32-bit value, whatever the sign it was passed with.