fix(app): a phrase field keeps its focus as the app goes and comes back

SecretScreen built its child alone, and a stack around it with the cover
while the app was away (iOS), so each switch of apps built the child anew:
a phrase field lost its focus, its caret and what the keyboard was
composing (the short reading's L4). It always builds the stack now, laid
out as the child alone (StackFit.passthrough), the cover in it only while
away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 18:02:35 +02:00
commit 1ca4e083bb
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 44 additions and 10 deletions

View file

@ -108,18 +108,19 @@ class _SecretScreenState() extends State<SecretScreen> {
}
@override
Widget build(BuildContext context) {
if (!(_away && (widget.coversWhenAway ?? Platform.isIOS))) {
return widget.child;
}
return Stack(
children: [
widget.child,
Widget build(BuildContext context) => Stack(
// Always the stack, the cover in it only while away: [child] keeps its
// place in the tree, so a field in it keeps its focus and what the
// keyboard was composing as the app goes and comes back (the short
// reading's L4); laid out as without it.
fit: StackFit.passthrough,
children: [
widget.child,
if (_away && (widget.coversWhenAway ?? Platform.isIOS))
Positioned.fill(
key: SecretScreen.coverKey,
child: ColoredBox(color: Theme.of(context).colorScheme.surface),
),
],
);
}
],
);
}

View file

@ -46,6 +46,39 @@ void main() {
expect(find.byKey(SecretScreen.coverKey), findsNothing);
});
testWidgets('a field under the cover keeps its text, focus and place as '
'the app goes and comes back (the short reading, L4)', (tester) async {
final typed = TextEditingController();
addTearDown(typed.dispose);
await pump(
tester,
SecretScreen(
guard: FakeScreenGuard(),
coversWhenAway: true,
child: TextField(controller: typed),
),
);
await tester.tap(find.byType(TextField));
await tester.enterText(find.byType(TextField), 'abandon abil');
await tester.pump();
final field = tester.state<EditableTextState>(find.byType(EditableText));
expect(field.widget.focusNode.hasFocus, isTrue);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
await tester.pump();
expect(find.byKey(SecretScreen.coverKey), findsOneWidget);
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await tester.pump();
expect(find.byKey(SecretScreen.coverKey), findsNothing);
expect(
tester.state<EditableTextState>(find.byType(EditableText)),
same(field),
);
expect(field.widget.focusNode.hasFocus, isTrue);
expect(typed.text, 'abandon abil');
});
testWidgets('the phrase, shown or typed back, the recovery codes and an '
'authenticator are each shown as a secret (typed back: the reading of '
'the fixes)', (tester) async {