-
Notifications
You must be signed in to change notification settings - Fork 47
feat(mix): add MixScope.inherit to combine parent and child tokens #853
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,6 +66,65 @@ class MixScope extends InheritedModel<String> { | |
| : _tokens = null, | ||
| orderOfModifiers = null; | ||
|
|
||
| /// Creates a [MixScope] that inherits tokens from the nearest parent [MixScope] | ||
| /// and adds (or overrides with) the given tokens. | ||
| /// | ||
| /// Use this when you need additional Mix tokens (e.g. [ColorToken], | ||
| /// [TextStyleToken], [SpaceToken]) inside a subtree that already has an outer | ||
| /// scope, | ||
| /// without replacing upstream tokens. The resulting scope has a single merged | ||
| /// token map: parent tokens first, then these tokens (so local tokens override | ||
| /// parent when keys collide). | ||
| /// | ||
| /// Keeps the outer scope's tokens available while allowing additive custom | ||
| /// tokens in the same widget subtree. Valid for both light and dark themes | ||
| /// when the parent scope provides theme-dependent tokens. | ||
| static Widget inherit({ | ||
| Map<MixToken, Object>? tokens, | ||
| Map<ColorToken, Color>? colors, | ||
| Map<TextStyleToken, TextStyle>? textStyles, | ||
| Map<SpaceToken, double>? spaces, | ||
| Map<DoubleToken, double>? doubles, | ||
| Map<RadiusToken, Radius>? radii, | ||
| Map<BreakpointToken, Breakpoint>? breakpoints, | ||
| Map<ShadowToken, List<Shadow>>? shadows, | ||
| Map<BoxShadowToken, List<BoxShadow>>? boxShadows, | ||
| Map<BorderSideToken, BorderSide>? borders, | ||
| Map<FontWeightToken, FontWeight>? fontWeights, | ||
| List<Type>? orderOfModifiers, | ||
| required Widget child, | ||
| Key? key, | ||
| }) { | ||
| final childTokens = <MixToken, Object>{ | ||
| ...?tokens, | ||
| ...?colors?.cast<MixToken, Object>(), | ||
| ...?textStyles?.cast<MixToken, Object>(), | ||
| ...?spaces?.cast<MixToken, Object>(), | ||
| ...?doubles?.cast<MixToken, Object>(), | ||
| ...?radii?.cast<MixToken, Object>(), | ||
| ...?breakpoints?.cast<MixToken, Object>(), | ||
| ...?shadows?.cast<MixToken, Object>(), | ||
| ...?boxShadows?.cast<MixToken, Object>(), | ||
| ...?borders?.cast<MixToken, Object>(), | ||
| ...?fontWeights?.cast<MixToken, Object>(), | ||
| }; | ||
|
|
||
| final nestedScope = MixScope._( | ||
| key: key, | ||
| tokens: childTokens, | ||
| orderOfModifiers: orderOfModifiers, | ||
| child: child, | ||
| ); | ||
|
|
||
| return Builder( | ||
| builder: (context) { | ||
| final parent = MixScope.maybeOf(context); | ||
|
|
||
| return MixScope.combine(scopes: [?parent, nestedScope], child: child); | ||
|
||
| }, | ||
| ); | ||
| } | ||
|
|
||
| /// Creates a widget with Material Design tokens pre-configured. | ||
| static Widget withMaterial({ | ||
| Map<MixToken, Object>? tokens, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:mix/mix.dart'; | ||
|
|
||
| // Tokens used to simulate "Fortal" (outer) and custom (inner) scope. | ||
| const _fortalPrimary = ColorToken('fortal.color.primary'); | ||
| const _fortalSurface = ColorToken('fortal.color.surface'); | ||
| const _customGap = SpaceToken('custom.space.gap'); | ||
| const _customAccent = ColorToken('custom.color.accent'); | ||
|
|
||
| void main() { | ||
| group('MixScope.inherit', () { | ||
| testWidgets( | ||
| 'outer scope token and inner custom token both resolve in same subtree', | ||
| (tester) async { | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| home: MixScope( | ||
| colors: { | ||
| _fortalPrimary: Colors.blue, | ||
| _fortalSurface: Colors.white, | ||
| }, | ||
| child: MixScope.inherit( | ||
| spaces: {_customGap: 12.0}, | ||
| colors: {_customAccent: Colors.orange}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| expect( | ||
| scope.getToken(_fortalPrimary, context), | ||
| Colors.blue, | ||
| ); | ||
| expect( | ||
| scope.getToken(_fortalSurface, context), | ||
| Colors.white, | ||
| ); | ||
| expect(scope.getToken(_customGap, context), 12.0); | ||
| expect( | ||
| scope.getToken(_customAccent, context), | ||
| Colors.orange, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'Material scope + MixScope.inherit: both Material and custom tokens resolve', | ||
| (tester) async { | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData.light(), | ||
| home: MixScope.withMaterial( | ||
| child: MixScope.inherit( | ||
| spaces: {_customGap: 8.0}, | ||
| colors: {_customAccent: Colors.purple}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| final md = const MaterialTokens(); | ||
| expect( | ||
| scope.getToken(md.colorScheme.primary, context), | ||
| Theme.of(context).colorScheme.primary, | ||
| ); | ||
| expect( | ||
| scope.getToken(md.colorScheme.surface, context), | ||
| Theme.of(context).colorScheme.surface, | ||
| ); | ||
| expect(scope.getToken(_customGap, context), 8.0); | ||
| expect( | ||
| scope.getToken(_customAccent, context), | ||
| Colors.purple, | ||
| ); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'resolution valid for .light within createFortalScope-like outer scope', | ||
| (tester) async { | ||
| const lightPrimary = Color(0xFF0D47A1); | ||
| const lightSurface = Color(0xFFFAFAFA); | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData.light().copyWith( | ||
| colorScheme: ColorScheme.light( | ||
| primary: lightPrimary, | ||
| surface: lightSurface, | ||
| ), | ||
| ), | ||
| home: MixScope.withMaterial( | ||
| child: MixScope.inherit( | ||
| spaces: {_customGap: 16.0}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| final md = const MaterialTokens(); | ||
| expect( | ||
| scope.getToken(md.colorScheme.primary, context), | ||
| lightPrimary, | ||
| ); | ||
| expect( | ||
| scope.getToken(md.colorScheme.surface, context), | ||
| lightSurface, | ||
| ); | ||
| expect(scope.getToken(_customGap, context), 16.0); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets( | ||
| 'resolution valid for .dark within createFortalScope-like outer scope', | ||
| (tester) async { | ||
| const darkPrimary = Color(0xFF90CAF9); | ||
| const darkSurface = Color(0xFF121212); | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| theme: ThemeData.dark().copyWith( | ||
| colorScheme: ColorScheme.dark( | ||
| primary: darkPrimary, | ||
| surface: darkSurface, | ||
| ), | ||
| ), | ||
| home: MixScope.withMaterial( | ||
| child: MixScope.inherit( | ||
| spaces: {_customGap: 24.0}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| final md = const MaterialTokens(); | ||
| expect( | ||
| scope.getToken(md.colorScheme.primary, context), | ||
| darkPrimary, | ||
| ); | ||
| expect( | ||
| scope.getToken(md.colorScheme.surface, context), | ||
| darkSurface, | ||
| ); | ||
| expect(scope.getToken(_customGap, context), 24.0); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| testWidgets('inner token overrides parent when same key is provided', ( | ||
| tester, | ||
| ) async { | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| home: MixScope( | ||
| colors: {_fortalPrimary: Colors.blue}, | ||
| child: MixScope.inherit( | ||
| colors: {_fortalPrimary: Colors.green}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| expect(scope.getToken(_fortalPrimary, context), Colors.green); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }); | ||
|
|
||
| testWidgets( | ||
| 'MixScope.inherit with no parent still provides only child tokens', | ||
| (tester) async { | ||
| await tester.pumpWidget( | ||
| MaterialApp( | ||
| home: MixScope.inherit( | ||
| colors: {_customAccent: Colors.teal}, | ||
| child: Builder( | ||
| builder: (context) { | ||
| final scope = MixScope.of(context); | ||
| expect(scope.getToken(_customAccent, context), Colors.teal); | ||
| return const SizedBox(); | ||
| }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
| }, | ||
| ); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doc: Invalid Dart — duplicate named parameter
colors:This constructor call has
colors:twice, which is a compile error in Dart. The second entry should be merged into a single map: