Skip to content

Commit d7247cb

Browse files
fix: a tag should not style as link if href is not provided (#1265)
1 parent 2ffa1dd commit d7247cb

File tree

5 files changed

+126
-20
lines changed

5 files changed

+126
-20
lines changed

lib/src/builtins/interactive_element_builtin.dart

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,32 @@ import 'package:flutter_html/src/utils.dart';
55
import 'package:html/dom.dart' as dom;
66

77
/// Defines the way an anchor ('a') element is lexed and parsed.
8+
///
9+
/// An `<a>` element with no `href` attribute is not interactive and is thus
10+
/// not handled by this BuiltIn.
811
class InteractiveElementBuiltIn extends HtmlExtension {
912
const InteractiveElementBuiltIn();
1013

1114
@override
1215
Set<String> get supportedTags => {'a'};
1316

17+
@override
18+
bool matches(ExtensionContext context) {
19+
return supportedTags.contains(context.elementName) &&
20+
context.attributes.containsKey("href");
21+
}
22+
1423
@override
1524
StyledElement prepare(
1625
ExtensionContext context, List<StyledElement> children) {
17-
if (context.attributes.containsKey('href')) {
18-
return InteractiveElement(
19-
name: context.elementName,
20-
children: children,
21-
href: context.attributes['href'],
22-
style: Style(
23-
color: Colors.blue,
24-
textDecoration: TextDecoration.underline,
25-
),
26-
node: context.node,
27-
elementId: context.id,
28-
);
29-
}
30-
// When <a> tag have no href, it must be unclickable and without decoration.
31-
return StyledElement(
26+
return InteractiveElement(
3227
name: context.elementName,
3328
children: children,
34-
style: Style(),
29+
href: context.attributes['href'],
30+
style: Style(
31+
color: Colors.blue,
32+
textDecoration: TextDecoration.underline,
33+
),
3534
node: context.node,
3635
elementId: context.id,
3736
);
@@ -72,10 +71,7 @@ class InteractiveElementBuiltIn extends HtmlExtension {
7271
child: MultipleTapGestureDetector(
7372
onTap: onTap,
7473
child: GestureDetector(
75-
key: AnchorKey.of(
76-
context.parser.key,
77-
context
78-
.styledElement), //TODO this replaced context.key. Does it work?
74+
key: AnchorKey.of(context.parser.key, context.styledElement),
7975
onTap: onTap,
8076
child: (childSpan as WidgetSpan).child,
8177
),

lib/src/builtins/styled_element_builtin.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class StyledElementBuiltIn extends HtmlExtension {
1313

1414
@override
1515
Set<String> get supportedTags => {
16+
"a",
1617
"abbr",
1718
"acronym",
1819
"address",

lib/src/processing/relative_sizes.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class RelativeSizesProcessing {
1212
/// applies relative calculations
1313
static StyledElement _calculateRelativeValues(
1414
StyledElement tree, double devicePixelRatio) {
15+
tree.style.fontSize ??= FontSize.medium;
16+
1517
double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value);
1618

1719
//If the root element has a rem-based fontSize, then give it the default

test/elements/a_test.dart

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_html/flutter_html.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
5+
import '../test_data.dart';
6+
7+
void main() {
8+
testWidgets('<a> test', (WidgetTester tester) async {
9+
await tester.pumpWidget(
10+
TestApp(
11+
child: Html(
12+
data: """<a>Hello, world!</a>""",
13+
),
14+
),
15+
);
16+
expect(find.text("Hello, world!", findRichText: true), findsOneWidget);
17+
});
18+
19+
testWidgets('<a> test with href', (WidgetTester tester) async {
20+
await tester.pumpWidget(
21+
TestApp(
22+
child: Html(
23+
data: """<a href="https://example.com">Hello, world!</a>""",
24+
),
25+
),
26+
);
27+
expect(find.text("Hello, world!", findRichText: true), findsOneWidget);
28+
});
29+
30+
testWidgets('<a> with widget child renders', (WidgetTester tester) async {
31+
await tester.pumpWidget(
32+
TestApp(
33+
child: Html(
34+
data: """<a href="https://example.com"><icon></icon></a>""",
35+
extensions: [
36+
TagExtension(
37+
tagsToExtend: {"icon"},
38+
child: const Icon(Icons.check),
39+
),
40+
],
41+
),
42+
),
43+
);
44+
expect(find.byIcon(Icons.check), findsOneWidget);
45+
});
46+
47+
testWidgets('Tapping <a> test', (WidgetTester tester) async {
48+
String tappedUrl = "";
49+
50+
await tester.pumpWidget(
51+
TestApp(
52+
child: Html(
53+
data: """<a href="https://example.com">Hello, world!</a>""",
54+
onLinkTap: (url, _, __) {
55+
tappedUrl = url ?? "";
56+
},
57+
),
58+
),
59+
);
60+
expect(find.text("Hello, world!", findRichText: true), findsOneWidget);
61+
expect(tappedUrl, equals(""));
62+
await tester.tap(find.text("Hello, world!", findRichText: true));
63+
expect(tappedUrl, equals("https://example.com"));
64+
});
65+
66+
testWidgets('Tapping <a> with widget works', (WidgetTester tester) async {
67+
String tappedUrl = "";
68+
69+
await tester.pumpWidget(
70+
TestApp(
71+
child: Html(
72+
data: """<a href="https://example.com"><icon></icon></a>""",
73+
onLinkTap: (url, _, __) {
74+
tappedUrl = url ?? "";
75+
},
76+
extensions: [
77+
TagExtension(
78+
tagsToExtend: {"icon"},
79+
child: const Icon(Icons.check),
80+
),
81+
],
82+
),
83+
),
84+
);
85+
expect(find.byIcon(Icons.check), findsOneWidget);
86+
expect(tappedUrl, equals(""));
87+
await tester.tap(find.byIcon(Icons.check));
88+
expect(tappedUrl, equals("https://example.com"));
89+
});
90+
}

test/test_data.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
import 'package:flutter/material.dart';
2+
3+
class TestApp extends StatelessWidget {
4+
final Widget child;
5+
6+
const TestApp({Key? key, required this.child}) : super(key: key);
7+
8+
@override
9+
Widget build(BuildContext context) {
10+
return MaterialApp(
11+
home: Scaffold(
12+
body: child,
13+
),
14+
);
15+
}
16+
}
17+
118
const testData = <String, String>{
219
'a': '<a>Hello, World!</a>',
320
'abbr': '<abbr>HLO-WRLD</abbr>',

0 commit comments

Comments
 (0)