react-native-nitro-modal 0.2.0 → 0.3.0-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 (83) hide show
  1. package/README.md +48 -4
  2. package/android/src/main/java/com/margelo/nitro/nitromodal/BottomSheetPresenter.kt +335 -34
  3. package/android/src/main/java/com/margelo/nitro/nitromodal/HybridNitroModal.kt +63 -22
  4. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalConfig.kt +31 -1
  5. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalContentRoot.kt +48 -5
  6. package/android/src/main/java/com/margelo/nitro/nitromodal/ModalPresenter.kt +63 -4
  7. package/android/src/main/java/com/margelo/nitro/nitromodal/NitroModalHostView.kt +68 -8
  8. package/ios/HybridNitroModal.swift +111 -20
  9. package/ios/ModalConfig.swift +44 -2
  10. package/ios/NitroModalHostView.swift +73 -8
  11. package/ios/SheetView.swift +575 -0
  12. package/ios/SheetViewController.swift +19 -450
  13. package/lib/module/NitroModal.js +38 -5
  14. package/lib/module/NitroModal.js.map +1 -1
  15. package/lib/module/NitroModalView.web.js +55 -25
  16. package/lib/module/NitroModalView.web.js.map +1 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/web/AnimatedValue.js +3 -0
  19. package/lib/module/web/AnimatedValue.js.map +1 -1
  20. package/lib/module/web/ModalConfig.js +8 -4
  21. package/lib/module/web/ModalConfig.js.map +1 -1
  22. package/lib/module/web/ModalController.js +29 -2
  23. package/lib/module/web/ModalController.js.map +1 -1
  24. package/lib/module/web/ModalPresenter.js +47 -12
  25. package/lib/module/web/ModalPresenter.js.map +1 -1
  26. package/lib/module/web/SheetPresenter.js +54 -29
  27. package/lib/module/web/SheetPresenter.js.map +1 -1
  28. package/lib/module/web/dom.js +13 -0
  29. package/lib/module/web/dom.js.map +1 -1
  30. package/lib/typescript/src/NitroModal.d.ts +33 -2
  31. package/lib/typescript/src/NitroModal.d.ts.map +1 -1
  32. package/lib/typescript/src/NitroModal.nitro.d.ts +29 -2
  33. package/lib/typescript/src/NitroModal.nitro.d.ts.map +1 -1
  34. package/lib/typescript/src/NitroModalView.web.d.ts.map +1 -1
  35. package/lib/typescript/src/index.d.ts +2 -2
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/lib/typescript/src/web/AnimatedValue.d.ts +1 -0
  38. package/lib/typescript/src/web/AnimatedValue.d.ts.map +1 -1
  39. package/lib/typescript/src/web/ModalConfig.d.ts +6 -0
  40. package/lib/typescript/src/web/ModalConfig.d.ts.map +1 -1
  41. package/lib/typescript/src/web/ModalController.d.ts +5 -0
  42. package/lib/typescript/src/web/ModalController.d.ts.map +1 -1
  43. package/lib/typescript/src/web/ModalPresenter.d.ts +9 -0
  44. package/lib/typescript/src/web/ModalPresenter.d.ts.map +1 -1
  45. package/lib/typescript/src/web/SheetPresenter.d.ts +1 -1
  46. package/lib/typescript/src/web/SheetPresenter.d.ts.map +1 -1
  47. package/lib/typescript/src/web/dom.d.ts +2 -0
  48. package/lib/typescript/src/web/dom.d.ts.map +1 -1
  49. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.cpp +60 -6
  50. package/nitrogen/generated/android/c++/JHybridNitroModalSpec.hpp +10 -2
  51. package/nitrogen/generated/android/c++/JNamedSheetDetent.hpp +64 -0
  52. package/nitrogen/generated/android/c++/JSheetDetent.cpp +26 -0
  53. package/nitrogen/generated/android/c++/JSheetDetent.hpp +42 -37
  54. package/nitrogen/generated/android/c++/views/JHybridNitroModalStateUpdater.cpp +20 -0
  55. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/HybridNitroModalSpec.kt +40 -0
  56. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/NamedSheetDetent.kt +25 -0
  57. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromodal/SheetDetent.kt +47 -10
  58. package/nitrogen/generated/android/nitromodal+autolinking.cmake +1 -0
  59. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Bridge.hpp +53 -8
  60. package/nitrogen/generated/ios/NitroModal-Swift-Cxx-Umbrella.hpp +4 -3
  61. package/nitrogen/generated/ios/c++/HybridNitroModalSpecSwift.hpp +32 -5
  62. package/nitrogen/generated/ios/c++/views/HybridNitroModalComponent.mm +24 -0
  63. package/nitrogen/generated/ios/swift/HybridNitroModalSpec.swift +4 -0
  64. package/nitrogen/generated/ios/swift/HybridNitroModalSpec_cxx.swift +110 -5
  65. package/nitrogen/generated/ios/swift/NamedSheetDetent.swift +48 -0
  66. package/nitrogen/generated/ios/swift/SheetDetent.swift +15 -33
  67. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.cpp +8 -0
  68. package/nitrogen/generated/shared/c++/HybridNitroModalSpec.hpp +14 -5
  69. package/nitrogen/generated/shared/c++/{SheetDetent.hpp → NamedSheetDetent.hpp} +17 -17
  70. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.cpp +9 -1
  71. package/nitrogen/generated/shared/c++/views/HybridNitroModalComponent.hpp +15 -2
  72. package/nitrogen/generated/shared/json/NitroModalConfig.json +4 -0
  73. package/package.json +1 -1
  74. package/src/NitroModal.nitro.ts +30 -2
  75. package/src/NitroModal.tsx +68 -4
  76. package/src/NitroModalView.web.tsx +54 -12
  77. package/src/index.tsx +2 -0
  78. package/src/web/AnimatedValue.ts +4 -0
  79. package/src/web/ModalConfig.ts +13 -3
  80. package/src/web/ModalController.ts +35 -2
  81. package/src/web/ModalPresenter.ts +46 -11
  82. package/src/web/SheetPresenter.ts +98 -33
  83. package/src/web/dom.ts +17 -0
@@ -6,7 +6,12 @@ import type {
6
6
  NitroModalMethods,
7
7
  NitroModalProps,
8
8
  } from '../NitroModal.nitro';
9
- import { canUseDOM, estimateViewport, readSafeAreaInsets } from './dom';
9
+ import {
10
+ canUseDOM,
11
+ estimateViewport,
12
+ inlineInsets,
13
+ readSafeAreaInsets,
14
+ } from './dom';
10
15
  import { isSameConfig, makeConfig, type ModalConfig } from './ModalConfig';
11
16
  import type {
12
17
  ModalElements,
@@ -58,6 +63,8 @@ export class ModalController implements PresenterListener {
58
63
  private isAttached = false;
59
64
  private presenter: ModalPresenter | null = null;
60
65
  private elements: ModalElements | null = null;
66
+ /** Inline: the element the sheet fills. */
67
+ private host: HTMLElement | null = null;
61
68
  private lastContentArea: ModalContentArea | null = null;
62
69
 
63
70
  /** `setMounted` renders (or removes) the modal's elements. */
@@ -110,6 +117,15 @@ export class ModalController implements PresenterListener {
110
117
  }
111
118
  }
112
119
 
120
+ /** Inline: the element the sheet fills, rendered in place. */
121
+ hostDidChange(host: HTMLElement | null) {
122
+ if (host === this.host) return;
123
+ this.host = host;
124
+ if (!this.presenter) {
125
+ this.reportEstimatedContentArea();
126
+ }
127
+ }
128
+
113
129
  elementsDidMount(elements: ModalElements) {
114
130
  this.elements = elements;
115
131
  if (this.presenter && !this.presenter.isShown) {
@@ -273,6 +289,10 @@ export class ModalController implements PresenterListener {
273
289
  this.props?.onDetentChange?.(index);
274
290
  }
275
291
 
292
+ onPullToRefresh() {
293
+ this.props?.onPullToRefresh?.();
294
+ }
295
+
276
296
  onContentAreaChange(width: number, height: number) {
277
297
  const area = { width: Math.floor(width), height: Math.floor(height) };
278
298
  const last = this.lastContentArea;
@@ -288,12 +308,25 @@ export class ModalController implements PresenterListener {
288
308
  private reportEstimatedContentArea() {
289
309
  const { config } = this;
290
310
  if (!canUseDOM || !this.isAttached || !config) return;
311
+ if (config.isInline) {
312
+ const { host } = this;
313
+ if (!host || host.clientWidth === 0 || host.clientHeight === 0) return;
314
+ const viewport = { width: host.clientWidth, height: host.clientHeight };
315
+ const area = sheetContentArea(
316
+ config.detents,
317
+ viewport,
318
+ inlineInsets(host),
319
+ true
320
+ );
321
+ this.onContentAreaChange(area.width, area.height);
322
+ return;
323
+ }
291
324
  const viewport = estimateViewport();
292
325
  const insets = readSafeAreaInsets();
293
326
  const area =
294
327
  config.mode === 'popup'
295
328
  ? popupContentArea(viewport, insets)
296
- : sheetContentArea(config.detents, viewport, insets);
329
+ : sheetContentArea(config.detents, viewport, insets, false);
297
330
  this.onContentAreaChange(area.width, area.height);
298
331
  }
299
332
 
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  canScrollWithin,
5
5
  clipOverflow,
6
+ inlineInsets,
6
7
  readKeyboardHeight,
7
8
  readSafeAreaInsets,
8
9
  toCSSColor,
@@ -34,6 +35,8 @@ export interface PresenterListener {
34
35
  onSwipeDismiss(): void;
35
36
  onDetentChange(index: number): void;
36
37
  onContentAreaChange(width: number, height: number): void;
38
+ /** The user pulled the sheet past its lowest detent and let go. */
39
+ onPullToRefresh(): void;
37
40
  }
38
41
 
39
42
  /**
@@ -45,6 +48,9 @@ export interface PresenterListener {
45
48
  * Transitions are interruptible: dismissing mid-enter turns the enter around,
46
49
  * and `cancelDismiss` turns an exit back around. Once an exit starts the modal
47
50
  * stops taking input.
51
+ *
52
+ * Inline, the elements fill the host view in place of a full-window layer:
53
+ * only the surface takes input, and the keyboard is left to the page.
48
54
  */
49
55
  export abstract class ModalPresenter {
50
56
  protected elements: ModalElements | null = null;
@@ -59,6 +65,8 @@ export abstract class ModalPresenter {
59
65
  private finished = false;
60
66
  private observedContent: HTMLElement | null = null;
61
67
  private resizeObserver: ResizeObserver | null = null;
68
+ /** Inline: follows the host's size, which can change without a window resize. */
69
+ private hostObserver: ResizeObserver | null = null;
62
70
 
63
71
  constructor(
64
72
  protected config: ModalConfig,
@@ -74,18 +82,26 @@ export abstract class ModalPresenter {
74
82
  if (this.elements || this.finished) return;
75
83
  this.elements = elements;
76
84
  // The elements can outlive a previous presentation (an immediate re-present).
77
- elements.overlay.style.pointerEvents = '';
85
+ this.setInteractive(true);
78
86
  elements.surface.style.overflow = clipOverflow();
79
87
  elements.surface.style.opacity = '';
80
88
  elements.surface.style.transform = '';
89
+ elements.backdrop.style.display = this.config.isInline ? 'none' : '';
81
90
 
82
91
  window.addEventListener('resize', this.handleResize);
83
- window.visualViewport?.addEventListener('resize', this.handleKeyboard);
84
- window.visualViewport?.addEventListener('scroll', this.handleKeyboard);
85
- elements.backdrop.addEventListener('click', this.handleBackdropClick);
86
- elements.overlay.addEventListener('wheel', this.handleWheel, {
87
- passive: false,
88
- });
92
+ if (this.config.isInline) {
93
+ if (typeof ResizeObserver !== 'undefined') {
94
+ this.hostObserver = new ResizeObserver(this.handleResize);
95
+ this.hostObserver.observe(elements.overlay);
96
+ }
97
+ } else {
98
+ window.visualViewport?.addEventListener('resize', this.handleKeyboard);
99
+ window.visualViewport?.addEventListener('scroll', this.handleKeyboard);
100
+ elements.backdrop.addEventListener('click', this.handleBackdropClick);
101
+ elements.overlay.addEventListener('wheel', this.handleWheel, {
102
+ passive: false,
103
+ });
104
+ }
89
105
  if (typeof ResizeObserver !== 'undefined') {
90
106
  this.resizeObserver = new ResizeObserver(this.handleContentResize);
91
107
  }
@@ -122,7 +138,7 @@ export abstract class ModalPresenter {
122
138
  if (!elements || this.isDismissing || this.finished) return;
123
139
  this.isDismissing = true;
124
140
  this.entering = false;
125
- elements.overlay.style.pointerEvents = 'none';
141
+ this.setInteractive(false);
126
142
  // Also hides the on-screen keyboard.
127
143
  const focused = document.activeElement;
128
144
  if (focused instanceof HTMLElement && elements.overlay.contains(focused)) {
@@ -138,7 +154,7 @@ export abstract class ModalPresenter {
138
154
  cancelDismiss(): boolean {
139
155
  if (!this.elements || !this.isDismissing || this.finished) return false;
140
156
  this.isDismissing = false;
141
- this.elements.overlay.style.pointerEvents = '';
157
+ this.setInteractive(true);
142
158
  this.entering = true;
143
159
  this.animateIn();
144
160
  return true;
@@ -196,6 +212,8 @@ export abstract class ModalPresenter {
196
212
  this.elements?.overlay.removeEventListener('wheel', this.handleWheel);
197
213
  this.resizeObserver?.disconnect();
198
214
  this.resizeObserver = null;
215
+ this.hostObserver?.disconnect();
216
+ this.hostObserver = null;
199
217
  this.observedContent = null;
200
218
  }
201
219
 
@@ -226,14 +244,30 @@ export abstract class ModalPresenter {
226
244
  : 'Canvas';
227
245
  }
228
246
 
247
+ /** Inline, only the surface takes input; the rest reaches the page. */
248
+ private setInteractive(interactive: boolean) {
249
+ const { overlay, surface } = this.elements!;
250
+ if (this.config.isInline) {
251
+ overlay.style.pointerEvents = 'none';
252
+ surface.style.pointerEvents = interactive ? 'auto' : 'none';
253
+ } else {
254
+ overlay.style.pointerEvents = interactive ? '' : 'none';
255
+ }
256
+ }
257
+
229
258
  private measureViewport() {
230
259
  const { overlay } = this.elements!;
231
260
  this.viewport = {
232
261
  width: overlay.clientWidth,
233
262
  height: overlay.clientHeight,
234
263
  };
235
- this.insets = readSafeAreaInsets();
236
- this.keyboardHeight = readKeyboardHeight(this.viewport.height);
264
+ if (this.config.isInline) {
265
+ this.insets = inlineInsets(overlay);
266
+ this.keyboardHeight = 0;
267
+ } else {
268
+ this.insets = readSafeAreaInsets();
269
+ this.keyboardHeight = readKeyboardHeight(this.viewport.height);
270
+ }
237
271
  }
238
272
 
239
273
  /** Re-targets the observer at the content's root element. Returns whether its size changed. */
@@ -275,6 +309,7 @@ export abstract class ModalPresenter {
275
309
  };
276
310
 
277
311
  private handleKeyboard = () => {
312
+ if (this.config.isInline) return;
278
313
  const height = readKeyboardHeight(this.viewport.height);
279
314
  if (height === this.keyboardHeight) return;
280
315
  this.keyboardHeight = height;
@@ -13,8 +13,10 @@ import {
13
13
  import type { ModalConfig } from './ModalConfig';
14
14
  import { ModalPresenter, type PresenterListener } from './ModalPresenter';
15
15
 
16
- /** Gap kept above the sheet at its tallest. */
16
+ /** Gap kept above a modal sheet at its tallest. */
17
17
  const TOP_GAP = 10;
18
+ /** How far below its lowest detent the sheet must be pulled to refresh. */
19
+ const REFRESH_THRESHOLD = 56;
18
20
  /**
19
21
  * Extra sheet height below the viewport so it stays attached to the bottom
20
22
  * edge while rubber-banding upward.
@@ -47,39 +49,77 @@ interface Drag {
47
49
  rawTop: number;
48
50
  /** Whether the last movement moved the sheet rather than the scroller. */
49
51
  sheetDrove: boolean;
52
+ /**
53
+ * The scroller moved during this gesture. It then keeps the downward part
54
+ * too: scrolled content goes back to its top, and only a new gesture moves
55
+ * the sheet.
56
+ */
57
+ contentScrolled: boolean;
58
+ /** Started with the sheet resting on its lowest detent, so pulling it further down refreshes. */
59
+ canRefresh: boolean;
60
+ /** Pulled far enough that letting go refreshes. */
61
+ refreshArmed: boolean;
50
62
  samples: { time: number; y: number }[];
51
63
  }
52
64
 
53
- function sheetWidth(available: number): number {
54
- return Math.min(available, MAX_WIDTH);
65
+ function sheetWidth(available: number, isInline: boolean): number {
66
+ return isInline ? available : Math.min(available, MAX_WIDTH);
55
67
  }
56
68
 
57
69
  /** Tallest height the content may occupy (excludes the bottom safe area). */
58
- function maximumDetentValue(viewportHeight: number, insets: Insets): number {
59
- return Math.max(0, viewportHeight - insets.top - insets.bottom - TOP_GAP);
70
+ function maximumDetentValue(
71
+ viewportHeight: number,
72
+ insets: Insets,
73
+ isInline: boolean
74
+ ): number {
75
+ const gap = isInline ? 0 : TOP_GAP;
76
+ return Math.max(0, viewportHeight - insets.top - insets.bottom - gap);
77
+ }
78
+
79
+ /** Content height at `detent`; `fitContent` uses the measured `fitting` height. */
80
+ function detentHeight(
81
+ detent: SheetDetent,
82
+ maximum: number,
83
+ fitting: number
84
+ ): number {
85
+ switch (detent) {
86
+ case 'small':
87
+ return maximum * 0.25;
88
+ case 'medium':
89
+ return maximum * 0.5;
90
+ case 'large':
91
+ return maximum;
92
+ case 'fitContent':
93
+ return Math.min(Math.max(fitting, 1), maximum);
94
+ default:
95
+ return Number.isFinite(detent)
96
+ ? Math.min(Math.max(detent, 1), maximum)
97
+ : maximum;
98
+ }
60
99
  }
61
100
 
62
101
  /** Height the content may occupy at the largest of `detents`. */
63
102
  function contentHeight(detents: SheetDetent[], maximum: number): number {
64
- if (
65
- detents.length === 0 ||
66
- detents.includes('large') ||
67
- detents.includes('fitContent')
68
- ) {
69
- return maximum;
70
- }
71
- return detents.includes('medium') ? maximum * 0.5 : maximum * 0.25;
103
+ if (detents.length === 0) return maximum;
104
+ // `fitContent` content can grow up to the maximum.
105
+ return Math.max(
106
+ ...detents.map((detent) => detentHeight(detent, maximum, maximum))
107
+ );
72
108
  }
73
109
 
74
110
  /** Best guess of the content area before the sheet is on screen. */
75
111
  export function sheetContentArea(
76
112
  detents: SheetDetent[],
77
113
  viewport: Size,
78
- insets: Insets
114
+ insets: Insets,
115
+ isInline: boolean
79
116
  ): Size {
80
117
  return {
81
- width: sheetWidth(viewport.width),
82
- height: contentHeight(detents, maximumDetentValue(viewport.height, insets)),
118
+ width: sheetWidth(viewport.width, isInline),
119
+ height: contentHeight(
120
+ detents,
121
+ maximumDetentValue(viewport.height, insets, isInline)
122
+ ),
83
123
  };
84
124
  }
85
125
 
@@ -200,7 +240,11 @@ export class SheetPresenter extends ModalPresenter {
200
240
  }
201
241
 
202
242
  private get maximumDetentValue(): number {
203
- return maximumDetentValue(this.viewport.height, this.insets);
243
+ return maximumDetentValue(
244
+ this.viewport.height,
245
+ this.insets,
246
+ this.config.isInline
247
+ );
204
248
  }
205
249
 
206
250
  private get keyboardLift(): number {
@@ -210,17 +254,11 @@ export class SheetPresenter extends ModalPresenter {
210
254
  }
211
255
 
212
256
  private height(detent: SheetDetent): number {
213
- const maximum = this.maximumDetentValue;
214
- switch (detent) {
215
- case 'small':
216
- return maximum * 0.25;
217
- case 'medium':
218
- return maximum * 0.5;
219
- case 'large':
220
- return maximum;
221
- case 'fitContent':
222
- return Math.min(Math.max(this.contentSize.height, 1), maximum);
223
- }
257
+ return detentHeight(
258
+ detent,
259
+ this.maximumDetentValue,
260
+ this.contentSize.height
261
+ );
224
262
  }
225
263
 
226
264
  /** Sheet top when resting on `detents[index]`. The bottom safe area is added below the content. */
@@ -233,7 +271,10 @@ export class SheetPresenter extends ModalPresenter {
233
271
  this.insets.bottom -
234
272
  this.keyboardLift -
235
273
  this.height(detent);
236
- return Math.max(top, this.insets.top + TOP_GAP);
274
+ return Math.max(
275
+ top,
276
+ this.insets.top + (this.config.isInline ? 0 : TOP_GAP)
277
+ );
237
278
  }
238
279
 
239
280
  private get detentTops(): number[] {
@@ -242,7 +283,7 @@ export class SheetPresenter extends ModalPresenter {
242
283
 
243
284
  private layout() {
244
285
  const { surface, content, grabber } = this.elements!;
245
- const width = sheetWidth(this.viewport.width);
286
+ const width = sheetWidth(this.viewport.width, this.config.isInline);
246
287
  const radius = `${this.config.cornerRadius ?? DEFAULT_CORNER_RADIUS}px`;
247
288
  Object.assign(surface.style, {
248
289
  left: `${(this.viewport.width - width) / 2}px`,
@@ -324,6 +365,9 @@ export class SheetPresenter extends ModalPresenter {
324
365
  active: false,
325
366
  rawTop: 0,
326
367
  sheetDrove: false,
368
+ contentScrolled: false,
369
+ canRefresh: false,
370
+ refreshArmed: false,
327
371
  samples: [],
328
372
  };
329
373
  }
@@ -351,8 +395,14 @@ export class SheetPresenter extends ModalPresenter {
351
395
 
352
396
  private activate(drag: Drag) {
353
397
  drag.active = true;
398
+ const isSettled = !this.top.isAnimating;
354
399
  this.top.stop();
355
400
  drag.rawTop = this.top.value;
401
+ drag.canRefresh =
402
+ this.config.pullToRefreshEnabled &&
403
+ !this.config.dismissOnSwipe &&
404
+ isSettled &&
405
+ Math.abs(drag.rawTop - Math.max(...this.detentTops)) < 1;
356
406
  const { surface } = this.elements!;
357
407
  // A native scroll on an ancestor makes React Native Web cancel the press
358
408
  // under the pointer, so dragging from a button doesn't leave it pressed.
@@ -379,21 +429,33 @@ export class SheetPresenter extends ModalPresenter {
379
429
  const { scroller } = drag;
380
430
  if (scroller) {
381
431
  // The browser's own scrolling is prevented for this gesture, so the
382
- // content is scrolled here whenever the sheet can't move.
432
+ // content is scrolled here whenever the sheet can't move. Up: expand
433
+ // the sheet before scrolling. Down: scroll back to the top before
434
+ // collapsing the sheet, in a separate gesture.
383
435
  const minTop = Math.min(...this.detentTops);
384
436
  const drive =
385
- delta < 0 ? drag.rawTop > minTop + 0.5 : scroller.scrollTop <= 0;
437
+ delta < 0
438
+ ? drag.rawTop > minTop + 0.5
439
+ : !drag.contentScrolled && scroller.scrollTop <= 0;
386
440
  if (drive) {
387
441
  drag.rawTop = Math.max(drag.rawTop + delta, minTop);
388
442
  } else {
389
443
  scroller.scrollTop -= delta;
444
+ if (delta !== 0) {
445
+ drag.contentScrolled = true;
446
+ }
390
447
  }
391
448
  drag.sheetDrove = drive;
392
449
  } else {
393
450
  drag.rawTop += delta;
394
451
  drag.sheetDrove = true;
395
452
  }
396
- this.top.set(this.constrained(drag.rawTop));
453
+ const top = this.constrained(drag.rawTop);
454
+ if (drag.canRefresh) {
455
+ drag.refreshArmed =
456
+ top - Math.max(...this.detentTops) >= REFRESH_THRESHOLD;
457
+ }
458
+ this.top.set(top);
397
459
  }
398
460
 
399
461
  private release(drag: Drag) {
@@ -434,6 +496,9 @@ export class SheetPresenter extends ModalPresenter {
434
496
  this.listener.onDetentChange(index);
435
497
  }
436
498
  this.settle(SETTLE, velocity);
499
+ if (drag.refreshArmed) {
500
+ this.listener.onPullToRefresh();
501
+ }
437
502
  }
438
503
 
439
504
  /** Abandons the gesture without settling (the exit or the content takes over). */
package/src/web/dom.ts CHANGED
@@ -53,6 +53,23 @@ export function readSafeAreaInsets(): Insets {
53
53
  };
54
54
  }
55
55
 
56
+ /** The part of the safe area insets that `element` overlaps. */
57
+ export function inlineInsets(element: Element): Insets {
58
+ const safeArea = readSafeAreaInsets();
59
+ const rect = element.getBoundingClientRect();
60
+ const clamp = (value: number, max: number) =>
61
+ Math.min(Math.max(value, 0), max);
62
+ return {
63
+ top: clamp(safeArea.top - rect.top, rect.height),
64
+ right: clamp(rect.right - (window.innerWidth - safeArea.right), rect.width),
65
+ bottom: clamp(
66
+ rect.bottom - (window.innerHeight - safeArea.bottom),
67
+ rect.height
68
+ ),
69
+ left: clamp(safeArea.left - rect.left, rect.width),
70
+ };
71
+ }
72
+
56
73
  /**
57
74
  * Height of the on-screen keyboard over a layout viewport `layoutHeight` tall.
58
75
  * Mobile browsers keep the layout viewport and shrink the visual one.