MultitabPane: drag-to-reorder gesture wiring (T-84)
Each tab is wrapped in a Draggable (when allowReorder is true and the entry itself is reorderable) and a DragTarget (always — the controller's barrier logic decides whether the move actually happens). Drops insert the dragged entry at the target tab's index. A 2px leading insertion indicator highlights the active drop target. The widget harness now wraps children in an Overlay so Draggable's feedback can mount without each test re-wrapping. Sized by the test view's bounds to avoid disturbing existing tests that query find.byType(SizedBox).first. Four widget tests cover the gesture path: drop reorders, pinned barrier blocks, pinned tabs aren't draggable, and allowReorder=false disables drag entirely. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,13 @@ import 'package:flutter/widgets.dart';
|
||||
import 'kernel_fixture.dart';
|
||||
|
||||
/// Wraps a widget in the minimum tree a primitive needs to resolve
|
||||
/// theme + i18n: `Directionality → ClideKernel → ClideTheme → child`.
|
||||
/// theme + i18n + Overlay (for Draggable feedback / Tooltip / etc.):
|
||||
/// `Directionality → ClideKernel → ClideTheme → MediaQuery →
|
||||
/// Overlay → child`.
|
||||
///
|
||||
/// The Overlay is sized by the test view's bounds via the surrounding
|
||||
/// MediaQuery; no extra SizedBox is added so existing tests that
|
||||
/// query `find.byType(SizedBox).first` still find their target.
|
||||
Widget harness(KernelFixture fixture, Widget child) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
@@ -14,7 +20,11 @@ Widget harness(KernelFixture fixture, Widget child) {
|
||||
controller: fixture.services.theme,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: child,
|
||||
child: Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (_) => child),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -151,5 +151,96 @@ void main() {
|
||||
);
|
||||
expect(find.byKey(const ValueKey('body-a')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('drag a tab onto another to reorder', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b'), entry('c')]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
|
||||
// Drag tab 'a' to where tab 'c' sits.
|
||||
final from = tester.getCenter(find.text('a'));
|
||||
final to = tester.getCenter(find.text('c'));
|
||||
final gesture = await tester.startGesture(from);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.moveTo(to);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(c.entries.map((e) => e.id), ['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
testWidgets('drag respects pinned barrier', (tester) async {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
entry('b'),
|
||||
]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
|
||||
// Try to drag 'a' before pinned 'p' — controller's barrier
|
||||
// logic should reject and the order stays.
|
||||
final from = tester.getCenter(find.text('a'));
|
||||
final to = tester.getCenter(find.text('p'));
|
||||
final gesture = await tester.startGesture(from);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.moveTo(to);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(c.entries.map((e) => e.id), ['p', 'a', 'b']);
|
||||
});
|
||||
|
||||
testWidgets('pinned tabs are not draggable', (tester) async {
|
||||
final c = MultitabController<String>(initial: [
|
||||
entry('p', reorderable: false),
|
||||
entry('a'),
|
||||
]);
|
||||
await tester.pumpWidget(
|
||||
harness(f, MultitabPane<String>(controller: c, bodyBuilder: body)),
|
||||
);
|
||||
|
||||
// Attempt to drag pinned 'p' to position of 'a'.
|
||||
final from = tester.getCenter(find.text('p'));
|
||||
final to = tester.getCenter(find.text('a'));
|
||||
final gesture = await tester.startGesture(from);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.moveTo(to);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Order unchanged; pinned tab refused to be dragged.
|
||||
expect(c.entries.map((e) => e.id), ['p', 'a']);
|
||||
});
|
||||
|
||||
testWidgets('allowReorder=false disables drag entirely', (tester) async {
|
||||
final c = MultitabController<String>(initial: [entry('a'), entry('b')]);
|
||||
await tester.pumpWidget(
|
||||
harness(
|
||||
f,
|
||||
MultitabPane<String>(
|
||||
controller: c,
|
||||
bodyBuilder: body,
|
||||
allowReorder: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final from = tester.getCenter(find.text('a'));
|
||||
final to = tester.getCenter(find.text('b'));
|
||||
final gesture = await tester.startGesture(from);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.moveTo(to);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(c.entries.map((e) => e.id), ['a', 'b']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user