@astryxdesign/lab 0.2.0-canary.8d64648 → 0.2.0-canary.8dfd7cf
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/dist/BottomSheet/BottomSheet.d.ts +41 -35
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +222 -75
- package/dist/BottomSheet/snapOffsets.d.ts +54 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +101 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +55 -12
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +383 -40
- package/dist/Chart/index.d.ts +1 -1
- package/dist/Chart/index.d.ts.map +1 -1
- package/dist/Chart/index.js +1 -1
- package/dist/Stepper/Step.d.ts +1 -1
- package/dist/Stepper/Step.js +1 -1
- package/dist/Stepper/Stepper.d.ts +1 -1
- package/dist/Stepper/Stepper.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lab.css +15 -8
- package/package.json +3 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +13 -13
- package/src/BottomSheet/BottomSheet.test.tsx +81 -4
- package/src/BottomSheet/BottomSheet.tsx +333 -97
- package/src/BottomSheet/snapOffsets.test.ts +146 -0
- package/src/BottomSheet/snapOffsets.ts +126 -0
- package/src/BottomSheet/useSheetGestures.doc.mjs +7 -6
- package/src/BottomSheet/useSheetGestures.test.ts +315 -65
- package/src/BottomSheet/useSheetGestures.ts +515 -55
- package/src/Chart/ChartArea.doc.mjs +1 -1
- package/src/Chart/ChartBar.doc.mjs +1 -1
- package/src/Chart/ChartCandlestick.doc.mjs +1 -1
- package/src/Chart/ChartDot.doc.mjs +1 -1
- package/src/Chart/ChartDotGL.doc.mjs +1 -1
- package/src/Chart/ChartDotGLInteractive.doc.mjs +1 -1
- package/src/Chart/ChartHeatmapGL.doc.mjs +1 -1
- package/src/Chart/ChartLine.doc.mjs +1 -1
- package/src/Chart/ChartStreamGL.doc.mjs +1 -1
- package/src/Chart/index.ts +0 -1
- package/src/Chat/ChatEmojiPicker.doc.mjs +1 -1
- package/src/Chat/ChatReactionBar.doc.mjs +1 -1
- package/src/Chat/ChatTypingIndicator.doc.mjs +1 -1
- package/src/Chat/ChatUnreadDivider.doc.mjs +1 -1
- package/src/ChatReasoning/ChatReasoning.doc.mjs +1 -1
- package/src/CircularProgress/CircularProgress.doc.mjs +3 -3
- package/src/CodeEditor/CodeEditor.doc.mjs +1 -1
- package/src/Drawer/Drawer.doc.mjs +3 -3
- package/src/InfoTip/InfoTip.doc.mjs +3 -3
- package/src/LogStream/LogStream.doc.mjs +1 -1
- package/src/Radial/RadialChart.doc.mjs +1 -1
- package/src/RichTextEditor/RichTextEditor.doc.mjs +1 -1
- package/src/SVGIcon/SVGIcon.doc.mjs +1 -1
- package/src/Sankey/SankeyChart.doc.mjs +1 -1
- package/src/Schedule/Schedule.doc.mjs +1 -1
- package/src/Stat/Stat.doc.mjs +3 -3
- package/src/Stepper/Step.doc.mjs +1 -1
- package/src/Stepper/Step.tsx +1 -1
- package/src/Stepper/Stepper.doc.mjs +3 -3
- package/src/Stepper/Stepper.tsx +1 -1
- package/src/ThreeD/3DBar.doc.mjs +1 -1
- package/src/ThreeD/3DChart.doc.mjs +1 -1
- package/src/ThreeD/3DScatter.doc.mjs +1 -1
- package/src/ThreeD/3DScatterGL.doc.mjs +1 -1
- package/src/ThreeD/3DSurface.doc.mjs +1 -1
- package/src/index.ts +1 -1
|
@@ -36,7 +36,6 @@ function pointerEvent(
|
|
|
36
36
|
|
|
37
37
|
function makeTarget(): HTMLElement {
|
|
38
38
|
const el = document.createElement('div');
|
|
39
|
-
el.setAttribute('data-astryx-sheet', '');
|
|
40
39
|
el.setPointerCapture = vi.fn();
|
|
41
40
|
el.releasePointerCapture = vi.fn();
|
|
42
41
|
el.getBoundingClientRect = () => ({height: SHEET_HEIGHT}) as DOMRect;
|
|
@@ -73,70 +72,174 @@ function setup(options: Partial<UseSheetGesturesOptions> = {}) {
|
|
|
73
72
|
return {hook, onDismiss};
|
|
74
73
|
}
|
|
75
74
|
|
|
75
|
+
type Hook = {
|
|
76
|
+
result: {current: ReturnType<typeof useSheetGestures>};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function down(hook: Hook, y: number, t: number, target: HTMLElement) {
|
|
80
|
+
// Register the sheet element the way the component does on mount, so the
|
|
81
|
+
// hook can measure its height (it no longer queries the DOM for it).
|
|
82
|
+
act(() => hook.result.current.sheetRef(target));
|
|
83
|
+
act(() =>
|
|
84
|
+
hook.result.current.handleProps.onPointerDown(pointerEvent(y, t, target)),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function move(hook: Hook, y: number, t: number, target: HTMLElement) {
|
|
88
|
+
act(() =>
|
|
89
|
+
hook.result.current.handleProps.onPointerMove(pointerEvent(y, t, target)),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
function up(hook: Hook, y: number, t: number, target: HTMLElement) {
|
|
93
|
+
act(() =>
|
|
94
|
+
hook.result.current.handleProps.onPointerUp(pointerEvent(y, t, target)),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
describe('useSheetGestures', () => {
|
|
77
|
-
it('dismisses
|
|
78
|
-
const {hook, onDismiss} = setup();
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Drag down slowly, well past 25% of 400px, low velocity.
|
|
85
|
-
act(() => {
|
|
86
|
-
handleProps.onPointerMove(pointerEvent(60, 200, target));
|
|
87
|
-
handleProps.onPointerMove(pointerEvent(140, 600, target));
|
|
88
|
-
});
|
|
89
|
-
act(() => {
|
|
90
|
-
handleProps.onPointerUp(pointerEvent(140, 620, target));
|
|
91
|
-
});
|
|
99
|
+
it('dismisses on a fast downward flick (swipe-to-close)', () => {
|
|
100
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200]});
|
|
101
|
+
const t = makeTarget();
|
|
102
|
+
down(hook, 0, 0, t);
|
|
103
|
+
// 60px in 20ms = 3px/ms, and 60px > the flick distance floor.
|
|
104
|
+
move(hook, 60, 20, t);
|
|
105
|
+
up(hook, 60, 22, t);
|
|
92
106
|
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
93
107
|
});
|
|
94
108
|
|
|
95
|
-
it('
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
109
|
+
it('expands to the tallest detent on a fast upward flick', () => {
|
|
110
|
+
const onSnap = vi.fn();
|
|
111
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200], onSnap});
|
|
112
|
+
const t = makeTarget();
|
|
113
|
+
// Start rested at a lower detent by first snapping there via a slow drag.
|
|
114
|
+
down(hook, 0, 0, t);
|
|
115
|
+
move(hook, 200, 400, t); // slow drag down toward the 200px detent
|
|
116
|
+
up(hook, 200, 800, t);
|
|
117
|
+
onSnap.mockClear();
|
|
118
|
+
// Now flick up fast: 60px in 20ms = 3px/ms, over the distance floor.
|
|
119
|
+
down(hook, 200, 1000, t);
|
|
120
|
+
move(hook, 140, 1020, t);
|
|
121
|
+
up(hook, 140, 1022, t);
|
|
122
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
123
|
+
expect(onSnap).toHaveBeenLastCalledWith(SHEET_HEIGHT); // full height
|
|
124
|
+
expect(hook.result.current.settledOffset).toBe(0);
|
|
110
125
|
});
|
|
111
126
|
|
|
112
|
-
it('
|
|
113
|
-
const {hook, onDismiss} = setup();
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
handleProps.onPointerMove(pointerEvent(20, 300, target));
|
|
121
|
-
});
|
|
122
|
-
act(() => {
|
|
123
|
-
handleProps.onPointerUp(pointerEvent(20, 620, target));
|
|
124
|
-
});
|
|
127
|
+
it('does not flick-dismiss on a fast but short nudge', () => {
|
|
128
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200]});
|
|
129
|
+
const t = makeTarget();
|
|
130
|
+
// Fast (20px/10ms = 2px/ms) but only 20px of travel — under the distance
|
|
131
|
+
// floor, so it settles rather than dismissing.
|
|
132
|
+
down(hook, 0, 0, t);
|
|
133
|
+
move(hook, 20, 10, t);
|
|
134
|
+
up(hook, 20, 12, t);
|
|
125
135
|
expect(onDismiss).not.toHaveBeenCalled();
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('settles to the nearest detent on a slow drag', () => {
|
|
139
|
+
const onSnap = vi.fn();
|
|
140
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200], onSnap});
|
|
141
|
+
const t = makeTarget();
|
|
142
|
+
// Detents: full=400 (offset 0) and 200 (offset 200). Drag slowly to ~180
|
|
143
|
+
// offset -> nearest is the 200px detent (offset 200).
|
|
144
|
+
down(hook, 0, 0, t);
|
|
145
|
+
move(hook, 90, 300, t);
|
|
146
|
+
move(hook, 180, 700, t);
|
|
147
|
+
up(hook, 180, 1100, t);
|
|
148
|
+
expect(onDismiss).not.toHaveBeenCalled();
|
|
149
|
+
expect(onSnap).toHaveBeenLastCalledWith(200);
|
|
150
|
+
expect(hook.result.current.settledOffset).toBe(200);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('a downward drag from a middle detent never snaps back up past it', () => {
|
|
154
|
+
// Three detents on a 600px sheet: full (offset 0), mid 360 (offset 240),
|
|
155
|
+
// short 160 (offset 440). Rest at the mid detent, then drag DOWN a modest
|
|
156
|
+
// amount that's still closer to mid than to short. It must not snap back
|
|
157
|
+
// UP to full — only to mid (stay) or further down.
|
|
158
|
+
const onSnap = vi.fn();
|
|
159
|
+
const {hook} = setup({snapHeights: () => [160, 360], onSnap});
|
|
160
|
+
const t = makeTarget();
|
|
161
|
+
t.getBoundingClientRect = () => ({height: 600}) as DOMRect;
|
|
162
|
+
// Drag to the mid detent (offset 240).
|
|
163
|
+
down(hook, 0, 0, t);
|
|
164
|
+
move(hook, 120, 300, t);
|
|
165
|
+
move(hook, 240, 700, t);
|
|
166
|
+
up(hook, 240, 1100, t);
|
|
167
|
+
expect(hook.result.current.settledOffset).toBe(240);
|
|
168
|
+
onSnap.mockClear();
|
|
169
|
+
// Small slow drag DOWN from mid: offset 240 -> ~300.
|
|
170
|
+
down(hook, 300, 2000, t);
|
|
171
|
+
move(hook, 340, 2200, t);
|
|
172
|
+
move(hook, 360, 2600, t);
|
|
173
|
+
up(hook, 360, 3000, t);
|
|
174
|
+
// Must not have jumped back up to full (offset 0).
|
|
175
|
+
expect(hook.result.current.settledOffset).not.toBe(0);
|
|
176
|
+
expect(hook.result.current.settledOffset).toBeGreaterThanOrEqual(240);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('dismisses when dragged well below the shortest detent', () => {
|
|
180
|
+
const {hook, onDismiss} = setup({snapHeights: () => [200]});
|
|
181
|
+
const t = makeTarget();
|
|
182
|
+
// Shortest detent offset = 200 (height 200). Drag slowly past it by
|
|
183
|
+
// >40% of 200 -> offset ~320 dismisses.
|
|
184
|
+
down(hook, 0, 0, t);
|
|
185
|
+
move(hook, 160, 400, t);
|
|
186
|
+
move(hook, 330, 900, t);
|
|
187
|
+
up(hook, 330, 1400, t);
|
|
188
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('fades the scrim from full to hidden as it collapses onto the peek detent', () => {
|
|
192
|
+
const onScrimOpacity = vi.fn();
|
|
193
|
+
// Sheet 400, detents at 200 and 300 -> offsets [0, 100, 200]. The fade
|
|
194
|
+
// spans the mid detent (offset 100) to the peek detent (offset 200):
|
|
195
|
+
// full at/above 100, hidden at/below 200.
|
|
196
|
+
const {hook} = setup({snapHeights: () => [200, 300], onScrimOpacity});
|
|
197
|
+
const t = makeTarget();
|
|
198
|
+
down(hook, 0, 0, t);
|
|
199
|
+
move(hook, 60, 200, t); // offset 60, above the mid detent -> full
|
|
200
|
+
move(hook, 150, 600, t); // offset 150, halfway through the fade -> ~0.5
|
|
201
|
+
move(hook, 220, 1000, t); // offset 220, past the peek -> hidden
|
|
202
|
+
const values = onScrimOpacity.mock.calls.map(c => c[0]);
|
|
203
|
+
expect(values[0]).toBe(1);
|
|
204
|
+
expect(values[1]).toBeGreaterThan(0.4);
|
|
205
|
+
expect(values[1]).toBeLessThan(0.6);
|
|
206
|
+
expect(values[values.length - 1]).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('keeps the scrim hidden when settled at the peek detent', () => {
|
|
210
|
+
const onScrimOpacity = vi.fn();
|
|
211
|
+
const {hook} = setup({snapHeights: () => [200, 300], onScrimOpacity});
|
|
212
|
+
const t = makeTarget();
|
|
213
|
+
// Slow drag down to the peek detent (offset 200).
|
|
214
|
+
down(hook, 0, 0, t);
|
|
215
|
+
move(hook, 100, 400, t);
|
|
216
|
+
move(hook, 200, 900, t);
|
|
217
|
+
up(hook, 200, 1400, t);
|
|
218
|
+
expect(hook.result.current.settledOffset).toBe(200);
|
|
219
|
+
// Last reported opacity (on settle) is hidden.
|
|
220
|
+
const values = onScrimOpacity.mock.calls.map(c => c[0]);
|
|
221
|
+
expect(values[values.length - 1]).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('magnetically settles the live drag onto a nearby detent', () => {
|
|
225
|
+
// Detents for a 400px sheet with snaps 300 and 200 -> visible-height snaps
|
|
226
|
+
// become offsets [0, 100, 200]. Approaching the middle detent (offset 100)
|
|
227
|
+
// from just below (raw ~90) — inside MAGNET_RANGE and BELOW the shortest
|
|
228
|
+
// detent so the between-detents magnet applies — should ease toward 100.
|
|
229
|
+
const {hook} = setup({snapHeights: () => [200, 300]});
|
|
230
|
+
const t = makeTarget();
|
|
231
|
+
down(hook, 0, 0, t);
|
|
232
|
+
move(hook, 90, 100, t); // raw offset 90, 10px from the 100 detent
|
|
233
|
+
const off = hook.result.current.dragOffset;
|
|
234
|
+
expect(off).toBeGreaterThan(90); // pulled up toward 100
|
|
235
|
+
expect(off).toBeLessThanOrEqual(100);
|
|
128
236
|
});
|
|
129
237
|
|
|
130
238
|
it('translates the surface live during a drag', () => {
|
|
131
|
-
const {hook} = setup();
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
handleProps.onPointerDown(pointerEvent(0, 0, target));
|
|
136
|
-
});
|
|
137
|
-
act(() => {
|
|
138
|
-
handleProps.onPointerMove(pointerEvent(50, 100, target));
|
|
139
|
-
});
|
|
239
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
240
|
+
const t = makeTarget();
|
|
241
|
+
down(hook, 0, 0, t);
|
|
242
|
+
move(hook, 50, 100, t);
|
|
140
243
|
expect(hook.result.current.dragOffset).toBe(50);
|
|
141
244
|
expect(hook.result.current.isDragging).toBe(true);
|
|
142
245
|
expect(hook.result.current.contentProps.style.transform).toBe(
|
|
@@ -144,17 +247,164 @@ describe('useSheetGestures', () => {
|
|
|
144
247
|
);
|
|
145
248
|
});
|
|
146
249
|
|
|
147
|
-
it('
|
|
148
|
-
const {hook} = setup();
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
250
|
+
it('rubber-bands an upward drag past the fully-open position', () => {
|
|
251
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
252
|
+
const t = makeTarget();
|
|
253
|
+
down(hook, 100, 0, t);
|
|
254
|
+
move(hook, 20, 100, t); // drag up 80px past the top of a rested-at-top sheet
|
|
255
|
+
// Overscroll is allowed but damped: a resisted fraction of the raw -80,
|
|
256
|
+
// and negative (above the top), not the raw distance and not clamped to 0.
|
|
257
|
+
const off = hook.result.current.dragOffset;
|
|
258
|
+
expect(off).toBeLessThan(0);
|
|
259
|
+
expect(off).toBeGreaterThan(-80); // resisted, so smaller in magnitude
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('dismisses a single-height sheet on a slow drag past the floor', () => {
|
|
263
|
+
const {hook, onDismiss} = setup(); // no snapHeights
|
|
264
|
+
const t = makeTarget();
|
|
265
|
+
// Only detent is the full 400px height (offset 0). A slow drag past 40%
|
|
266
|
+
// of it (>160px) with no detent to catch it dismisses.
|
|
267
|
+
down(hook, 0, 0, t);
|
|
268
|
+
move(hook, 120, 400, t);
|
|
269
|
+
move(hook, 240, 900, t);
|
|
270
|
+
up(hook, 240, 1400, t);
|
|
271
|
+
expect(onDismiss).toHaveBeenCalledTimes(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('body overscroll', () => {
|
|
275
|
+
// A scrollable body element nested inside the sheet, with a settable
|
|
276
|
+
// scrollTop so we can simulate "at the top" vs "scrolled".
|
|
277
|
+
function makeBody(scrollTop: number) {
|
|
278
|
+
const body = document.createElement('div');
|
|
279
|
+
Object.defineProperty(body, 'scrollTop', {
|
|
280
|
+
value: scrollTop,
|
|
281
|
+
writable: true,
|
|
282
|
+
});
|
|
283
|
+
body.setPointerCapture = vi.fn();
|
|
284
|
+
body.releasePointerCapture = vi.fn();
|
|
285
|
+
body.getBoundingClientRect = () => ({height: SHEET_HEIGHT}) as DOMRect;
|
|
286
|
+
return body;
|
|
287
|
+
}
|
|
288
|
+
function bodyDown(hook: Hook, y: number, t: number, el: HTMLElement) {
|
|
289
|
+
// Register a sheet element so the hook can measure height (it reads the
|
|
290
|
+
// tracked node, not the DOM). The body stands in with the right height.
|
|
291
|
+
act(() => hook.result.current.sheetRef(el));
|
|
292
|
+
act(() =>
|
|
293
|
+
hook.result.current.bodyProps.onPointerDown(pointerEvent(y, t, el)),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function bodyMove(hook: Hook, y: number, t: number, el: HTMLElement) {
|
|
297
|
+
act(() =>
|
|
298
|
+
hook.result.current.bodyProps.onPointerMove(pointerEvent(y, t, el)),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
it('promotes a top-overscroll pull-down into a sheet drag', () => {
|
|
303
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
304
|
+
const body = makeBody(0); // at the top
|
|
305
|
+
bodyDown(hook, 0, 0, body);
|
|
306
|
+
bodyMove(hook, 40, 100, body); // pull down while at the top
|
|
307
|
+
expect(hook.result.current.isDragging).toBe(true);
|
|
308
|
+
expect(hook.result.current.dragOffset).toBe(40);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('does not hijack scrolling when the body is scrolled down', () => {
|
|
312
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
313
|
+
const body = makeBody(120); // scrolled, not at the top
|
|
314
|
+
bodyDown(hook, 0, 0, body);
|
|
315
|
+
bodyMove(hook, 40, 100, body);
|
|
316
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
153
317
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
318
|
+
|
|
319
|
+
it('does not start a drag on an upward move from the top', () => {
|
|
320
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
321
|
+
const body = makeBody(0);
|
|
322
|
+
bodyDown(hook, 100, 0, body);
|
|
323
|
+
bodyMove(hook, 60, 100, body); // moving up = scrolling intent
|
|
324
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('body touch handoff', () => {
|
|
329
|
+
// Attach a real DOM node via the body ref, then dispatch touch events. The
|
|
330
|
+
// hook's non-passive touchmove listener is how the handoff works on touch
|
|
331
|
+
// devices (pointer events get cancelled once native panning starts).
|
|
332
|
+
function makeScroller(opts: {
|
|
333
|
+
scrollTop: number;
|
|
334
|
+
clientHeight: number;
|
|
335
|
+
scrollHeight: number;
|
|
336
|
+
}) {
|
|
337
|
+
const sheet = makeTarget();
|
|
338
|
+
const el = document.createElement('div');
|
|
339
|
+
Object.defineProperty(el, 'scrollTop', {
|
|
340
|
+
value: opts.scrollTop,
|
|
341
|
+
writable: true,
|
|
342
|
+
});
|
|
343
|
+
Object.defineProperty(el, 'clientHeight', {value: opts.clientHeight});
|
|
344
|
+
Object.defineProperty(el, 'scrollHeight', {value: opts.scrollHeight});
|
|
345
|
+
el.getBoundingClientRect = () => ({height: SHEET_HEIGHT}) as DOMRect;
|
|
346
|
+
sheet.appendChild(el);
|
|
347
|
+
document.body.appendChild(sheet);
|
|
348
|
+
return el;
|
|
349
|
+
}
|
|
350
|
+
function touch(el: HTMLElement, type: string, y: number, id = 1) {
|
|
351
|
+
const ev = new Event(type, {bubbles: true, cancelable: true});
|
|
352
|
+
// jsdom lacks TouchEvent; attach the fields the handler reads.
|
|
353
|
+
Object.defineProperty(ev, 'changedTouches', {
|
|
354
|
+
value: [{identifier: id, clientY: y}],
|
|
355
|
+
});
|
|
356
|
+
Object.defineProperty(ev, 'currentTarget', {value: el});
|
|
357
|
+
el.dispatchEvent(ev);
|
|
358
|
+
return ev;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
it('promotes a top pull-down (touch) into a sheet drag', () => {
|
|
362
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
363
|
+
const el = makeScroller({
|
|
364
|
+
scrollTop: 0,
|
|
365
|
+
clientHeight: 200,
|
|
366
|
+
scrollHeight: 800,
|
|
367
|
+
});
|
|
368
|
+
act(() => hook.result.current.sheetRef(el));
|
|
369
|
+
act(() => hook.result.current.bodyProps.ref(el));
|
|
370
|
+
act(() => {
|
|
371
|
+
touch(el, 'touchstart', 0);
|
|
372
|
+
touch(el, 'touchmove', 50);
|
|
373
|
+
});
|
|
374
|
+
expect(hook.result.current.isDragging).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('promotes a bottom pull-up (touch) into a sheet drag', () => {
|
|
378
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
379
|
+
// scrolled to the bottom: scrollTop + clientHeight === scrollHeight
|
|
380
|
+
const el = makeScroller({
|
|
381
|
+
scrollTop: 600,
|
|
382
|
+
clientHeight: 200,
|
|
383
|
+
scrollHeight: 800,
|
|
384
|
+
});
|
|
385
|
+
act(() => hook.result.current.sheetRef(el));
|
|
386
|
+
act(() => hook.result.current.bodyProps.ref(el));
|
|
387
|
+
act(() => {
|
|
388
|
+
touch(el, 'touchstart', 300);
|
|
389
|
+
touch(el, 'touchmove', 250); // pull up
|
|
390
|
+
});
|
|
391
|
+
expect(hook.result.current.isDragging).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('leaves native scrolling alone in the middle of the content', () => {
|
|
395
|
+
const {hook} = setup({snapHeights: () => [200]});
|
|
396
|
+
const el = makeScroller({
|
|
397
|
+
scrollTop: 300,
|
|
398
|
+
clientHeight: 200,
|
|
399
|
+
scrollHeight: 800,
|
|
400
|
+
});
|
|
401
|
+
act(() => hook.result.current.sheetRef(el));
|
|
402
|
+
act(() => hook.result.current.bodyProps.ref(el));
|
|
403
|
+
act(() => {
|
|
404
|
+
touch(el, 'touchstart', 300);
|
|
405
|
+
touch(el, 'touchmove', 250);
|
|
406
|
+
});
|
|
407
|
+
expect(hook.result.current.isDragging).toBe(false);
|
|
157
408
|
});
|
|
158
|
-
expect(hook.result.current.dragOffset).toBe(0);
|
|
159
409
|
});
|
|
160
410
|
});
|