Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions lib/features/onboarding/welcome_tour_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,23 +146,37 @@ class _WelcomeTourDialogState extends State<WelcomeTourDialog> {
super.dispose();
}

/// Whether the last move went to a later step: the new slide then comes in
/// from the right and the old one leaves to the left, and the other way
/// round for Back or an earlier dot (#1645).
var _forward = true;

void _goToStep(int step) {
if (step >= 0 && step < _totalSteps && step != _currentStep) {
setState(() => _currentStep = step);
setState(() {
_forward = step > _currentStep;
_currentStep = step;
});
}
}

void _next() {
if (_currentStep < _totalSteps - 1) {
setState(() => _currentStep++);
setState(() {
_forward = true;
_currentStep++;
});
} else {
_finish();
}
}

void _previous() {
if (_currentStep > 0) {
setState(() => _currentStep--);
setState(() {
_forward = false;
_currentStep--;
});
}
}

Expand Down Expand Up @@ -287,11 +301,17 @@ onPressed: _finish,
switchInCurve: context.motionCurve(QueryaMotion.enter),
switchOutCurve: context.motionCurve(QueryaMotion.exit),
transitionBuilder: (child, animation) {
// The incoming slide starts on the side we move towards;
// the outgoing one (its animation runs back to 0) ends
// on the other side.
final incoming =
child.key == material.ValueKey(_currentStep);
final side = (_forward ? 1.0 : -1.0) * (incoming ? 1 : -1);
return material.FadeTransition(
opacity: animation,
child: material.SlideTransition(
position: material.Tween<material.Offset>(
begin: const material.Offset(0.04, 0),
begin: material.Offset(0.04 * side, 0),
end: material.Offset.zero,
).animate(animation),
child: child,
Expand Down
33 changes: 24 additions & 9 deletions lib/shared/widgets/app_toast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ enum AppToastVariant {
/// Toasts share one entry in the root overlay and stack upwards. Each one
/// fades and slides in over [QueryaMotion.standard] (instantly under reduced
/// motion), stays for [showDuration] and closes itself, or earlier through its
/// close button.
/// close button. Closing plays the entry backwards and gives its height back
/// gradually, so the toasts above it slide down instead of jumping.
void showAppToast({
required material.BuildContext context,
required String message,
Expand Down Expand Up @@ -129,8 +130,8 @@ class _ToastViewState extends material.State<_ToastView>
void initState() {
super.initState();
final d = widget.data;
// The controller only plays the entry; a timer ends the toast, so a test
// that settles frames does not run its whole lifetime.
// The controller plays the entry and the exit; a timer starts the exit,
// so a test that settles frames does not run the toast's whole lifetime.
_controller = material.AnimationController(
vsync: this,
duration: d.enter == Duration.zero ? const Duration(milliseconds: 1) : d.enter,
Expand All @@ -145,7 +146,15 @@ class _ToastViewState extends material.State<_ToastView>
void _close() {
if (_closed) return;
_closed = true;
widget.onClosed();
_timer.cancel();
if (widget.data.enter == Duration.zero) {
widget.onClosed(); // reduced motion: gone at once
return;
}
// Out the way it came in (#1645); the list drops it when that is done.
_controller.reverse().whenCompleteOrCancel(() {
if (mounted) widget.onClosed();
});
}

@override
Expand All @@ -170,11 +179,17 @@ class _ToastViewState extends material.State<_ToastView>

return material.AnimatedBuilder(
animation: _in,
builder: (context, child) => material.Opacity(
opacity: _in.value,
child: material.Transform.translate(
offset: material.Offset(0, (1 - _in.value) * 12),
child: child,
// The height follows the same animation, so neighbours move smoothly
// when a toast arrives or leaves.
builder: (context, child) => material.SizeTransition(
sizeFactor: _in,
axisAlignment: -1,
child: material.Opacity(
opacity: _in.value,
child: material.Transform.translate(
offset: material.Offset(0, (1 - _in.value) * 12),
child: child,
),
),
),
child: material.Padding(
Expand Down
40 changes: 40 additions & 0 deletions test/features/onboarding/welcome_tour_dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,46 @@ void main() {
expect(find.text('Compound Filter Bar & Quick Calc'), findsOneWidget);
});

testWidgets('Back slides the earlier step in from the left (#1645)',
(tester) async {
await tester.pumpWidget(
queryaThemeTestShell(
child: const material.Scaffold(body: WelcomeTourDialog()),
),
);
await tester.pumpAndSettle();

double dxOf(int step) => tester
.widget<material.SlideTransition>(
find
.ancestor(
of: find.byKey(ValueKey(step)),
matching: find.byType(material.SlideTransition),
)
.first,
)
.position
.value
.dx;

// Forward: the new step comes from the right, the old one leaves left.
await tester.tap(find.byKey(const Key('welcome_tour_next_button')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
expect(dxOf(1), greaterThan(0));
expect(dxOf(0), lessThan(0));
await tester.pumpAndSettle();

// Back: mirrored.
await tester.tap(find.byKey(const Key('welcome_tour_prev_button')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
expect(dxOf(0), lessThan(0));
expect(dxOf(1), greaterThan(0));
await tester.pumpAndSettle();
expect(find.text('Step 1 of 6'), findsOneWidget);
});

testWidgets('WelcomeTourDialog launches demo playground on action button tap',
(tester) async {
var demoLaunched = false;
Expand Down
46 changes: 46 additions & 0 deletions test/shared/app_toast_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,50 @@ void main() {
await tester.pump(const Duration(seconds: 6));
await tester.pumpAndSettle();
});

testWidgets('closing fades the toast out before it is removed (#1645)',
(tester) async {
await tester.pumpWidget(
_app(
onShow: (context) => showAppToast(context: context, message: 'Leaving'),
),
);
await tester.tap(find.text('Show'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));

await tester.tap(find.byIcon(material.Icons.close_rounded));
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));

expect(find.text('Leaving'), findsOneWidget);
final opacity = tester.widget<material.Opacity>(
find
.ancestor(
of: find.text('Leaving'),
matching: find.byType(material.Opacity),
)
.first,
);
expect(opacity.opacity, lessThan(1));

await tester.pumpAndSettle();
expect(find.text('Leaving'), findsNothing);
});

testWidgets('under reduced motion a closed toast goes at once', (tester) async {
await tester.pumpWidget(
_app(
disableAnimations: true,
onShow: (context) => showAppToast(context: context, message: 'Quick'),
),
);
await tester.tap(find.text('Show'));
await tester.pump();

await tester.tap(find.byIcon(material.Icons.close_rounded));
await tester.pump();

expect(find.text('Quick'), findsNothing);
});
}
Loading