Skip to content

Commit 7a462d4

Browse files
authored
Migrate page feedback component to Jaspr (#6874)
Contributes to #6853
1 parent eb57b16 commit 7a462d4

File tree

8 files changed

+175
-139
lines changed

8 files changed

+175
-139
lines changed

site/lib/_sass/components/_trailing.scss

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,39 +30,13 @@
3030
}
3131
}
3232

33-
.initial-feedback {
34-
.feedback-buttons {
35-
display: flex;
36-
flex-direction: row;
37-
gap: 0.5rem;
38-
39-
span.material-symbols {
40-
font-size: 20px;
41-
}
42-
}
43-
}
44-
45-
.good-feedback, .bad-feedback {
46-
display: none;
47-
}
48-
49-
&.feedback-up {
50-
.initial-feedback {
51-
display: none;
52-
}
53-
54-
.good-feedback {
55-
display: flex;
56-
}
57-
}
58-
59-
&.feedback-down {
60-
.initial-feedback {
61-
display: none;
62-
}
33+
.feedback-buttons {
34+
display: flex;
35+
flex-direction: row;
36+
gap: 0.5rem;
6337

64-
.bad-feedback {
65-
display: flex;
38+
span.material-symbols {
39+
font-size: 20px;
6640
}
6741
}
6842
}

site/lib/jaspr_options.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:jaspr/jaspr.dart';
88
import 'package:dart_dev_site/src/archive/archive_table.dart' as prefix0;
99
import 'package:dart_dev_site/src/components/cookie_notice.dart' as prefix1;
1010
import 'package:dart_dev_site/src/components/copy_button.dart' as prefix2;
11+
import 'package:dart_dev_site/src/components/feedback.dart' as prefix3;
1112

1213
/// Default [JasprOptions] for use with your jaspr project.
1314
///
@@ -40,6 +41,11 @@ JasprOptions get defaultJasprOptions => JasprOptions(
4041
'src/components/copy_button',
4142
params: _prefix2CopyButton,
4243
),
44+
45+
prefix3.FeedbackComponent: ClientTarget<prefix3.FeedbackComponent>(
46+
'src/components/feedback',
47+
params: _prefix3FeedbackComponent,
48+
),
4349
},
4450
styles: () => [],
4551
);
@@ -53,3 +59,6 @@ Map<String, dynamic> _prefix2CopyButton(prefix2.CopyButton c) => {
5359
'classes': c.classes,
5460
'title': c.title,
5561
};
62+
Map<String, dynamic> _prefix3FeedbackComponent(prefix3.FeedbackComponent c) => {
63+
'issueUrl': c.issueUrl,
64+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:meta/meta.dart';
6+
7+
import 'analytics_server.dart'
8+
if (dart.library.js_interop) 'analytics_web.dart';
9+
10+
/// Used to report analytic events.
11+
final analytics = AnalyticsImplementation();
12+
13+
/// Contains methods for reporting analytics events.
14+
abstract class Analytics {
15+
@internal
16+
void sendEvent(String eventName, Map<String, Object?> parameters);
17+
18+
void sendFeedback(bool helpful) {
19+
sendEvent('feedback', {'feedback_type': helpful ? 'up' : 'down'});
20+
}
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:meta/meta.dart';
6+
7+
import 'analytics.dart';
8+
9+
/// Server implementation of [Analytics].
10+
///
11+
/// Don't use directly. Access through [analytics] instead.
12+
@internal
13+
final class AnalyticsImplementation extends Analytics {
14+
@override
15+
void sendEvent(String eventName, Map<String, Object?> parameters) {
16+
// Ignore on the server.
17+
}
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:meta/meta.dart';
6+
import 'package:universal_web/js_interop.dart';
7+
import 'package:universal_web/web.dart' as web;
8+
9+
import 'analytics.dart';
10+
11+
/// Web implementation of [Analytics].
12+
///
13+
/// Don't use directly. Access through [analytics] instead.
14+
@internal
15+
final class AnalyticsImplementation extends Analytics {
16+
@override
17+
void sendEvent(String eventName, Map<String, Object?> parameters) {
18+
final dataLayer = web.window['dataLayer'];
19+
if (dataLayer.isA<JSArray>()) {
20+
(dataLayer as JSArray).toDart.add(
21+
{
22+
'event': eventName,
23+
...parameters,
24+
}.jsify(),
25+
);
26+
}
27+
}
28+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:jaspr/jaspr.dart';
6+
7+
import '../analytics/analytics.dart';
8+
import 'button.dart';
9+
10+
/// Provides the user options to provide feedback on the specified page.
11+
@client
12+
final class FeedbackComponent extends StatefulComponent {
13+
const FeedbackComponent({required this.issueUrl});
14+
15+
final String issueUrl;
16+
17+
@override
18+
State<FeedbackComponent> createState() => _FeedbackComponentState();
19+
}
20+
21+
final class _FeedbackComponentState extends State<FeedbackComponent> {
22+
_FeedbackState feedback = _FeedbackState.none;
23+
24+
void _provideFeedback({required bool helpful}) {
25+
if (!kIsWeb) return;
26+
27+
setState(
28+
() => feedback = helpful
29+
? _FeedbackState.helpful
30+
: _FeedbackState.unhelpful,
31+
);
32+
analytics.sendFeedback(helpful);
33+
}
34+
35+
@override
36+
Component build(BuildContext context) {
37+
return div(id: 'page-feedback', [
38+
div(classes: 'feedback', [
39+
div([Component.text(feedback.introduction)]),
40+
...switch (feedback) {
41+
_FeedbackState.none => [
42+
div(classes: 'feedback-buttons', [
43+
Button(
44+
icon: 'thumb_up',
45+
title: 'Yes, this page was helpful.',
46+
onClick: () => _provideFeedback(helpful: true),
47+
),
48+
Button(
49+
icon: 'thumb_down',
50+
title: 'No, this page was not helpful or had an issue',
51+
onClick: () => _provideFeedback(helpful: false),
52+
),
53+
]),
54+
],
55+
_FeedbackState.helpful => [
56+
Button(
57+
content: 'Provide details',
58+
icon: 'feedback',
59+
title: 'Provide detailed feedback.',
60+
href: component.issueUrl,
61+
attributes: {'target': '_blank', 'rel': 'noopener'},
62+
),
63+
],
64+
_FeedbackState.unhelpful => [
65+
Button(
66+
content: 'Provide details',
67+
icon: 'bug_report',
68+
title: 'Provide feedback or report an issue.',
69+
href: component.issueUrl,
70+
attributes: {'target': '_blank', 'rel': 'noopener'},
71+
),
72+
],
73+
},
74+
]),
75+
]);
76+
}
77+
}
78+
79+
enum _FeedbackState {
80+
none('Was this page\'s content helpful?'),
81+
helpful('Thank you for your feedback!'),
82+
unhelpful(
83+
'Thank you for your feedback! '
84+
'Please let us know what we can do to improve.',
85+
);
86+
87+
const _FeedbackState(this.introduction);
88+
89+
final String introduction;
90+
}

site/lib/src/components/trailing_content.dart

Lines changed: 3 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import 'package:jaspr/jaspr.dart';
66
import 'package:jaspr_content/jaspr_content.dart';
77

8+
import 'feedback.dart';
9+
810
/// The trailing content of a content documentation page, such as
911
/// its last updated information, report an issue links, and similar.
1012
class TrailingContent extends StatelessComponent {
@@ -50,90 +52,7 @@ class TrailingContent extends StatelessComponent {
5052
id: 'trailing-content',
5153
attributes: {'data-nosnippet': 'true'},
5254
[
53-
div(id: 'page-feedback', [
54-
div(classes: 'feedback initial-feedback', [
55-
div([text('Was this page\'s content helpful?')]),
56-
div(classes: 'feedback-buttons', [
57-
button(
58-
id: 'feedback-up-button',
59-
classes: 'icon-button',
60-
attributes: {
61-
'aria-label': 'Yes, this page was helpful',
62-
'title': 'Helpful',
63-
},
64-
[
65-
span(
66-
classes: 'material-symbols',
67-
attributes: {'aria-hidden': 'true'},
68-
[text('thumb_up')],
69-
),
70-
],
71-
),
72-
button(
73-
id: 'feedback-down-button',
74-
classes: 'icon-button',
75-
attributes: {
76-
'aria-label': 'No, this page was not helpful or had an issue',
77-
'title': 'Not helpful or had issue',
78-
},
79-
[
80-
span(
81-
classes: 'material-symbols',
82-
attributes: {'aria-hidden': 'true'},
83-
[text('thumb_down')],
84-
),
85-
],
86-
),
87-
]),
88-
]),
89-
90-
div(classes: 'feedback good-feedback', [
91-
div([text('Thank you for your feedback!')]),
92-
a(
93-
href: issueUrl,
94-
classes: 'text-button',
95-
attributes: {
96-
'aria-label': 'Provide feedback',
97-
'target': '_blank',
98-
'rel': 'noopener',
99-
},
100-
[
101-
span(
102-
classes: 'material-symbols',
103-
attributes: {'aria-hidden': 'true'},
104-
[text('feedback')],
105-
),
106-
span([text('Provide details')]),
107-
],
108-
),
109-
]),
110-
111-
div(classes: 'feedback bad-feedback', [
112-
div([
113-
text(
114-
'Thank you for your feedback!'
115-
'Please let us know what we can do to improve.',
116-
),
117-
]),
118-
a(
119-
href: issueUrl,
120-
classes: 'text-button',
121-
attributes: {
122-
'aria-label': 'Provide feedback or report an issue',
123-
'target': '_blank',
124-
'rel': 'noopener',
125-
},
126-
[
127-
span(
128-
classes: 'material-symbols',
129-
attributes: {'aria-hidden': 'true'},
130-
[text('bug_report')],
131-
),
132-
span([text('Provide details')]),
133-
],
134-
),
135-
]),
136-
]),
55+
FeedbackComponent(issueUrl: issueUrl),
13756

13857
p(id: 'page-github-links', [
13958
span([

site/web/assets/js/main.js

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -378,28 +378,6 @@ function setupExpandableCards() {
378378
});
379379
}
380380

381-
function setupFeedback() {
382-
const feedbackContainer =
383-
document.getElementById('page-feedback');
384-
if (!feedbackContainer) return;
385-
386-
const feedbackUpButton = feedbackContainer.querySelector('#feedback-up-button');
387-
const feedbackDownButton = feedbackContainer.querySelector('#feedback-down-button');
388-
if (!feedbackUpButton || !feedbackDownButton) return;
389-
390-
feedbackUpButton.addEventListener('click', (_) => {
391-
window.dataLayer?.push({'event': 'inline_feedback', 'feedback_type': 'up'});
392-
393-
feedbackContainer.classList.add('feedback-up');
394-
}, { once: true });
395-
396-
feedbackDownButton.addEventListener('click', (_) => {
397-
window.dataLayer?.push({'event': 'inline_feedback', 'feedback_type': 'down'});
398-
399-
feedbackContainer.classList.add('feedback-down');
400-
}, { once: true });
401-
}
402-
403381
function _setupSite() {
404382
setupTheme();
405383
setupSidenav();
@@ -452,7 +430,6 @@ function _setupSite() {
452430

453431
setupTableOfContents();
454432
setupExpandableCards();
455-
setupFeedback();
456433
}
457434

458435
// Run setup if DOM is loaded, otherwise do it after it has loaded.

0 commit comments

Comments
 (0)