diff --git a/lib/shared/widgets/querya_tab_strip.dart b/lib/shared/widgets/querya_tab_strip.dart index c606de27..7c5ffc0e 100644 --- a/lib/shared/widgets/querya_tab_strip.dart +++ b/lib/shared/widgets/querya_tab_strip.dart @@ -56,6 +56,10 @@ class QueryaTabStripState extends material.State with material.TickerProviderStateMixin { late List _focusNodes; late List _focused; + + /// The tab whose focus came from a click: it takes focus (so arrow keys + /// work) but draws no ring until a key is pressed (#1438). + int? _pointerFocusedIndex; late List _tabKeys; final material.GlobalKey _stripKey = material.GlobalKey(); @@ -186,6 +190,9 @@ class QueryaTabStripState extends material.State material.KeyEventResult _onKeyEvent(int index, KeyEvent event) { if (event is! KeyDownEvent) return material.KeyEventResult.ignored; + if (_pointerFocusedIndex != null) { + setState(() => _pointerFocusedIndex = null); + } // Delete / Backspace close the focused tab, as the close button does. if (event.logicalKey == LogicalKeyboardKey.delete || @@ -267,7 +274,8 @@ class QueryaTabStripState extends material.State children: [ ...List.generate(widget.labels.length, (index) { final selected = widget.selectedIndex == index; - final focused = _focused[index]; + final focused = + _focused[index] && _pointerFocusedIndex != index; final label = widget.labels[index]; final showClose = _canCloseAt(index); return material.Padding( @@ -276,8 +284,12 @@ class QueryaTabStripState extends material.State key: _tabKeys[index], child: material.Focus( focusNode: _focusNodes[index], - onFocusChange: (value) => - setState(() => _focused[index] = value), + onFocusChange: (value) => setState(() { + _focused[index] = value; + if (!value && _pointerFocusedIndex == index) { + _pointerFocusedIndex = null; + } + }), onKeyEvent: (_, event) => _onKeyEvent(index, event), child: material.Semantics( button: true, @@ -289,7 +301,10 @@ class QueryaTabStripState extends material.State child: material.GestureDetector( excludeFromSemantics: true, behavior: material.HitTestBehavior.opaque, - onTap: () => _selectAndFocus(index), + onTap: () { + _pointerFocusedIndex = index; + _selectAndFocus(index); + }, child: material.AnimatedContainer( key: material.ValueKey('querya_tab_$label'), duration: context.motionDuration(QueryaMotion.fast), diff --git a/test/shared/querya_tab_strip_test.dart b/test/shared/querya_tab_strip_test.dart index 4a3d2d7a..06645ced 100644 --- a/test/shared/querya_tab_strip_test.dart +++ b/test/shared/querya_tab_strip_test.dart @@ -112,6 +112,43 @@ void main() { ); }); + testWidgets('a clicked tab has no focus ring until a key is pressed', + (tester) async { + var selected = 0; + await tester.pumpWidget( + stripShell( + child: material.StatefulBuilder( + builder: (context, setState) => QueryaTabStrip( + labels: const ['Server', 'SQL', 'History'], + selectedIndex: selected, + onSelected: (index) => setState(() => selected = index), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + material.Color ringOf(String label) { + final tab = tester.widget( + find.byKey(material.ValueKey('querya_tab_$label')), + ); + final decoration = tab.decoration! as material.BoxDecoration; + return (decoration.border! as material.Border).top.color; + } + + await tester.tap(find.byKey(const material.ValueKey('querya_tab_SQL'))); + await tester.pumpAndSettle(); + + expect(selected, 1); + expect(ringOf('SQL'), material.Colors.transparent); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pumpAndSettle(); + + expect(selected, 2); + expect(ringOf('History'), isNot(material.Colors.transparent)); + }); + testWidgets('renders sliding indicator under selected tab', (tester) async { await pumpSettledStrip( tester,