diff --git a/packages/material_ui/lib/src/bottom_sheet.dart b/packages/material_ui/lib/src/bottom_sheet.dart index 52a81119732..1d235479b42 100644 --- a/packages/material_ui/lib/src/bottom_sheet.dart +++ b/packages/material_ui/lib/src/bottom_sheet.dart @@ -971,7 +971,9 @@ class ModalBottomSheetRoute extends PopupRoute { /// Specifies the color of the modal barrier that darkens everything below the /// bottom sheet. /// - /// Defaults to `Colors.black54` if not provided. + /// If this is null, then [BottomSheetThemeData.modalBarrierColor] is used. + /// If that is also null, then [ColorScheme.scrim] is used with an opacity + /// matching [Colors.black54]. final Color? modalBarrierColor; /// Specifies whether the bottom sheet will be dismissed @@ -1103,7 +1105,17 @@ class ModalBottomSheetRoute extends PopupRoute { final String? barrierLabel; @override - Color get barrierColor => modalBarrierColor ?? Colors.black54; + Color get barrierColor { + if (modalBarrierColor != null) { + return modalBarrierColor!; + } + if (navigator != null) { + final ThemeData theme = Theme.of(navigator!.context); + return theme.bottomSheetTheme.modalBarrierColor ?? + theme.colorScheme.scrim.withValues(alpha: Colors.black54.a); + } + return Colors.black54; + } AnimationController? _animationController; @@ -1343,7 +1355,10 @@ Future showModalBottomSheet({ clipBehavior: clipBehavior, constraints: constraints, isDismissible: isDismissible, - modalBarrierColor: barrierColor ?? Theme.of(context).bottomSheetTheme.modalBarrierColor, + modalBarrierColor: + barrierColor ?? + Theme.of(context).bottomSheetTheme.modalBarrierColor ?? + Theme.of(context).colorScheme.scrim.withValues(alpha: Colors.black54.a), enableDrag: enableDrag, showDragHandle: showDragHandle, settings: routeSettings, diff --git a/packages/material_ui/lib/src/dialog.dart b/packages/material_ui/lib/src/dialog.dart index 63f144562a9..6f9417f3826 100644 --- a/packages/material_ui/lib/src/dialog.dart +++ b/packages/material_ui/lib/src/dialog.dart @@ -1569,10 +1569,11 @@ class _DialogContentPage extends Page { /// /// The `barrierColor` argument is used to specify the color of the modal /// barrier that darkens everything below the dialog. If `null` the `barrierColor` -/// field from `DialogThemeData` is used. If that is `null` the default color -/// `Colors.black54` is used. If windowing is enabled via `flutter config -/// --enable-windowing`, then this argument is ignored as dialogs are displayed -/// in their own windows which do not have a modal barrier. +/// field from `DialogThemeData` is used. If that is also `null`, +/// [ColorScheme.scrim] is used with an opacity matching [Colors.black54]. +/// If windowing is enabled via `flutter config --enable-windowing`, then this +/// argument is ignored as dialogs are displayed in their own windows which do +/// not have a modal barrier. /// /// The `useSafeArea` argument is used to indicate if the dialog should only /// display in 'safe' areas of the screen not used by the operating system @@ -1706,7 +1707,7 @@ Future showDialog({ barrierColor ?? DialogTheme.of(context).barrierColor ?? Theme.of(context).dialogTheme.barrierColor ?? - Colors.black54, + Theme.of(context).colorScheme.scrim.withValues(alpha: Colors.black54.a), barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, useSafeArea: useSafeArea, @@ -1843,8 +1844,9 @@ bool _debugIsActive(BuildContext context) { /// barrier will dismiss the dialog. It is `true` by default and cannot be `null`. /// /// The `barrierColor` argument is used to specify the color of the modal -/// barrier that darkens everything below the dialog. If `null`, the default -/// color `Colors.black54` is used. +/// barrier that darkens everything below the dialog. If `null`, the +/// `barrierColor` field from [DialogThemeData] is used. If that is also `null`, +/// [ColorScheme.scrim] is used with an opacity matching [Colors.black54]. /// /// The `useSafeArea` argument is used to indicate if the dialog should only /// display in 'safe' areas of the screen not used by the operating system @@ -1872,7 +1874,7 @@ class DialogRoute extends RawDialogRoute { required BuildContext context, required WidgetBuilder builder, CapturedThemes? themes, - super.barrierColor = Colors.black54, + Color? barrierColor, super.barrierDismissible, String? barrierLabel, bool useSafeArea = true, @@ -1884,6 +1886,11 @@ class DialogRoute extends RawDialogRoute { AnimationStyle? animationStyle, }) : _animationStyle = animationStyle, super( + barrierColor: + barrierColor ?? + DialogTheme.of(context).barrierColor ?? + Theme.of(context).dialogTheme.barrierColor ?? + Theme.of(context).colorScheme.scrim.withValues(alpha: Colors.black54.a), pageBuilder: ( BuildContext buildContext, diff --git a/packages/material_ui/lib/src/drawer.dart b/packages/material_ui/lib/src/drawer.dart index f2d8b55ce52..bf2a389aa78 100644 --- a/packages/material_ui/lib/src/drawer.dart +++ b/packages/material_ui/lib/src/drawer.dart @@ -398,7 +398,8 @@ class DrawerController extends StatefulWidget { /// a drawer is open. /// /// If this is null, then [DrawerThemeData.scrimColor] is used. If that - /// is also null, then it defaults to [Colors.black54]. + /// is also null, then [ColorScheme.scrim] is used with an opacity matching + /// [Colors.black54]. final Color? scrimColor; /// Determines if the [Drawer] can be opened with a drag gesture. @@ -713,7 +714,9 @@ class DrawerControllerState extends State with SingleTickerPro }; final Color scrimColor = - widget.scrimColor ?? DrawerTheme.of(context).scrimColor ?? Colors.black54; + widget.scrimColor ?? + DrawerTheme.of(context).scrimColor ?? + Theme.of(context).colorScheme.scrim.withValues(alpha: Colors.black54.a); final Color effectiveScrimColor = scrimColor.withValues( alpha: scrimColor.a * _controller.value, ); diff --git a/packages/material_ui/lib/src/scaffold.dart b/packages/material_ui/lib/src/scaffold.dart index 168fc5b7ab9..f7e1826caf4 100644 --- a/packages/material_ui/lib/src/scaffold.dart +++ b/packages/material_ui/lib/src/scaffold.dart @@ -8,6 +8,7 @@ /// @docImport 'bottom_app_bar.dart'; /// @docImport 'bottom_navigation_bar.dart'; /// @docImport 'bottom_sheet_theme.dart'; +/// @docImport 'color_scheme.dart'; /// @docImport 'drawer_theme.dart'; /// @docImport 'icon_button.dart'; /// @docImport 'tab_controller.dart'; @@ -1955,7 +1956,8 @@ class Scaffold extends StatefulWidget { /// The color to use for the scrim that obscures primary content while a drawer is open. /// /// If this is null, then [DrawerThemeData.scrimColor] is used. If that - /// is also null, then it defaults to [Colors.black54]. + /// is also null, then [ColorScheme.scrim] is used with an opacity matching + /// [Colors.black54]. final Color? drawerScrimColor; /// A builder for the widget that obscures primary content while a bottom sheet is open. diff --git a/packages/material_ui/pending_changelogs/change_2026_09_04_scrim_color.yaml b/packages/material_ui/pending_changelogs/change_2026_09_04_scrim_color.yaml new file mode 100644 index 00000000000..bb89f151361 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_09_04_scrim_color.yaml @@ -0,0 +1,3 @@ +changelog: | + - Uses `ColorScheme.scrim` as the default fallback for dialog, modal bottom sheet, and drawer barrier/scrim colors. +version: patch diff --git a/packages/material_ui/test/bottom_sheet_test.dart b/packages/material_ui/test/bottom_sheet_test.dart index 59a118eb777..83d47123e31 100644 --- a/packages/material_ui/test/bottom_sheet_test.dart +++ b/packages/material_ui/test/bottom_sheet_test.dart @@ -1018,6 +1018,222 @@ void main() { expect(modalBarrier.color, barrierColor); }); + testWidgets('Modal bottom sheet barrier color uses ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + late BuildContext savedContext; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return const SizedBox.expand(); + }, + ), + ), + ); + + unawaited( + showModalBottomSheet( + context: savedContext, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, scrim.withValues(alpha: Colors.black54.a)); + }); + + testWidgets('BottomSheetTheme.modalBarrierColor takes precedence over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color themeBarrierColor = Colors.blue; + late BuildContext savedContext; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + bottomSheetTheme: const BottomSheetThemeData(modalBarrierColor: themeBarrierColor), + ), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return const SizedBox.expand(); + }, + ), + ), + ); + + unawaited( + showModalBottomSheet( + context: savedContext, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, themeBarrierColor); + }); + + testWidgets('Explicit barrierColor takes precedence over ColorScheme.scrim for bottom sheets', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color explicitBarrierColor = Colors.green; + late BuildContext savedContext; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + bottomSheetTheme: const BottomSheetThemeData(modalBarrierColor: Colors.blue), + ), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return const SizedBox.expand(); + }, + ), + ), + ); + + unawaited( + showModalBottomSheet( + context: savedContext, + barrierColor: explicitBarrierColor, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, explicitBarrierColor); + }); + + testWidgets('Bottom sheet ColorScheme.scrim alpha is normalized to Colors.black54 opacity', ( + WidgetTester tester, + ) async { + final Color scrim = Colors.red.withValues(alpha: 0.2); + late BuildContext savedContext; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Builder( + builder: (BuildContext context) { + savedContext = context; + return const SizedBox.expand(); + }, + ), + ), + ); + + unawaited( + showModalBottomSheet( + context: savedContext, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, scrim.withValues(alpha: Colors.black54.a)); + }); + + testWidgets('ModalBottomSheetRoute barrier color uses ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () { + Navigator.of(context).push( + ModalBottomSheetRoute( + isScrollControlled: false, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + }, + child: const Text('Open'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, scrim.withValues(alpha: Colors.black54.a)); + }); + + testWidgets( + 'ModalBottomSheetRoute uses BottomSheetTheme.modalBarrierColor over ColorScheme.scrim', + (WidgetTester tester) async { + const Color scrim = Colors.red; + const Color themeBarrierColor = Colors.blue; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + bottomSheetTheme: const BottomSheetThemeData(modalBarrierColor: themeBarrierColor), + ), + home: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () { + Navigator.of(context).push( + ModalBottomSheetRoute( + isScrollControlled: false, + builder: (BuildContext context) { + return const SizedBox(height: 200, child: Text('BottomSheet')); + }, + ), + ); + }, + child: const Text('Open'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + + final ModalBarrier modalBarrier = tester.widget(find.byType(ModalBarrier).last); + expect(modalBarrier.color, themeBarrierColor); + }, + ); + testWidgets('Material3 - BottomSheet uses fallback values', (WidgetTester tester) async { const Color surfaceColor = Colors.pink; const Color surfaceTintColor = Colors.blue; diff --git a/packages/material_ui/test/dialog_test.dart b/packages/material_ui/test/dialog_test.dart index ab59e78b108..d0d80f32170 100644 --- a/packages/material_ui/test/dialog_test.dart +++ b/packages/material_ui/test/dialog_test.dart @@ -603,6 +603,186 @@ void main() { expect(tester.widget(find.byType(ModalBarrier).last).color, Colors.pink); }); + testWidgets('Barrier color uses ColorScheme.scrim', (WidgetTester tester) async { + const Color scrim = Colors.red; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: const Center(child: Text('Test')), + ), + ); + final BuildContext context = tester.element(find.text('Test')); + + unawaited( + showDialog( + context: context, + builder: (BuildContext context) { + return const Text('Dialog'); + }, + ), + ); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(ModalBarrier).last).color, + scrim.withValues(alpha: Colors.black54.a), + ); + }); + + testWidgets('DialogTheme.barrierColor takes precedence over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color themeBarrierColor = Colors.blue; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + dialogTheme: const DialogThemeData(barrierColor: themeBarrierColor), + ), + home: const Center(child: Text('Test')), + ), + ); + final BuildContext context = tester.element(find.text('Test')); + + unawaited( + showDialog( + context: context, + builder: (BuildContext context) { + return const Text('Dialog'); + }, + ), + ); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(ModalBarrier).last).color, themeBarrierColor); + }); + + testWidgets('Explicit barrierColor takes precedence over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color explicitBarrierColor = Colors.green; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + dialogTheme: const DialogThemeData(barrierColor: Colors.blue), + ), + home: const Center(child: Text('Test')), + ), + ); + final BuildContext context = tester.element(find.text('Test')); + + unawaited( + showDialog( + context: context, + barrierColor: explicitBarrierColor, + builder: (BuildContext context) { + return const Text('Dialog'); + }, + ), + ); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(ModalBarrier).last).color, explicitBarrierColor); + }); + + testWidgets('ColorScheme.scrim alpha is normalized to Colors.black54 opacity', ( + WidgetTester tester, + ) async { + final Color scrim = Colors.red.withValues(alpha: 0.2); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: const Center(child: Text('Test')), + ), + ); + final BuildContext context = tester.element(find.text('Test')); + + unawaited( + showDialog( + context: context, + builder: (BuildContext context) { + return const Text('Dialog'); + }, + ), + ); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(ModalBarrier).last).color, + scrim.withValues(alpha: Colors.black54.a), + ); + }); + + testWidgets('DialogRoute barrier color uses ColorScheme.scrim', (WidgetTester tester) async { + const Color scrim = Colors.red; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () { + Navigator.of(context).push( + DialogRoute( + context: context, + builder: (BuildContext context) => const Text('Dialog'), + ), + ); + }, + child: const Text('Open'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + expect( + tester.widget(find.byType(ModalBarrier).last).color, + scrim.withValues(alpha: Colors.black54.a), + ); + }); + + testWidgets('DialogRoute uses DialogTheme.barrierColor over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color themeBarrierColor = Colors.blue; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + dialogTheme: const DialogThemeData(barrierColor: themeBarrierColor), + ), + home: Builder( + builder: (BuildContext context) { + return TextButton( + onPressed: () { + Navigator.of(context).push( + DialogRoute( + context: context, + builder: (BuildContext context) => const Text('Dialog'), + ), + ); + }, + child: const Text('Open'), + ); + }, + ), + ), + ); + + await tester.tap(find.text('Open')); + await tester.pumpAndSettle(); + expect(tester.widget(find.byType(ModalBarrier).last).color, themeBarrierColor); + }); + testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const buttonText = 'A button covered by dialog overlay'; diff --git a/packages/material_ui/test/drawer_test.dart b/packages/material_ui/test/drawer_test.dart index 5bbfd15379f..180e7923228 100644 --- a/packages/material_ui/test/drawer_test.dart +++ b/packages/material_ui/test/drawer_test.dart @@ -344,6 +344,155 @@ void main() { await checkScrim(const Color(0xFF323232)); }); + testWidgets('Drawer scrim uses ColorScheme.scrim', (WidgetTester tester) async { + const Color scrim = Colors.red; + final Color expectedScrim = scrim.withValues(alpha: Colors.black54.a); + final scaffoldKey = GlobalKey(); + + Widget getScrim() { + return tester + .widget( + find.descendant( + of: find.byType(DrawerController), + matching: find.byWidgetPredicate((Widget widget) { + return widget is Semantics && widget.properties.label == 'Dismiss'; + }), + ), + ) + .child!; + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Scaffold( + key: scaffoldKey, + drawer: const Drawer(child: SizedBox.expand()), + ), + ), + ); + + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + expect((getScrim() as ColoredBox).color, isSameColorAs(expectedScrim)); + }); + + testWidgets('DrawerTheme.scrimColor takes precedence over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color themeScrimColor = Colors.blue; + final scaffoldKey = GlobalKey(); + + Widget getScrim() { + return tester + .widget( + find.descendant( + of: find.byType(DrawerController), + matching: find.byWidgetPredicate((Widget widget) { + return widget is Semantics && widget.properties.label == 'Dismiss'; + }), + ), + ) + .child!; + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + drawerTheme: const DrawerThemeData(scrimColor: themeScrimColor), + ), + home: Scaffold( + key: scaffoldKey, + drawer: const Drawer(child: SizedBox.expand()), + ), + ), + ); + + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + expect((getScrim() as ColoredBox).color, isSameColorAs(themeScrimColor)); + }); + + testWidgets('Scaffold.drawerScrimColor takes precedence over ColorScheme.scrim', ( + WidgetTester tester, + ) async { + const Color scrim = Colors.red; + const Color drawerScrimColor = Colors.green; + final scaffoldKey = GlobalKey(); + + Widget getScrim() { + return tester + .widget( + find.descendant( + of: find.byType(DrawerController), + matching: find.byWidgetPredicate((Widget widget) { + return widget is Semantics && widget.properties.label == 'Dismiss'; + }), + ), + ) + .child!; + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + drawerTheme: const DrawerThemeData(scrimColor: Colors.blue), + ), + home: Scaffold( + key: scaffoldKey, + drawerScrimColor: drawerScrimColor, + drawer: const Drawer(child: SizedBox.expand()), + ), + ), + ); + + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + expect((getScrim() as ColoredBox).color, isSameColorAs(drawerScrimColor)); + }); + + testWidgets('Drawer ColorScheme.scrim alpha is normalized to Colors.black54 opacity', ( + WidgetTester tester, + ) async { + final Color scrim = Colors.red.withValues(alpha: 0.2); + final Color expectedScrim = scrim.withValues(alpha: Colors.black54.a); + final scaffoldKey = GlobalKey(); + + Widget getScrim() { + return tester + .widget( + find.descendant( + of: find.byType(DrawerController), + matching: find.byWidgetPredicate((Widget widget) { + return widget is Semantics && widget.properties.label == 'Dismiss'; + }), + ), + ) + .child!; + } + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple, scrim: scrim), + ), + home: Scaffold( + key: scaffoldKey, + drawer: const Drawer(child: SizedBox.expand()), + ), + ), + ); + + scaffoldKey.currentState!.openDrawer(); + await tester.pumpAndSettle(); + expect((getScrim() as ColoredBox).color, isSameColorAs(expectedScrim)); + }); + testWidgets('Open/close drawers by flinging', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(