mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add flag to exclude focus for hidden children in Visibility, maintainFocusability. Set maintainFocusability to false in IndexedStack (#159133)
Fixes: https://github.com/flutter/flutter/issues/114213 and is a prerequisite for fixing: https://github.com/flutter/flutter/issues/148405 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
4226470eb6
commit
a44675cb39
@ -4242,7 +4242,14 @@ class IndexedStack extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Widget> wrappedChildren = List<Widget>.generate(children.length, (int i) {
|
||||
return Visibility.maintain(visible: i == index, child: children[i]);
|
||||
return Visibility(
|
||||
visible: i == index,
|
||||
maintainInteractivity: true,
|
||||
maintainSize: true,
|
||||
maintainState: true,
|
||||
maintainAnimation: true,
|
||||
child: children[i],
|
||||
);
|
||||
});
|
||||
return _RawIndexedStack(
|
||||
alignment: alignment,
|
||||
|
||||
@ -12,6 +12,7 @@ library;
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'focus_scope.dart';
|
||||
import 'framework.dart';
|
||||
import 'sliver.dart';
|
||||
import 'ticker_provider.dart';
|
||||
@ -66,6 +67,7 @@ class Visibility extends StatelessWidget {
|
||||
this.maintainSize = false,
|
||||
this.maintainSemantics = false,
|
||||
this.maintainInteractivity = false,
|
||||
this.maintainFocusability = false,
|
||||
}) : assert(
|
||||
maintainState || !maintainAnimation,
|
||||
'Cannot maintain animations if the state is not also maintained.',
|
||||
@ -95,6 +97,7 @@ class Visibility extends StatelessWidget {
|
||||
maintainSize = true,
|
||||
maintainSemantics = true,
|
||||
maintainInteractivity = true,
|
||||
maintainFocusability = true,
|
||||
replacement = const SizedBox.shrink(); // Unused since maintainState is always true.
|
||||
|
||||
/// The widget to show or hide, as controlled by [visible].
|
||||
@ -215,6 +218,11 @@ class Visibility extends StatelessWidget {
|
||||
/// true, then touch events will nonetheless be passed through.
|
||||
final bool maintainInteractivity;
|
||||
|
||||
/// Whether to allow the widget to receive focus when hidden. Only in effect if [visible] is false.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool maintainFocusability;
|
||||
|
||||
/// Tells the visibility state of an element in the tree based off its
|
||||
/// ancestor [Visibility] elements.
|
||||
///
|
||||
@ -245,7 +253,7 @@ class Visibility extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result = child;
|
||||
Widget result = ExcludeFocus(excluding: !visible && !maintainFocusability, child: child);
|
||||
if (maintainSize) {
|
||||
result = _Visibility(
|
||||
visible: visible,
|
||||
|
||||
@ -489,6 +489,74 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('IndexedStack excludes focus for hidden children', (WidgetTester tester) async {
|
||||
const List<Widget> children = <Widget>[
|
||||
Focus(child: Text('child 0')),
|
||||
Focus(child: Text('child 1')),
|
||||
];
|
||||
|
||||
Future<void> pumpIndexedStack(int? activeIndex) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: IndexedStack(index: activeIndex, children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> requestFocusAndPump(FocusNode node) async {
|
||||
node.requestFocus();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await pumpIndexedStack(0);
|
||||
|
||||
final Element child0 = tester.element(find.text('child 0', skipOffstage: false));
|
||||
final Element child1 = tester.element(find.text('child 1', skipOffstage: false));
|
||||
final FocusNode child0FocusNode = Focus.of(child0);
|
||||
final FocusNode child1FocusNode = Focus.of(child1);
|
||||
|
||||
await requestFocusAndPump(child0FocusNode);
|
||||
|
||||
expect(child0FocusNode.hasFocus, true);
|
||||
expect(child1FocusNode.hasFocus, false);
|
||||
|
||||
await requestFocusAndPump(child1FocusNode);
|
||||
|
||||
expect(child0FocusNode.hasFocus, true);
|
||||
expect(child1FocusNode.hasFocus, false);
|
||||
|
||||
await pumpIndexedStack(1);
|
||||
await requestFocusAndPump(child1FocusNode);
|
||||
|
||||
expect(child0FocusNode.hasFocus, false);
|
||||
expect(child1FocusNode.hasFocus, true);
|
||||
|
||||
await requestFocusAndPump(child0FocusNode);
|
||||
|
||||
expect(child0FocusNode.hasFocus, false);
|
||||
expect(child1FocusNode.hasFocus, true);
|
||||
});
|
||||
|
||||
testWidgets('IndexedStack: hidden children can not receive tap events', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
bool tapped = false;
|
||||
final List<Widget> children = <Widget>[
|
||||
const Text('child'),
|
||||
GestureDetector(onTap: () => tapped = true, child: const Text('hiddenChild')),
|
||||
];
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(textDirection: TextDirection.ltr, child: IndexedStack(children: children)),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('hiddenChild', skipOffstage: false), warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
expect(tapped, false);
|
||||
});
|
||||
|
||||
testWidgets('Stack clip test', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
|
||||
@ -396,6 +396,95 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Visibility with maintain* false excludes focus of child when not visible', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
Future<void> pumpVisibility(bool visible) async {
|
||||
await tester.pumpWidget(
|
||||
Visibility(
|
||||
visible: visible,
|
||||
child: const Focus(child: Text('child', textDirection: TextDirection.ltr)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await pumpVisibility(true);
|
||||
|
||||
final Element child = tester.element(find.text('child', skipOffstage: false));
|
||||
final FocusNode childFocusNode = Focus.of(child);
|
||||
|
||||
childFocusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(childFocusNode.hasFocus, true);
|
||||
|
||||
await pumpVisibility(false);
|
||||
childFocusNode.requestFocus();
|
||||
|
||||
expect(childFocusNode.hasFocus, false);
|
||||
});
|
||||
|
||||
testWidgets('Visibility with maintain* true does not exclude focus of child when not visible', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
Future<void> pumpVisibility(bool visible) async {
|
||||
await tester.pumpWidget(
|
||||
Visibility.maintain(
|
||||
visible: visible,
|
||||
child: const Focus(child: Text('child', textDirection: TextDirection.ltr)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await pumpVisibility(true);
|
||||
|
||||
final Element child = tester.element(find.text('child', skipOffstage: false));
|
||||
final FocusNode childFocusNode = Focus.of(child);
|
||||
|
||||
childFocusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(childFocusNode.hasFocus, true);
|
||||
|
||||
await pumpVisibility(false);
|
||||
|
||||
expect(childFocusNode.hasFocus, true);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Visibility with maintain* true except maintainFocusability which is false excludes focus of child when not visible',
|
||||
(WidgetTester tester) async {
|
||||
Future<void> pumpVisibility(bool visible) async {
|
||||
await tester.pumpWidget(
|
||||
Visibility(
|
||||
visible: visible,
|
||||
maintainState: true,
|
||||
maintainAnimation: true,
|
||||
maintainInteractivity: true,
|
||||
maintainSemantics: true,
|
||||
maintainSize: true,
|
||||
child: const Focus(child: Text('child', textDirection: TextDirection.ltr)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await pumpVisibility(true);
|
||||
|
||||
final Element child = tester.element(find.text('child', skipOffstage: false));
|
||||
final FocusNode childFocusNode = Focus.of(child);
|
||||
|
||||
childFocusNode.requestFocus();
|
||||
await tester.pump();
|
||||
|
||||
expect(childFocusNode.hasFocus, true);
|
||||
|
||||
await pumpVisibility(false);
|
||||
childFocusNode.requestFocus();
|
||||
|
||||
expect(childFocusNode.hasFocus, false);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Visibility does not force compositing when visible and maintain*', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user