diff --git a/packages/devtools_app/lib/src/shared/ui/side_panel.dart b/packages/devtools_app/lib/src/shared/ui/side_panel.dart index 16d19434ebd..9174f9296b3 100644 --- a/packages/devtools_app/lib/src/shared/ui/side_panel.dart +++ b/packages/devtools_app/lib/src/shared/ui/side_panel.dart @@ -182,6 +182,17 @@ class SidePanel extends AnimatedWidget { : Expanded( child: Markdown( data: markdownData!, + styleSheet: MarkdownStyleSheet( + // [MarkdownStyleSheet.fromTheme], which supplies the + // rest of the style sheet, hard codes + // `Colors.blue.shade100` as the blockquote fill while + // taking the text color from the theme. In the dark + // theme that draws light gray text on light blue. + blockquoteDecoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: defaultBorderRadius, + ), + ), onTapLink: (text, url, title) => unawaited(launchUrlWithErrorHandling(url!)), ), diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index f589bd1821b..87c945c27e6 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -15,7 +15,9 @@ To learn more about DevTools, check out the ## General updates -TODO: Remove this section if there are not any updates. +* Fixed unreadable text in the release notes panel, where blockquotes were + drawn on a hard coded light blue background in the dark theme. + [#9957](https://github.com/flutter/devtools/pull/9957) ## Inspector updates diff --git a/packages/devtools_app/test/shared/ui/side_panel_test.dart b/packages/devtools_app/test/shared/ui/side_panel_test.dart new file mode 100644 index 00000000000..8d003a08080 --- /dev/null +++ b/packages/devtools_app/test/shared/ui/side_panel_test.dart @@ -0,0 +1,102 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// The smallest contrast ratio WCAG 2.1 accepts for body text at level AA. +/// +/// See https://www.w3.org/TR/WCAG21/#contrast-minimum. +const _minimumContrastRatio = 4.5; + +void main() { + setUp(() { + setGlobal(IdeTheme, IdeTheme()); + }); + + group('$SidePanelViewer', () { + // `MarkdownStyleSheet.fromTheme` fills blockquotes with + // `Colors.blue.shade100` but takes the text color from the theme, so a + // release note that opens with a blockquote drew `onSurface` text on light + // blue in the dark theme. + // Regression test for https://github.com/flutter/devtools/issues/9945. + for (final useDarkTheme in [true, false]) { + final themeName = useDarkTheme ? 'dark' : 'light'; + testWidgets('blockquote text is legible in the $themeName theme', ( + tester, + ) async { + const summary = 'Release notes for Dart and Flutter DevTools.'; + final controller = SidePanelController(); + await tester.pumpWidget( + MaterialApp( + theme: themeFor( + isDarkTheme: useDarkTheme, + ideTheme: IdeTheme(), + theme: ThemeData( + useMaterial3: true, + colorScheme: useDarkTheme ? darkColorScheme : lightColorScheme, + ), + ), + home: SidePanelViewer(controller: controller), + ), + ); + controller.markdown.value = '# Release notes\n\n> $summary'; + controller.toggleVisibility(true); + await tester.pumpAndSettle(); + + final summaryFinder = find.byWidgetPredicate( + (widget) => + widget is RichText && widget.text.toPlainText() == summary, + ); + expect(summaryFinder, findsOneWidget); + + final textColor = _colorOfSpan( + tester.widget(summaryFinder).text, + summary, + ); + final fillColor = tester + .widgetList( + find.ancestor( + of: summaryFinder, + matching: find.byType(DecoratedBox), + ), + ) + .map((box) => box.decoration) + .whereType() + .map((decoration) => decoration.color) + .nonNulls + .first; + + expect( + _contrastRatio(textColor!, fillColor), + greaterThanOrEqualTo(_minimumContrastRatio), + ); + }); + } + }); +} + +/// The color the span holding [text] is painted with, or null if [root] holds +/// no such span. +Color? _colorOfSpan(InlineSpan root, String text) { + Color? color; + root.visitChildren((span) { + if (span is TextSpan && span.text == text) { + color = span.style?.color; + return false; + } + return true; + }); + return color; +} + +/// The WCAG contrast ratio between [a] and [b], from 1 (identical) to 21 +/// (black on white). +double _contrastRatio(Color a, Color b) { + final luminances = [a.computeLuminance(), b.computeLuminance()]..sort(); + return (luminances.last + 0.05) / (luminances.first + 0.05); +}