diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 845bacf..7baafae 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -1283,37 +1283,37 @@ class _ConnectFormState extends State<_ConnectForm> { final FocusNode _nickFocus = FocusNode(); final FocusNode _passwordFocus = FocusNode(); - /// Workaround for flutter/flutter#181474 (open, P2, triaged-text- - /// input, iOS 26+): after the soft keyboard is dismissed, the - /// FocusNode still reports `hasFocus == true`. Because the node is - /// already focused, the next user tap on the same TextField does - /// not trigger any focus-change side effect, so the platform - /// TextInput channel is never re-opened, and iOS keeps the soft - /// keyboard hidden. The user has to tap twice (the second tap - /// triggers an explicit re-focus path that re-shows the keyboard). - /// - /// The community-recommended workaround in the issue thread is to - /// **unfocus first, then re-request focus** on every tap. That - /// forces a real focus-change transition (`false -> true`) which - /// re-opens TextInput and slides the keyboard up on the first tap. - /// - /// This costs nothing on hosts where the bug doesn't reproduce - /// (the unfocus call is a no-op when the node isn't focused; the - /// re-request just reaffirms focus and EditableText's own attach - /// path handles the rest). - void _kickFocus(FocusNode node) { - // Drop focus synchronously, then re-grab it on the next frame. - // Doing both in one frame can race with EditableText's internal - // bookkeeping; deferring the re-grab by one microtask sidesteps - // that and matches the workaround pattern in the issue thread. - if (node.hasFocus) { - node.unfocus(); - } - Future.microtask(() { - if (!mounted) return; - node.requestFocus(); - }); - } + // _kickFocus (unfocus + Future.microtask refocus on every onTap) + // was removed after user-reported 500\u20131000 ms keyboard appearance + // latency on iPhone 16 Pro / iOS 26. + // + // The microtask deferral was the source of the lag: it forces + // EditableText's attach-to-TextInput path to wait one frame past + // the user's pointer-up, and iOS 26's keyboard slide-up animation + // then dovetails into that extra frame in a way that adds another + // 200\u2013800 ms before the keyboard actually appears. + // + // The tap-outside-to-unfocus GestureDetector wrapping the connect + // form Column (see build() below) already guarantees the FocusNode + // is in the unfocused state when the user taps a field, because + // any prior keyboard dismissal (tap outside / tap a sibling field) + // goes through FocusScope.unfocus(). Therefore TextField's native + // tap path can attach TextInput on the first frame with no help + // from us, and the keyboard appears instantly. + // + // If iOS regresses again such that focus state desyncs from + // keyboard visibility, the workaround to re-introduce here is the + // canonical pattern from flutter/flutter#181474: + // + // void _kickFocus(FocusNode node) { + // if (node.hasFocus) node.unfocus(); + // Future.microtask(() { + // if (!mounted) return; + // node.requestFocus(); + // }); + // } + // + // and wire onTap: () => _kickFocus(_xxxFocus) on each TextField. @override void dispose() { @@ -1347,7 +1347,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.hostCtl, focusNode: _hostFocus, - onTap: () => _kickFocus(_hostFocus), // Server addresses are URL-shaped: hostname or // hostname:port, all-lowercase ASCII, never user- // friendly prose. Configure the on-screen keyboard @@ -1391,7 +1390,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.nickCtl, focusNode: _nickFocus, - onTap: () => _kickFocus(_nickFocus), textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, @@ -1404,7 +1402,6 @@ class _ConnectFormState extends State<_ConnectForm> { TextField( controller: widget.passwordCtl, focusNode: _passwordFocus, - onTap: () => _kickFocus(_passwordFocus), obscureText: true, textInputAction: TextInputAction.done, autocorrect: false,