Skip to content

Commit 4cd46b1

Browse files
committed
Add accessibility tab placeholder
1 parent af5e1b9 commit 4cd46b1

9 files changed

Lines changed: 346 additions & 0 deletions

File tree

packages/devtools_app/lib/src/app.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import 'framework/notifications_view.dart';
2323
import 'framework/observer/disconnect_observer.dart';
2424
import 'framework/release_notes.dart';
2525
import 'framework/scaffold/scaffold.dart';
26+
import 'screens/accessibility/accessibility_controller.dart';
27+
import 'screens/accessibility/accessibility_screen.dart';
2628
import 'screens/app_size/app_size_controller.dart';
2729
import 'screens/app_size/app_size_screen.dart';
2830
import 'screens/debugger/debugger_controller.dart';
@@ -731,6 +733,11 @@ List<DevToolsScreen> defaultScreens({
731733
DTDToolsScreen(),
732734
createController: (_) => DTDToolsController(),
733735
),
736+
if (FeatureFlags.accessibility.isEnabled)
737+
DevToolsScreen<AccessibilityController>(
738+
AccessibilityScreen(),
739+
createController: (_) => AccessibilityController(),
740+
),
734741
];
735742
}
736743

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'dart:async';
6+
7+
import 'package:devtools_app_shared/utils.dart';
8+
import 'package:flutter/foundation.dart';
9+
10+
import '../../shared/diagnostics/diagnostics_node.dart';
11+
import '../../shared/framework/screen.dart';
12+
import '../../shared/framework/screen_controllers.dart';
13+
14+
class AccessibilityController extends DevToolsScreenController
15+
with AutoDisposeControllerMixin {
16+
@override
17+
String get screenId => ScreenMetaData.accessibility.id;
18+
19+
/// The root node of the semantics tree.
20+
ValueListenable<RemoteDiagnosticsNode?> get rootNode => _rootNode;
21+
final _rootNode = ValueNotifier<RemoteDiagnosticsNode?>(null);
22+
23+
/// Whether the accessibility feature is enabled.
24+
ValueListenable<bool> get accessibilityEnabled => _accessibilityEnabled;
25+
final _accessibilityEnabled = ValueNotifier<bool>(false);
26+
27+
ValueListenable<double> get textScaleFactor => _textScaleFactor;
28+
final _textScaleFactor = ValueNotifier<double>(1.0);
29+
30+
ValueListenable<bool> get highContrastEnabled => _highContrastEnabled;
31+
final _highContrastEnabled = ValueNotifier<bool>(false);
32+
33+
ValueListenable<bool> get autoAuditEnabled => _autoAuditEnabled;
34+
final _autoAuditEnabled = ValueNotifier<bool>(false);
35+
36+
Future<void> toggleAccessibility(bool enable) async {
37+
_accessibilityEnabled.value = enable;
38+
if (enable) {
39+
// TODO(kenz): enable semantics and other accessibility features.
40+
} else {
41+
// TODO(kenz): disable semantics and other accessibility features.
42+
}
43+
}
44+
45+
Future<void> setTextScaleFactor(double factor) async {
46+
_textScaleFactor.value = factor;
47+
// TODO(kenz): set text scale factor on device.
48+
}
49+
50+
Future<void> toggleHighContrast(bool enable) async {
51+
_highContrastEnabled.value = enable;
52+
// TODO(kenz): set high contrast on device.
53+
}
54+
55+
Future<void> toggleAutoAudit(bool enable) async {
56+
_autoAuditEnabled.value = enable;
57+
if (enable) {
58+
await runAudit();
59+
}
60+
}
61+
62+
Future<void> runAudit() async {
63+
// TODO(kenz): run accessibility audit.
64+
}
65+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'dart:async';
2+
3+
import 'package:devtools_app_shared/ui.dart';
4+
import 'package:flutter/material.dart';
5+
6+
import '../../shared/globals.dart';
7+
import 'accessibility_controller.dart';
8+
9+
class AccessibilityControls extends StatelessWidget {
10+
const AccessibilityControls({super.key});
11+
12+
@override
13+
Widget build(BuildContext context) {
14+
return Column(
15+
children: [
16+
const AreaPaneHeader(title: Text('Settings & Controls')),
17+
Expanded(
18+
child: ListView(
19+
padding: const EdgeInsets.all(defaultSpacing),
20+
children: const [
21+
_SystemSimulationControls(),
22+
SizedBox(height: defaultSpacing),
23+
_AuditControls(),
24+
],
25+
),
26+
),
27+
],
28+
);
29+
}
30+
}
31+
32+
class _SystemSimulationControls extends StatelessWidget {
33+
const _SystemSimulationControls();
34+
35+
@override
36+
Widget build(BuildContext context) {
37+
final controller = screenControllers.lookup<AccessibilityController>();
38+
return Column(
39+
crossAxisAlignment: CrossAxisAlignment.start,
40+
children: [
41+
Text(
42+
'SYSTEM SIMULATION',
43+
style: Theme.of(context).textTheme.titleSmall,
44+
),
45+
const SizedBox(height: denseSpacing),
46+
ValueListenableBuilder<bool>(
47+
valueListenable: controller.highContrastEnabled,
48+
builder: (context, enabled, _) {
49+
return SwitchListTile(
50+
title: const Text('High Contrast Mode'),
51+
value: enabled,
52+
onChanged: (value) =>
53+
unawaited(controller.toggleHighContrast(value)),
54+
);
55+
},
56+
),
57+
const SizedBox(height: denseSpacing),
58+
ValueListenableBuilder<double>(
59+
valueListenable: controller.textScaleFactor,
60+
builder: (context, factor, _) {
61+
return Column(
62+
crossAxisAlignment: CrossAxisAlignment.start,
63+
children: [
64+
Text('Text Scale Factor: ${factor.toStringAsFixed(1)}x'),
65+
Slider(
66+
value: factor,
67+
min: 0.5,
68+
max: 3.0,
69+
divisions: 25,
70+
label: factor.toStringAsFixed(1),
71+
onChanged: (value) =>
72+
unawaited(controller.setTextScaleFactor(value)),
73+
),
74+
],
75+
);
76+
},
77+
),
78+
],
79+
);
80+
}
81+
}
82+
83+
class _AuditControls extends StatelessWidget {
84+
const _AuditControls();
85+
86+
@override
87+
Widget build(BuildContext context) {
88+
final controller = screenControllers.lookup<AccessibilityController>();
89+
return Column(
90+
crossAxisAlignment: CrossAxisAlignment.start,
91+
children: [
92+
Text('AUDIT CONTROLS', style: Theme.of(context).textTheme.titleSmall),
93+
const SizedBox(height: denseSpacing),
94+
SizedBox(
95+
width: double.infinity,
96+
child: ElevatedButton.icon(
97+
icon: const Icon(Icons.play_arrow),
98+
label: const Text('Run Audit'),
99+
onPressed: () => unawaited(controller.runAudit()),
100+
),
101+
),
102+
const SizedBox(height: denseSpacing),
103+
ValueListenableBuilder<bool>(
104+
valueListenable: controller.autoAuditEnabled,
105+
builder: (context, enabled, _) {
106+
return SwitchListTile(
107+
title: const Text('Auto-run Audit'),
108+
value: enabled,
109+
onChanged: (value) =>
110+
unawaited(controller.toggleAutoAudit(value)),
111+
);
112+
},
113+
),
114+
],
115+
);
116+
}
117+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:devtools_app_shared/ui.dart';
2+
import 'package:flutter/material.dart';
3+
4+
class AccessibilityResults extends StatelessWidget {
5+
const AccessibilityResults({super.key});
6+
7+
@override
8+
Widget build(BuildContext context) {
9+
return Column(
10+
children: [
11+
const AreaPaneHeader(title: Text('Audit Results')),
12+
Expanded(
13+
child: ListView.builder(
14+
itemCount: 0,
15+
itemBuilder: (context, index) {
16+
return const ListTile(title: Text('Violation Placeholder'));
17+
},
18+
),
19+
),
20+
],
21+
);
22+
}
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'package:devtools_app_shared/ui.dart';
2+
import 'package:flutter/material.dart';
3+
4+
import '../../shared/framework/screen.dart';
5+
import '../../shared/globals.dart';
6+
import 'accessibility_controller.dart';
7+
import 'accessibility_controls.dart';
8+
import 'accessibility_results.dart';
9+
10+
class AccessibilityScreen extends Screen {
11+
AccessibilityScreen() : super.fromMetaData(ScreenMetaData.accessibility);
12+
13+
static final id = ScreenMetaData.accessibility.id;
14+
15+
@override
16+
Widget buildScreenBody(BuildContext context) {
17+
return const AccessibilityScreenBody();
18+
}
19+
}
20+
21+
class AccessibilityScreenBody extends StatefulWidget {
22+
const AccessibilityScreenBody({super.key});
23+
24+
@override
25+
State<AccessibilityScreenBody> createState() =>
26+
_AccessibilityScreenBodyState();
27+
}
28+
29+
class _AccessibilityScreenBodyState extends State<AccessibilityScreenBody> {
30+
late final AccessibilityController controller;
31+
32+
@override
33+
void initState() {
34+
super.initState();
35+
controller = screenControllers.lookup<AccessibilityController>();
36+
}
37+
38+
@override
39+
Widget build(BuildContext context) {
40+
return SplitPane(
41+
axis: Axis.horizontal,
42+
initialFractions: const [0.3, 0.7],
43+
children: const [AccessibilityControls(), AccessibilityResults()],
44+
);
45+
}
46+
}

packages/devtools_app/lib/src/shared/feature_flags.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ extension FeatureFlags on Never {
9393
enabled: enableExperiments,
9494
);
9595

96+
/// Flag to enable the Accessibility screen.
97+
///
98+
/// https://github.com/flutter/devtools/issues/9044
99+
static final accessibility = BooleanFeatureFlag(
100+
name: 'accessibility',
101+
enabled: true,
102+
);
103+
96104
/// A set of all the boolean feature flags for debugging purposes.
97105
///
98106
/// When adding a new boolean flag, you are responsible for adding it to this
@@ -104,6 +112,7 @@ extension FeatureFlags on Never {
104112
dapDebugging,
105113
inspectorV2,
106114
aiAssistant,
115+
accessibility,
107116
};
108117

109118
/// A set of all the Flutter channel feature flags for debugging purposes.

packages/devtools_app/lib/src/shared/framework/screen.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ enum ScreenMetaData {
129129
requiresAdvancedDeveloperMode: true,
130130
requiresConnection: false,
131131
),
132+
accessibility(
133+
'accessibility',
134+
title: 'Accessibility',
135+
icon: Icons.accessibility_new_rounded,
136+
),
132137
simple('simple');
133138

134139
const ScreenMetaData(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import 'package:devtools_app/src/app.dart';
2+
import 'package:devtools_app/src/screens/accessibility/accessibility_screen.dart';
3+
import 'package:devtools_app/src/service/service_manager.dart';
4+
import 'package:devtools_app/src/shared/feature_flags.dart';
5+
import 'package:devtools_app/src/shared/framework/screen.dart';
6+
import 'package:devtools_app/src/shared/globals.dart';
7+
import 'package:devtools_app/src/shared/managers/banner_messages.dart';
8+
import 'package:devtools_app/src/shared/managers/notifications.dart';
9+
import 'package:devtools_app/src/shared/offline/offline_data.dart';
10+
import 'package:devtools_app/src/shared/preferences/preferences.dart';
11+
import 'package:devtools_app_shared/ui.dart';
12+
import 'package:devtools_app_shared/utils.dart';
13+
import 'package:devtools_test/devtools_test.dart';
14+
import 'package:devtools_test/helpers.dart';
15+
import 'package:flutter/material.dart';
16+
import 'package:flutter_test/flutter_test.dart';
17+
18+
void main() {
19+
const screen = ScreenMetaData.accessibility;
20+
21+
group('AccessibilityScreen', () {
22+
late FakeServiceConnectionManager fakeServiceConnection;
23+
24+
setUp(() {
25+
fakeServiceConnection = FakeServiceConnectionManager();
26+
setGlobal(ServiceConnectionManager, fakeServiceConnection);
27+
setGlobal(IdeTheme, IdeTheme());
28+
setGlobal(OfflineDataController, OfflineDataController());
29+
setGlobal(NotificationService, NotificationService());
30+
setGlobal(BannerMessagesController, BannerMessagesController());
31+
setGlobal(PreferencesController, PreferencesController());
32+
FeatureFlags.accessibility.setEnabledForTests(true);
33+
});
34+
35+
tearDown(() {
36+
FeatureFlags.accessibility.setEnabledForTests(false);
37+
});
38+
39+
testWidgets('builds its body', (WidgetTester tester) async {
40+
await tester.pumpWidget(
41+
wrap(
42+
Builder(builder: (context) => AccessibilityScreen().build(context)),
43+
),
44+
);
45+
expect(find.byType(AccessibilityScreenBody), findsOneWidget);
46+
expect(find.text('Accessibility Screen Placeholder'), findsOneWidget);
47+
});
48+
49+
test('is included in defaultScreens when enabled', () {
50+
devtoolsScreens = null;
51+
expect(
52+
defaultScreens().any((s) => s.screen is AccessibilityScreen),
53+
isTrue,
54+
);
55+
});
56+
57+
test('is invalid in defaultScreens when disabled', () {
58+
FeatureFlags.accessibility.setEnabledForTests(false);
59+
devtoolsScreens = null;
60+
expect(
61+
defaultScreens().any((s) => s.screen is AccessibilityScreen),
62+
isFalse,
63+
);
64+
});
65+
});
66+
}

0 commit comments

Comments
 (0)