Polish floating bottom bar: elevation, height, overlap, and FAB shape

- Give the floating bar and search satellite the app's one shadow
  (nooDialogShadow) plus a smaller, tighter height on both platforms.
- Let every tab's scrollable list compute its own bottom clearance
  (bottomBarClearance) instead of a flat 100px, so the floating bar no
  longer overlaps the last row/card.
- Square the Android upload FAB off to a rounded-square radius when the
  bottom bar is attached, keeping it fully round only when floating.
- Let a swipe on a file row trigger its action directly once dragged
  past 1.8x the reveal width, instead of requiring a second tap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 16:30:14 -04:00
co-authored by Claude Sonnet 5
parent 5f4caa2a3b
commit 5e5acc3fc6
18 changed files with 361 additions and 55 deletions
+49 -2
View File
@@ -12,6 +12,7 @@ import 'package:noo/widgets/noo/core/noo_progress_bar.dart';
import 'package:noo/widgets/noo/core/noo_search_field.dart';
import 'package:noo/widgets/noo/core/noo_segmented_control.dart';
import 'package:noo/widgets/noo/core/noo_toggle.dart';
import 'package:noo/widgets/noo/nav/noo_nav_style.dart';
import 'noo_test_utils.dart';
@@ -247,7 +248,11 @@ void main() {
group('NooFab', () {
testNooWidgets('shows label and fires onTap', (tester, theme, c) async {
var taps = 0;
await pumpNoo(tester, NooFab(onTap: () => taps++), theme: theme);
await pumpNoo(
tester,
NooFab(barStyle: NooBottomBarStyle.floating, onTap: () => taps++),
theme: theme,
);
expect(find.text('Upload'), findsOneWidget);
expect(tester.getSize(find.byType(NooFab)).height, 56);
expect(tester.getSize(find.byType(NooFab)).width, lessThan(200));
@@ -263,7 +268,11 @@ void main() {
var taps = 0;
await pumpNoo(
tester,
NooFab(collapsed: true, onTap: () => taps++),
NooFab(
collapsed: true,
barStyle: NooBottomBarStyle.floating,
onTap: () => taps++,
),
theme: theme,
);
await tester.pumpAndSettle();
@@ -274,6 +283,44 @@ void main() {
await tester.tap(find.byType(NooFab));
expect(taps, 1);
});
testNooWidgets('floating bar style stays fully round', (
tester,
theme,
c,
) async {
await pumpNoo(
tester,
const NooFab(barStyle: NooBottomBarStyle.floating),
theme: theme,
);
final material = tester.widget<Material>(
find.descendant(
of: find.byType(NooFab),
matching: find.byType(Material),
),
);
expect((material.borderRadius as BorderRadius).topLeft.x, NooRadii.pill);
});
testNooWidgets('attached bar style squares off to a rounded square', (
tester,
theme,
c,
) async {
await pumpNoo(
tester,
const NooFab(barStyle: NooBottomBarStyle.attached),
theme: theme,
);
final material = tester.widget<Material>(
find.descendant(
of: find.byType(NooFab),
matching: find.byType(Material),
),
);
expect((material.borderRadius as BorderRadius).topLeft.x, NooRadii.card);
});
});
group('NooProgressBar', () {
+106
View File
@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:noo/widgets/noo/files/noo_swipe_action.dart';
import 'noo_test_utils.dart';
void main() {
setUpNooTests();
group('NooSwipeAction', () {
testNooWidgets('a moderate swipe opens the action without triggering it', (
tester,
theme,
c,
) async {
var triggered = 0;
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 64,
child: NooSwipeAction(
endAction: NooSwipeActionSpec(
kind: NooSwipeActionKind.delete,
onTriggered: () => triggered++,
),
child: const ColoredBox(
color: Colors.white,
child: SizedBox(width: 360, height: 64),
),
),
),
theme: theme,
);
// Half the block's own width (96) is well past the "snap open"
// threshold but nowhere near the 1.8x trigger extent.
await tester.drag(find.byType(NooSwipeAction), const Offset(-60, 0));
await tester.pumpAndSettle();
expect(triggered, 0);
expect(find.text('Delete'), findsOneWidget);
});
testNooWidgets(
'swiping past the trigger extent fires the action on release - no '
'second tap needed',
(tester, theme, c) async {
var triggered = 0;
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 64,
child: NooSwipeAction(
endAction: NooSwipeActionSpec(
kind: NooSwipeActionKind.delete,
onTriggered: () => triggered++,
),
child: const ColoredBox(
color: Colors.white,
child: SizedBox(width: 360, height: 64),
),
),
),
theme: theme,
);
// 300px of drag on a 96px-wide block clears the 1.8x (~173px)
// trigger extent well past the clamp ceiling.
await tester.drag(find.byType(NooSwipeAction), const Offset(-300, 0));
await tester.pump();
expect(triggered, 1);
},
);
testNooWidgets('tapping the revealed block still triggers the action too', (
tester,
theme,
c,
) async {
var triggered = 0;
await pumpNoo(
tester,
SizedBox(
width: 360,
height: 64,
child: NooSwipeAction(
startAction: NooSwipeActionSpec(
kind: NooSwipeActionKind.favorite,
onTriggered: () => triggered++,
),
child: const ColoredBox(
color: Colors.white,
child: SizedBox(width: 360, height: 64),
),
),
),
theme: theme,
);
await tester.drag(find.byType(NooSwipeAction), const Offset(60, 0));
await tester.pumpAndSettle();
expect(triggered, 0);
await tester.tap(find.text('Favorite'));
await tester.pump();
expect(triggered, 1);
});
});
}