From 6b0008568e2a346f5580df93be224fd7011aec50 Mon Sep 17 00:00:00 2001 From: Remi Rousselet Date: Fri, 2 Oct 2020 21:25:58 +0100 Subject: [PATCH] Update to handle latest Flutter --- .github/ISSUE_TEMPLATE/bug_report.md | 16 ++ .github/ISSUE_TEMPLATE/config.yml | 4 + .github/ISSUE_TEMPLATE/example_request.md | 18 ++ .github/ISSUE_TEMPLATE/feature_request.md | 18 ++ .github/workflows/build.yml | 38 ++++ all_lint_rules.yaml | 175 +++++++++++++++++ analysis_options.yaml | 136 ++++++------- example/analysis_options.yaml | 5 + example/lib/contextual_menu.dart | 16 +- example/lib/date_picker.dart | 18 +- example/lib/issues.dart | 10 +- example/lib/main.dart | 8 + example/lib/medium_clap.dart | 12 +- example/pubspec.lock | 44 +++-- lib/src/custom_follower.dart | 51 ++++- lib/src/portal.dart | 124 ++++++++---- pubspec.lock | 228 ++++++++++++++++++++-- pubspec.yaml | 1 + test/widget_test.dart | 128 ++++++------ 19 files changed, 812 insertions(+), 238 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/example_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/workflows/build.yml create mode 100644 all_lint_rules.yaml create mode 100644 example/analysis_options.yaml create mode 100644 example/lib/main.dart diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..666296c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: There is a problem in how provider behaves +title: "" +labels: bug, needs triage +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + + + +**Expected behavior** +A clear and concise description of what you expected to happen. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..214f81a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: false +contact_links: + - name: I have a problem and I need help + url: https://stackoverflow.com/questions/tagged/flutter \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/example_request.md b/.github/ISSUE_TEMPLATE/example_request.md new file mode 100644 index 0000000..20c7db0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/example_request.md @@ -0,0 +1,18 @@ +--- +name: Documentation improvement request +about: >- + Suggest a new example/documentation or ask for clarification about an + existing one. +title: "" +labels: documentation, needs triage +--- + +**Describe what scenario you think is uncovered by the existing examples/articles** +A clear and concise description of the problem that you want explained. + +**Describe why existing examples/articles do not cover this case** +Explain which examples/articles you have seen before making this request, and +why they did not help you with your problem. + +**Additional context** +Add any other context or screenshots about the documentation request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..b6157e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: enhancement, needs triage +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b9a7084 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: [push, pull_request] + +jobs: + flutter: + runs-on: ubuntu-latest + + strategy: + matrix: + package: + - . + - /example + channel: + - dev + + steps: + - uses: actions/checkout@v2 + + - uses: subosito/flutter-action@v1 + with: + channel: ${{ matrix.channel }} + + - name: Install dependencies + run: flutter pub get + working-directory: ${{ matrix.package }} + + - name: Check format + run: flutter format --set-exit-if-changed . + working-directory: ${{ matrix.package }} + + - name: Analyze + run: flutter analyze + working-directory: ${{ matrix.package }} + + - name: Run tests + run: flutter test + working-directory: ${{ matrix.package }} diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 0000000..f62db49 --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,175 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - always_specify_types + - always_use_package_imports + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_as + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null + - avoid_returning_null_for_future + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - close_sinks + - comment_references + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - diagnostic_describe_all_properties + - directives_ordering + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - invariant_booleans + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - sized_box_for_whitespace + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - type_init_formals + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + - unsafe_html + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index ea423c0..9b82daf 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,79 +1,73 @@ +include: all_lint_rules.yaml analyzer: exclude: - "**/*.g.dart" + - "**/*.freezed.dart" strong-mode: implicit-casts: false implicit-dynamic: false + errors: + # Otherwise cause the import of all_lint_rules to warn because of some rules conflicts. + # We explicitly enabled even conflicting rules and are fixing the conflict + # in this file + included_file_warning: ignore + + # Causes false positives (https://github.com/dart-lang/sdk/issues/41571 + top_level_function_literal_block: ignore + linter: rules: - # - public_member_api_docs - - annotate_overrides - - avoid_empty_else - - avoid_function_literals_in_foreach_calls - - avoid_init_to_null - - avoid_null_checks_in_equality_operators - - avoid_relative_lib_imports - - avoid_renaming_method_parameters - - avoid_return_types_on_setters - - avoid_returning_null - - avoid_types_as_parameter_names - - avoid_unused_constructor_parameters - - await_only_futures - - camel_case_types - - cancel_subscriptions - # - cascade_invocations - - comment_references - - constant_identifier_names - - control_flow_in_finally - - directives_ordering - - empty_catches - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - - invariant_booleans - - iterable_contains_unrelated_type - - library_names - - library_prefixes - - list_remove_unrelated_type - # - lines_longer_than_80_chars - - no_adjacent_strings_in_list - - no_duplicate_case_values - - non_constant_identifier_names - - null_closures - - omit_local_variable_types - - only_throw_errors - - overridden_fields - - package_api_docs - - package_names - - package_prefixed_library_names - - prefer_adjacent_string_concatenation - - prefer_collection_literals - - prefer_conditional_assignment - - prefer_const_constructors - - prefer_contains - - prefer_equal_for_default_values - - prefer_final_fields - - prefer_initializing_formals - - prefer_interpolation_to_compose_strings - - prefer_is_empty - - prefer_is_not_empty - - prefer_single_quotes - - prefer_typing_uninitialized_variables - - recursive_getters - - slash_for_doc_comments - - test_types_in_equals - - throw_in_finally - - type_init_formals - - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_const - - unnecessary_getters_setters - - unnecessary_lambdas - - unnecessary_new - - unnecessary_null_aware_assignments - - unnecessary_statements - - unnecessary_this - - unrelated_type_equality_checks - - use_rethrow_when_possible - - valid_regexps + # Personal preference. I don't find it more readable + cascade_invocations: false + + # Conflicts with `prefer_single_quotes` + # Single quotes are easier to type and don't compromise on readability. + prefer_double_quotes: false + + # Conflicts with `omit_local_variable_types` and other rules. + # As per Dart guidelines, we want to avoid unnecessary types to make the code + # more readable. + # See https://dart.dev/guides/language/effective-dart/design#avoid-type-annotating-initialized-local-variables + always_specify_types: false + + # Incompatible with `prefer_final_locals` + # Having immutable local variables makes larger functions more predictible + # so we will use `prefer_final_locals` instead. + unnecessary_final: false + + # Not quite suitable for Flutter, which may have a `build` method with a single + # return, but that return is still complex enough that a "body" is worth it. + prefer_expression_function_bodies: false + + # Conflicts with the convention used by flutter, which puts `Key key` + # and `@required Widget child` last. + always_put_required_named_parameters_first: false + + # `as` is not that bad (especially with the upcoming non-nullable types). + # Explicit exceptions is better than implicit exceptions. + avoid_as: false + + # This project doesn't use Flutter-style todos + flutter_style_todos: false + + # There are situations where we voluntarily want to catch everything, + # especially as a library. + avoid_catches_without_on_clauses: false + + # Boring as it sometimes force a line of 81 characters to be split in two. + # As long as we try to respect that 80 characters limit, going slightly + # above is fine. + lines_longer_than_80_chars: false + + # Conflicts with disabling `implicit-dynamic` + avoid_annotating_with_dynamic: false + + # conflicts with `prefer_relative_imports` + always_use_package_imports: false + + # Disabled for now until we have NNBD as it otherwise conflicts with `missing_return` + no_default_cases: false + + public_member_api_docs: false + prefer_asserts_with_message: false + \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..042ec09 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: ../analysis_options.yaml + +linter: + rules: + diagnostic_describe_all_properties: false diff --git a/example/lib/contextual_menu.dart b/example/lib/contextual_menu.dart index 5f5a95a..c5ea967 100644 --- a/example/lib/contextual_menu.dart +++ b/example/lib/contextual_menu.dart @@ -3,9 +3,11 @@ import 'package:flutter_portal/flutter_portal.dart'; // a contextual menu -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { + const MyApp({Key key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -17,7 +19,7 @@ class MyApp extends StatelessWidget { body: Container( padding: const EdgeInsets.all(10), alignment: Alignment.centerLeft, - child: ContextualMenuExample(), + child: const ContextualMenuExample(), ), ), ); @@ -25,20 +27,20 @@ class MyApp extends StatelessWidget { } class ContextualMenuExample extends StatefulWidget { - ContextualMenuExample({Key key}) : super(key: key); + const ContextualMenuExample({Key key}) : super(key: key); @override _ContextualMenuExampleState createState() => _ContextualMenuExampleState(); } class _ContextualMenuExampleState extends State { - bool showMenu = false; + bool _showMenu = false; @override Widget build(BuildContext context) { return ModalEntry( - visible: showMenu, - onClose: () => setState(() => showMenu = false), + visible: _showMenu, + onClose: () => setState(() => _showMenu = false), childAnchor: Alignment.topRight, menuAnchor: Alignment.topLeft, menu: const Menu( @@ -54,7 +56,7 @@ class _ContextualMenuExampleState extends State { ], ), child: RaisedButton( - onPressed: () => setState(() => showMenu = true), + onPressed: () => setState(() => _showMenu = true), child: const Text('show menu'), ), ); diff --git a/example/lib/date_picker.dart b/example/lib/date_picker.dart index 58906f9..8f2b0c2 100644 --- a/example/lib/date_picker.dart +++ b/example/lib/date_picker.dart @@ -3,7 +3,7 @@ import 'package:flutter_portal/flutter_portal.dart'; // A minimalistic date picker -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class DeclarativeDatePicker extends StatelessWidget { const DeclarativeDatePicker({ @@ -51,23 +51,29 @@ class DeclarativeDatePicker extends StatelessWidget { } class MyApp extends StatelessWidget { + const MyApp({Key key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( builder: (_, child) => Portal(child: child), home: Scaffold( - appBar: AppBar( - title: const Text('Example'), - ), + appBar: AppBar(title: const Text('Example')), body: LayoutBuilder( - builder: (_, __) => - LayoutBuilder(builder: (_, __) => DatePickerUsageExample())), + builder: (_, __) { + return LayoutBuilder(builder: (_, __) { + return const DatePickerUsageExample(); + }); + }, + ), ), ); } } class DatePickerUsageExample extends StatefulWidget { + const DatePickerUsageExample({Key key}) : super(key: key); + @override _DatePickerUsageExampleState createState() => _DatePickerUsageExampleState(); } diff --git a/example/lib/issues.dart b/example/lib/issues.dart index 8d3c4fe..e84c837 100644 --- a/example/lib/issues.dart +++ b/example/lib/issues.dart @@ -5,10 +5,12 @@ import 'package:flutter_portal/flutter_portal.dart'; // This implements Medium's clap button void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key key}) : super(key: key); + @override Widget build(BuildContext context) { debugPaintLayerBordersEnabled = false; @@ -23,13 +25,13 @@ class MyApp extends StatelessWidget { child: PortalEntry( portalAnchor: Alignment.center, childAnchor: Alignment.center, - portal: Material( + portal: const Material( elevation: 9999, shadowColor: Colors.transparent, - child: Container( + child: SizedBox( height: 800, width: 100, - child: const Material( + child: Material( elevation: 2, child: Text('21'), ), diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..33f620c --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'medium_clap.dart'; + +void main() { + runApp(const MyApp()); +} diff --git a/example/lib/medium_clap.dart b/example/lib/medium_clap.dart index 34f2ba9..c66b583 100644 --- a/example/lib/medium_clap.dart +++ b/example/lib/medium_clap.dart @@ -5,9 +5,11 @@ import 'package:flutter_portal/flutter_portal.dart'; // This implements Medium's clap button -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { + const MyApp({Key key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( @@ -16,16 +18,14 @@ class MyApp extends StatelessWidget { appBar: AppBar( title: const Text('Example'), ), - body: Center( - child: ClapButton(), - ), + body: const Center(child: ClapButton()), ), ); } } class ClapButton extends StatefulWidget { - ClapButton({Key key}) : super(key: key); + const ClapButton({Key key}) : super(key: key); @override _ClapButtonState createState() => _ClapButtonState(); @@ -47,7 +47,7 @@ class _ClapButtonState extends State { elevation: 8, borderRadius: BorderRadius.circular(40), child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8), child: Text('$clapCount'), ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 9a6ccd0..cbd4427 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,35 +7,42 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.5.0-nullsafety.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.1" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0-nullsafety.1" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0-nullsafety.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.15.0-nullsafety.3" cupertino_icons: dependency: "direct main" description: @@ -49,7 +56,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.1" flutter: dependency: "direct main" description: flutter @@ -73,21 +80,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.10-nullsafety.1" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0-nullsafety.3" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety.1" sky_engine: dependency: transitive description: flutter @@ -99,55 +106,56 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety.2" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0-nullsafety.1" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.1" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0-nullsafety.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.19-nullsafety.2" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.3" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0-nullsafety.3" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.10.0-110 <=2.11.0-176.0.dev" + flutter: ">=1.21.0-9.0.pre <2.0.0" diff --git a/lib/src/custom_follower.dart b/lib/src/custom_follower.dart index be1c8f2..ee9dd9e 100644 --- a/lib/src/custom_follower.dart +++ b/lib/src/custom_follower.dart @@ -1,7 +1,9 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +/// @nodoc class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { + /// @nodoc const MyCompositedTransformFollower({ Key key, @required this.link, @@ -11,9 +13,16 @@ class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { Widget child, }) : super(key: key, child: child); + /// @nodoc final Alignment childAnchor; + + /// @nodoc final Alignment portalAnchor; + + /// @nodoc final LayerLink link; + + /// @nodoc final Size targetSize; @override @@ -33,9 +42,22 @@ class MyCompositedTransformFollower extends SingleChildRenderObjectWidget { ..childAnchor = childAnchor ..portalAnchor = portalAnchor; } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('childAnchor', childAnchor)); + properties.add( + DiagnosticsProperty('portalAnchor', portalAnchor), + ); + properties.add(DiagnosticsProperty('link', link)); + properties.add(DiagnosticsProperty('targetSize', targetSize)); + } } +/// @nodoc class MyRenderFollowerLayer extends RenderProxyBox { + /// @nodoc MyRenderFollowerLayer({ @required LayerLink link, RenderBox child, @@ -43,6 +65,8 @@ class MyRenderFollowerLayer extends RenderProxyBox { super(child); Alignment _childAnchor; + + /// @nodoc Alignment get childAnchor => _childAnchor; set childAnchor(Alignment childAnchor) { if (childAnchor != _childAnchor) { @@ -52,6 +76,8 @@ class MyRenderFollowerLayer extends RenderProxyBox { } Alignment _portalAnchor; + + /// @nodoc Alignment get portalAnchor => _portalAnchor; set portalAnchor(Alignment portalAnchor) { if (portalAnchor != _portalAnchor) { @@ -60,10 +86,14 @@ class MyRenderFollowerLayer extends RenderProxyBox { } } - LayerLink get link => _link; LayerLink _link; + + /// @nodoc + LayerLink get link => _link; set link(LayerLink value) { - if (_link == value) return; + if (_link == value) { + return; + } if (_link == null || value == null) { markNeedsCompositingBitsUpdate(); markNeedsLayoutForSizedByParentChange(); @@ -72,11 +102,15 @@ class MyRenderFollowerLayer extends RenderProxyBox { markNeedsPaint(); } - Size get targetSize => _targetSize; Size _targetSize; + + /// @nodoc + Size get targetSize => _targetSize; set targetSize(Size value) { - assert(value != null); - if (_targetSize == value) return; + assert(value != null, 'targetSize cannot be null'); + if (_targetSize == value) { + return; + } _targetSize = value; markNeedsPaint(); } @@ -96,6 +130,7 @@ class MyRenderFollowerLayer extends RenderProxyBox { @override FollowerLayer get layer => super.layer as FollowerLayer; + /// @nodoc Matrix4 getCurrentTransform() { return layer?.getLastTransform() ?? Matrix4.identity(); } @@ -113,7 +148,7 @@ class MyRenderFollowerLayer extends RenderProxyBox { return result.addWithPaintTransform( transform: getCurrentTransform(), position: position, - hitTest: (BoxHitTestResult result, Offset position) { + hitTest: (result, position) { return super.hitTestChildren(result, position: position); }, ); @@ -175,7 +210,9 @@ class MyRenderFollowerLayer extends RenderProxyBox { @override void applyPaintTransform(RenderBox child, Matrix4 transform) { - if (link != null) transform.multiply(getCurrentTransform()); + if (link != null) { + transform.multiply(getCurrentTransform()); + } } @override diff --git a/lib/src/portal.dart b/lib/src/portal.dart index 52bc135..dce564b 100644 --- a/lib/src/portal.dart +++ b/lib/src/portal.dart @@ -2,7 +2,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_portal/src/custom_follower.dart'; + +import 'custom_follower.dart'; class Portal extends StatefulWidget { const Portal({Key key, @required this.child}) @@ -16,14 +17,14 @@ class Portal extends StatefulWidget { } class _PortalState extends State { - final _OverlayLink overlayLink = _OverlayLink(); + final _overlayLink = _OverlayLink(); @override Widget build(BuildContext context) { return _PortalLinkScope( - overlayLink: overlayLink, + overlayLink: _overlayLink, child: _PortalTheater( - overlayLink: overlayLink, + overlayLink: _overlayLink, child: widget.child, ), ); @@ -31,7 +32,7 @@ class _PortalState extends State { } class _OverlayLink { - RenderPortalTheater theater; + _RenderPortalTheater theater; BoxConstraints get constraints => theater.constraints; final Set overlays = {}; @@ -40,52 +41,57 @@ class _OverlayLink { class _PortalLinkScope extends InheritedWidget { const _PortalLinkScope({ Key key, - @required this.overlayLink, + @required _OverlayLink overlayLink, @required Widget child, - }) : super(key: key, child: child); + }) : _overlayLink = overlayLink, + super(key: key, child: child); - final _OverlayLink overlayLink; + final _OverlayLink _overlayLink; @override bool updateShouldNotify(_PortalLinkScope oldWidget) { - return oldWidget.overlayLink != overlayLink; + return oldWidget._overlayLink != _overlayLink; } } class _PortalTheater extends SingleChildRenderObjectWidget { const _PortalTheater({ Key key, - @required this.overlayLink, + @required _OverlayLink overlayLink, @required Widget child, - }) : super(key: key, child: child); + }) : _overlayLink = overlayLink, + super(key: key, child: child); - final _OverlayLink overlayLink; + final _OverlayLink _overlayLink; @override - RenderPortalTheater createRenderObject(BuildContext context) { - return RenderPortalTheater(overlayLink); + _RenderPortalTheater createRenderObject(BuildContext context) { + return _RenderPortalTheater(_overlayLink); } @override void updateRenderObject( BuildContext context, - RenderPortalTheater renderObject, + _RenderPortalTheater renderObject, ) { - renderObject.overlayLink = overlayLink; + renderObject.overlayLink = _overlayLink; } } -class RenderPortalTheater extends RenderProxyBox { - RenderPortalTheater(_OverlayLink _overlayLink) { +class _RenderPortalTheater extends RenderProxyBox { + _RenderPortalTheater(_OverlayLink _overlayLink) { overlayLink = _overlayLink; } _OverlayLink _overlayLink; _OverlayLink get overlayLink => _overlayLink; set overlayLink(_OverlayLink value) { - assert(value != null); + assert(value != null, 'overlayLink cannot be null'); if (_overlayLink != value) { - assert(value.theater == null); + assert( + value.theater == null, + 'overlayLink already assigned to another portal', + ); _overlayLink?.theater = null; _overlayLink = value; value.theater = this; @@ -120,10 +126,18 @@ class RenderPortalTheater extends RenderProxyBox { return super.hitTestChildren(result, position: position); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty<_OverlayLink>('overlayLink', overlayLink), + ); + } } class PortalEntry extends StatefulWidget { - PortalEntry({ + const PortalEntry({ Key key, bool visible = true, this.childAnchor, @@ -156,7 +170,8 @@ class PortalEntry extends StatefulWidget { } class _PortalEntryState extends State { - final link = LayerLink(); + final _link = LayerLink(); + @override Widget build(BuildContext context) { final scope = @@ -168,7 +183,7 @@ class _PortalEntryState extends State { if (widget.portal == null || widget.portalAnchor == null) { return _PortalEntryTheater( portal: widget.portal, - overlayLink: scope.overlayLink, + overlayLink: scope._overlayLink, child: widget.child, ); } @@ -176,17 +191,17 @@ class _PortalEntryState extends State { return Stack( children: [ CompositedTransformTarget( - link: link, + link: _link, child: widget.child, ), Positioned.fill( child: LayoutBuilder( builder: (context, constraints) { return _PortalEntryTheater( - overlayLink: scope.overlayLink, + overlayLink: scope._overlayLink, loosen: true, portal: MyCompositedTransformFollower( - link: link, + link: _link, childAnchor: widget.childAnchor, portalAnchor: widget.portalAnchor, targetSize: constraints.biggest, @@ -203,14 +218,14 @@ class _PortalEntryState extends State { } class _PortalEntryTheater extends SingleChildRenderObjectWidget { - _PortalEntryTheater({ + const _PortalEntryTheater({ Key key, @required this.portal, @required this.overlayLink, this.loosen = false, @required Widget child, - }) : assert(child != null), - assert(overlayLink != null), + }) : assert(child != null, 'child cannot be null'), + assert(overlayLink != null, 'overlayLink cannot be null'), super(key: key, child: child); final Widget portal; @@ -233,7 +248,15 @@ class _PortalEntryTheater extends SingleChildRenderObjectWidget { } @override - SingleChildRenderObjectElement createElement() => PortalEntryElement(this); + SingleChildRenderObjectElement createElement() => _PortalEntryElement(this); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('loosen', loosen)); + properties.add( + DiagnosticsProperty<_OverlayLink>('overlayLink', overlayLink), + ); + } } class _RenderPortalEntry extends RenderProxyBox { @@ -336,18 +359,31 @@ class _RenderPortalEntry extends RenderProxyBox { @override void redepthChildren() { super.redepthChildren(); - if (branch != null) redepthChild(branch); + if (branch != null) { + redepthChild(branch); + } } @override void visitChildren(RenderObjectVisitor visitor) { super.visitChildren(visitor); - if (branch != null) visitor(branch); + if (branch != null) { + visitor(branch); + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + .add(DiagnosticsProperty<_OverlayLink>('overlayLink', overlayLink)); + properties.add(DiagnosticsProperty('loosen', loosen)); + properties.add(DiagnosticsProperty('branch', branch)); } } -class PortalEntryElement extends SingleChildRenderObjectElement { - PortalEntryElement(_PortalEntryTheater widget) : super(widget); +class _PortalEntryElement extends SingleChildRenderObjectElement { + _PortalEntryElement(_PortalEntryTheater widget) : super(widget); @override _PortalEntryTheater get widget => super.widget as _PortalEntryTheater; @@ -373,7 +409,7 @@ class PortalEntryElement extends SingleChildRenderObjectElement { } @override - void visitChildren(visitor) { + void visitChildren(ElementVisitor visitor) { // branch first so that it is unmounted before the main tree if (_branch != null) { visitor(_branch); @@ -391,27 +427,31 @@ class PortalEntryElement extends SingleChildRenderObjectElement { } @override - void insertChildRenderObject(RenderObject child, dynamic slot) { + void insertRenderObjectChild(RenderObject child, dynamic slot) { if (slot == _branchSlot) { renderObject.branch = child as RenderBox; } else { - super.insertChildRenderObject(child, slot); + super.insertRenderObjectChild(child, slot); } } @override - void moveChildRenderObject(RenderObject child, dynamic slot) { - if (slot != _branchSlot) { - super.moveChildRenderObject(child, slot); + void moveRenderObjectChild( + RenderObject child, + dynamic oldSlot, + dynamic newSlot, + ) { + if (newSlot != _branchSlot) { + super.moveRenderObjectChild(child, oldSlot, newSlot); } } @override - void removeChildRenderObject(RenderObject child) { + void removeRenderObjectChild(RenderObject child, dynamic slot) { if (child == renderObject.branch) { renderObject.branch = null; } else { - super.removeChildRenderObject(child); + super.removeRenderObjectChild(child, slot); } } } diff --git a/pubspec.lock b/pubspec.lock index 8dc5196..fb8d0db 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,48 +1,146 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.39.17" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.5.0-nullsafety.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.1" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.2" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "7.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0-nullsafety.3" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0-nullsafety.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0-nullsafety.1" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.4.1" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.12" + version: "1.15.0-nullsafety.3" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.5" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.16.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.6" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.1" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.11" flutter: dependency: "direct main" description: flutter @@ -53,94 +151,186 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0+3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.6" + version: "0.12.10-nullsafety.1" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0-nullsafety.3" mockito: dependency: "direct dev" description: name: mockito url: "https://pub.dartlang.org" source: hosted - version: "4.1.1" + version: "4.1.2" + node_interop: + dependency: transitive + description: + name: node_interop + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + node_io: + dependency: transitive + description: + name: node_io + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety.1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+1" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0-nullsafety.2" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.10.0-nullsafety.1" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0-nullsafety.1" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0-nullsafety.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0-nullsafety.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.15" + version: "0.2.19-nullsafety.2" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.6" + version: "1.3.0-nullsafety.3" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0-nullsafety.3" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+15" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" sdks: - dart: ">=2.7.0 <3.0.0" + dart: ">=2.10.0-110 <=2.11.0-176.0.dev" + flutter: ">=1.21.0-9.0.pre <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 92d31c1..189e9ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,6 +7,7 @@ authors: environment: sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.21.0-9.0.pre <2.0.0" dependencies: flutter: diff --git a/test/widget_test.dart b/test/widget_test.dart index 097f0a3..34cde07 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -29,11 +29,11 @@ Future fetchFont() async { /// It needs to be done because flutter_test blocks access to package assets /// (see https://github.com/flutter/flutter/issues/12999). -void main() async { +Future main() async { final fontLoader = FontLoader('Roboto')..addFont(fetchFont()); await fontLoader.load(); - testWidgets('PortalProvider updates child', (WidgetTester tester) async { + testWidgets('PortalProvider updates child', (tester) async { await tester.pumpWidget( const Portal( child: Text( @@ -58,6 +58,7 @@ void main() async { expect(find.text('first'), findsNothing); expect(find.text('second'), findsOneWidget); }); + test('PortalEntry requires a child', () { expect( () => PortalEntry( @@ -67,19 +68,18 @@ void main() async { throwsAssertionError, ); }); + test('PortalEntry requires portal if visible is true ', () { expect( - () => PortalEntry( - visible: true, - portal: null, - child: Container(), - ), + () => PortalEntry(child: Container()), throwsAssertionError, ); }); + test('Portal required either portalBuilder or portal', () { // TODO: }); + testWidgets('Portal synchronously add portals to PortalProvider', (tester) async { final firstChild = @@ -92,7 +92,6 @@ void main() async { child: Portal( child: Center( child: PortalEntry( - visible: true, portal: firstPortal, child: firstChild, ), @@ -112,6 +111,7 @@ void main() async { await expectLater(find.byType(Portal), matchesGoldenFile('mounted.png')); }); + testWidgets( "portals aren't inserted if mounted is false, and visible can be changed any time", (tester) async { @@ -138,10 +138,9 @@ void main() async { final portalChildElement = tester.element(find.text('firstChild')); - portal.value = PortalEntry( - visible: true, - portal: const Text('secondPortal'), - child: const Text('secondChild'), + portal.value = const PortalEntry( + portal: Text('secondPortal'), + child: Text('secondChild'), ); await tester.pump(); @@ -172,6 +171,7 @@ void main() async { reason: 'the child state must be preserved when toggling `visible`', ); }); + testWidgets('Unmounting Portal removes it on PortalProvider synchronously', (tester) async { final child = @@ -184,7 +184,6 @@ void main() async { final portal = ValueNotifier( Center( child: PortalEntry( - visible: true, portal: portalChild, child: Center(child: child), ), @@ -214,12 +213,12 @@ void main() async { await expectLater(find.byType(Portal), matchesGoldenFile('unmounted.png')); }); + testWidgets('throws if no PortalEntry were found', (tester) async { await tester.pumpWidget( - PortalEntry( - visible: true, - portal: const Text('portal', textDirection: TextDirection.ltr), - child: const Text('child', textDirection: TextDirection.ltr), + const PortalEntry( + portal: Text('portal', textDirection: TextDirection.ltr), + child: Text('child', textDirection: TextDirection.ltr), ), ); @@ -229,6 +228,7 @@ void main() async { Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childAnchor: null, portal: Text, child: Text). ''')); }); + testWidgets('hiding two entries at once', (tester) async { final notifier = ValueNotifier(true); @@ -277,6 +277,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA await expectLater(find.byType(Boilerplate), matchesGoldenFile('hiding_multiple_entries/1.png')); }); + testWidgets('visible defaults to true', (tester) async { final child = Container(height: 42, width: 42, color: Colors.red.withOpacity(.5)); @@ -304,6 +305,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA matchesGoldenFile('visible_default.png'), ); }); + testWidgets( 'can insert a portal without rebuilding PortalProvider at the same time', (tester) async { @@ -314,7 +316,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA final portal = Container(height: 42, width: 42, color: Colors.yellow.withOpacity(.5)); - var child = ValueNotifier(first); + final child = ValueNotifier(first); final builder = ValueListenableBuilder( valueListenable: child, builder: (_, child, __) => child, @@ -344,6 +346,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA matchesGoldenFile('mounted_no_rebuild.png'), ); }); + testWidgets('clicking on portal if above child clicks only the portal', (tester) async { var portalClickCount = 0; @@ -371,6 +374,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(portalClickCount, equals(1)); expect(childClickCount, equals(0)); }); + testWidgets('if portal is not above child, we can click on both', (tester) async { var portalClickCount = 0; @@ -408,11 +412,12 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(childClickCount, equals(1)); expect(portalClickCount, equals(1)); }); + testWidgets('alignment/size', (tester) async { const portalKey = Key('portal'); const childKey = Key('child'); await tester.pumpWidget( - Directionality( + const Directionality( textDirection: TextDirection.ltr, child: Portal( child: Align( @@ -420,8 +425,8 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA child: PortalEntry( portalAnchor: Alignment.topLeft, childAnchor: Alignment.bottomLeft, - portal: Container(key: portalKey, height: 42, width: 24), - child: Container(key: childKey, height: 10, width: 10), + portal: SizedBox(key: portalKey, height: 42, width: 24), + child: SizedBox(key: childKey, height: 10, width: 10), ), ), ), @@ -444,7 +449,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA ); await tester.pumpWidget( - Directionality( + const Directionality( textDirection: TextDirection.ltr, child: Portal( child: Align( @@ -452,8 +457,8 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA child: PortalEntry( portalAnchor: Alignment.topRight, childAnchor: Alignment.bottomRight, - portal: Container(key: portalKey, height: 24, width: 42), - child: Container(key: childKey, height: 20, width: 20), + portal: SizedBox(key: portalKey, height: 24, width: 42), + child: SizedBox(key: childKey, height: 20, width: 20), ), ), ), @@ -479,7 +484,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA ); await tester.pumpWidget( - Directionality( + const Directionality( textDirection: TextDirection.ltr, child: Portal( child: Align( @@ -487,8 +492,8 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA child: PortalEntry( childAnchor: Alignment.topRight, portalAnchor: Alignment.bottomRight, - portal: Container(key: portalKey, height: 20, width: 20), - child: Container(key: childKey, height: 10, width: 10), + portal: SizedBox(key: portalKey, height: 20, width: 20), + child: SizedBox(key: childKey, height: 10, width: 10), ), ), ), @@ -519,14 +524,13 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA const childKey = Key('child'); await tester.pumpWidget( - Directionality( + const Directionality( textDirection: TextDirection.ltr, child: Portal( child: Align( - alignment: Alignment.center, child: PortalEntry( - portal: Container(key: portalKey, height: 20, width: 20), - child: Container(key: childKey, height: 10, width: 10), + portal: SizedBox(key: portalKey, height: 20, width: 20), + child: SizedBox(key: childKey, height: 10, width: 10), ), ), ), @@ -551,8 +555,9 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA equals(Offset.zero), ); }); + testWidgets('click works when switching between anchor/fill', (tester) async { - final child = const Text('a', textDirection: TextDirection.ltr); + const child = Text('a', textDirection: TextDirection.ltr); const portalKey = Key('portal'); const childKey = Key('child'); @@ -668,24 +673,25 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(portalClickCount, equals(2)); expect(childClickCount, equals(0)); }); + testWidgets('anchor not null then null still clicks', (tester) async { var didClickChild = false; var didClickPortal = false; final portal = GestureDetector( onTap: () => didClickPortal = true, - child: Container( + child: const SizedBox( height: 40, width: 40, - child: const Text('portal'), + child: Text('portal'), ), ); final child = GestureDetector( onTap: () => didClickChild = true, - child: Container( + child: const SizedBox( height: 40, width: 40, - child: const Text('child'), + child: Text('child'), ), ); @@ -755,6 +761,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA await tester.tap(find.text('portal')); expect(didClickPortal, isTrue); }); + testWidgets('PortalEntry target its generic parameter', (tester) async { // final portalKey = UniqueKey(); @@ -820,9 +827,9 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA await tester.pumpWidget( MaterialApp( builder: (_, child) => Portal(child: child), - home: PortalEntry( - portal: const Text('portal'), - child: const Text('child'), + home: const PortalEntry( + portal: Text('portal'), + child: Text('child'), ), ), ); @@ -830,14 +837,15 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(find.text('child'), findsOneWidget); expect(find.text('portal'), findsOneWidget); }); + testWidgets('Portal can be added above navigator but under CupertinoApp', (tester) async { await tester.pumpWidget( CupertinoApp( builder: (_, child) => Portal(child: child), - home: PortalEntry( - portal: const Text('portal'), - child: const Text('child'), + home: const PortalEntry( + portal: Text('portal'), + child: Text('child'), ), ), ); @@ -845,10 +853,10 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(find.text('child'), findsOneWidget); expect(find.text('portal'), findsOneWidget); }); + testWidgets('if one anchor is null, the other one must be', (tester) async { expect( () => PortalEntry( - portalAnchor: null, childAnchor: Alignment.center, portal: Container(), child: Container(), @@ -858,7 +866,6 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect( () => PortalEntry( portalAnchor: Alignment.center, - childAnchor: null, portal: Container(), child: Container(), ), @@ -866,8 +873,6 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA ); PortalEntry( - childAnchor: null, - portalAnchor: null, portal: Container(), child: Container(), ); @@ -926,14 +931,15 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA ]); verifyNoMoreInteractions(entryBuild); }); + testWidgets('layout builder between portal and entry on first build', (tester) async { await tester.pumpWidget(Portal( child: LayoutBuilder( builder: (_, __) { - return PortalEntry( - portal: const Text('portal', textDirection: TextDirection.ltr), - child: const Text('child', textDirection: TextDirection.ltr), + return const PortalEntry( + portal: Text('portal', textDirection: TextDirection.ltr), + child: Text('child', textDirection: TextDirection.ltr), ); }, ), @@ -942,6 +948,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(find.text('child'), findsOneWidget); expect(find.text('portal'), findsOneWidget); }); + testWidgets( 'layout builder between portal and entry without rebuilding portl', (tester) async { @@ -964,9 +971,9 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA notifier.value = LayoutBuilder( builder: (_, __) { - return PortalEntry( - portal: const Text('portal', textDirection: TextDirection.ltr), - child: const Text('child2', textDirection: TextDirection.ltr), + return const PortalEntry( + portal: Text('portal', textDirection: TextDirection.ltr), + child: Text('child2', textDirection: TextDirection.ltr), ); }, ); @@ -976,6 +983,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(find.text('child2'), findsOneWidget); expect(find.text('portal'), findsOneWidget); }); + testWidgets('handles reparenting with GlobalKey', (tester) async { // final firstPortal = UniqueKey(); // final secondPortal = UniqueKey(); @@ -1062,21 +1070,23 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA // expect(secondPortalElement.theater.renderObject.builders.length, 0); // expect(secondPortalElement.theater.renderObject.childCount, 0); }); - // TODO: clip overflow + + testWidgets('clip overflow', (tester) async {}, skip: true); + testWidgets('can have multiple portals', (tester) async { - var topLeft = PortalEntry( + final topLeft = PortalEntry( portal: const Align(alignment: Alignment.topLeft), child: Container(), ); - var topRight = PortalEntry( + final topRight = PortalEntry( portal: const Align(alignment: Alignment.topRight), child: Container(), ); - var bottomRight = PortalEntry( + final bottomRight = PortalEntry( portal: const Align(alignment: Alignment.bottomRight), child: Container(), ); - var bottomLeft = PortalEntry( + final bottomLeft = PortalEntry( portal: const Align(alignment: Alignment.bottomLeft), child: Container(), ); @@ -1144,6 +1154,7 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA expect(didClickFirst, isFalse); expect(didClickSecond, isTrue); }); + testWidgets('portals paints in order of addition (last paints last)', (tester) async { tester.binding.window.physicalSizeTestValue = const Size(300, 300); @@ -1176,9 +1187,10 @@ Error: Could not find a Portal above this PortalEntry(portalAnchor: null, childA } class Boilerplate extends StatelessWidget { + const Boilerplate({Key key, this.child}) : super(key: key); + final Widget child; - const Boilerplate({Key key, this.child}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp(