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
3 changes: 3 additions & 0 deletions lib/features/connections/environment_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class _Option extends material.StatelessWidget {
button: true,
selected: selected,
label: label,
// The text inside is the same label; one node, not two.
excludeSemantics: true,
onTap: onTap,
child: material.InkWell(
key: material.Key('environment_option_$optionKey'),
onTap: onTap,
Expand Down
35 changes: 8 additions & 27 deletions lib/features/connections/forms/connection_auth_section.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart' as material;
import 'package:querya_desktop/core/ui/querya_control_tokens.dart';
import 'package:querya_desktop/features/connections/remove_saved_password_option.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

Expand Down Expand Up @@ -35,8 +34,6 @@ class ConnectionAuthSection extends material.StatefulWidget {
}

class _ConnectionAuthSectionState extends material.State<ConnectionAuthSection> {
bool _showPassword = false;

material.Widget _buildUsernameField() {
return material.Column(
crossAxisAlignment: material.CrossAxisAlignment.stretch,
Expand All @@ -46,6 +43,7 @@ class _ConnectionAuthSectionState extends material.State<ConnectionAuthSection>
const Gap(8),
QueryaTextField(
controller: widget.usernameController,
semanticLabel: widget.usernameLabel,
placeholder: widget.usernamePlaceholder,
),
],
Expand All @@ -64,30 +62,13 @@ class _ConnectionAuthSectionState extends material.State<ConnectionAuthSection>
onChanged: widget.onRemoveSavedPasswordChanged!,
),
const Gap(8),
material.Stack(
children: [
QueryaTextField(
controller: widget.passwordController,
enabled: !widget.removeSavedPassword,
placeholder: widget.isEditing ? 'Leave blank to keep existing' : 'Password',
obscureText: !_showPassword,
),
material.Positioned(
right: 8,
top: 0,
bottom: 0,
child: material.Center(
child: QueryaIconButton(
icon: material.Icon(_showPassword
? material.Icons.visibility_off
: material.Icons.visibility),
tooltip: _showPassword ? 'Hide password' : 'Show password',
size: QueryaControlSize.sm,
onPressed: () => setState(() => _showPassword = !_showPassword),
),
),
),
],
QueryaTextField(
controller: widget.passwordController,
enabled: !widget.removeSavedPassword,
placeholder: widget.isEditing ? 'Leave blank to keep existing' : 'Password',
semanticLabel: widget.passwordLabel,
// The field brings its own show / hide button.
obscureText: true,
),
],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class ConnectionHostPortSection extends StatelessWidget {
const Gap(8),
QueryaTextField(
controller: hostController,
semanticLabel: hostLabel,
placeholder: hostPlaceholder,
),
],
Expand All @@ -58,6 +59,7 @@ class ConnectionHostPortSection extends StatelessWidget {
const Gap(8),
QueryaTextField(
controller: portController,
semanticLabel: portLabel,
placeholder: portPlaceholder,
),
],
Expand Down
11 changes: 11 additions & 0 deletions lib/features/connections/new_connection_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,17 @@ class _DbTypeCard extends material.StatelessWidget {
material.Widget build(material.BuildContext context) {
final t = theme;
final highlight = t.muted.withValues(alpha: 0.4);
return material.Semantics(
button: true,
selected: selected,
label: choice.label,
onTap: onTap,
excludeSemantics: true,
child: _surface(t, highlight),
);
}

material.Widget _surface(QueryaUiColors t, material.Color highlight) {
return QueryaHoverSurface(
borderRadius: material.BorderRadius.circular(10),
padding: const material.EdgeInsets.symmetric(vertical: 10, horizontal: 8),
Expand Down
23 changes: 15 additions & 8 deletions lib/features/connections/querya_connection_tree_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,7 @@ class QueryaConnectionTreeRow extends material.StatelessWidget {
onOpenSqlWorkspace != null ||
onTogglePin != null;
if (!wantsMenu) {
return expanded == null
? row
: material.Semantics(
button: true,
expanded: expanded,
child: row,
);
return expanded == null ? row : _expandSemantics(row);
}
final menu = QueryaContextMenu(
items: [
Expand Down Expand Up @@ -311,10 +305,23 @@ class QueryaConnectionTreeRow extends material.StatelessWidget {
child: row,
);
if (expanded == null) return menu;
return _expandSemantics(menu);
}

/// One labelled node with the expand / collapse state and a tap action, so
/// a screen reader or a switch can open the branch (#1822). The inner
/// nodes are excluded: the label is the row's own, not a second one.
material.Widget _expandSemantics(material.Widget child) {
return material.Semantics(
container: true,
button: true,
expanded: expanded,
child: menu,
selected: isSelected,
label: label,
onTap: onTap,
// A trailing control keeps its own node.
excludeSemantics: trailing == null,
child: child,
);
}

Expand Down
37 changes: 9 additions & 28 deletions lib/features/redis/redis_connection_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ class _RedisConnectionFormContentState

bool _removeSavedPassword = false;
bool _useSSL = false;
bool _showPassword = false;
bool _isTesting = false;
String? _testResult;
Timer? _dismissTimer;
Expand Down Expand Up @@ -448,33 +447,15 @@ class _RedisConnectionFormContentState
setState(() => _removeSavedPassword = v),
),
const Gap(8),
material.Stack(
children: [
QueryaTextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: _isEditing
? 'Leave blank to keep existing'
: 'Password',
obscureText: !_showPassword,
),
material.Positioned(
right: 8,
top: 0,
bottom: 0,
child: material.Center(
child: QueryaIconButton(
icon: material.Icon(_showPassword
? material.Icons.visibility_off
: material.Icons.visibility),
tooltip: _showPassword ? 'Hide password' : 'Show password',
size: QueryaControlSize.sm,
onPressed: () => setState(
() => _showPassword = !_showPassword),
),
),
),
],
QueryaTextField(
controller: _passwordController,
enabled: !_removeSavedPassword,
placeholder: _isEditing
? 'Leave blank to keep existing'
: 'Password',
semanticLabel: 'Password',
// The field brings its own show / hide button.
obscureText: true,
),
material.Row(
children: [
Expand Down
12 changes: 11 additions & 1 deletion lib/shared/widgets/querya_text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class QueryaTextField extends material.StatefulWidget {
this.textAlign = material.TextAlign.start,
this.autocorrect = true,
this.contentPadding,
this.semanticLabel,
});

final material.TextEditingController? controller;
Expand Down Expand Up @@ -88,6 +89,10 @@ class QueryaTextField extends material.StatefulWidget {
/// Replaces the padding between the border and the text.
final material.EdgeInsetsGeometry? contentPadding;

/// Spoken name of the field, usually its caption (#1822); without it a
/// screen reader hears only the placeholder.
final String? semanticLabel;

@override
material.State<QueryaTextField> createState() => _QueryaTextFieldState();
}
Expand Down Expand Up @@ -154,7 +159,7 @@ class _QueryaTextFieldState extends material.State<QueryaTextField> {
icon: _revealed
? material.Icons.visibility_outlined
: material.Icons.visibility_off_outlined,
tooltip: _revealed ? 'Hide' : 'Show',
tooltip: _revealed ? 'Hide password' : 'Show password',
size: QueryaControlSize.sm,
onPressed: widget.enabled
? () => setState(() => _revealed = !_revealed)
Expand Down Expand Up @@ -252,6 +257,11 @@ class _QueryaTextFieldState extends material.State<QueryaTextField> {
)
: field;

final label = widget.semanticLabel;
if (label != null) {
content = material.Semantics(label: label, child: content);
}

if (hasError) {
content = material.Column(
crossAxisAlignment: material.CrossAxisAlignment.start,
Expand Down
7 changes: 4 additions & 3 deletions test/features/connections/connection_form_sections_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ void main() {
expect(find.text('Username'), findsOneWidget);
// Label plus the field's own placeholder.
expect(find.text('Password'), findsNWidgets(2));
expect(find.byIcon(material.Icons.visibility), findsOneWidget);
expect(find.byIcon(material.Icons.visibility_off_outlined),
findsOneWidget);

await tester.tap(find.byIcon(material.Icons.visibility));
await tester.tap(find.byIcon(material.Icons.visibility_off_outlined));
await tester.pumpAndSettle();

expect(find.byIcon(material.Icons.visibility_off), findsOneWidget);
expect(find.byIcon(material.Icons.visibility_outlined), findsOneWidget);
});

testWidgets('renders in row layout and shows remove saved password when editing',
Expand Down
70 changes: 70 additions & 0 deletions test/features/connections/connection_form_semantics_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/features/connections/environment_section.dart';
import 'package:querya_desktop/features/connections/forms/connection_auth_section.dart';
import 'package:querya_desktop/features/connections/forms/connection_host_port_section.dart';

import '../../support/querya_theme_test_shell.dart';

int _nodesLabelled(WidgetTester tester, String label) {
var count = 0;
void visit(SemanticsNode node) {
if (node.label.split('\n').first.trim() == label) count++;
node.visitChildren((c) {
visit(c);
return true;
});
}

visit(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!);
return count;
}

void main() {
testWidgets('host, port, user and password fields carry their caption '
'and the password has one show button (#1822)', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
queryaThemeTestShell(
child: material.SingleChildScrollView(
child: material.Column(
children: [
ConnectionHostPortSection(
hostController: material.TextEditingController(),
portController: material.TextEditingController(),
),
ConnectionAuthSection(
usernameController: material.TextEditingController(),
passwordController: material.TextEditingController(),
),
],
),
),
),
);
await tester.pump();

for (final label in ['Host', 'Port', 'Username', 'Password']) {
final labelled = find.semantics.byLabel(label);
expect(labelled, findsWidgets, reason: label);
}
expect(find.byTooltip('Show password'), findsOneWidget);
expect(find.byTooltip('Show'), findsNothing);
handle.dispose();
});

testWidgets('an environment option is one semantics node (#1822)',
(tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
queryaThemeTestShell(
child: EnvironmentSection(value: null, onChanged: (_) {}),
),
);
await tester.pump();

expect(_nodesLabelled(tester, 'None'), 1);
handle.dispose();
});
}
35 changes: 35 additions & 0 deletions test/features/connections/querya_connection_tree_row_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'package:flutter/material.dart' as material;
import 'dart:ui' show Tristate;

import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/theme/querya_theme.dart';
import 'package:querya_desktop/features/connections/querya_connection_tree_row.dart';
Expand All @@ -7,6 +10,38 @@ import '../../support/querya_theme_test_shell.dart';

void main() {
group('QueryaConnectionTreeRow', () {
testWidgets(
'an expandable row is one labelled node that a semantics tap opens '
'(#1822)', (tester) async {
final handle = tester.ensureSemantics();
var taps = 0;
await tester.pumpWidget(
queryaThemeTestShell(
child: material.Material(
child: QueryaConnectionTreeRow(
label: 'querya',
expanded: false,
onTap: () => taps++,
textStyle: const material.TextStyle(fontSize: 13),
),
),
),
);
await tester.pump();

final node = tester.getSemantics(find.text('querya'));
expect(node.label, 'querya');
expect(
node.getSemanticsData().hasAction(SemanticsAction.tap),
isTrue,
);
expect(node.getSemanticsData().flagsCollection.isExpanded, Tristate.isFalse);
tester.binding.pipelineOwner.semanticsOwner!
.performAction(node.id, SemanticsAction.tap);
expect(taps, 1);
handle.dispose();
});

testWidgets('renders pinned star icon with workbench.warning (#1643)',
(tester) async {
await tester.pumpWidget(
Expand Down
Loading