@octane-xplat/motion 0.7.3 → 0.10.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 +31 -2
- package/UPSTREAM.md +295 -27
- package/dist/native/index.js +980 -112
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index.js +1104 -166
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +25 -16
- package/src/clock.ts +13 -1
- package/src/components.mobile.test.tsrx +58 -1
- package/src/components.tsrx +185 -39
- package/src/components.web.test.tsrx +236 -3
- package/src/controller.ts +122 -10
- package/src/delegation.test.ts +269 -0
- package/src/delegation.ts +29 -0
- package/src/delegation.web.test.ts +270 -0
- package/src/drag.mobile.test.ts +81 -0
- package/src/drag.test.ts +167 -0
- package/src/drag.ts +157 -0
- package/src/driver.ts +249 -0
- package/src/driver.web.ts +180 -0
- package/src/engine.test.ts +99 -0
- package/src/engine.ts +140 -18
- package/src/hooks.tsrx +53 -2
- package/src/host-types.ts +44 -1
- package/src/host.ts +39 -2
- package/src/host.web.ts +51 -10
- package/src/index.ts +18 -4
- package/src/index.web.ts +18 -4
- package/src/pan.ts +92 -0
- package/src/pan.web.ts +116 -0
- package/src/presence-input.ts +11 -4
- package/src/presence-input.web.ts +13 -4
- package/src/presence-state.ts +27 -7
- package/src/presence.tsrx +6 -7
- package/src/reduced-motion.ts +23 -4
- package/src/types.ts +100 -11
- package/src/value.ts +18 -8
- package/src/variants.test.ts +197 -0
- package/src/variants.ts +226 -0
- package/types/engine.d.ts +8 -2
- package/types/index.d.ts +34 -6
- package/types/types.d.ts +94 -11
- package/types/value.d.ts +7 -4
package/src/host.web.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { attachPan } from './pan.web'
|
|
2
|
+
import { delegatedRun } from './driver.web'
|
|
3
|
+
import type { DelegatedRequest, DelegatedRun, HostAdapter } from './host-types'
|
|
4
|
+
import type { Target } from './types'
|
|
2
5
|
export function attachHost(node: HTMLElement): HostAdapter {
|
|
3
6
|
const transform = node.style.transform
|
|
4
7
|
const opacity = node.style.opacity
|
|
@@ -7,7 +10,18 @@ export function attachHost(node: HTMLElement): HostAdapter {
|
|
|
7
10
|
throw new Error('motion: put existing CSS transforms on an outer container')
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
const write = (values: Target) => {
|
|
14
|
+
if (values.opacity !== undefined) {
|
|
15
|
+
node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Object.keys(values).some((key) => key !== 'opacity')) {
|
|
19
|
+
node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
return {
|
|
24
|
+
pan: (axis, callbacks) => attachPan(node, axis, callbacks),
|
|
11
25
|
read: () => ({
|
|
12
26
|
opacity: Number(computed.opacity || 1),
|
|
13
27
|
x: 0,
|
|
@@ -17,18 +31,45 @@ export function attachHost(node: HTMLElement): HostAdapter {
|
|
|
17
31
|
scaleY: 1,
|
|
18
32
|
rotate: 0,
|
|
19
33
|
}),
|
|
20
|
-
write
|
|
21
|
-
|
|
22
|
-
node.style.opacity = String(Math.min(1, Math.max(0, values.opacity)))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (Object.keys(values).some((key) => key !== 'opacity')) {
|
|
26
|
-
node.style.transform = `translateX(${values.x ?? 0}px) translateY(${values.y ?? 0}px) scale(${values.scale ?? 1}) scaleX(${values.scaleX ?? 1}) scaleY(${values.scaleY ?? 1}) rotate(${values.rotate ?? 0}deg)`
|
|
27
|
-
}
|
|
28
|
-
},
|
|
34
|
+
write,
|
|
35
|
+
delegate: (req: DelegatedRequest): DelegatedRun | null => delegatedRun(node, req, write),
|
|
29
36
|
restore() {
|
|
30
37
|
node.style.transform = transform
|
|
31
38
|
node.style.opacity = opacity
|
|
32
39
|
},
|
|
40
|
+
gesture(kind, callbacks) {
|
|
41
|
+
if (kind === 'press') {
|
|
42
|
+
const down = (event: PointerEvent) => {
|
|
43
|
+
try {
|
|
44
|
+
// Preserve descendant click targets when this host wraps controls.
|
|
45
|
+
;(event.target as Element | null)?.setPointerCapture?.(event.pointerId)
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Synthetic pointer dispatch has no active browser pointer.
|
|
48
|
+
if (event.isTrusted || (error as Error).name !== 'NotFoundError') {
|
|
49
|
+
throw error
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
callbacks.start()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const end = () => callbacks.end()
|
|
57
|
+
node.addEventListener('pointerdown', down)
|
|
58
|
+
node.addEventListener('pointerup', end)
|
|
59
|
+
node.addEventListener('pointercancel', end)
|
|
60
|
+
return () => {
|
|
61
|
+
node.removeEventListener('pointerdown', down)
|
|
62
|
+
node.removeEventListener('pointerup', end)
|
|
63
|
+
node.removeEventListener('pointercancel', end)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
node.addEventListener('focus', callbacks.start)
|
|
68
|
+
node.addEventListener('blur', callbacks.end)
|
|
69
|
+
return () => {
|
|
70
|
+
node.removeEventListener('focus', callbacks.start)
|
|
71
|
+
node.removeEventListener('blur', callbacks.end)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
33
74
|
}
|
|
34
75
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
|
|
2
|
-
/** Bounded motion hosts for shared xplat UI primitives.
|
|
3
|
-
|
|
1
|
+
import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
|
|
2
|
+
/** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
|
|
3
|
+
* any component that accepts `ref`/`style`/`children`. */
|
|
4
|
+
export const motion = {
|
|
5
|
+
View: MotionView,
|
|
6
|
+
Row: MotionRow,
|
|
7
|
+
Pressable: MotionPressable,
|
|
8
|
+
create: createMotion,
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export { MotionConfig, useReducedMotion } from './config.tsrx'
|
|
5
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
useMotionValue,
|
|
14
|
+
useTransform,
|
|
15
|
+
useSpring,
|
|
16
|
+
useMotionValueEvent,
|
|
17
|
+
useAnimate,
|
|
18
|
+
} from './hooks.tsrx'
|
|
19
|
+
|
|
6
20
|
export type { MotionValue, MotionValueEvents } from './value'
|
|
7
21
|
export type { AnimationControls, AnimationResult } from './engine'
|
|
8
22
|
export type * from './types'
|
package/src/index.web.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { MotionView, MotionRow, MotionPressable } from './components.tsrx'
|
|
2
|
-
/** Bounded motion hosts for shared xplat UI primitives.
|
|
3
|
-
|
|
1
|
+
import { MotionView, MotionRow, MotionPressable, createMotion } from './components.tsrx'
|
|
2
|
+
/** Bounded motion hosts for shared xplat UI primitives; `motion.create` wraps
|
|
3
|
+
* any component that accepts `ref`/`style`/`children`. */
|
|
4
|
+
export const motion = {
|
|
5
|
+
View: MotionView,
|
|
6
|
+
Row: MotionRow,
|
|
7
|
+
Pressable: MotionPressable,
|
|
8
|
+
create: createMotion,
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
export { MotionConfig, useReducedMotion } from './config.tsrx'
|
|
5
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
useMotionValue,
|
|
14
|
+
useTransform,
|
|
15
|
+
useSpring,
|
|
16
|
+
useMotionValueEvent,
|
|
17
|
+
useAnimate,
|
|
18
|
+
} from './hooks.tsrx'
|
|
19
|
+
|
|
6
20
|
export type { MotionValue, MotionValueEvents } from './value'
|
|
7
21
|
export type { AnimationControls, AnimationResult } from './engine'
|
|
8
22
|
export type * from './types'
|
package/src/pan.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Manager,
|
|
3
|
+
HandlerType,
|
|
4
|
+
GestureState,
|
|
5
|
+
GestureHandlerStateEvent,
|
|
6
|
+
GestureHandlerTouchEvent,
|
|
7
|
+
} from '@nativescript-community/gesturehandler'
|
|
8
|
+
|
|
9
|
+
import { Utils } from '@nativescript/core'
|
|
10
|
+
import type { PanCallbacks } from './host-types'
|
|
11
|
+
import type { DragInfo } from './types'
|
|
12
|
+
|
|
13
|
+
// Separate tag range from the plugin's built-in gesture observers. Manager
|
|
14
|
+
// shares the registry; applications should reserve this range for motion.
|
|
15
|
+
let tag = 700000000
|
|
16
|
+
|
|
17
|
+
export function attachPan(node: any, axis: true | 'x' | 'y', callbacks: PanCallbacks) {
|
|
18
|
+
// 2.0.45 config setters take raw Android pixels (their converter is
|
|
19
|
+
// getter-only); payloads already convert back to DIP.
|
|
20
|
+
const androidHost = Boolean((globalThis as any).android)
|
|
21
|
+
const slop = androidHost ? Utils.layout.toDevicePixels(8) : 8
|
|
22
|
+
const handler = Manager.getInstance().createGestureHandler(HandlerType.PAN, tag++, {
|
|
23
|
+
shouldCancelWhenOutside: false,
|
|
24
|
+
// Android retains its radial touch-slop default alongside activeOffset.
|
|
25
|
+
// Disable that alternative trigger for single-axis arbitration.
|
|
26
|
+
...(androidHost && axis !== true ? { minDist: 1e9 } : {}),
|
|
27
|
+
...(axis === true
|
|
28
|
+
? { minDist: slop }
|
|
29
|
+
: axis === 'x'
|
|
30
|
+
? {
|
|
31
|
+
activeOffsetXStart: -slop,
|
|
32
|
+
activeOffsetXEnd: slop,
|
|
33
|
+
failOffsetYStart: -slop,
|
|
34
|
+
failOffsetYEnd: slop,
|
|
35
|
+
}
|
|
36
|
+
: {
|
|
37
|
+
activeOffsetYStart: -slop,
|
|
38
|
+
activeOffsetYEnd: slop,
|
|
39
|
+
failOffsetXStart: -slop,
|
|
40
|
+
failOffsetXEnd: slop,
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
let active = false
|
|
45
|
+
let last = { x: 0, y: 0 }
|
|
46
|
+
const sample = (args: any, cancelled = false): DragInfo => {
|
|
47
|
+
const extra = args.data.extraData ?? {}
|
|
48
|
+
const offset = { x: extra.translationX ?? last.x, y: extra.translationY ?? last.y }
|
|
49
|
+
const delta = { x: offset.x - last.x, y: offset.y - last.y }
|
|
50
|
+
last = offset
|
|
51
|
+
return {
|
|
52
|
+
point: { x: extra.absoluteX ?? extra.x ?? 0, y: extra.absoluteY ?? extra.y ?? 0 },
|
|
53
|
+
delta,
|
|
54
|
+
offset,
|
|
55
|
+
velocity: { x: extra.velocityX ?? 0, y: extra.velocityY ?? 0 },
|
|
56
|
+
cancelled,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const state = (args: any) => {
|
|
61
|
+
const next = args.data.state
|
|
62
|
+
if (next === GestureState.ACTIVE && !active) {
|
|
63
|
+
last = { x: 0, y: 0 }
|
|
64
|
+
active = true
|
|
65
|
+
const info = sample(args)
|
|
66
|
+
callbacks.start(args, info)
|
|
67
|
+
callbacks.move(args, info)
|
|
68
|
+
} else if (
|
|
69
|
+
active &&
|
|
70
|
+
[GestureState.END, GestureState.CANCELLED, GestureState.FAILED].includes(next)
|
|
71
|
+
) {
|
|
72
|
+
active = false
|
|
73
|
+
callbacks.end(args, sample(args, next !== GestureState.END))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const touch = (args: any) => {
|
|
78
|
+
if (active && args.data.state === GestureState.ACTIVE) {
|
|
79
|
+
callbacks.move(args, sample(args))
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
handler.on(GestureHandlerStateEvent, state)
|
|
84
|
+
handler.on(GestureHandlerTouchEvent, touch)
|
|
85
|
+
handler.attachToView(node)
|
|
86
|
+
return () => {
|
|
87
|
+
active = false
|
|
88
|
+
handler.off(GestureHandlerStateEvent, state)
|
|
89
|
+
handler.off(GestureHandlerTouchEvent, touch)
|
|
90
|
+
handler.detachFromView(node)
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/pan.web.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { PanCallbacks } from './host-types'
|
|
2
|
+
import type { DragInfo } from './types'
|
|
3
|
+
|
|
4
|
+
export function attachPan(node: HTMLElement, axis: true | 'x' | 'y', callbacks: PanCallbacks) {
|
|
5
|
+
const touchAction = node.style.touchAction
|
|
6
|
+
node.style.touchAction = axis === 'x' ? 'pan-y' : axis === 'y' ? 'pan-x' : 'none'
|
|
7
|
+
let pointer: number | undefined
|
|
8
|
+
let active = false
|
|
9
|
+
let start = { x: 0, y: 0 }
|
|
10
|
+
let last = start
|
|
11
|
+
let time = 0
|
|
12
|
+
let velocity = { x: 0, y: 0 }
|
|
13
|
+
let velocityTime = 0
|
|
14
|
+
const sample = (event: PointerEvent, cancelled = false): DragInfo => {
|
|
15
|
+
const point = cancelled ? last : { x: event.clientX, y: event.clientY }
|
|
16
|
+
const delta = { x: point.x - last.x, y: point.y - last.y }
|
|
17
|
+
const elapsed = event.timeStamp - time
|
|
18
|
+
if (elapsed > 0 && (delta.x || delta.y)) {
|
|
19
|
+
velocity = { x: (delta.x * 1000) / elapsed, y: (delta.y * 1000) / elapsed }
|
|
20
|
+
velocityTime = event.timeStamp
|
|
21
|
+
} else if (event.timeStamp - velocityTime > 100) {
|
|
22
|
+
velocity = { x: 0, y: 0 }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
last = point
|
|
26
|
+
time = event.timeStamp
|
|
27
|
+
return {
|
|
28
|
+
point,
|
|
29
|
+
delta,
|
|
30
|
+
offset: { x: point.x - start.x, y: point.y - start.y },
|
|
31
|
+
velocity,
|
|
32
|
+
cancelled,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const down = (event: PointerEvent) => {
|
|
37
|
+
if (pointer !== undefined || event.button !== 0 || event.isPrimary === false) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pointer = event.pointerId
|
|
42
|
+
start = last = { x: event.clientX, y: event.clientY }
|
|
43
|
+
time = velocityTime = event.timeStamp
|
|
44
|
+
velocity = { x: 0, y: 0 }
|
|
45
|
+
try {
|
|
46
|
+
node.setPointerCapture?.(event.pointerId)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// Synthetic pointer dispatch has no active browser pointer.
|
|
49
|
+
if (event.isTrusted || (error as Error).name !== 'NotFoundError') {
|
|
50
|
+
throw error
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const move = (event: PointerEvent) => {
|
|
56
|
+
if (event.pointerId !== pointer) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const info = sample(event)
|
|
61
|
+
if (!active) {
|
|
62
|
+
const primary = axis === 'y' ? info.offset.y : info.offset.x
|
|
63
|
+
const other = axis === 'y' ? info.offset.x : info.offset.y
|
|
64
|
+
if (axis !== true && Math.abs(other) > 8 && Math.abs(primary) <= 8) {
|
|
65
|
+
finish(event, true)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ((axis === true ? Math.hypot(info.offset.x, info.offset.y) : Math.abs(primary)) <= 8) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
active = true
|
|
74
|
+
callbacks.start(event, info)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
callbacks.move(event, info)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const finish = (event: PointerEvent, cancelled: boolean) => {
|
|
81
|
+
if (pointer === undefined || event.pointerId !== pointer) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const id = pointer
|
|
86
|
+
pointer = undefined
|
|
87
|
+
if (active) {
|
|
88
|
+
active = false
|
|
89
|
+
callbacks.end(event, sample(event, cancelled))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (node.hasPointerCapture?.(id)) {
|
|
93
|
+
node.releasePointerCapture(id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const up = (event: PointerEvent) => finish(event, false)
|
|
98
|
+
const cancel = (event: PointerEvent) => finish(event, true)
|
|
99
|
+
node.addEventListener('pointerdown', down)
|
|
100
|
+
node.addEventListener('pointermove', move)
|
|
101
|
+
node.addEventListener('pointerup', up)
|
|
102
|
+
node.addEventListener('pointercancel', cancel)
|
|
103
|
+
node.addEventListener('lostpointercapture', cancel)
|
|
104
|
+
return () => {
|
|
105
|
+
node.removeEventListener('pointerdown', down)
|
|
106
|
+
node.removeEventListener('pointermove', move)
|
|
107
|
+
node.removeEventListener('pointerup', up)
|
|
108
|
+
node.removeEventListener('pointercancel', cancel)
|
|
109
|
+
node.removeEventListener('lostpointercapture', cancel)
|
|
110
|
+
if (pointer !== undefined && node.hasPointerCapture?.(pointer)) {
|
|
111
|
+
node.releasePointerCapture(pointer)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
node.style.touchAction = touchAction
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/presence-input.ts
CHANGED
|
@@ -15,12 +15,19 @@ export function blockInput(node: any): () => void {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
node.isUserInteractionEnabled = false
|
|
18
|
-
if (node.ios) {
|
|
18
|
+
if (node.ios) {
|
|
19
|
+
node.ios.accessibilityElementsHidden = true
|
|
20
|
+
}
|
|
21
|
+
|
|
19
22
|
node.android?.setImportantForAccessibility?.(4) // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
20
23
|
return () => {
|
|
21
24
|
node.isUserInteractionEnabled = enabled
|
|
22
|
-
if (node.ios) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if (node.ios) {
|
|
26
|
+
node.ios.accessibilityElementsHidden = iosHidden
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (androidImportance !== undefined) {
|
|
30
|
+
node.android?.setImportantForAccessibility?.(androidImportance)
|
|
31
|
+
}
|
|
25
32
|
}
|
|
26
33
|
}
|
|
@@ -3,14 +3,23 @@ export function blockInput(node: HTMLElement): () => void {
|
|
|
3
3
|
const hidden = node.getAttribute('aria-hidden')
|
|
4
4
|
const pointerEvents = node.style.pointerEvents
|
|
5
5
|
const focused = node.ownerDocument.activeElement as HTMLElement | null
|
|
6
|
-
if (focused && node.contains(focused)) {
|
|
6
|
+
if (focused && node.contains(focused)) {
|
|
7
|
+
focused.blur()
|
|
8
|
+
}
|
|
9
|
+
|
|
7
10
|
node.setAttribute('inert', '')
|
|
8
11
|
node.setAttribute('aria-hidden', 'true')
|
|
9
12
|
node.style.pointerEvents = 'none'
|
|
10
13
|
return () => {
|
|
11
|
-
if (!inert) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
if (!inert) {
|
|
15
|
+
node.removeAttribute('inert')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (hidden === null) {
|
|
19
|
+
node.removeAttribute('aria-hidden')
|
|
20
|
+
} else {
|
|
21
|
+
node.setAttribute('aria-hidden', hidden)
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
node.style.pointerEvents = pointerEvents
|
|
16
25
|
}
|
package/src/presence-state.ts
CHANGED
|
@@ -20,7 +20,10 @@ export class PresenceState {
|
|
|
20
20
|
) {}
|
|
21
21
|
register(member: PresenceMember): () => void {
|
|
22
22
|
this.members.add(member)
|
|
23
|
-
if (!this.present) {
|
|
23
|
+
if (!this.present) {
|
|
24
|
+
this.restart(member)
|
|
25
|
+
}
|
|
26
|
+
|
|
24
27
|
return () => {
|
|
25
28
|
this.members.delete(member)
|
|
26
29
|
this.pending.delete(member)
|
|
@@ -28,26 +31,43 @@ export class PresenceState {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
setPresent(present: boolean) {
|
|
31
|
-
if (this.disposed || present === this.present) {
|
|
34
|
+
if (this.disposed || present === this.present) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
this.present = present
|
|
33
39
|
++this.generation
|
|
34
40
|
this.removed = false
|
|
35
41
|
this.pending.clear()
|
|
36
42
|
if (present) {
|
|
37
|
-
for (const member of this.members) {
|
|
43
|
+
for (const member of this.members) {
|
|
44
|
+
member.enter()
|
|
45
|
+
}
|
|
38
46
|
} else {
|
|
39
|
-
for (const member of this.members) {
|
|
47
|
+
for (const member of this.members) {
|
|
48
|
+
this.restart(member)
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
this.check()
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
restart(member: PresenceMember) {
|
|
44
|
-
if (this.disposed || this.present || !this.members.has(member)) {
|
|
55
|
+
if (this.disposed || this.present || !this.members.has(member)) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
const token = ++this.sequence
|
|
46
60
|
this.pending.set(member, token)
|
|
47
61
|
void member.exit().then((result) => {
|
|
48
|
-
if (this.disposed || this.present || this.pending.get(member) !== token) {
|
|
62
|
+
if (this.disposed || this.present || this.pending.get(member) !== token) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
// Replacement is owned by the next restart. Cancellation cannot remove a live subtree.
|
|
50
|
-
if (result !== 'finished') {
|
|
67
|
+
if (result !== 'finished') {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
this.pending.delete(member)
|
|
52
72
|
this.check()
|
|
53
73
|
})
|
package/src/presence.tsrx
CHANGED
|
@@ -11,13 +11,13 @@ export function Presence(props: PresenceProps) @{
|
|
|
11
11
|
const [retained, setRetained] = useState(props.present)
|
|
12
12
|
const latest = useRef(props)
|
|
13
13
|
latest.current = props
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
14
|
+
const stateRef = useRef<PresenceState | null>(null)
|
|
15
|
+
if (!stateRef.current) stateRef.current = new PresenceState(props.present, () => {
|
|
16
16
|
setRetained(false)
|
|
17
17
|
latest.current.onExitComplete?.()
|
|
18
18
|
})
|
|
19
19
|
|
|
20
|
-
const state =
|
|
20
|
+
const state = stateRef.current
|
|
21
21
|
const host = useRef<any>(null)
|
|
22
22
|
const restoreInput = useRef<(() => void) | null>(null)
|
|
23
23
|
const syncInput = () => {
|
|
@@ -38,14 +38,13 @@ export function Presence(props: PresenceProps) @{
|
|
|
38
38
|
host.current = null
|
|
39
39
|
}, [])
|
|
40
40
|
|
|
41
|
-
const { present, onExitComplete, children,
|
|
41
|
+
const { present, onExitComplete, children, ref, ...viewProps } = props
|
|
42
42
|
|
|
43
43
|
@if (props.present || retained) {
|
|
44
|
-
<View {...viewProps}
|
|
44
|
+
<View {...viewProps} ref={[(node: any) => {
|
|
45
45
|
if (host.current !== node) { restoreInput.current?.(); restoreInput.current = null; host.current = node }
|
|
46
46
|
syncInput()
|
|
47
|
-
|
|
48
|
-
}}>
|
|
47
|
+
}, ref]}>
|
|
49
48
|
<PresenceContext value={state}>{children}</PresenceContext>
|
|
50
49
|
</View>
|
|
51
50
|
}
|
package/src/reduced-motion.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Application, Utils } from '@nativescript/core'
|
|
1
|
+
import { Application, Device, Utils } from '@nativescript/core'
|
|
2
2
|
|
|
3
3
|
export function readReducedMotion(): boolean {
|
|
4
4
|
const platform = globalThis as any
|
|
@@ -6,7 +6,19 @@ export function readReducedMotion(): boolean {
|
|
|
6
6
|
return Boolean(platform.UIAccessibilityIsReduceMotionEnabled?.())
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
if (!platform.android) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (
|
|
14
|
+
Number(Device.sdkVersion) >= 26 &&
|
|
15
|
+
platform.android.animation?.ValueAnimator?.areAnimatorsEnabled?.() === false
|
|
16
|
+
) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `Utils.android` is a {} stub in the pinned Windows core build.
|
|
21
|
+
const context = (Utils.android as any).getApplicationContext()
|
|
10
22
|
return (
|
|
11
23
|
platform.android.provider.Settings.Global.getFloat(
|
|
12
24
|
context.getContentResolver(),
|
|
@@ -16,9 +28,16 @@ export function readReducedMotion(): boolean {
|
|
|
16
28
|
)
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
//
|
|
20
|
-
//
|
|
31
|
+
// iOS posts a notification on preference change; Android has no event, so it
|
|
32
|
+
// polls only while subscribed (also catches developer-setting changes) and
|
|
33
|
+
// refreshes on resume.
|
|
21
34
|
export function observeReducedMotion(notify: () => void): () => void {
|
|
35
|
+
if (Application.ios) {
|
|
36
|
+
const name = (globalThis as any).UIAccessibilityReduceMotionStatusDidChangeNotification
|
|
37
|
+
const observer = Application.ios.addNotificationObserver(name, notify)
|
|
38
|
+
return () => Application.ios.removeNotificationObserver(observer, name)
|
|
39
|
+
}
|
|
40
|
+
|
|
22
41
|
let previous = readReducedMotion()
|
|
23
42
|
const check = () => {
|
|
24
43
|
const next = readReducedMotion()
|