Skip to content

Commit 21f1838

Browse files
authored
[A11y] Create a11y page with placeholders. (#9889)
The screen is gated by the FeatureFlags.accessibility flag. internal link: go/devtool-a11y-userjourney ### General checklist ### Issues checklist ### Tests checklist ### AI-tooling checklist * [ ] I read the [AI contributions guidelines] and agree to follow them. * [ ] I reviewed all AI-generated code before opening this PR. * [ ] I understand and am able to discuss the code in this PR. * [ ] I have verifed the accuracy of any AI-generated text included in the PR description. * [ ] I commit to verifying the accuracy of any AI-generated code or text that I upload in response to review comments. ### Feature-change checklist * [ ] I added the `release-notes-not-required` label or left a comment requesting the label be added. * [ ] I added an entry to `packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md`. * [ ] I included before/after screenshots and/or a GIF demo of the new UI to my PR description. * [ ] I ran the DevTools app locally to manually verify my changes. ![build.yaml badge] If you need help, consider asking for help on [Discord]. [`contributions-welcome`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Acontributions-welcome [`good-first-issue`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Agood-first-issue [build.yaml badge]: https://github.com/flutter/devtools/actions/workflows/build.yaml/badge.svg
1 parent a469731 commit 21f1838

8 files changed

Lines changed: 270 additions & 0 deletions

File tree

packages/devtools_app/lib/devtools_app.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export 'src/framework/notifications_view.dart';
1010
export 'src/framework/release_notes.dart';
1111
export 'src/framework/scaffold/app_bar.dart';
1212
export 'src/framework/scaffold/status_line.dart';
13+
export 'src/screens/accessibility/accessibility_controller.dart';
14+
export 'src/screens/accessibility/accessibility_screen.dart';
1315
export 'src/screens/app_size/app_size_controller.dart';
1416
export 'src/screens/app_size/app_size_screen.dart';
1517
export 'src/screens/debugger/breakpoint_manager.dart';

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';
@@ -713,6 +715,11 @@ List<DevToolsScreen> defaultScreens({
713715
LoggingScreen(),
714716
createController: (_) => LoggingController(),
715717
),
718+
if (FeatureFlags.accessibility.isEnabled)
719+
DevToolsScreen<AccessibilityController>(
720+
AccessibilityScreen(),
721+
createController: (_) => AccessibilityController(),
722+
),
716723
DevToolsScreen<DevToolsScreenController>(ProviderScreen()),
717724
DevToolsScreen<AppSizeController>(
718725
AppSizeScreen(),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2026 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 'package:devtools_app_shared/utils.dart';
6+
import 'package:flutter/foundation.dart';
7+
8+
import '../../shared/framework/screen.dart';
9+
import '../../shared/framework/screen_controllers.dart';
10+
11+
/// Controller for the Accessibility screen.
12+
class AccessibilityController extends DevToolsScreenController
13+
with AutoDisposeControllerMixin {
14+
AccessibilityController() {
15+
_initListeners();
16+
}
17+
18+
void _initListeners() {
19+
addAutoDisposeListener(brightness, _onBrightnessChanged);
20+
addAutoDisposeListener(textScale, _onTextScaleChanged);
21+
addAutoDisposeListener(boldText, _onBoldTextChanged);
22+
addAutoDisposeListener(screenReader, _onScreenReaderChanged);
23+
addAutoDisposeListener(highContrast, _onHighContrastChanged);
24+
}
25+
26+
void _onBrightnessChanged() {
27+
// TODO(hannah-hyj): Implement VM service extension call for brightness override.
28+
// e.g. using 'ext.flutter.brightnessOverride'.
29+
}
30+
31+
void _onTextScaleChanged() {
32+
// TODO(hannah-hyj): Implement VM service extension call for text scale override.
33+
}
34+
35+
void _onBoldTextChanged() {
36+
// TODO(hannah-hyj): Implement VM service extension call for bold text override.
37+
}
38+
39+
void _onScreenReaderChanged() {
40+
// TODO(hannah-hyj): Implement VM service extension call for screen reader / semantics debugger.
41+
// e.g. using 'ext.flutter.showSemanticsDebugger'.
42+
}
43+
44+
void _onHighContrastChanged() {
45+
// TODO(hannah-hyj): Implement VM service extension call for high contrast override.
46+
}
47+
48+
@override
49+
final screenId = ScreenMetaData.accessibility.id;
50+
51+
// --- Accessibility Overrides State ---
52+
final brightness = ValueNotifier<String>('System');
53+
final textScale = ValueNotifier<double>(1.0);
54+
final boldText = ValueNotifier<bool>(false);
55+
final screenReader = ValueNotifier<bool>(false);
56+
final highContrast = ValueNotifier<bool>(false);
57+
58+
@override
59+
void dispose() {
60+
brightness.dispose();
61+
textScale.dispose();
62+
boldText.dispose();
63+
screenReader.dispose();
64+
highContrast.dispose();
65+
super.dispose();
66+
}
67+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2026 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 'package:devtools_app_shared/ui.dart';
6+
import 'package:devtools_app_shared/utils.dart';
7+
import 'package:flutter/material.dart';
8+
9+
import '../../shared/framework/screen.dart';
10+
import '../../shared/globals.dart';
11+
import '../../shared/ui/common_widgets.dart';
12+
import 'accessibility_controller.dart';
13+
14+
/// A screen that displays accessibility information.
15+
class AccessibilityScreen extends Screen {
16+
AccessibilityScreen() : super.fromMetaData(ScreenMetaData.accessibility);
17+
18+
static final id = ScreenMetaData.accessibility.id;
19+
20+
@override
21+
Widget buildScreenBody(BuildContext context) =>
22+
const AccessibilityScreenBody();
23+
}
24+
25+
class AccessibilityScreenBody extends StatefulWidget {
26+
const AccessibilityScreenBody({super.key});
27+
28+
@override
29+
State<AccessibilityScreenBody> createState() =>
30+
_AccessibilityScreenBodyState();
31+
}
32+
33+
class _AccessibilityScreenBodyState extends State<AccessibilityScreenBody>
34+
with AutoDisposeMixin {
35+
late AccessibilityController controller;
36+
37+
@override
38+
void initState() {
39+
super.initState();
40+
controller = screenControllers.lookup<AccessibilityController>();
41+
}
42+
43+
@override
44+
Widget build(BuildContext context) {
45+
final splitAxis = _splitAxisFor(context);
46+
return SplitPane(
47+
axis: splitAxis,
48+
initialFractions: const [0.6, 0.4],
49+
children: const [
50+
AccessibilitySemanticsTreePane(),
51+
AccessibilityOverridesPane(),
52+
],
53+
);
54+
}
55+
56+
Axis _splitAxisFor(BuildContext context) {
57+
final screenSize = MediaQuery.of(context).size;
58+
return screenSize.width > 1000 ? Axis.horizontal : Axis.vertical;
59+
}
60+
}
61+
62+
/// A pane that displays the semantics tree of the connected app.
63+
class AccessibilitySemanticsTreePane extends StatelessWidget {
64+
const AccessibilitySemanticsTreePane({super.key});
65+
66+
@override
67+
Widget build(BuildContext context) {
68+
return const DevToolsAreaPane(
69+
header: AreaPaneHeader(title: Text('Semantics Tree')),
70+
child: CenteredMessage(
71+
message:
72+
'Accessibility semantics tree placeholder.\n'
73+
'// TODO(hannah-hyj): Implement semantics tree view and details explorer.',
74+
),
75+
);
76+
}
77+
}
78+
79+
/// A pane that displays the accessibility overrides controls.
80+
class AccessibilityOverridesPane extends StatelessWidget {
81+
const AccessibilityOverridesPane({super.key});
82+
83+
@override
84+
Widget build(BuildContext context) {
85+
return const DevToolsAreaPane(
86+
header: AreaPaneHeader(title: Text('Accessibility Overrides')),
87+
child: CenteredMessage(
88+
message:
89+
'Accessibility overrides placeholder.\n'
90+
'// TODO(hannah-hyj): Implement setting overrides (brightness, text scale, bold text, screen reader, high contrast).',
91+
),
92+
);
93+
}
94+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ extension FeatureFlags on Never {
6969
enabled: enableExperiments,
7070
);
7171

72+
/// Flag to enable the Accessibility page.
73+
static final accessibility = BooleanFeatureFlag(
74+
name: 'accessibility',
75+
enabled: enableExperiments,
76+
);
77+
7278
/// A set of all the boolean feature flags for debugging purposes.
7379
///
7480
/// When adding a new boolean flag, you are responsible for adding it to this
@@ -77,6 +83,7 @@ extension FeatureFlags on Never {
7783
memorySaveLoad,
7884
devToolsExtensions,
7985
dapDebugging,
86+
accessibility,
8087
};
8188

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ enum ScreenMetaData {
9494
supportsWebServerDevice: true,
9595
tutorialVideoTimestamp: '?t=558',
9696
),
97+
accessibility(
98+
'accessibility',
99+
title: 'Accessibility',
100+
icon: Icons.accessibility,
101+
requiresFlutter: true,
102+
),
97103
provider(
98104
'provider',
99105
title: 'Provider',
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2026 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+
@TestOn('vm')
6+
library;
7+
8+
import 'package:devtools_app/devtools_app.dart';
9+
import 'package:devtools_app_shared/ui.dart';
10+
import 'package:devtools_app_shared/utils.dart';
11+
import 'package:devtools_test/devtools_test.dart';
12+
import 'package:devtools_test/helpers.dart';
13+
import 'package:flutter/material.dart';
14+
import 'package:flutter_test/flutter_test.dart';
15+
import 'package:mockito/mockito.dart';
16+
17+
void main() {
18+
late AccessibilityScreen screen;
19+
late AccessibilityController controller;
20+
const windowSize = Size(1000.0, 1000.0);
21+
22+
group('Accessibility Screen', () {
23+
Future<void> pumpAccessibilityScreen(WidgetTester tester) async {
24+
await tester.pumpWidget(
25+
wrapWithControllers(
26+
const AccessibilityScreenBody(),
27+
accessibility: controller,
28+
),
29+
);
30+
}
31+
32+
setUp(() {
33+
final fakeServiceConnection = FakeServiceConnectionManager();
34+
when(
35+
fakeServiceConnection.serviceManager.connectedApp!.isFlutterWebAppNow,
36+
).thenReturn(false);
37+
when(
38+
fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow,
39+
).thenReturn(false);
40+
when(
41+
fakeServiceConnection.errorBadgeManager.errorCountNotifier(
42+
'accessibility',
43+
),
44+
).thenReturn(ValueNotifier<int>(0));
45+
46+
setGlobal(NotificationService, NotificationService());
47+
setGlobal(
48+
DevToolsEnvironmentParameters,
49+
ExternalDevToolsEnvironmentParameters(),
50+
);
51+
setGlobal(PreferencesController, PreferencesController());
52+
setGlobal(ServiceConnectionManager, fakeServiceConnection);
53+
setGlobal(IdeTheme, IdeTheme());
54+
55+
controller = AccessibilityController();
56+
screen = AccessibilityScreen();
57+
});
58+
59+
testWidgets('builds its tab', (WidgetTester tester) async {
60+
await tester.pumpWidget(wrap(Builder(builder: screen.buildTab)));
61+
expect(find.text('Accessibility'), findsOneWidget);
62+
});
63+
64+
testWidgetsWithWindowSize('builds split view with panes', windowSize, (
65+
WidgetTester tester,
66+
) async {
67+
await pumpAccessibilityScreen(tester);
68+
await tester.pumpAndSettle();
69+
70+
expect(find.byType(AccessibilityScreenBody), findsOneWidget);
71+
expect(find.byType(SplitPane), findsAtLeastNWidgets(1));
72+
73+
// Overrides pane should be visible
74+
expect(find.byType(AccessibilityOverridesPane), findsOneWidget);
75+
76+
// Semantics Tree pane should be visible
77+
expect(find.byType(AccessibilitySemanticsTreePane), findsOneWidget);
78+
});
79+
});
80+
}

packages/devtools_test/lib/src/helpers/wrappers.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Widget wrapWithControllers(
9696
ReleaseNotesController? releaseNotes,
9797
VMDeveloperToolsController? vmDeveloperTools,
9898
DTDToolsController? dtdTools,
99+
AccessibilityController? accessibility,
99100
bool includeRouter = true,
100101
DevToolsQueryParams? queryParams,
101102
}) {
@@ -173,6 +174,12 @@ Widget wrapWithControllers(
173174
offline: offline,
174175
);
175176
}
177+
if (accessibility != null) {
178+
screenControllers.register<AccessibilityController>(
179+
() => accessibility,
180+
offline: offline,
181+
);
182+
}
176183

177184
var child = wrapWithNotifications(widget);
178185
final providers = [

0 commit comments

Comments
 (0)