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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.0.1

* fix focus traversal

## 1.0.0

Initial release, please refer to the readme and the example for available functionality.
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.1"
version: "1.0.1"
leak_tracker:
dependency: transitive
description:
Expand Down
7 changes: 6 additions & 1 deletion lib/inline_tab_view.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:inline_tab_view/src/inline_tab_view_widget.dart';
import 'package:inline_tab_view/src/offscreen_focus_exclusion_builder.dart';

/// A height adjusting widget switcher that displays the widget which
/// corresponds to the currently selected tab.
Expand Down Expand Up @@ -60,9 +61,13 @@ class InlineTabView extends StatelessWidget {

return ClipRect(
clipBehavior: clipBehavior,
child: InlineTabViewWidget(
child: OffscreenFocusExclusionBuilder(
controller: controller!,
children: children,
builder: (List<Widget> children) => InlineTabViewWidget(
controller: controller,
children: children,
),
),
);
}
Expand Down
1 change: 0 additions & 1 deletion lib/src/inline_tab_view_render_object.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ class InlineTabViewRenderObject extends RenderBox
// TODO: consider pointer cancel event
_attemptSnap();
_dragStartPos = null;
markNeedsLayout();
} else if (event is PointerMoveEvent) {
final delta = event.position.dx - _dragStartPos!;
double offset = delta / size.width;
Expand Down
41 changes: 41 additions & 0 deletions lib/src/offscreen_focus_exclusion_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';

/// Only allow focus of the visible widget and block focus during animation by
/// wrapping children in [ExcludeFocus].
class OffscreenFocusExclusionBuilder extends StatelessWidget {
/// Only allow focus of the visible widget and block focus during animation.
const OffscreenFocusExclusionBuilder({
super.key,
required this.controller,
required this.children,
required this.builder,
});

/// This widget's selection and animation state.
final TabController controller;

/// One widget per tab.
///
/// Its length must match the length of the [TabBar.tabs]
/// list, as well as the [controller]'s [TabController.length].
final List<Widget> children;

/// Child builder, takes the wrapped children.
final Widget Function(List<Widget> children) builder;

@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: controller,
builder: (BuildContext context, Widget? _child) {
if (controller.indexIsChanging)
return ExcludeFocus(child: builder(children));
return builder([
for (int i = 0; i < children.length; i++)
ExcludeFocus(
excluding: i != controller.index,
child: children[i],
)
]);
},
);
}
7 changes: 2 additions & 5 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: inline_tab_view
description: "A TabBarView that can be nested in scrollables."
version: 1.0.0
description: "A TabBarView that can be nested in scrollables while sticking to flutter best practices and avoiding hacks."
version: 1.0.1
repository: https://github.com/derdilla/inline_tab_view

environment:
Expand All @@ -14,6 +14,3 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter


# flutter:
140 changes: 140 additions & 0 deletions test/src/offscreen_focus_exclusion_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:inline_tab_view/inline_tab_view.dart';
import 'package:inline_tab_view/src/offscreen_focus_exclusion_builder.dart';

void main() {
testWidgets('hidden children are not focusable', (tester) async {
final controller = TabController(length: 2, vsync: const TestVSync());
addTearDown(controller.dispose);

final leadingFocus = FocusNode();
addTearDown(leadingFocus.dispose);
final trailingFocus = FocusNode();
addTearDown(trailingFocus.dispose);
final tab1Wid1Focus = FocusNode();
addTearDown(tab1Wid1Focus.dispose);
final tab1Wid2Focus = FocusNode();
addTearDown(tab1Wid2Focus.dispose);
final tab1Wid3Focus = FocusNode();
addTearDown(tab1Wid3Focus.dispose);
final tab2Wid1Focus = FocusNode();
addTearDown(tab2Wid1Focus.dispose);
final tab2Wid2Focus = FocusNode();
addTearDown(tab2Wid2Focus.dispose);
final tab2Wid3Focus = FocusNode();
addTearDown(tab2Wid3Focus.dispose);

await tester.pumpWidget(MaterialApp(
home: Column(
children: [
Focus(
key: Key('leading'),
focusNode: leadingFocus,
child: SizedBox.square(dimension: 10)),
InlineTabView(
controller: controller,
children: [
Column(
children: [
Focus(
key: Key('Tab 1 - 1'),
focusNode: tab1Wid1Focus,
child: SizedBox.square(dimension: 10)),
Focus(
key: Key('Tab 1 - 2'),
focusNode: tab1Wid2Focus,
child: SizedBox.square(dimension: 10)),
Focus(
key: Key('Tab 1 - 3'),
focusNode: tab1Wid3Focus,
child: SizedBox.square(dimension: 10)),
],
),
Column(
children: [
Focus(
key: Key('Tab 2 - 1'),
focusNode: tab2Wid1Focus,
child: SizedBox.square(dimension: 10)),
Focus(
key: Key('Tab 2 - 2'),
focusNode: tab2Wid2Focus,
child: SizedBox.square(dimension: 10)),
Focus(
key: Key('Tab 2 - 3'),
focusNode: tab2Wid3Focus,
child: SizedBox.square(dimension: 10)),
],
)
],
),
Focus(
key: Key('trailing'),
focusNode: trailingFocus,
child: SizedBox.square(dimension: 10)),
],
),
));
expect(find.byType(OffscreenFocusExclusionBuilder), findsOneWidget);

tab1Wid1Focus.requestFocus();
await tester.pumpAndSettle();
expect(leadingFocus.hasFocus, false);
expect(tab1Wid1Focus.hasFocus, true);
expect(tab1Wid2Focus.hasFocus, false);
expect(tab1Wid3Focus.hasFocus, false);
expect(tab2Wid1Focus.hasFocus, false);
expect(tab2Wid2Focus.hasFocus, false);
expect(tab2Wid3Focus.hasFocus, false);
expect(trailingFocus.hasFocus, false);

// it doesn't (shouldn't) matter which node is used to request the next focus.
leadingFocus.nextFocus();
await tester.pumpAndSettle();
expect(leadingFocus.hasFocus, false);
expect(tab1Wid1Focus.hasFocus, false);
expect(tab1Wid2Focus.hasFocus, true);
expect(tab1Wid3Focus.hasFocus, false);
expect(tab2Wid1Focus.hasFocus, false);
expect(tab2Wid2Focus.hasFocus, false);
expect(tab2Wid3Focus.hasFocus, false);
expect(trailingFocus.hasFocus, false);

leadingFocus.nextFocus();
await tester.pumpAndSettle();
expect(leadingFocus.hasFocus, false);
expect(tab1Wid1Focus.hasFocus, false);
expect(tab1Wid2Focus.hasFocus, false);
expect(tab1Wid3Focus.hasFocus, true);
expect(tab2Wid1Focus.hasFocus, false);
expect(tab2Wid2Focus.hasFocus, false);
expect(tab2Wid3Focus.hasFocus, false);
expect(trailingFocus.hasFocus, false);

leadingFocus.nextFocus();
await tester.pumpAndSettle();
expect(leadingFocus.hasFocus, false);
expect(tab1Wid1Focus.hasFocus, false);
expect(tab1Wid2Focus.hasFocus, false);
expect(tab1Wid3Focus.hasFocus, false);
expect(tab2Wid1Focus.hasFocus, false);
expect(tab2Wid2Focus.hasFocus, false);
expect(tab2Wid3Focus.hasFocus, false);
expect(trailingFocus.hasFocus, true);

leadingFocus.previousFocus();
leadingFocus.previousFocus();
leadingFocus.previousFocus();
leadingFocus.previousFocus();
await tester.pumpAndSettle();
expect(leadingFocus.hasFocus, true);
expect(tab1Wid1Focus.hasFocus, false);
expect(tab1Wid2Focus.hasFocus, false);
expect(tab1Wid3Focus.hasFocus, false);
expect(tab2Wid1Focus.hasFocus, false);
expect(tab2Wid2Focus.hasFocus, false);
expect(tab2Wid3Focus.hasFocus, false);
expect(trailingFocus.hasFocus, false);
});
}