@rafaelaugusto/jolt 0.1.0-rc.2 → 0.1.0-rc.4
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 +4 -4
- package/dist/animate.d.ts.map +1 -1
- package/dist/animate.js +20 -5
- package/dist/animate.js.map +1 -1
- package/dist/bottom-sheet/geometry.d.ts +7 -2
- package/dist/bottom-sheet/geometry.d.ts.map +1 -1
- package/dist/bottom-sheet/geometry.js +7 -3
- package/dist/bottom-sheet/geometry.js.map +1 -1
- package/dist/bottom-sheet/index.d.ts +10 -0
- package/dist/bottom-sheet/index.d.ts.map +1 -1
- package/dist/bottom-sheet/index.js +72 -22
- package/dist/bottom-sheet/index.js.map +1 -1
- package/dist/bottom-sheet/native.d.ts +5 -0
- package/dist/bottom-sheet/native.d.ts.map +1 -1
- package/dist/bottom-sheet/native.js +6 -0
- package/dist/bottom-sheet/native.js.map +1 -1
- package/dist/driver/android.d.ts.map +1 -1
- package/dist/driver/android.js +15 -7
- package/dist/driver/android.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interactive-navigation/index.d.ts +2 -0
- package/dist/interactive-navigation/index.d.ts.map +1 -1
- package/dist/interactive-navigation/index.js +1 -0
- package/dist/interactive-navigation/index.js.map +1 -1
- package/dist/interactive-navigation/modal.d.ts +34 -0
- package/dist/interactive-navigation/modal.d.ts.map +1 -0
- package/dist/interactive-navigation/modal.js +253 -0
- package/dist/interactive-navigation/modal.js.map +1 -0
- package/dist/live-spring-driver.d.ts +5 -0
- package/dist/live-spring-driver.d.ts.map +1 -1
- package/dist/live-spring-driver.js +13 -0
- package/dist/live-spring-driver.js.map +1 -1
- package/dist/mixer.d.ts +7 -1
- package/dist/mixer.d.ts.map +1 -1
- package/dist/mixer.js +57 -22
- package/dist/mixer.js.map +1 -1
- package/dist/motion-value.d.ts +12 -1
- package/dist/motion-value.d.ts.map +1 -1
- package/dist/motion-value.js +115 -7
- package/dist/motion-value.js.map +1 -1
- package/dist/scroll-header/geometry.d.ts +1 -1
- package/dist/scroll-header/geometry.d.ts.map +1 -1
- package/dist/scroll-header/geometry.js +7 -2
- package/dist/scroll-header/geometry.js.map +1 -1
- package/dist/scroll-header/index.d.ts.map +1 -1
- package/dist/scroll-header/index.js +13 -0
- package/dist/scroll-header/index.js.map +1 -1
- package/dist/shapes/index.d.ts +1 -1
- package/dist/shapes/index.d.ts.map +1 -1
- package/dist/shapes/index.js +23 -9
- package/dist/shapes/index.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/value-track.d.ts +9 -0
- package/dist/value-track.d.ts.map +1 -1
- package/dist/value-track.js +28 -16
- package/dist/value-track.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/BlurEffect.java +14 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/GradientLayer.java +30 -8
- package/platforms/android/java/org/rafaelaugusto/jolt/KeyframeFactory.java +26 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/LiveSpring.java +36 -9
- package/platforms/android/java/org/rafaelaugusto/jolt/ScrollHeader.java +3 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/ShapeLayer.java +17 -7
- package/platforms/android/proguard-rules.pro +1 -0
- package/platforms/ios/src/JoltInteractiveModal.h +16 -0
- package/platforms/ios/src/JoltInteractiveModal.m +178 -0
- package/platforms/ios/src/JoltLiveSpring.h +1 -0
- package/platforms/ios/src/JoltLiveSpring.m +183 -67
- package/platforms/ios/src/module.modulemap +1 -0
- package/src/android-plugin.d.ts +10 -0
- package/src/animate.ts +23 -5
- package/src/bottom-sheet/geometry.ts +9 -2
- package/src/bottom-sheet/index.ts +94 -20
- package/src/bottom-sheet/native.ts +6 -0
- package/src/driver/android.ts +31 -14
- package/src/index.ts +1 -1
- package/src/interactive-navigation/index.ts +7 -0
- package/src/interactive-navigation/modal.ts +306 -0
- package/src/ios-plugin.d.ts +23 -0
- package/src/live-spring-driver.ts +15 -0
- package/src/mixer.ts +62 -22
- package/src/motion-value.ts +121 -9
- package/src/scroll-header/geometry.ts +7 -2
- package/src/scroll-header/index.ts +12 -0
- package/src/shapes/index.ts +20 -9
- package/src/types.ts +5 -2
- package/src/value-track.ts +25 -14
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
GridLayout,
|
|
4
4
|
ScrollView,
|
|
5
5
|
Trace,
|
|
6
|
+
Utils,
|
|
6
7
|
View,
|
|
7
8
|
isAndroid,
|
|
8
9
|
} from '@nativescript/core'
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import {
|
|
20
21
|
captureFocus,
|
|
21
22
|
dismissKeyboard,
|
|
23
|
+
hostInsets,
|
|
22
24
|
revealFocusedField,
|
|
23
25
|
sheetActions,
|
|
24
26
|
windowHeight,
|
|
@@ -42,10 +44,25 @@ export interface BottomSheetOptions {
|
|
|
42
44
|
keyboard?: 'follow' | 'none'
|
|
43
45
|
/** Whether dragging, the backdrop, back and accessibility can dismiss. Defaults to true. */
|
|
44
46
|
dismissible?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Overrides the host's safe-area insets, in DIPs. Core already lays the host's children
|
|
49
|
+
* out inside its insets, so a top override only adds to that placement.
|
|
50
|
+
*/
|
|
45
51
|
safeArea?: { top?: number; bottom?: number }
|
|
46
52
|
returnFocus?: View
|
|
47
53
|
spring?: Pick<SpringValueOptions, 'stiffness' | 'damping' | 'mass'>
|
|
48
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Spoken labels for the handle. Expand and Collapse appear only where a larger or smaller
|
|
56
|
+
* stop exists; `value` formats the spoken value per stop, with `null` when closed, and
|
|
57
|
+
* replaces the default, which is omitted for a single stop.
|
|
58
|
+
*/
|
|
59
|
+
accessibility?: {
|
|
60
|
+
label?: string
|
|
61
|
+
expand?: string
|
|
62
|
+
collapse?: string
|
|
63
|
+
dismiss?: string
|
|
64
|
+
value?: (point: SheetSnap | null) => string | undefined
|
|
65
|
+
}
|
|
49
66
|
onChange?: (point: SheetSnap | null) => void
|
|
50
67
|
/** Called after a close settles; interrupted closes do not dismiss. */
|
|
51
68
|
onDismiss?: () => void
|
|
@@ -142,6 +159,8 @@ export function bottomSheet(
|
|
|
142
159
|
const original = {
|
|
143
160
|
contentAlignment: content?.verticalAlignment,
|
|
144
161
|
height: panel.height,
|
|
162
|
+
ignoreSafeArea: panel.iosIgnoreSafeArea,
|
|
163
|
+
paddingBottom: panel.paddingBottom,
|
|
145
164
|
marginTop: panel.marginTop,
|
|
146
165
|
marginBottom: panel.marginBottom,
|
|
147
166
|
alignment: panel.verticalAlignment,
|
|
@@ -155,6 +174,7 @@ export function bottomSheet(
|
|
|
155
174
|
value: handle.accessibilityValue,
|
|
156
175
|
}
|
|
157
176
|
let geometry: SheetGeometry | undefined, input: Gestures | undefined
|
|
177
|
+
let extended = 0
|
|
158
178
|
let opened = false,
|
|
159
179
|
point = points[0]!,
|
|
160
180
|
ended = false,
|
|
@@ -173,6 +193,44 @@ export function bottomSheet(
|
|
|
173
193
|
restoreFocus = () => {}
|
|
174
194
|
const dismissible = options.dismissible !== false
|
|
175
195
|
const followKeyboard = options.keyboard !== 'none'
|
|
196
|
+
const labels = options.accessibility
|
|
197
|
+
const spoken = (next: SheetSnap | null) =>
|
|
198
|
+
labels?.value
|
|
199
|
+
? labels.value(next)
|
|
200
|
+
: points.length > 1
|
|
201
|
+
? next === null
|
|
202
|
+
? 'Closed'
|
|
203
|
+
: String(next)
|
|
204
|
+
: undefined
|
|
205
|
+
const announce = (next: SheetSnap | null) => {
|
|
206
|
+
handle.accessibilityValue = spoken(next) as string
|
|
207
|
+
}
|
|
208
|
+
let installed: string | undefined
|
|
209
|
+
// Only actions that can do something are offered; the set follows the selected stop.
|
|
210
|
+
function installActions() {
|
|
211
|
+
if (ended || !handle.isLoaded) return
|
|
212
|
+
const current = geometry?.stops.find((stop) => stop.point === point)
|
|
213
|
+
const larger =
|
|
214
|
+
!!current && geometry!.stops.some((stop) => stop.height > current.height + 0.01)
|
|
215
|
+
const smaller =
|
|
216
|
+
!!current && geometry!.stops.some((stop) => stop.height < current.height - 0.01)
|
|
217
|
+
const actions = [
|
|
218
|
+
...(larger
|
|
219
|
+
? [{ label: labels?.expand ?? 'Expand', run: () => adjacent(true) }]
|
|
220
|
+
: []),
|
|
221
|
+
...(smaller
|
|
222
|
+
? [{ label: labels?.collapse ?? 'Collapse', run: () => adjacent(false) }]
|
|
223
|
+
: []),
|
|
224
|
+
...(dismissible
|
|
225
|
+
? [{ label: labels?.dismiss ?? 'Dismiss', run: () => setTarget(null) }]
|
|
226
|
+
: []),
|
|
227
|
+
]
|
|
228
|
+
const signature = actions.map((action) => action.label).join('\n')
|
|
229
|
+
if (signature === installed) return
|
|
230
|
+
releaseActions()
|
|
231
|
+
releaseActions = sheetActions(handle, actions)
|
|
232
|
+
installed = signature
|
|
233
|
+
}
|
|
176
234
|
const keyboardOpen = () => followKeyboard && keyboard.visible
|
|
177
235
|
const targetY = () =>
|
|
178
236
|
!opened
|
|
@@ -253,7 +311,8 @@ export function bottomSheet(
|
|
|
253
311
|
pendingDismiss ||= wasOpen
|
|
254
312
|
dismissKeyboard(panel)
|
|
255
313
|
}
|
|
256
|
-
|
|
314
|
+
announce(opened ? point : null)
|
|
315
|
+
installActions()
|
|
257
316
|
move()
|
|
258
317
|
if (changed) report(() => options.onChange?.(next))
|
|
259
318
|
}
|
|
@@ -280,11 +339,16 @@ export function bottomSheet(
|
|
|
280
339
|
return
|
|
281
340
|
const size = host.getActualSize()
|
|
282
341
|
if (!(size.height > 0)) return
|
|
283
|
-
const safe = host
|
|
284
|
-
|
|
342
|
+
const safe = hostInsets(host)
|
|
343
|
+
// The host places its children below its own top inset; the panel can only start lower.
|
|
344
|
+
const top = Math.max(safe.top, options.safeArea?.top ?? safe.top)
|
|
285
345
|
const bottom = options.safeArea?.bottom ?? safe.bottom
|
|
346
|
+
// The padding that keeps rows above the bottom inset is not sheet chrome.
|
|
286
347
|
const chrome = scroll
|
|
287
|
-
? Math.max(
|
|
348
|
+
? Math.max(
|
|
349
|
+
0,
|
|
350
|
+
panel.getActualSize().height - scroll.getActualSize().height - extended,
|
|
351
|
+
)
|
|
288
352
|
: 0
|
|
289
353
|
const contentHeight = (content?.getActualSize().height ?? 0) + chrome
|
|
290
354
|
const next = sheetGeometry(
|
|
@@ -296,14 +360,24 @@ export function bottomSheet(
|
|
|
296
360
|
top,
|
|
297
361
|
bottom,
|
|
298
362
|
contentHeight,
|
|
363
|
+
safe.bottom === 0,
|
|
299
364
|
)
|
|
300
365
|
const previous = geometry
|
|
301
366
|
geometry = next
|
|
302
|
-
const panelHeight =
|
|
303
|
-
|
|
304
|
-
|
|
367
|
+
const panelHeight =
|
|
368
|
+
(keyboardOpen()
|
|
369
|
+
? next.height
|
|
370
|
+
: Math.max(...next.stops.map((stop) => stop.height))) + next.extension
|
|
305
371
|
if (panel.height !== panelHeight) panel.height = panelHeight
|
|
306
|
-
|
|
372
|
+
const margin = next.top - safe.top
|
|
373
|
+
if (panel.marginTop !== margin) panel.marginTop = margin
|
|
374
|
+
if (extended !== next.extension) {
|
|
375
|
+
const base =
|
|
376
|
+
Utils.layout.toDeviceIndependentPixels(panel.effectivePaddingBottom ?? 0) -
|
|
377
|
+
extended
|
|
378
|
+
panel.paddingBottom = base + next.extension
|
|
379
|
+
extended = next.extension
|
|
380
|
+
}
|
|
307
381
|
if (!input) {
|
|
308
382
|
input = gestures(panel, {
|
|
309
383
|
pan: { axis: 'y', bounds: { top: minimum(), bottom: next.closedY } },
|
|
@@ -317,7 +391,7 @@ export function bottomSheet(
|
|
|
317
391
|
releaseClose = () => {}
|
|
318
392
|
opened = true
|
|
319
393
|
pendingDismiss = false
|
|
320
|
-
|
|
394
|
+
announce(point)
|
|
321
395
|
if (!wasOpen) report(() => options.onChange?.(point))
|
|
322
396
|
},
|
|
323
397
|
onContactEnd(event) {
|
|
@@ -345,6 +419,7 @@ export function bottomSheet(
|
|
|
345
419
|
releasePanel = bind(panel, { y: input.y })
|
|
346
420
|
if (held || !opened) input.pause()
|
|
347
421
|
}
|
|
422
|
+
installActions()
|
|
348
423
|
if (
|
|
349
424
|
!previous ||
|
|
350
425
|
previous.closedY !== next.closedY ||
|
|
@@ -393,15 +468,8 @@ export function bottomSheet(
|
|
|
393
468
|
restoreFocus = captureFocus(panel, options.returnFocus)
|
|
394
469
|
focusCaptured = true
|
|
395
470
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
releaseActions = sheetActions(handle, [
|
|
399
|
-
{ label: labels?.expand ?? 'Expand', run: () => adjacent(true) },
|
|
400
|
-
{ label: labels?.collapse ?? 'Collapse', run: () => adjacent(false) },
|
|
401
|
-
...(dismissible
|
|
402
|
-
? [{ label: labels?.dismiss ?? 'Dismiss', run: () => setTarget(null) }]
|
|
403
|
-
: []),
|
|
404
|
-
])
|
|
471
|
+
installed = undefined
|
|
472
|
+
installActions()
|
|
405
473
|
releaseKeyboard()
|
|
406
474
|
releaseKeyboard = onKeyboard((state) => {
|
|
407
475
|
keyboard = state
|
|
@@ -421,6 +489,7 @@ export function bottomSheet(
|
|
|
421
489
|
if (backdrop) backdrop.visibility = 'hidden'
|
|
422
490
|
releaseActions()
|
|
423
491
|
releaseActions = () => {}
|
|
492
|
+
installed = undefined
|
|
424
493
|
releaseKeyboard()
|
|
425
494
|
releaseKeyboard = () => {}
|
|
426
495
|
releasePanel()
|
|
@@ -486,6 +555,8 @@ export function bottomSheet(
|
|
|
486
555
|
Application.android.off(Application.android.activityBackPressedEvent, back)
|
|
487
556
|
if (content) content.verticalAlignment = original.contentAlignment!
|
|
488
557
|
panel.height = original.height
|
|
558
|
+
panel.iosIgnoreSafeArea = original.ignoreSafeArea
|
|
559
|
+
panel.paddingBottom = original.paddingBottom
|
|
489
560
|
panel.marginTop = original.marginTop
|
|
490
561
|
panel.marginBottom = original.marginBottom
|
|
491
562
|
panel.verticalAlignment = original.alignment
|
|
@@ -504,6 +575,9 @@ export function bottomSheet(
|
|
|
504
575
|
}
|
|
505
576
|
owners.set(panel, control)
|
|
506
577
|
if (content && points.includes('content')) content.verticalAlignment = 'top'
|
|
578
|
+
// The geometry owns the panel's placement and its rows' distance from the bottom inset;
|
|
579
|
+
// Core would otherwise stretch it to the screen edges or inset its rows a second time.
|
|
580
|
+
panel.iosIgnoreSafeArea = true
|
|
507
581
|
panel.verticalAlignment = 'top'
|
|
508
582
|
panel.marginBottom = 0
|
|
509
583
|
panel.visibility = 'hidden'
|
|
@@ -511,7 +585,7 @@ export function bottomSheet(
|
|
|
511
585
|
handle.accessible = true
|
|
512
586
|
handle.accessibilityLabel =
|
|
513
587
|
options.accessibility?.label ?? (handle.accessibilityLabel || 'Bottom sheet')
|
|
514
|
-
|
|
588
|
+
announce(null)
|
|
515
589
|
if (backdrop) backdrop.visibility = 'hidden'
|
|
516
590
|
for (const view of [panel, handle, ...(scroll ? [scroll] : [])])
|
|
517
591
|
view.on('loaded', attach)
|
|
@@ -16,6 +16,12 @@ function firstResponder(view: UIView): UIView | undefined {
|
|
|
16
16
|
return undefined
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Core reports a view's safe-area insets in device pixels; the sheet works in DIPs. */
|
|
20
|
+
export function hostInsets(view: View): { top: number; bottom: number } {
|
|
21
|
+
const insets = view.getSafeAreaInsets()
|
|
22
|
+
const dip = Utils.layout.toDeviceIndependentPixels
|
|
23
|
+
return { top: dip(insets.top), bottom: dip(insets.bottom) }
|
|
24
|
+
}
|
|
19
25
|
export function windowHeight(view: View): number {
|
|
20
26
|
if (isIOS)
|
|
21
27
|
return (
|
package/src/driver/android.ts
CHANGED
|
@@ -130,20 +130,37 @@ export function createAndroidDriver(): Driver {
|
|
|
130
130
|
)
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
let animator: android.animation.ObjectAnimator
|
|
134
|
+
if (
|
|
135
|
+
typeof org !== 'undefined' &&
|
|
136
|
+
typeof org.rafaelaugusto?.jolt?.KeyframeFactory === 'function'
|
|
137
|
+
) {
|
|
138
|
+
animator = org.rafaelaugusto.jolt.KeyframeFactory.create(
|
|
139
|
+
nativeView,
|
|
140
|
+
nativeProperties[track.property],
|
|
141
|
+
track.times,
|
|
142
|
+
track.values,
|
|
143
|
+
scale,
|
|
144
|
+
)
|
|
145
|
+
} else {
|
|
146
|
+
// Keep JS-only updates working with an app binary built before the factory.
|
|
147
|
+
const keyframes = Array.create(android.animation.Keyframe, track.values.length)
|
|
148
|
+
track.values.forEach((value, i) => {
|
|
149
|
+
keyframes[i] = android.animation.Keyframe.ofFloat(
|
|
150
|
+
track.times[i]!,
|
|
151
|
+
value * scale,
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
const holders = Array.create(android.animation.PropertyValuesHolder, 1)
|
|
155
|
+
holders[0] = android.animation.PropertyValuesHolder.ofKeyframe(
|
|
156
|
+
nativeProperties[track.property],
|
|
157
|
+
keyframes,
|
|
158
|
+
)
|
|
159
|
+
animator = android.animation.ObjectAnimator.ofPropertyValuesHolder(
|
|
160
|
+
nativeView,
|
|
161
|
+
holders,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
147
164
|
let restoreLayer: (() => void) | undefined
|
|
148
165
|
const releaseLayer = () => {
|
|
149
166
|
restoreLayer?.()
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { timeline } from './sequence.js'
|
|
2
2
|
export type { TimelineAnimation, TimelineStep, PlayableAnimation } from './sequence.js'
|
|
3
3
|
export { playbackProgress } from './motion-value.js'
|
|
4
|
-
export { animate, decay, set, stop } from './animate.js'
|
|
4
|
+
export { animate, decay, set, stop, toView } from './animate.js'
|
|
5
5
|
export { bind, motionValue, scrollValue, springValue } from './motion-value.js'
|
|
6
6
|
export type {
|
|
7
7
|
Bindings,
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { ReturnScope } from '../shared-navigation/return-signal.js'
|
|
2
|
+
export { interactiveModal } from './modal.js'
|
|
3
|
+
export type {
|
|
4
|
+
InteractiveModal,
|
|
5
|
+
InteractiveModalEvent,
|
|
6
|
+
InteractiveModalOptions,
|
|
7
|
+
InteractiveModalState,
|
|
8
|
+
} from './modal.js'
|
|
2
9
|
import {
|
|
3
10
|
Application,
|
|
4
11
|
Image,
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Application,
|
|
3
|
+
isIOS,
|
|
4
|
+
SharedTransition,
|
|
5
|
+
Trace,
|
|
6
|
+
Transition,
|
|
7
|
+
View,
|
|
8
|
+
type NavigationTransition,
|
|
9
|
+
} from '@nativescript/core'
|
|
10
|
+
import { onReducedMotion, reducedMotion, timeScale } from '../config.js'
|
|
11
|
+
import { springPhysics } from '../spring.js'
|
|
12
|
+
import { springs, type Spring } from '../transition.js'
|
|
13
|
+
|
|
14
|
+
export type InteractiveModalState =
|
|
15
|
+
| 'ready'
|
|
16
|
+
| 'presenting'
|
|
17
|
+
| 'idle'
|
|
18
|
+
| 'dragging'
|
|
19
|
+
| 'finishing'
|
|
20
|
+
| 'cancelling'
|
|
21
|
+
| 'closed'
|
|
22
|
+
| 'disposed'
|
|
23
|
+
export interface InteractiveModalEvent {
|
|
24
|
+
state: InteractiveModalState
|
|
25
|
+
outcome?: 'completed' | 'cancelled'
|
|
26
|
+
/** Modal heights per second; supplied only at release. */
|
|
27
|
+
release?: Readonly<{ progress: number; velocity: number }>
|
|
28
|
+
}
|
|
29
|
+
export interface InteractiveModalOptions {
|
|
30
|
+
spring?:
|
|
31
|
+
| keyof typeof springs
|
|
32
|
+
| Pick<Spring, 'duration' | 'bounce' | 'stiffness' | 'damping' | 'mass'>
|
|
33
|
+
/** Synchronous gesture gate. Core's cancelable:false is also respected. */
|
|
34
|
+
canBegin?: () => boolean
|
|
35
|
+
/** Boundaries only; gesture updates and settling execute in Objective-C. */
|
|
36
|
+
onState?: (event: InteractiveModalEvent) => void
|
|
37
|
+
}
|
|
38
|
+
export interface InteractiveModal {
|
|
39
|
+
readonly supported: boolean
|
|
40
|
+
/** Use for one Core showModal / NativeScript-Vue $showModal presentation. */
|
|
41
|
+
readonly transition: NavigationTransition | undefined
|
|
42
|
+
readonly state: InteractiveModalState
|
|
43
|
+
/** Close once, forwarding Core's callback arguments. Queues during opening. */
|
|
44
|
+
close(...args: unknown[]): boolean
|
|
45
|
+
/** Roll back a held drag; an already committed release finishes normally. */
|
|
46
|
+
cancel(): void
|
|
47
|
+
/** Detach without closing the modal. Core closeModal remains available. */
|
|
48
|
+
dispose(): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A full-window iOS modal transition using NativeScript's modal owner/delegate. */
|
|
52
|
+
export function interactiveModal(
|
|
53
|
+
options: InteractiveModalOptions = {},
|
|
54
|
+
): InteractiveModal {
|
|
55
|
+
const input =
|
|
56
|
+
typeof options.spring === 'string' ? springs[options.spring] : options.spring
|
|
57
|
+
if (options.spring !== undefined && !input) throw new TypeError('Unknown modal spring.')
|
|
58
|
+
for (const [key, value] of Object.entries(input ?? {}) as [string, unknown][]) {
|
|
59
|
+
if (!['type', 'duration', 'bounce', 'stiffness', 'damping', 'mass'].includes(key))
|
|
60
|
+
throw new TypeError(`Unsupported modal spring option: ${key}.`)
|
|
61
|
+
if (key === 'type' && value !== 'spring')
|
|
62
|
+
throw new TypeError('Modal motion requires a spring.')
|
|
63
|
+
if (key !== 'type' && (typeof value !== 'number' || !Number.isFinite(value)))
|
|
64
|
+
throw new RangeError(`${key} must be finite.`)
|
|
65
|
+
if (['duration', 'stiffness', 'mass'].includes(key) && (value as number) <= 0)
|
|
66
|
+
throw new RangeError(`${key} must be positive.`)
|
|
67
|
+
if (key === 'damping' && (value as number) < 0)
|
|
68
|
+
throw new RangeError('damping must be nonnegative.')
|
|
69
|
+
}
|
|
70
|
+
const physics = springPhysics(input ?? springs.smooth)
|
|
71
|
+
if (Object.values(physics).some((value) => !Number.isFinite(value)))
|
|
72
|
+
throw new RangeError('Modal spring physics must be finite.')
|
|
73
|
+
const supported =
|
|
74
|
+
isIOS &&
|
|
75
|
+
typeof JoltInteractiveModal !== 'undefined' &&
|
|
76
|
+
typeof View.closedModallyEvent === 'string'
|
|
77
|
+
let state: InteractiveModalState = 'ready'
|
|
78
|
+
let transition: NavigationTransition | undefined
|
|
79
|
+
let view: View | undefined, native: JoltInteractiveModal | undefined
|
|
80
|
+
let start: (() => void) | undefined, offMotion: (() => void) | undefined
|
|
81
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
82
|
+
let pendingClose: unknown[] | undefined
|
|
83
|
+
let disposing = false,
|
|
84
|
+
suspended = false,
|
|
85
|
+
shown = false,
|
|
86
|
+
returning = false
|
|
87
|
+
const update = (value: {
|
|
88
|
+
interactiveBegan?: boolean
|
|
89
|
+
interactiveCancelled?: boolean
|
|
90
|
+
}) => {
|
|
91
|
+
if (transition?.instance) SharedTransition.updateState(transition.instance.id, value)
|
|
92
|
+
}
|
|
93
|
+
const emit = (
|
|
94
|
+
next: InteractiveModalState,
|
|
95
|
+
outcome?: InteractiveModalEvent['outcome'],
|
|
96
|
+
release?: InteractiveModalEvent['release'],
|
|
97
|
+
) => {
|
|
98
|
+
if (state === 'disposed') return
|
|
99
|
+
state = next
|
|
100
|
+
try {
|
|
101
|
+
options.onState?.({
|
|
102
|
+
state,
|
|
103
|
+
...(outcome ? { outcome } : {}),
|
|
104
|
+
...(release ? { release } : {}),
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
Trace.write(
|
|
108
|
+
`Interactive modal observer failed: ${String(error)}`,
|
|
109
|
+
'Jolt',
|
|
110
|
+
Trace.messageType.error,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const release = () => {
|
|
115
|
+
if (state === 'disposed') return
|
|
116
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
117
|
+
timer = undefined
|
|
118
|
+
view?.off('shownModally', arrived)
|
|
119
|
+
view?.off('closedModally', closed)
|
|
120
|
+
view?.off('unloaded', unloaded)
|
|
121
|
+
Application.off(Application.suspendEvent, suspend)
|
|
122
|
+
Application.off(Application.resumeEvent, resume)
|
|
123
|
+
offMotion?.()
|
|
124
|
+
offMotion = undefined
|
|
125
|
+
native?.dispose()
|
|
126
|
+
native = undefined
|
|
127
|
+
if (transition?.instance) SharedTransition.finishState(transition.instance.id)
|
|
128
|
+
view = undefined
|
|
129
|
+
start = undefined
|
|
130
|
+
pendingClose = undefined
|
|
131
|
+
emit('disposed')
|
|
132
|
+
}
|
|
133
|
+
const cancelled = () => {
|
|
134
|
+
if (state === 'disposed') return
|
|
135
|
+
// UIKit's dismissal completion must observe interactiveCancelled before we
|
|
136
|
+
// clear it. Core clears interactiveBegan after preserving the modal's owner.
|
|
137
|
+
const core = transition?.instance && SharedTransition.getState(transition.instance.id)
|
|
138
|
+
if (core?.interactiveBegan) {
|
|
139
|
+
timer = setTimeout(cancelled, 10)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
timer = undefined
|
|
143
|
+
update({ interactiveCancelled: false })
|
|
144
|
+
emit('idle', 'cancelled')
|
|
145
|
+
if (disposing) release()
|
|
146
|
+
}
|
|
147
|
+
const arrived = () => {
|
|
148
|
+
shown = true
|
|
149
|
+
// UIKit skips animator creation for Core's animated:false. Keep ordinary
|
|
150
|
+
// close usable; no native interactive recognizer is attached in that mode.
|
|
151
|
+
if (state === 'ready') emit('idle')
|
|
152
|
+
if (pendingClose && state === 'idle') {
|
|
153
|
+
const args = pendingClose
|
|
154
|
+
pendingClose = undefined
|
|
155
|
+
control.close(...args)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const closed = () => {
|
|
159
|
+
if (state === 'disposed' || state === 'closed') return
|
|
160
|
+
emit('closed', 'completed')
|
|
161
|
+
release()
|
|
162
|
+
}
|
|
163
|
+
const checkDetached = () => {
|
|
164
|
+
timer = undefined
|
|
165
|
+
if (
|
|
166
|
+
!suspended &&
|
|
167
|
+
view &&
|
|
168
|
+
!view.isLoaded &&
|
|
169
|
+
!view.viewController?.presentingViewController
|
|
170
|
+
)
|
|
171
|
+
control.dispose()
|
|
172
|
+
}
|
|
173
|
+
const unloaded = () => {
|
|
174
|
+
if (state === 'cancelling') return
|
|
175
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
176
|
+
timer = setTimeout(checkDetached, 0)
|
|
177
|
+
}
|
|
178
|
+
const suspend = () => {
|
|
179
|
+
suspended = true
|
|
180
|
+
native?.cancel()
|
|
181
|
+
}
|
|
182
|
+
const resume = () => {
|
|
183
|
+
suspended = false
|
|
184
|
+
if (state !== 'cancelling') unloaded()
|
|
185
|
+
}
|
|
186
|
+
class ModalMotion extends Transition {
|
|
187
|
+
override setupInteractiveGesture(callback: () => void, modal: View) {
|
|
188
|
+
if (view || state === 'disposed')
|
|
189
|
+
throw new Error('Use a fresh interactiveModal for each presentation.')
|
|
190
|
+
view = modal
|
|
191
|
+
start = callback
|
|
192
|
+
view.on('shownModally', arrived)
|
|
193
|
+
view.on('closedModally', closed)
|
|
194
|
+
view.on('unloaded', unloaded)
|
|
195
|
+
}
|
|
196
|
+
override iosPresentedController(presented: UIViewController) {
|
|
197
|
+
if (native || state === 'disposed' || !view) return undefined
|
|
198
|
+
returning = false
|
|
199
|
+
native = JoltInteractiveModal.attachStiffnessDampingMassShouldBeginOnEvent(
|
|
200
|
+
presented,
|
|
201
|
+
physics.stiffness,
|
|
202
|
+
physics.damping,
|
|
203
|
+
physics.mass,
|
|
204
|
+
() => {
|
|
205
|
+
try {
|
|
206
|
+
return (
|
|
207
|
+
!disposing &&
|
|
208
|
+
shown &&
|
|
209
|
+
state === 'idle' &&
|
|
210
|
+
!suspended &&
|
|
211
|
+
(options.canBegin?.() ?? true) === true
|
|
212
|
+
)
|
|
213
|
+
} catch {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
(event) => {
|
|
218
|
+
if (state === 'disposed') return
|
|
219
|
+
if (event === 2) {
|
|
220
|
+
update({ interactiveBegan: true, interactiveCancelled: false })
|
|
221
|
+
emit('dragging')
|
|
222
|
+
start?.()
|
|
223
|
+
} else if (event === 3 || event === 4) {
|
|
224
|
+
update({ interactiveCancelled: event === 4 })
|
|
225
|
+
emit(
|
|
226
|
+
event === 3 ? 'finishing' : 'cancelling',
|
|
227
|
+
undefined,
|
|
228
|
+
native && { progress: native.progress, velocity: native.velocity },
|
|
229
|
+
)
|
|
230
|
+
} else if (event === 0) emit(returning ? 'finishing' : 'presenting')
|
|
231
|
+
else if (event === 1) {
|
|
232
|
+
emit('idle')
|
|
233
|
+
if (disposing) release()
|
|
234
|
+
else if (shown) arrived()
|
|
235
|
+
} else if (event === 6) cancelled()
|
|
236
|
+
// Core's closedModally event owns final disposal and callback delivery.
|
|
237
|
+
},
|
|
238
|
+
)
|
|
239
|
+
native.preparePresentingReducedTimeScale(true, reducedMotion(), timeScale())
|
|
240
|
+
return native
|
|
241
|
+
}
|
|
242
|
+
override iosDismissedController() {
|
|
243
|
+
if (!native || state === 'disposed') return undefined
|
|
244
|
+
returning = true
|
|
245
|
+
native.preparePresentingReducedTimeScale(false, reducedMotion(), timeScale())
|
|
246
|
+
return native
|
|
247
|
+
}
|
|
248
|
+
override iosInteractionDismiss() {
|
|
249
|
+
return native?.interactive ? native : undefined
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const control: InteractiveModal = {
|
|
253
|
+
supported,
|
|
254
|
+
get transition() {
|
|
255
|
+
return transition
|
|
256
|
+
},
|
|
257
|
+
get state() {
|
|
258
|
+
return state
|
|
259
|
+
},
|
|
260
|
+
close(...args) {
|
|
261
|
+
if (
|
|
262
|
+
!supported ||
|
|
263
|
+
disposing ||
|
|
264
|
+
['closed', 'disposed', 'dragging', 'finishing', 'cancelling'].includes(state)
|
|
265
|
+
)
|
|
266
|
+
return false
|
|
267
|
+
if (!shown || state === 'presenting' || state === 'ready') {
|
|
268
|
+
if (pendingClose) return false
|
|
269
|
+
pendingClose = args
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
if (!view) return false
|
|
273
|
+
update({ interactiveBegan: false, interactiveCancelled: false })
|
|
274
|
+
emit('finishing')
|
|
275
|
+
view.closeModal(...args)
|
|
276
|
+
return true
|
|
277
|
+
},
|
|
278
|
+
cancel() {
|
|
279
|
+
native?.cancel()
|
|
280
|
+
},
|
|
281
|
+
dispose() {
|
|
282
|
+
if (state === 'disposed' || disposing) return
|
|
283
|
+
disposing = true
|
|
284
|
+
pendingClose = undefined
|
|
285
|
+
native?.cancel()
|
|
286
|
+
if (
|
|
287
|
+
state !== 'dragging' &&
|
|
288
|
+
state !== 'cancelling' &&
|
|
289
|
+
state !== 'finishing' &&
|
|
290
|
+
state !== 'presenting'
|
|
291
|
+
)
|
|
292
|
+
release()
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
if (supported) {
|
|
296
|
+
transition = SharedTransition.custom(new ModalMotion(), {
|
|
297
|
+
interactive: { dismiss: {} },
|
|
298
|
+
})
|
|
299
|
+
Application.on(Application.suspendEvent, suspend)
|
|
300
|
+
Application.on(Application.resumeEvent, resume)
|
|
301
|
+
offMotion = onReducedMotion((value) => {
|
|
302
|
+
if (value) control.cancel()
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
return control
|
|
306
|
+
}
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -116,6 +116,7 @@ declare class JoltLiveSpring extends NSObject {
|
|
|
116
116
|
readonly moving: boolean
|
|
117
117
|
followMappings(source: JoltLiveSpring, mappings: string): void
|
|
118
118
|
updateInput(snapshot: string): void
|
|
119
|
+
setInputsRest(snapshots: string, rest: number): void
|
|
119
120
|
setTarget(value: number): void
|
|
120
121
|
setReduced(value: boolean): void
|
|
121
122
|
setPaused(value: boolean): void
|
|
@@ -343,6 +344,28 @@ declare class JoltInteractiveNavigation extends NSObject {
|
|
|
343
344
|
cancel(): void
|
|
344
345
|
disposeRestoringSwipe(enabled: boolean): void
|
|
345
346
|
}
|
|
347
|
+
declare class JoltInteractiveModal extends NSObject {
|
|
348
|
+
static attachStiffnessDampingMassShouldBeginOnEvent(
|
|
349
|
+
controller: UIViewController,
|
|
350
|
+
stiffness: number,
|
|
351
|
+
damping: number,
|
|
352
|
+
mass: number,
|
|
353
|
+
shouldBegin: () => boolean,
|
|
354
|
+
onEvent: (event: number) => void,
|
|
355
|
+
): JoltInteractiveModal
|
|
356
|
+
static activeCount(): number
|
|
357
|
+
static recognizerCount(): number
|
|
358
|
+
readonly interactive: boolean
|
|
359
|
+
readonly progress: number
|
|
360
|
+
readonly velocity: number
|
|
361
|
+
preparePresentingReducedTimeScale(
|
|
362
|
+
presenting: boolean,
|
|
363
|
+
reduced: boolean,
|
|
364
|
+
timeScale: number,
|
|
365
|
+
): void
|
|
366
|
+
cancel(): void
|
|
367
|
+
dispose(): void
|
|
368
|
+
}
|
|
346
369
|
|
|
347
370
|
declare class JoltCircleWorld extends NSObject {
|
|
348
371
|
static createOptions(container: UIView, options: string): JoltCircleWorld
|