mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix channel list scroll interruption (#5815)
## Summary - keep transparent channel-list gaps in Flutter's gesture arena - allow a new drag to interrupt active ballistic scrolling immediately - add a behavioral fling-and-counter-drag regression test ## Scope audit - audited mobile list and scroll constructors across `mobile/lib` - channels is the only app scrollable overriding `hitTestBehavior` - all other lists retain Flutter's default opaque hit testing and do not share this defect ## Verification - regression test fails before the production change: ballistic offset continues from `271.17` to `345.56` - focused interruption regression passes with the fix - profile/community control test passes - pre-commit: Dart formatting and Flutter analyzer pass - pre-push: complete mobile suite passes, 1323 tests - simulator: immediate counter-drag from the transparent gutter interrupts deceleration Simulator evidence: `/Users/wesb/.buzz/.scratch/mobile-scroll-videos/interruption-verified.mp4` Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -45,9 +45,10 @@ class _ChannelsBody extends StatelessWidget {
|
||||
onRefresh: onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
// The transparent gap shows the top section and must not absorb
|
||||
// taps meant for the community or profile controls beneath it.
|
||||
hitTestBehavior: HitTestBehavior.deferToChild,
|
||||
// Transparent list gaps must remain hit-testable so a new drag
|
||||
// can interrupt ballistic scrolling. The app bar is painted
|
||||
// later and retains its community and profile controls.
|
||||
hitTestBehavior: HitTestBehavior.translucent,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: SizedBox(height: barHeight)),
|
||||
if (usesPinnedGradient)
|
||||
|
||||
@@ -232,6 +232,55 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('interrupts a ballistic scroll from a transparent list gap', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(320, 480);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final channels = List.generate(
|
||||
40,
|
||||
(index) => Channel(
|
||||
id: 'channel-$index',
|
||||
name: 'channel-$index',
|
||||
channelType: 'stream',
|
||||
visibility: 'open',
|
||||
description: 'Channel $index',
|
||||
createdBy: 'abc',
|
||||
createdAt: DateTime(2025),
|
||||
memberCount: 10,
|
||||
isMember: true,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
channelsProvider.overrideWith(() => _FakeNotifier(channels)),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final scrollView = find.byType(CustomScrollView);
|
||||
final scrollable = tester.state<ScrollableState>(
|
||||
find.descendant(of: scrollView, matching: find.byType(Scrollable)).first,
|
||||
);
|
||||
await tester.fling(scrollView, const Offset(0, -300), 2400);
|
||||
await tester.pump(const Duration(milliseconds: 32));
|
||||
final ballisticOffset = scrollable.position.pixels;
|
||||
await tester.pump(const Duration(milliseconds: 32));
|
||||
expect(scrollable.position.pixels, greaterThan(ballisticOffset));
|
||||
|
||||
// x=1 is inside the scroll viewport but outside the padded section rows.
|
||||
// A drag beginning here must still enter the scrollable's gesture arena.
|
||||
final interruptingDrag = await tester.startGesture(const Offset(1, 300));
|
||||
await interruptingDrag.moveBy(const Offset(0, 80));
|
||||
await tester.pump();
|
||||
|
||||
expect(scrollable.position.pixels, lessThan(ballisticOffset));
|
||||
await interruptingDrag.up();
|
||||
});
|
||||
|
||||
testWidgets('keeps the last channel above the floating tab bar', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user