fix(ui): on a phone, a snack bar stands above the screen's floating button
Below medium windows the frame's scaffold showed the screens' snack bars, under the floating button of the screen's own scaffold, so a tap on a snack bar's Undo hit the button instead (found 9 Nov 2026, Todo's Undo after deleting the completed tasks). The screens get a messenger of their own there, and their scaffolds lift the snack bars above their buttons, as Material places them. From medium windows on the frame's scaffold still shows them, once: a messenger shows a snack bar on each of its scaffolds not inside another, as two panes side by side are. As the window grows past a phone's, the phone's snack bars go, since a scaffold whose messenger changes keeps the one it showed for good. Whats-New: On a phone, a message's Undo is no longer hidden under the + button. Whats-New-De: Auf dem Handy liegt das Rückgängig einer Meldung nicht mehr unter der +-Schaltfläche. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
6356bed37d
commit
560de2ed93
3 changed files with 134 additions and 5 deletions
|
|
@ -147,8 +147,9 @@ void main() {
|
||||||
expect(fadesInto(), scheme.surfaceContainerLow);
|
expect(fadesInto(), scheme.surfaceContainerLow);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('a snack bar floats 8 above the navigation bar and its fade, '
|
testWidgets("a snack bar floats above the screen's floating button, "
|
||||||
'16 in from the sides; from medium windows on 560 wide, centred', (
|
'clear of the navigation bar and its fade, 16 in from the sides; from '
|
||||||
|
'medium windows on 560 wide, centred', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||||
|
|
@ -171,12 +172,15 @@ void main() {
|
||||||
|
|
||||||
await showApp(tester, width: 400);
|
await showApp(tester, width: 400);
|
||||||
final narrow = await snackBar();
|
final narrow = await snackBar();
|
||||||
|
final button = tester.getRect(find.byType(FloatingActionButton));
|
||||||
expect(narrow.left, 16);
|
expect(narrow.left, 16);
|
||||||
expect(narrow.right, 400 - 16);
|
expect(narrow.right, 400 - 16);
|
||||||
// The fade's top, less 8.
|
// Above the Library's floating button, which would otherwise cover its
|
||||||
|
// action (found 9 Nov 2026), and so above the fade's top, less 8.
|
||||||
|
expect(narrow.bottom, lessThanOrEqualTo(button.top));
|
||||||
expect(
|
expect(
|
||||||
narrow.bottom,
|
narrow.bottom,
|
||||||
900 - 24 - 64 - FlushNavigationBar.fadeHeight - 8,
|
lessThan(900 - 24 - 64 - FlushNavigationBar.fadeHeight - 8),
|
||||||
);
|
);
|
||||||
|
|
||||||
await showApp(tester);
|
await showApp(tester);
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,15 @@ class const NavigationFrame({
|
||||||
class _NavigationFrameState() extends State<NavigationFrame> {
|
class _NavigationFrameState() extends State<NavigationFrame> {
|
||||||
final _tint = ScreenTint();
|
final _tint = ScreenTint();
|
||||||
|
|
||||||
|
/// The screens' own messenger below medium windows: their snack bars
|
||||||
|
/// show on the screens' own scaffolds, which lift them above their
|
||||||
|
/// floating buttons, as Material places them; on the frame's they lay
|
||||||
|
/// under the button, whose taps took the snack bar's Undo (found 9 Nov
|
||||||
|
/// 2026). From medium windows on the frame's scaffold shows them, once:
|
||||||
|
/// a messenger shows its snack bars on each of its scaffolds not inside
|
||||||
|
/// another, as two panes side by side are.
|
||||||
|
final _phoneMessenger = GlobalKey<ScaffoldMessengerState>();
|
||||||
|
|
||||||
/// The latest wave sent by a tap; none before one.
|
/// The latest wave sent by a tap; none before one.
|
||||||
NavigationWave? _wave;
|
NavigationWave? _wave;
|
||||||
|
|
||||||
|
|
@ -109,6 +118,13 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final destinations = widget.destinations;
|
final destinations = widget.destinations;
|
||||||
if (context.windowSize.isAtLeastMedium) {
|
if (context.windowSize.isAtLeastMedium) {
|
||||||
|
// The phone's messenger goes as the window grows, and a scaffold
|
||||||
|
// whose messenger changes would keep the snack bar it showed for
|
||||||
|
// good: they go first. Its scaffolds are below this frame, so they
|
||||||
|
// may be marked to build again while it builds.
|
||||||
|
_phoneMessenger.currentState
|
||||||
|
?..clearSnackBars()
|
||||||
|
..removeCurrentSnackBar();
|
||||||
return ScreenTintScope(
|
return ScreenTintScope(
|
||||||
notifier: _tint,
|
notifier: _tint,
|
||||||
child: TintFrame(
|
child: TintFrame(
|
||||||
|
|
@ -218,7 +234,7 @@ class _NavigationFrameState() extends State<NavigationFrame> {
|
||||||
// it keeps its size and lifts the field instead (Halkyon Learn's ADR
|
// it keeps its size and lifts the field instead (Halkyon Learn's ADR
|
||||||
// 0021, D69).
|
// 0021, D69).
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
body: widget.child,
|
body: ScaffoldMessenger(key: _phoneMessenger, child: widget.child),
|
||||||
// Without the bar, a strip as high as the system's own bar, in the
|
// Without the bar, a strip as high as the system's own bar, in the
|
||||||
// screen's background, keeps every screen inside clear of it.
|
// screen's background, keeps every screen inside clear of it.
|
||||||
bottomNavigationBar: widget.hidesNavigationBar
|
bottomNavigationBar: widget.hidesNavigationBar
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,115 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets("a screen's snack bar stands above its floating button, so "
|
||||||
|
'the button never takes its Undo (found 9 Nov 2026)', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(400, 800);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
var undone = 0;
|
||||||
|
var added = 0;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
testApp(
|
||||||
|
NavigationFrame(
|
||||||
|
appName: 'Halkyon Test',
|
||||||
|
icon: icon,
|
||||||
|
accent: const Color(0xFFEC3E7C),
|
||||||
|
selectedIndex: 0,
|
||||||
|
onSelected: (_) {},
|
||||||
|
destinations: const [
|
||||||
|
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
|
||||||
|
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
|
||||||
|
],
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => showUndoSnackBar(
|
||||||
|
context,
|
||||||
|
message: 'Task completed',
|
||||||
|
onUndo: () => undone++,
|
||||||
|
),
|
||||||
|
child: const Text('Complete'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: FlushNavigationBar.buttonLiftOf(context),
|
||||||
|
),
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () => added++,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Complete'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.text('Undo')).bottom,
|
||||||
|
lessThanOrEqualTo(
|
||||||
|
tester.getRect(find.byType(FloatingActionButton)).top,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Undo'));
|
||||||
|
expect((undone, added), (1, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("a screen's snack bar shown on a phone doesn't stay once the "
|
||||||
|
'window grows past it', (tester) async {
|
||||||
|
final screens = GlobalKey();
|
||||||
|
tester.view.physicalSize = const Size(400, 800);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
testApp(
|
||||||
|
NavigationFrame(
|
||||||
|
appName: 'Halkyon Test',
|
||||||
|
icon: icon,
|
||||||
|
accent: const Color(0xFFEC3E7C),
|
||||||
|
selectedIndex: 0,
|
||||||
|
onSelected: (_) {},
|
||||||
|
destinations: const [
|
||||||
|
(label: 'Home', icon: Icon(Icons.home), selectedIcon: null),
|
||||||
|
(label: 'Maps', icon: Icon(Icons.map), selectedIcon: null),
|
||||||
|
],
|
||||||
|
// The screens keep their state as the frame's layout changes, as an
|
||||||
|
// app's router keeps its navigator by its key.
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: screens,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Archived')),
|
||||||
|
),
|
||||||
|
child: const Text('Archive'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Archive'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Archived'), findsOneWidget);
|
||||||
|
|
||||||
|
tester.view.physicalSize = const Size(1200, 800);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.pump(const Duration(seconds: 5));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Archived'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets("outside the frame, content leaves room for the system's bar "
|
testWidgets("outside the frame, content leaves room for the system's bar "
|
||||||
'alone', (tester) async {
|
'alone', (tester) async {
|
||||||
tester.view.padding = const FakeViewPadding(bottom: 24);
|
tester.view.padding = const FakeViewPadding(bottom: 24);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue