@astryxdesign/lab 0.7.9-canary.2ea8f68 → 0.7.9-canary.4e7823a

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 (45) hide show
  1. package/dist/RefreshControl/RefreshPullRecognizer.d.ts +40 -0
  2. package/dist/RefreshControl/RefreshPullRecognizer.d.ts.map +1 -0
  3. package/dist/RefreshControl/RefreshPullRecognizer.js +90 -0
  4. package/dist/RefreshControl/index.d.ts +11 -0
  5. package/dist/RefreshControl/index.d.ts.map +1 -0
  6. package/dist/RefreshControl/index.js +13 -0
  7. package/dist/RefreshControl/refreshIndicator.d.ts +10 -0
  8. package/dist/RefreshControl/refreshIndicator.d.ts.map +1 -0
  9. package/dist/RefreshControl/refreshIndicator.js +12 -0
  10. package/dist/RefreshControl/refreshModel.d.ts +61 -0
  11. package/dist/RefreshControl/refreshModel.d.ts.map +1 -0
  12. package/dist/RefreshControl/refreshModel.js +116 -0
  13. package/dist/RefreshControl/refreshPull.d.ts +69 -0
  14. package/dist/RefreshControl/refreshPull.d.ts.map +1 -0
  15. package/dist/RefreshControl/refreshPull.js +159 -0
  16. package/dist/RefreshControl/refreshTouchController.d.ts +45 -0
  17. package/dist/RefreshControl/refreshTouchController.d.ts.map +1 -0
  18. package/dist/RefreshControl/refreshTouchController.js +701 -0
  19. package/dist/RefreshControl/refreshValues.d.ts +76 -0
  20. package/dist/RefreshControl/refreshValues.d.ts.map +1 -0
  21. package/dist/RefreshControl/refreshValues.js +80 -0
  22. package/dist/RefreshControl/useMotionAnimator.d.ts +8 -0
  23. package/dist/RefreshControl/useMotionAnimator.d.ts.map +1 -0
  24. package/dist/RefreshControl/useMotionAnimator.js +25 -0
  25. package/dist/RefreshControl/useRefreshControl.d.ts +84 -0
  26. package/dist/RefreshControl/useRefreshControl.d.ts.map +1 -0
  27. package/dist/RefreshControl/useRefreshControl.js +207 -0
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +5 -1
  31. package/package.json +5 -5
  32. package/src/RefreshControl/RefreshPullRecognizer.ts +104 -0
  33. package/src/RefreshControl/__tests__/RefreshControl.interaction.browser.spec.ts +1454 -0
  34. package/src/RefreshControl/index.ts +18 -0
  35. package/src/RefreshControl/refreshIndicator.ts +12 -0
  36. package/src/RefreshControl/refreshModel.test.ts +174 -0
  37. package/src/RefreshControl/refreshModel.ts +117 -0
  38. package/src/RefreshControl/refreshPull.test.ts +270 -0
  39. package/src/RefreshControl/refreshPull.ts +186 -0
  40. package/src/RefreshControl/refreshTouchController.ts +838 -0
  41. package/src/RefreshControl/refreshValues.ts +80 -0
  42. package/src/RefreshControl/useMotionAnimator.ts +35 -0
  43. package/src/RefreshControl/useRefreshControl.test.tsx +1315 -0
  44. package/src/RefreshControl/useRefreshControl.ts +255 -0
  45. package/src/index.ts +9 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @file RefreshPullRecognizer.ts
3
+ * @input Touches from the gesture arena (Core's internal touch entry)
4
+ * @output Exports RefreshPullRecognizer, the vertical pan that reads a pull
5
+ * to refresh without taking the touch, and RefreshScrollPullRecognizer
6
+ * @position The gesture of useRefreshControl, on the foundation's
7
+ * PanGestureRecognizer
8
+ *
9
+ * SYNC: When modified, update:
10
+ * - /packages/lab/src/RefreshControl/useRefreshControl.ts
11
+ */
12
+ import { PanGestureRecognizer, type PanGestureRecognizerOptions } from '@astryxdesign/core/internal/touch';
13
+ import type { GestureEventContext, GestureTouch, PointerKind } from '@astryxdesign/core/internal/touch';
14
+ export declare class RefreshPullRecognizer extends PanGestureRecognizer {
15
+ /** Called when the first contact of a gesture lands, with its timestamp. */
16
+ onTouchDown: ((time: number) => void) | null;
17
+ /** The pointer of the gesture's first contact. */
18
+ pointerType: PointerKind;
19
+ /** Set when the gesture is a trackpad's scroll rather than a contact. */
20
+ scrollType: GestureTouch['scrollType'] | null;
21
+ constructor(options?: PanGestureRecognizerOptions);
22
+ /** Native scrolling keeps the finger: the pull only reads it. */
23
+ get claimsTouches(): boolean;
24
+ protected touchesBegan(touches: ReadonlyArray<GestureTouch>, event: GestureEventContext): void;
25
+ }
26
+ /**
27
+ * The scroll pull: a trackpad's two-finger scroll at the top. It is a
28
+ * scroll-edge pan that takes a trackpad's scroll and no pointer. So it takes a
29
+ * scroll only where nothing under the pointer could scroll: the owner at its
30
+ * top and the content moving down. The host cancels each wheel event of a
31
+ * gesture that it takes, so its element gets a blocking `wheel` listener. Only
32
+ * a recognizer that claims may register one. It takes no pointer, so it adds
33
+ * no `touchmove` listener.
34
+ */
35
+ export declare class RefreshScrollPullRecognizer extends RefreshPullRecognizer {
36
+ constructor(options?: PanGestureRecognizerOptions);
37
+ /** The scroll it takes is its own: the host cancels its wheel events. */
38
+ get claimsTouches(): boolean;
39
+ }
40
+ //# sourceMappingURL=RefreshPullRecognizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RefreshPullRecognizer.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/RefreshPullRecognizer.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAoBH,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EACjC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EACV,mBAAmB,EACnB,YAAY,EACZ,WAAW,EACZ,MAAM,mCAAmC,CAAC;AAE3C,qBAAa,qBAAsB,SAAQ,oBAAoB;IAC7D,4EAA4E;IAC5E,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IACpD,kDAAkD;IAClD,WAAW,EAAE,WAAW,CAAW;IACnC,yEAAyE;IACzE,UAAU,EAAE,YAAY,CAAC,YAAY,CAAC,GAAG,IAAI,CAAQ;gBAEzC,OAAO,GAAE,2BAAgC;IAUrD,iEAAiE;IACjE,IAAa,aAAa,IAAI,OAAO,CAEpC;cAEkB,YAAY,CAC7B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,EACpC,KAAK,EAAE,mBAAmB,GACzB,IAAI;CASR;AAED;;;;;;;;GAQG;AACH,qBAAa,2BAA4B,SAAQ,qBAAqB;gBACxD,OAAO,GAAE,2BAAgC;IAUrD,yEAAyE;IACzE,IAAa,aAAa,IAAI,OAAO,CAEpC;CACF"}
@@ -0,0 +1,90 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file RefreshPullRecognizer.ts
5
+ * @input Touches from the gesture arena (Core's internal touch entry)
6
+ * @output Exports RefreshPullRecognizer, the vertical pan that reads a pull
7
+ * to refresh without taking the touch, and RefreshScrollPullRecognizer
8
+ * @position The gesture of useRefreshControl, on the foundation's
9
+ * PanGestureRecognizer
10
+ *
11
+ * SYNC: When modified, update:
12
+ * - /packages/lab/src/RefreshControl/useRefreshControl.ts
13
+ */
14
+
15
+ // UIKit's refresh pull is the scroll view's own drag, not a second gesture.
16
+ // One touch can scroll, pull past the top, and scroll the same content again.
17
+ // So this pan never claims its touch, and only watches it. The browser keeps
18
+ // the finger for native scrolling, so nothing changes hands midway. The pan
19
+ // reads the finger to draw the overscroll, because the scroll owner's
20
+ // `overscroll-behavior-y` is `none` near the top.
21
+ //
22
+ // The pan begins on vertical travel past its hysteresis. The touch layer
23
+ // starts the pull where the content is at its top and the finger moves down.
24
+ // When the browser takes the touch to scroll, the pan keeps reading it to the
25
+ // lift. A second finger is the browser's, for pinch-zoom.
26
+ //
27
+ // A trackpad's scroll pulls through RefreshScrollPullRecognizer. A pen's or
28
+ // a mouse's drag stays the browser's, for text selection: the pan takes only
29
+ // a finger. `pointerType` and `scrollType` record the first contact's input.
30
+ // `onTouchDown` reports that contact landing, so a settle in flight can stop
31
+ // where it is, as UIScrollView brakes on touch-down.
32
+
33
+ import { PanGestureRecognizer } from '@astryxdesign/core/internal/touch';
34
+ export class RefreshPullRecognizer extends PanGestureRecognizer {
35
+ /** Called when the first contact of a gesture lands, with its timestamp. */
36
+ onTouchDown = null;
37
+ /** The pointer of the gesture's first contact. */
38
+ pointerType = 'touch';
39
+ /** Set when the gesture is a trackpad's scroll rather than a contact. */
40
+ scrollType = null;
41
+ constructor(options = {}) {
42
+ super({
43
+ name: 'RefreshPull',
44
+ direction: 'vertical',
45
+ maximumNumberOfTouches: 1,
46
+ allowedPointerTypes: ['touch'],
47
+ ...options
48
+ });
49
+ }
50
+
51
+ /** Native scrolling keeps the finger: the pull only reads it. */
52
+ get claimsTouches() {
53
+ return false;
54
+ }
55
+ touchesBegan(touches, event) {
56
+ if (this.numberOfTouches === touches.length) {
57
+ const touch = touches[0];
58
+ this.pointerType = touch?.pointerType ?? 'touch';
59
+ this.scrollType = touch?.scrollType ?? null;
60
+ this.onTouchDown?.(event.time);
61
+ }
62
+ super.touchesBegan(touches, event);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * The scroll pull: a trackpad's two-finger scroll at the top. It is a
68
+ * scroll-edge pan that takes a trackpad's scroll and no pointer. So it takes a
69
+ * scroll only where nothing under the pointer could scroll: the owner at its
70
+ * top and the content moving down. The host cancels each wheel event of a
71
+ * gesture that it takes, so its element gets a blocking `wheel` listener. Only
72
+ * a recognizer that claims may register one. It takes no pointer, so it adds
73
+ * no `touchmove` listener.
74
+ */
75
+ export class RefreshScrollPullRecognizer extends RefreshPullRecognizer {
76
+ constructor(options = {}) {
77
+ super({
78
+ name: 'RefreshScrollPull',
79
+ scrollEdge: true,
80
+ allowedPointerTypes: [],
81
+ allowedScrollTypes: ['continuous'],
82
+ ...options
83
+ });
84
+ }
85
+
86
+ /** The scroll it takes is its own: the host cancels its wheel events. */
87
+ get claimsTouches() {
88
+ return true;
89
+ }
90
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @file index.ts
3
+ * @input Imports from useRefreshControl
4
+ * @output Exports useRefreshControl, its types and the reveal's custom
5
+ * property
6
+ * @position Entry point for Lab's pull to refresh; re-exported by
7
+ * /packages/lab/src/index.ts
8
+ */
9
+ export { REFRESH_REVEAL_VAR, useRefreshControl } from './useRefreshControl.js';
10
+ export type { UseRefreshControlOptions, UseRefreshControlResult, } from './useRefreshControl.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/index.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAC1E,YAAY,EACV,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file index.ts
7
+ * @input Imports from useRefreshControl
8
+ * @output Exports useRefreshControl, its types and the reveal's custom
9
+ * property
10
+ * @position Entry point for Lab's pull to refresh; re-exported by
11
+ * /packages/lab/src/index.ts
12
+ */
13
+ export { REFRESH_REVEAL_VAR, useRefreshControl } from "./useRefreshControl.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file refreshIndicator.ts
3
+ * @output The name of the custom property the touch layer writes and the
4
+ * app's indicator styles may read
5
+ * @position Internal to useRefreshControl; re-exported for the app's
6
+ * indicator styles
7
+ */
8
+ /** CSS custom property the indicator reads: how much of it is revealed (0–1). */
9
+ export declare const REFRESH_REVEAL_VAR = "--astryx-refresh-reveal";
10
+ //# sourceMappingURL=refreshIndicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refreshIndicator.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/refreshIndicator.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAEH,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,4BAA4B,CAAC"}
@@ -0,0 +1,12 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file refreshIndicator.ts
5
+ * @output The name of the custom property the touch layer writes and the
6
+ * app's indicator styles may read
7
+ * @position Internal to useRefreshControl; re-exported for the app's
8
+ * indicator styles
9
+ */
10
+
11
+ /** CSS custom property the indicator reads: how much of it is revealed (0–1). */
12
+ export const REFRESH_REVEAL_VAR = '--astryx-refresh-reveal';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @file refreshModel.ts
3
+ * @input Pull events from the refresh pull recognizer, and programmatic begin
4
+ * and end requests
5
+ * @output Exports reduceRefresh (UIRefreshControl's state machine as a pure
6
+ * reducer that returns the effects to run), the initial state and types
7
+ * @position Pure logic for useRefreshControl; no DOM, no React
8
+ *
9
+ * SYNC: When modified, update:
10
+ * - /packages/lab/src/RefreshControl/refreshModel.test.ts
11
+ */
12
+ export interface RefreshState {
13
+ /** A refresh is in progress (`UIRefreshControl.isRefreshing`). */
14
+ readonly isRefreshing: boolean;
15
+ /** A finger is pulling the content. */
16
+ readonly isPulling: boolean;
17
+ /** The current pull has triggered (its mapping turns one to one). */
18
+ readonly hasTriggered: boolean;
19
+ }
20
+ export type RefreshEvent = {
21
+ readonly type: 'pullStart';
22
+ } | {
23
+ readonly type: 'pullMove';
24
+ readonly overscroll: number;
25
+ /** The trigger distance for this scroll owner. */
26
+ readonly trigger: number;
27
+ } | {
28
+ readonly type: 'pullEnd';
29
+ } | {
30
+ readonly type: 'pullCancel';
31
+ } | {
32
+ readonly type: 'begin';
33
+ readonly source: 'user' | 'program';
34
+ } | {
35
+ readonly type: 'end';
36
+ };
37
+ export type RefreshEffect =
38
+ /** The pull reached the trigger: spin, announce, call the handler. */
39
+ {
40
+ readonly type: 'trigger';
41
+ }
42
+ /** A refresh started without a pull; `callHandler` for the user's request. */
43
+ | {
44
+ readonly type: 'begin';
45
+ readonly callHandler: boolean;
46
+ }
47
+ /** The finger let go: settle onto the inset or back to the top. */
48
+ | {
49
+ readonly type: 'settle';
50
+ readonly to: 'inset' | 'top';
51
+ }
52
+ /** The refresh ended: remove the inset and hide the indicator. */
53
+ | {
54
+ readonly type: 'end';
55
+ };
56
+ export declare const INITIAL_REFRESH_STATE: RefreshState;
57
+ export declare function reduceRefresh(state: RefreshState, event: RefreshEvent): {
58
+ state: RefreshState;
59
+ effects: ReadonlyArray<RefreshEffect>;
60
+ };
61
+ //# sourceMappingURL=refreshModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refreshModel.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/refreshModel.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAeH,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,MAAM,YAAY,GACpB;IAAC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAC,GAC5B;IACE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GAC1B;IAAC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAC,GAC7B;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAC,GAC7D;IAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAC,CAAC;AAE3B,MAAM,MAAM,aAAa;AACvB,sEAAsE;AACpE;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAC;AAC5B,8EAA8E;GAC5E;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAAC;AACzD,mEAAmE;GACjE;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,KAAK,CAAA;CAAC;AACzD,kEAAkE;GAChE;IAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAC,CAAC;AAE3B,eAAO,MAAM,qBAAqB,EAAE,YAInC,CAAC;AAEF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,YAAY,EACnB,KAAK,EAAE,YAAY,GAClB;IAAC,KAAK,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,CAAA;CAAC,CAgD9D"}
@@ -0,0 +1,116 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file refreshModel.ts
5
+ * @input Pull events from the refresh pull recognizer, and programmatic begin
6
+ * and end requests
7
+ * @output Exports reduceRefresh (UIRefreshControl's state machine as a pure
8
+ * reducer that returns the effects to run), the initial state and types
9
+ * @position Pure logic for useRefreshControl; no DOM, no React
10
+ *
11
+ * SYNC: When modified, update:
12
+ * - /packages/lab/src/RefreshControl/refreshModel.test.ts
13
+ */
14
+
15
+ // The rules, as UIKit runs them:
16
+ // - A pull triggers once, with the finger down, when its overscroll reaches
17
+ // the trigger distance. Only overscroll that a finger drives triggers, never
18
+ // a momentum overshoot.
19
+ // - A release settles onto the refreshing inset while refreshing, and back to
20
+ // the top otherwise. A cancelled pull settles the same way.
21
+ // - A programmatic begin puts the control in the refreshing state. It calls
22
+ // the refresh handler only when it stands for the user's request, not when
23
+ // the app reports a refresh that it started.
24
+ // - Ending a refresh that is not running does nothing.
25
+ //
26
+ // One refresh runs at a time: a pull while refreshing only stretches.
27
+
28
+ export const INITIAL_REFRESH_STATE = {
29
+ isRefreshing: false,
30
+ isPulling: false,
31
+ hasTriggered: false
32
+ };
33
+ export function reduceRefresh(state, event) {
34
+ switch (event.type) {
35
+ case 'pullStart':
36
+ return {
37
+ state: {
38
+ ...state,
39
+ isPulling: true,
40
+ hasTriggered: false
41
+ },
42
+ effects: []
43
+ };
44
+ case 'pullMove':
45
+ {
46
+ // One refresh at a time: a pull while refreshing only stretches.
47
+ if (!state.isPulling || state.hasTriggered || state.isRefreshing || event.overscroll < event.trigger) {
48
+ return {
49
+ state,
50
+ effects: []
51
+ };
52
+ }
53
+ return {
54
+ state: {
55
+ ...state,
56
+ isRefreshing: true,
57
+ hasTriggered: true
58
+ },
59
+ effects: [{
60
+ type: 'trigger'
61
+ }]
62
+ };
63
+ }
64
+ case 'pullEnd':
65
+ case 'pullCancel':
66
+ if (!state.isPulling) {
67
+ return {
68
+ state,
69
+ effects: []
70
+ };
71
+ }
72
+ return {
73
+ state: {
74
+ ...state,
75
+ isPulling: false
76
+ },
77
+ effects: [{
78
+ type: 'settle',
79
+ to: state.isRefreshing ? 'inset' : 'top'
80
+ }]
81
+ };
82
+ case 'begin':
83
+ if (state.isRefreshing) {
84
+ return {
85
+ state,
86
+ effects: []
87
+ };
88
+ }
89
+ return {
90
+ state: {
91
+ ...state,
92
+ isRefreshing: true
93
+ },
94
+ effects: [{
95
+ type: 'begin',
96
+ callHandler: event.source === 'user'
97
+ }]
98
+ };
99
+ case 'end':
100
+ if (!state.isRefreshing) {
101
+ return {
102
+ state,
103
+ effects: []
104
+ };
105
+ }
106
+ return {
107
+ state: {
108
+ ...state,
109
+ isRefreshing: false
110
+ },
111
+ effects: [{
112
+ type: 'end'
113
+ }]
114
+ };
115
+ }
116
+ }
@@ -0,0 +1,69 @@
1
+ /** The refreshing inset and one-to-one stretch, pt. */
2
+ export declare const REFRESH_HEIGHT: number;
3
+ /**
4
+ * Which mapping a pull follows.
5
+ * - `reveal`: before the trigger. One to one up to `height`, then the rubber
6
+ * band on the excess.
7
+ * - `band`: after a trigger within twice the height, and for a pull from the
8
+ * refreshing inset. One to one up to twice the height, then the rubber band.
9
+ */
10
+ export type PullMode = 'reveal' | 'band';
11
+ /**
12
+ * Overscroll shown for `travel` px of pull (the pan's translation, which
13
+ * already leaves out its hysteresis). `dimension` is the scroll owner's
14
+ * height, the rubber band's dimension.
15
+ */
16
+ export declare function pullOverscroll(travel: number, dimension: number, mode: PullMode): number;
17
+ /**
18
+ * How fast the overscroll moves per px of finger travel at `travel`: 1 where
19
+ * it tracks the finger, the rubber band's slope past it. A release's speed
20
+ * times this slope is the content's speed, which its settle starts at.
21
+ */
22
+ export declare function pullSlope(travel: number, dimension: number, mode: PullMode): number;
23
+ /**
24
+ * The travel that shows `overscroll`: where a finger landing on content
25
+ * already pulled (a re-grab mid-settle) resumes without a jump.
26
+ */
27
+ export declare function pullTravel(overscroll: number, dimension: number, mode: PullMode): number;
28
+ /**
29
+ * The mapping after a trigger at `travel`. It is the one-to-one band when the
30
+ * trigger falls within it, and the next frame steps onto the finger. Otherwise
31
+ * it is the reveal mapping, as when a tall scroll view triggers past the band.
32
+ */
33
+ export declare function modeAfterTrigger(travel: number): PullMode;
34
+ /**
35
+ * The overscroll at which a pull triggers, for a scroll owner `dimension`
36
+ * tall: a fraction of its height, never below the minimum.
37
+ */
38
+ export declare function triggerDistance(dimension: number): number;
39
+ /** How much of the indicator a pull reveals, 0 to 1 (linear in the pull). */
40
+ export declare function revealFraction(overscroll: number, dimension: number): number;
41
+ /** The reveal fraction on UIKit's reveal curve: the indicator's opacity. */
42
+ export declare function revealProgress(overscroll: number, dimension: number): number;
43
+ /**
44
+ * Where the content comes to rest when the finger lifts: on the refreshing
45
+ * inset while refreshing, back at the top otherwise.
46
+ */
47
+ export declare function releaseTarget(isRefreshing: boolean): number;
48
+ /**
49
+ * How the end of a refresh moves the content. `scrolled` (≥ 0) is how far the
50
+ * scroll owner is scrolled with the inset still in place. While the gap
51
+ * shows, the content slides up until the gap closes: the inset animates from
52
+ * `height` to `scrolled`. Then the inset and the scroll offset drop together
53
+ * with no visible change. Scrolled past the gap, nothing moves, and the inset
54
+ * and the offset drop at once.
55
+ */
56
+ export declare function endMotion(scrolled: number): {
57
+ animateTo: number | null;
58
+ scrollBy: number;
59
+ };
60
+ /**
61
+ * The end's duration and curve: the theme tokens that `refreshControl` names,
62
+ * as the end's element computes them. Null when either token is missing or
63
+ * in a form it cannot read, such as `calc()`, `var()` or `steps()`.
64
+ */
65
+ export declare function endTiming(style: Pick<CSSStyleDeclaration, 'getPropertyValue'>): {
66
+ ms: number;
67
+ points: readonly [number, number, number, number];
68
+ } | null;
69
+ //# sourceMappingURL=refreshPull.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refreshPull.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/refreshPull.ts"],"names":[],"mappings":"AAmCA,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAA8B,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAQzC;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GACb,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GACb,MAAM,CAMR;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,QAAQ,GACb,MAAM,CASR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAEzD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKzD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAID,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAE3D;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC3C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAOA;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,GACnD;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CAAC,GAAG,IAAI,CASxE"}
@@ -0,0 +1,159 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file refreshPull.ts
5
+ * @input The pull values (refreshValues.ts), and the rubber band, the cubic
6
+ * Bézier and the motion-role parsers (Core's internal touch entry)
7
+ * @output Exports the pull mapping (finger travel → overscroll), its slope
8
+ * and its inverse, and the trigger distance. It also exports the reveal, a
9
+ * refresh's release and end targets, and the end's timing from the theme's
10
+ * tokens.
11
+ * @position Pure math for useRefreshControl; no DOM, no React
12
+ *
13
+ * SYNC: When modified, update:
14
+ * - /packages/lab/src/RefreshControl/refreshPull.test.ts
15
+ */
16
+
17
+ // UIKit's refresh pull, as measured: the first `height` of overscroll tracks
18
+ // the finger one to one, after the pan's hysteresis. Past it, the scroll
19
+ // view's rubber band applies to the excess. The trigger distance grows with
20
+ // the scroll view's height. After a trigger within twice the height, the
21
+ // overscroll steps back onto the finger. It stays one to one up to twice the
22
+ // height, with resistance past that.
23
+
24
+ import { cubicBezier } from '@astryxdesign/core/internal/touch';
25
+ import { rubberBand, rubberBandInverse, rubberBandSlope } from '@astryxdesign/core/internal/touch';
26
+ import { parseCssEasing, parseMotionRoleValue } from '@astryxdesign/core/internal/touch';
27
+ import { refreshControl } from "./refreshValues.js";
28
+
29
+ /** The refreshing inset and one-to-one stretch, pt. */
30
+ export const REFRESH_HEIGHT = refreshControl.height;
31
+
32
+ /**
33
+ * Which mapping a pull follows.
34
+ * - `reveal`: before the trigger. One to one up to `height`, then the rubber
35
+ * band on the excess.
36
+ * - `band`: after a trigger within twice the height, and for a pull from the
37
+ * refreshing inset. One to one up to twice the height, then the rubber band.
38
+ */
39
+
40
+ function linearStretch(mode) {
41
+ return mode === 'band' ? refreshControl.height * refreshControl.postTriggerLinearHeights : refreshControl.height;
42
+ }
43
+
44
+ /**
45
+ * Overscroll shown for `travel` px of pull (the pan's translation, which
46
+ * already leaves out its hysteresis). `dimension` is the scroll owner's
47
+ * height, the rubber band's dimension.
48
+ */
49
+ export function pullOverscroll(travel, dimension, mode) {
50
+ if (!(travel > 0)) {
51
+ return 0;
52
+ }
53
+ const linear = linearStretch(mode);
54
+ if (travel <= linear) {
55
+ return travel;
56
+ }
57
+ return linear + rubberBand(travel - linear, dimension);
58
+ }
59
+
60
+ /**
61
+ * How fast the overscroll moves per px of finger travel at `travel`: 1 where
62
+ * it tracks the finger, the rubber band's slope past it. A release's speed
63
+ * times this slope is the content's speed, which its settle starts at.
64
+ */
65
+ export function pullSlope(travel, dimension, mode) {
66
+ if (!(travel > 0)) {
67
+ return 0;
68
+ }
69
+ const linear = linearStretch(mode);
70
+ return travel <= linear ? 1 : rubberBandSlope(travel - linear, dimension);
71
+ }
72
+
73
+ /**
74
+ * The travel that shows `overscroll`: where a finger landing on content
75
+ * already pulled (a re-grab mid-settle) resumes without a jump.
76
+ */
77
+ export function pullTravel(overscroll, dimension, mode) {
78
+ if (!(overscroll > 0)) {
79
+ return 0;
80
+ }
81
+ const linear = linearStretch(mode);
82
+ if (overscroll <= linear) {
83
+ return overscroll;
84
+ }
85
+ return linear + rubberBandInverse(overscroll - linear, dimension);
86
+ }
87
+
88
+ /**
89
+ * The mapping after a trigger at `travel`. It is the one-to-one band when the
90
+ * trigger falls within it, and the next frame steps onto the finger. Otherwise
91
+ * it is the reveal mapping, as when a tall scroll view triggers past the band.
92
+ */
93
+ export function modeAfterTrigger(travel) {
94
+ return travel <= linearStretch('band') ? 'band' : 'reveal';
95
+ }
96
+
97
+ /**
98
+ * The overscroll at which a pull triggers, for a scroll owner `dimension`
99
+ * tall: a fraction of its height, never below the minimum.
100
+ */
101
+ export function triggerDistance(dimension) {
102
+ return Math.max(refreshControl.triggerMinimum, refreshControl.triggerHeightFraction * Math.max(0, dimension));
103
+ }
104
+
105
+ /** How much of the indicator a pull reveals, 0 to 1 (linear in the pull). */
106
+ export function revealFraction(overscroll, dimension) {
107
+ return Math.min(1, Math.max(0, overscroll / triggerDistance(dimension)));
108
+ }
109
+ const revealEasing = cubicBezier(refreshControl.revealCurve);
110
+
111
+ /** The reveal fraction on UIKit's reveal curve: the indicator's opacity. */
112
+ export function revealProgress(overscroll, dimension) {
113
+ return revealEasing(revealFraction(overscroll, dimension));
114
+ }
115
+
116
+ /**
117
+ * Where the content comes to rest when the finger lifts: on the refreshing
118
+ * inset while refreshing, back at the top otherwise.
119
+ */
120
+ export function releaseTarget(isRefreshing) {
121
+ return isRefreshing ? refreshControl.height : 0;
122
+ }
123
+
124
+ /**
125
+ * How the end of a refresh moves the content. `scrolled` (≥ 0) is how far the
126
+ * scroll owner is scrolled with the inset still in place. While the gap
127
+ * shows, the content slides up until the gap closes: the inset animates from
128
+ * `height` to `scrolled`. Then the inset and the scroll offset drop together
129
+ * with no visible change. Scrolled past the gap, nothing moves, and the inset
130
+ * and the offset drop at once.
131
+ */
132
+ export function endMotion(scrolled) {
133
+ const height = refreshControl.height;
134
+ const offset = Math.max(0, scrolled);
135
+ if (offset >= height) {
136
+ return {
137
+ animateTo: null,
138
+ scrollBy: -height
139
+ };
140
+ }
141
+ return {
142
+ animateTo: offset,
143
+ scrollBy: -offset
144
+ };
145
+ }
146
+
147
+ /**
148
+ * The end's duration and curve: the theme tokens that `refreshControl` names,
149
+ * as the end's element computes them. Null when either token is missing or
150
+ * in a form it cannot read, such as `calc()`, `var()` or `steps()`.
151
+ */
152
+ export function endTiming(style) {
153
+ const ms = parseMotionRoleValue('duration', style.getPropertyValue(refreshControl.endDurationToken));
154
+ const points = parseCssEasing(style.getPropertyValue(refreshControl.endEasingToken));
155
+ return ms == null || points == null ? null : {
156
+ ms,
157
+ points
158
+ };
159
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @file refreshTouchController.ts
3
+ * @input The refresh's state and dispatch (refreshModel.ts), the pull's
4
+ * mapping (refreshPull.ts), and Core's internal touch entry
5
+ * @output useRefreshTouchController: the refresh's touch layer (the pull, the
6
+ * inset, the indicator's place and every motion) and its refs
7
+ * @position Internal to useRefreshControl; useRefreshControl.ts is its one
8
+ * import site, so a page loads this code only with the hook.
9
+ *
10
+ * SYNC: When modified, update:
11
+ * - /packages/lab/src/RefreshControl/useRefreshControl.ts
12
+ * - /packages/lab/src/RefreshControl/useRefreshControl.test.tsx
13
+ */
14
+ import { type RefCallback } from 'react';
15
+ import type { RefreshEvent, RefreshState } from './refreshModel.js';
16
+ export interface RefreshTouchControllerOptions {
17
+ /** Whether the pull applies. */
18
+ readonly isPullEnabled: boolean;
19
+ /** The state machine's state, shared with the wiring. */
20
+ readonly model: {
21
+ readonly current: RefreshState;
22
+ };
23
+ /** The wiring's dispatch: the pull's events go to the state machine. */
24
+ readonly dispatchRef: {
25
+ readonly current: (event: RefreshEvent) => void;
26
+ };
27
+ }
28
+ export interface RefreshTouchController {
29
+ readonly contentRef: RefCallback<HTMLElement>;
30
+ readonly indicatorRef: RefCallback<HTMLElement>;
31
+ /** A pull triggered: the indicator shows in full; the release settles onto the inset. */
32
+ trigger(): void;
33
+ /** A refresh began without a pull: the inset arrives with no visible motion. */
34
+ begin(): void;
35
+ /** The release settles onto the inset or back to the top. */
36
+ settle(to: 'inset' | 'top'): void;
37
+ /** The refresh ended. */
38
+ end(): void;
39
+ }
40
+ /**
41
+ * The refresh's touch layer. The wiring owns the state and the
42
+ * announcements; this owns everything a finger drives.
43
+ */
44
+ export declare function useRefreshTouchController({ isPullEnabled, model, dispatchRef, }: RefreshTouchControllerOptions): RefreshTouchController;
45
+ //# sourceMappingURL=refreshTouchController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refreshTouchController.d.ts","sourceRoot":"","sources":["../../src/RefreshControl/refreshTouchController.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AA0BH,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,OAAO,CAAC;AAcf,OAAO,KAAK,EAAC,YAAY,EAAE,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAe/D,MAAM,WAAW,6BAA6B;IAC5C,gCAAgC;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE;QAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;KAAC,CAAC;IACjD,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE;QAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;KAAC,CAAC;CACzE;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IAChD,yFAAyF;IACzF,OAAO,IAAI,IAAI,CAAC;IAChB,gFAAgF;IAChF,KAAK,IAAI,IAAI,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;IAClC,yBAAyB;IACzB,GAAG,IAAI,IAAI,CAAC;CACb;AA6ED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,aAAa,EACb,KAAK,EACL,WAAW,GACZ,EAAE,6BAA6B,GAAG,sBAAsB,CA8oBxD"}