From d7247cb303c25d0011f85f9b2d3687924de3d83d Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Mon, 15 May 2023 11:05:09 -0600 Subject: [PATCH] fix: a tag should not style as link if href is not provided (#1265) --- .../builtins/interactive_element_builtin.dart | 36 ++++---- lib/src/builtins/styled_element_builtin.dart | 1 + lib/src/processing/relative_sizes.dart | 2 + test/elements/a_test.dart | 90 +++++++++++++++++++ test/test_data.dart | 17 ++++ 5 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 test/elements/a_test.dart diff --git a/lib/src/builtins/interactive_element_builtin.dart b/lib/src/builtins/interactive_element_builtin.dart index 19b66ff4fb..a6fc71cde5 100644 --- a/lib/src/builtins/interactive_element_builtin.dart +++ b/lib/src/builtins/interactive_element_builtin.dart @@ -5,33 +5,32 @@ import 'package:flutter_html/src/utils.dart'; import 'package:html/dom.dart' as dom; /// Defines the way an anchor ('a') element is lexed and parsed. +/// +/// An `` element with no `href` attribute is not interactive and is thus +/// not handled by this BuiltIn. class InteractiveElementBuiltIn extends HtmlExtension { const InteractiveElementBuiltIn(); @override Set get supportedTags => {'a'}; + @override + bool matches(ExtensionContext context) { + return supportedTags.contains(context.elementName) && + context.attributes.containsKey("href"); + } + @override StyledElement prepare( ExtensionContext context, List children) { - if (context.attributes.containsKey('href')) { - return InteractiveElement( - name: context.elementName, - children: children, - href: context.attributes['href'], - style: Style( - color: Colors.blue, - textDecoration: TextDecoration.underline, - ), - node: context.node, - elementId: context.id, - ); - } - // When tag have no href, it must be unclickable and without decoration. - return StyledElement( + return InteractiveElement( name: context.elementName, children: children, - style: Style(), + href: context.attributes['href'], + style: Style( + color: Colors.blue, + textDecoration: TextDecoration.underline, + ), node: context.node, elementId: context.id, ); @@ -72,10 +71,7 @@ class InteractiveElementBuiltIn extends HtmlExtension { child: MultipleTapGestureDetector( onTap: onTap, child: GestureDetector( - key: AnchorKey.of( - context.parser.key, - context - .styledElement), //TODO this replaced context.key. Does it work? + key: AnchorKey.of(context.parser.key, context.styledElement), onTap: onTap, child: (childSpan as WidgetSpan).child, ), diff --git a/lib/src/builtins/styled_element_builtin.dart b/lib/src/builtins/styled_element_builtin.dart index 6dad7de21e..edb4590edc 100644 --- a/lib/src/builtins/styled_element_builtin.dart +++ b/lib/src/builtins/styled_element_builtin.dart @@ -13,6 +13,7 @@ class StyledElementBuiltIn extends HtmlExtension { @override Set get supportedTags => { + "a", "abbr", "acronym", "address", diff --git a/lib/src/processing/relative_sizes.dart b/lib/src/processing/relative_sizes.dart index 5731ff6ec0..82b53efd37 100644 --- a/lib/src/processing/relative_sizes.dart +++ b/lib/src/processing/relative_sizes.dart @@ -12,6 +12,8 @@ class RelativeSizesProcessing { /// applies relative calculations static StyledElement _calculateRelativeValues( StyledElement tree, double devicePixelRatio) { + tree.style.fontSize ??= FontSize.medium; + double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value); //If the root element has a rem-based fontSize, then give it the default diff --git a/test/elements/a_test.dart b/test/elements/a_test.dart new file mode 100644 index 0000000000..d5fd2b8270 --- /dev/null +++ b/test/elements/a_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_data.dart'; + +void main() { + testWidgets(' test', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """Hello, world!""", + ), + ), + ); + expect(find.text("Hello, world!", findRichText: true), findsOneWidget); + }); + + testWidgets(' test with href', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """Hello, world!""", + ), + ), + ); + expect(find.text("Hello, world!", findRichText: true), findsOneWidget); + }); + + testWidgets(' with widget child renders', (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + child: Html( + data: """""", + extensions: [ + TagExtension( + tagsToExtend: {"icon"}, + child: const Icon(Icons.check), + ), + ], + ), + ), + ); + expect(find.byIcon(Icons.check), findsOneWidget); + }); + + testWidgets('Tapping test', (WidgetTester tester) async { + String tappedUrl = ""; + + await tester.pumpWidget( + TestApp( + child: Html( + data: """Hello, world!""", + onLinkTap: (url, _, __) { + tappedUrl = url ?? ""; + }, + ), + ), + ); + expect(find.text("Hello, world!", findRichText: true), findsOneWidget); + expect(tappedUrl, equals("")); + await tester.tap(find.text("Hello, world!", findRichText: true)); + expect(tappedUrl, equals("https://example.com")); + }); + + testWidgets('Tapping with widget works', (WidgetTester tester) async { + String tappedUrl = ""; + + await tester.pumpWidget( + TestApp( + child: Html( + data: """""", + onLinkTap: (url, _, __) { + tappedUrl = url ?? ""; + }, + extensions: [ + TagExtension( + tagsToExtend: {"icon"}, + child: const Icon(Icons.check), + ), + ], + ), + ), + ); + expect(find.byIcon(Icons.check), findsOneWidget); + expect(tappedUrl, equals("")); + await tester.tap(find.byIcon(Icons.check)); + expect(tappedUrl, equals("https://example.com")); + }); +} diff --git a/test/test_data.dart b/test/test_data.dart index f9c74ea1ce..0114cf2ed7 100644 --- a/test/test_data.dart +++ b/test/test_data.dart @@ -1,3 +1,20 @@ +import 'package:flutter/material.dart'; + +class TestApp extends StatelessWidget { + final Widget child; + + const TestApp({Key? key, required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: child, + ), + ); + } +} + const testData = { 'a': 'Hello, World!', 'abbr': 'HLO-WRLD',