-
Notifications
You must be signed in to change notification settings - Fork 101
Move feedback into the right side-nav #3324
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
Open
enf0rc3
wants to merge
9
commits into
main
Choose a base branch
from
wl/feedback-widget
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
74effe4
Remove the Feedback component
enf0rc3 b72d88a
Add the feedback widget to the table of contents column
enf0rc3 f01be32
Match the feedback widget to the design
enf0rc3 14ba4e1
Stop thanking the reader for feedback that never sent
enf0rc3 f69f3c4
Test the feedback widget and the form contract behind it
enf0rc3 796ea58
Inset the feedback widget from the rest of the sidebar column
enf0rc3 526c071
Move the feedback styles into the component
enf0rc3 a00b78f
Start the feedback widget from the component, not the global script
enf0rc3 2fa5eb7
Drop the Google Form contract tests
enf0rc3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,149 @@ | ||
| --- | ||
| import { accelerator } from '@lib/accelerator'; | ||
| import type { Frontmatter } from 'astro-accelerator-utils/types/Frontmatter'; | ||
| import { SITE } from '@config'; | ||
| import { Lang, Translations } from '@util/Languages'; | ||
| import Button from './Button.astro'; | ||
| import { RATING_NO, RATING_YES } from '../scripts/modules/feedback-form.js'; | ||
|
|
||
| const stats = new accelerator.statistics('components/Feedback.astro'); | ||
| stats.start(); | ||
|
|
||
| // Properties | ||
| type Props = { | ||
| lang: string; | ||
| frontmatter: Frontmatter; | ||
| headings: { depth: number; slug: string; text: string }[]; | ||
| }; | ||
| const { lang, frontmatter, headings } = Astro.props satisfies Props; | ||
| const { lang } = Astro.props satisfies Props; | ||
|
|
||
| // Logic | ||
| const heading = await accelerator.markdown.getTextFrom(frontmatter.title); | ||
| const formUrl = `https://docs.google.com/forms/d/e/1FAIpQLSehVdN2w6tgSvp5QX7lHGnHDmgKi2Yfvko7bM2izgWQaqg-Wg/viewform?usp=pp_url&entry.336432709=${encodeURIComponent(heading)}`; | ||
| // Language | ||
| const _ = Lang(lang); | ||
|
|
||
| stats.stop(); | ||
| --- | ||
|
|
||
| <h2>Help us continuously improve</h2> | ||
| <p>Please let us know if you have any feedback about this page.</p> | ||
| <p><a href={formUrl} class="button button--primary">Send feedback</a></p> | ||
| <section class="feedback" data-feedback aria-labelledby="feedback-question"> | ||
| <div class="feedback__vote"> | ||
| <p class="feedback__question" id="feedback-question"> | ||
| {_(Translations.octopus_feedback.question)} | ||
| </p> | ||
| <div class="feedback__actions"> | ||
| <Button | ||
| label={_(Translations.octopus_feedback.yes)} | ||
| icon="feedback__icon feedback__icon--yes" | ||
| size="small" | ||
| aria-pressed="false" | ||
| data-feedback-vote={RATING_YES} | ||
| /> | ||
| <Button | ||
| label={_(Translations.octopus_feedback.no)} | ||
| icon="feedback__icon feedback__icon--no" | ||
| size="small" | ||
| aria-pressed="false" | ||
| data-feedback-vote={RATING_NO} | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div class="feedback__comment" data-feedback-comment hidden> | ||
| <label class="feedback__label" for="feedback-comment"> | ||
| {_(Translations.octopus_feedback.comment_label)} | ||
| </label> | ||
| <textarea class="feedback__textarea" id="feedback-comment" rows="3" | ||
| ></textarea> | ||
| <Button | ||
| label={_(Translations.octopus_feedback.send)} | ||
| size="small" | ||
| importance="loud" | ||
| data-feedback-send | ||
| /> | ||
| </div> | ||
|
|
||
| <p class="feedback__thanks" data-feedback-thanks role="status" hidden> | ||
| {_(Translations.octopus_feedback.thanks)} | ||
| </p> | ||
| </section> | ||
|
|
||
| <script> | ||
| import '../scripts/modules/feedback.js'; | ||
| </script> | ||
|
|
||
| <style> | ||
| .feedback { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: flex-start; | ||
| gap: var(--space16); | ||
| margin-block-start: var(--space16); | ||
| padding: var(--space16); | ||
| color: var(--colorTextPrimary); | ||
| font: var(--textBodyRegularMedium); | ||
| } | ||
|
|
||
| .feedback__vote, | ||
| .feedback__comment { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: flex-start; | ||
| gap: var(--space8); | ||
| width: 100%; | ||
| } | ||
|
|
||
| /* Two classes, so this beats the `display: flex` above. */ | ||
| .feedback [hidden] { | ||
| display: none; | ||
| } | ||
|
|
||
| .feedback__actions { | ||
| display: flex; | ||
| gap: var(--space8); | ||
| } | ||
|
|
||
| /* Button.astro renders the icon span, so these sit outside this component's | ||
| scope. 20px slot, 16x14 glyph, as the design insets them, and masked | ||
| because the exported icons carry a fixed fill. */ | ||
| .feedback :global(.feedback__icon) { | ||
| background-color: var(--colorIconPrimary); | ||
| mask-position: center; | ||
| mask-repeat: no-repeat; | ||
| mask-size: 1rem 0.875rem; | ||
| } | ||
|
|
||
| .feedback :global(.feedback__icon--yes) { | ||
| mask-image: url('../assets/icons/thumbs-up.svg'); | ||
| } | ||
|
|
||
| /* Mirrored, as the design pairs the thumbs facing each other. */ | ||
| .feedback :global(.feedback__icon--no) { | ||
| mask-image: url('../assets/icons/thumbs-down.svg'); | ||
| transform: scaleX(-1); | ||
| } | ||
|
|
||
| /* The design calls this out: the label stays regular weight. */ | ||
| .feedback__label { | ||
| font: var(--textBodyRegularMedium); | ||
| } | ||
|
|
||
| .feedback__textarea { | ||
| box-sizing: border-box; | ||
| width: 100%; | ||
| min-height: 4.375rem; | ||
| padding: var(--space8); | ||
| border: var(--borderWidth1) solid var(--colorBorderBold); | ||
| border-radius: var(--borderRadiusSmall); | ||
| background: var(--colorBackgroundPrimaryDefault); | ||
| color: var(--colorTextPrimary); | ||
| font: var(--textBodyRegularMedium); | ||
| resize: vertical; | ||
| } | ||
|
|
||
| .feedback__textarea:focus-visible { | ||
| outline: var(--borderWidth2) solid var(--colorBorderSelected); | ||
| outline-offset: var(--borderWidth1); | ||
| } | ||
|
|
||
| /* The table of contents column restacks above the article below this width, so | ||
| the widget would ask the question before anything had been read. */ | ||
| @media (max-width: 1130px) { | ||
| .feedback { | ||
| display: none; | ||
| } | ||
| } | ||
| </style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // @ts-check | ||
|
|
||
| // The Google Form behind the feedback widget. Its three fields, read off the | ||
| // live form: | ||
| // entry.336432709 "Which page did you view?" short text, optional | ||
| // entry.128617088 "How useful was the content?" linear scale 1-5, required | ||
| // entry.434783109 "...what we did well, or what we could improve" required | ||
| // Yes maps to 5 and No to 1 - the form has no yes/no field to send to. | ||
| export const FORM_ID = | ||
| '1FAIpQLSehVdN2w6tgSvp5QX7lHGnHDmgKi2Yfvko7bM2izgWQaqg-Wg'; | ||
| export const FORM_URL = `https://docs.google.com/forms/d/e/${FORM_ID}/formResponse`; | ||
| export const FIELD_PAGE = 'entry.336432709'; | ||
| export const FIELD_RATING = 'entry.128617088'; | ||
| export const FIELD_COMMENT = 'entry.434783109'; | ||
| export const RATING_YES = '5'; | ||
| export const RATING_NO = '1'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| // @ts-check | ||
| import { qs, qsa } from './query.js'; | ||
| import { | ||
| FIELD_COMMENT, | ||
| FIELD_PAGE, | ||
| FIELD_RATING, | ||
| FORM_URL, | ||
| } from './feedback-form.js'; | ||
|
|
||
| /** | ||
| * Google Forms sends no CORS headers, so the POST has to go out as no-cors and | ||
| * the response comes back opaque. There is no way to read whether the form | ||
| * accepted it - a rejected submission looks identical to an accepted one, so | ||
| * anything Google answered at all counts as accepted. A change to the form's | ||
| * fields would therefore go unnoticed here. | ||
| * | ||
| * @param {string} rating | ||
| * @param {string} comment | ||
| * @returns {Promise<void>} | ||
| */ | ||
| async function submit(rating, comment) { | ||
| const body = new URLSearchParams(); | ||
| body.set(FIELD_PAGE, window.location.href); | ||
| body.set(FIELD_RATING, rating); | ||
| // The comment is marked required on the form, so a blank box still has to | ||
| // send something for the submission to be accepted at all. | ||
| body.set(FIELD_COMMENT, comment.trim() || ' '); | ||
|
|
||
| await fetch(FORM_URL, { method: 'POST', mode: 'no-cors', body }); | ||
| } | ||
|
|
||
| class Feedback { | ||
| /** @param {HTMLElement} root */ | ||
| constructor(root) { | ||
| this.root = root; | ||
| this.votes = qsa('[data-feedback-vote]', root); | ||
| this.comment = qs('[data-feedback-comment]', root); | ||
| this.textarea = qs('textarea', root); | ||
| this.send = qs('[data-feedback-send]', root); | ||
| this.thanks = qs('[data-feedback-thanks]', root); | ||
| /** @type {string | null} */ | ||
| this.rating = null; | ||
|
|
||
| this.addListeners(); | ||
| } | ||
|
|
||
| addListeners() { | ||
| this.votes.forEach((button) => { | ||
| button.addEventListener('click', () => this.vote(button)); | ||
| }); | ||
| this.send.addEventListener('click', () => this.submit()); | ||
| } | ||
|
|
||
| /** @param {HTMLElement} chosen */ | ||
| vote(chosen) { | ||
| this.rating = chosen.dataset.feedbackVote ?? null; | ||
| this.votes.forEach((button) => { | ||
| button.setAttribute('aria-pressed', String(button === chosen)); | ||
| }); | ||
| this.comment.hidden = false; | ||
| } | ||
|
|
||
| async submit() { | ||
| if (!this.rating) return; | ||
|
|
||
| // Guards against a second submission while the first is in flight. | ||
| this.send.setAttribute('disabled', ''); | ||
|
|
||
| try { | ||
| await submit(this.rating, this.textarea.value); | ||
| } catch (err) { | ||
| // Offline, or blocked by an extension - it never left the browser. | ||
| console.warn('[feedback] submission failed', err); | ||
| this.send.removeAttribute('disabled'); | ||
| return; | ||
| } | ||
|
|
||
| this.root.querySelectorAll('.feedback__vote, .feedback__comment').forEach( | ||
| /** @param {Element} el */ (el) => { | ||
| /** @type {HTMLElement} */ (el).hidden = true; | ||
| } | ||
| ); | ||
| this.thanks.hidden = false; | ||
| } | ||
| } | ||
|
|
||
| qsa('[data-feedback]').forEach((root) => new Feedback(root)); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This is really a "hack" to get the UI in before actually wiring up the real deal.
We should look at GTM or something to publish on click of thumbs up / down etc and maybe look at a different mechanism for the content rather than forms.