Skip to content

Commit 0f3af17

Browse files
authored
WEBDEV-7680 Add feedback survey component to present & handle multiple questions (#5)
* Add feedback survey component * Upgrade dependencies to get tests running * Add unit tests for new features * Add missing logic & tests for comment box styles * Switch from updated to willUpdate to avoid needless cycles * Add further unit tests for the feedback survey * Rename some vars * Update naming & exports * Ensure prompts fill max width * Update background color * Send 'rating' instead of 'vote', improve docs * Refactor for much greater extensibility * Reflect extra properties * Remove some unused types * Add focus traps, aria roles, and labels where necessary * Remove unneeded async from method * Correct outdated doc comment * Use ia-styles for button styling * Update README with information about the new survey features * Switch from CSS-based numbering to slotted text to satisfy Safari * Clean up template a bit for readability * Fix focus method to find the first focusable slotted child * Slightly simplify focusable condition
1 parent e69d886 commit 0f3af17

21 files changed

+5031
-961
lines changed

README.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Internet Archive Feature Feedback Component
44

5-
This is a widget that lets us collect feedback on features that we release. Written in LitElement.
5+
This is a collection of widgets that let us collect feedback on features that we release. Written in LitElement.
66

77
![Feature Feedback](/assets/screenshot.png "Screenshot")
88

@@ -15,6 +15,8 @@ This is a widget that lets us collect feedback on features that we release. Writ
1515

1616
## Usage
1717

18+
### Feature Feedback widget for single-question feedback
19+
1820
```ts
1921
import { RecaptchaManager } from '@internetarchive/recaptcha-manager';
2022
import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
@@ -39,6 +41,91 @@ html`
3941
></feature-feedback>`;
4042
```
4143

44+
### Feedback Survey widget for multi-question feedback
45+
46+
```ts
47+
import { RecaptchaManager } from '@internetarchive/recaptcha-manager';
48+
import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
49+
import { FeatureFeedbackService } from '@internetarchive/feature-feedback';
50+
import '@internetarchive/feature-feedback';
51+
52+
const recaptchaManager = new RecaptchaManager({
53+
defaultSiteKey: '',
54+
});
55+
const featureFeedbackService = new FeatureFeedbackService({
56+
serviceUrl: 'http://local.archive.org:5000',
57+
});
58+
const resizeObserver = new SharedResizeObserver();
59+
60+
html`
61+
<ia-feedback-survey
62+
.recaptchaManager=${recaptchaManager}
63+
.featureFeedbackService=${featureFeedbackService}
64+
.resizeObserver=${resizeObserver}
65+
.surveyIdentifier=${'demo-survey'}
66+
.buttonText=${'Take the survey'}
67+
showButtonThumbs
68+
showQuestionNumbers
69+
>
70+
<ia-survey-vote
71+
prompt="Do you find foos better than bars?"
72+
showComments
73+
required
74+
></ia-survey-vote>
75+
<ia-survey-vote
76+
prompt="Would you recommend foos to a friend?"
77+
></ia-survey-vote>
78+
<ia-survey-comment
79+
prompt="Anything else to add?"
80+
placeholder="Add optional comments..."
81+
></ia-survey-comment>
82+
<ia-survey-extra name="Search query" value=${this.query}>
83+
</ia-feedback-survey>
84+
`;
85+
```
86+
87+
#### Feedback survey question components
88+
89+
Currently this package defines three subcomponents that can be used as survey "questions":
90+
- `<ia-survey-vote>`: Seeks an up/down vote, optionally with a freeform text comment too -- like the single-question feature feedback widget.
91+
Accepts the following attributes/properties:
92+
- `prompt` (string): The question text to display beside the vote buttons. If not provided, no prompt text will be rendered.
93+
- `vote` ("up" | "down"): Optionally allows setting the initial up/down vote state of the question if desired.
94+
- `comment` (string): Optionally allows setting the comment response for the question if desired.
95+
- `showComments` (boolean): If true, shows a freeform textarea beneath the vote buttons.
96+
- `commentPlaceholder` (string): The placeholder text to render in the textarea when it is empty (see `placeholder` for `<ia-survey-comment>` below).
97+
- `required` (boolean): If true, ensures that the survey cannot be submitted unless a vote is entered for this question.
98+
A text comment is never required for these questions, even when shown. To require text input, use a separate `<ia-survey-comment>` question.
99+
- `disabled` (boolean): If true, the question will not allow the vote to be changed or the comment to be edited by the user.
100+
- `skipNumber` (boolean): If true, this question will not participate in question numbering -- it will neither display a number itself nor increment the
101+
number used for the next question.
102+
103+
- `<ia-survey-comment>`: Seeks a freeform text comment alone. Accepts the following attributes/properties:
104+
- `prompt` (string): The question text to display above the textarea. If not provided, no prompt text will be rendered.
105+
- `value` (string): Optionally allows setting the comment response for the question if desired.
106+
- `placeholder` (string): The placeholder text to render in the textarea when it is empty.
107+
If not provided, a default of either `Comments` or `Comments (optional)` will be used, depending on the `required` state of this component.
108+
- `required` (boolean): If true, ensures that the survey cannot be submitted unless a non-empty comment is entered for this question.
109+
- `disabled` (boolean): If true, the question will not allow the user to edit the comment.
110+
- `skipNumber` (boolean): If true, this question will not participate in question numbering -- it will neither display a number itself nor increment the
111+
number used for the next question.
112+
113+
- `<ia-survey-extra>`: Does not render a question to the user, but instead defines additional info to be submitted with survey responses for context.
114+
Accepts the following attributes/properties:
115+
- `name` (string): A name to associate with this extra info field.
116+
- `value` (string): The value of this field to be submitted in the survey response.
117+
118+
One may define additional question components that can participate in survey responses, as long as they conform to the `IASurveyQuestionInterface`.
119+
120+
Other arbitrary elements may also be included presentationally within the survey, to be slotted into the popup alongside any questions.
121+
Such elements will generally not have any effect on submitted survey responses unless they conform to the interface noted above.
122+
123+
#### Styling
124+
125+
Both the `<ia-survey-vote>` and `<ia-survey-comment>` components accept optional CSS variables to adjust the height or resizability of their comment textareas:
126+
- `--commentHeight` (defaults to `50px`)
127+
- `--commentResize` (`none`, `horizontal`, `vertical`, or `both` -- defaults to `none`). See [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/resize) for more details.
128+
42129

43130
## Local Demo with `web-dev-server`
44131
```bash

demo/app-root.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { RecaptchaManager } from '@internetarchive/recaptcha-manager';
22
import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
33
import { html, css, LitElement } from 'lit';
44
import { customElement } from 'lit/decorators.js';
5-
import '../src/feature-feedback';
65
import { FeatureFeedbackService } from '../src/feature-feedback-service';
6+
import '../src/feature-feedback';
7+
import '../src/survey/ia-feedback-survey';
8+
import '../src/survey/questions/ia-survey-vote';
9+
import '../src/survey/questions/ia-survey-comment';
10+
import '../src/survey/questions/ia-survey-extra';
711

812
@customElement('app-root')
913
export class AppRoot extends LitElement {
1014
recaptchaManager = new RecaptchaManager({
11-
defaultSiteKey: '',
15+
defaultSiteKey: 'foo',
1216
});
1317

1418
featureFeedbackService = new FeatureFeedbackService({
@@ -96,6 +100,50 @@ export class AppRoot extends LitElement {
96100
fugiat. Aute excepteur enim ad consectetur duis eu aute sint fugiat.
97101
Nulla cillum fugiat tempor esse non eu dolore adipisicing magna.
98102
</p>
103+
104+
<ia-feedback-survey
105+
showButtonThumbs
106+
showQuestionNumbers
107+
.surveyIdentifier=${'demo-survey'}
108+
.recaptchaManager=${this.recaptchaManager}
109+
.featureFeedbackService=${this.featureFeedbackService}
110+
.resizeObserver=${this.resizeObserver}
111+
>
112+
<ia-survey-vote
113+
prompt="How do you feel about foo?"
114+
required
115+
></ia-survey-vote>
116+
<ia-survey-extra name="foo" value="bar"></ia-survey-extra>
117+
<p>
118+
This is some embedded explanatory text. The above question requires a
119+
response, but the next question is optional.
120+
</p>
121+
<ia-survey-vote
122+
prompt="How do you feel about bar?"
123+
showComments
124+
commentPlaceholder="You may enter an optional comment as well..."
125+
></ia-survey-vote>
126+
<p>The following question should be disabled.</p>
127+
<ia-survey-vote
128+
prompt="How do you feel about baz?"
129+
disabled
130+
></ia-survey-vote>
131+
<p>The following question should not be numbered.</p>
132+
<ia-survey-vote
133+
prompt="How do you feel about quux?"
134+
skipNumber
135+
></ia-survey-vote>
136+
<ia-survey-comment
137+
prompt="What does foobar mean to you?"
138+
placeholder="You must answer this question."
139+
required
140+
></ia-survey-comment>
141+
<ia-survey-comment
142+
prompt="Anything else to add?"
143+
placeholder="Please share... (optional)"
144+
style="--commentResize: vertical;"
145+
></ia-survey-comment>
146+
</ia-feedback-survey>
99147
`;
100148
}
101149

@@ -107,5 +155,9 @@ export class AppRoot extends LitElement {
107155
.right {
108156
float: right;
109157
}
158+
159+
ia-feedback-survey > p {
160+
font-size: 12px;
161+
}
110162
`;
111163
}

demo/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1" />
66
<style>
7+
html, body {
8+
height: 100%;
9+
}
10+
711
html {
812
font-size: 10px; /* This is to match petabox's base font size */
913
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;

index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@ export {
33
FeatureFeedbackService,
44
FeatureFeedbackServiceInterface,
55
} from './src/feature-feedback-service';
6+
7+
export { IAFeedbackSurvey } from './src/survey/ia-feedback-survey';
8+
export { IASurveyComment } from './src/survey/questions/ia-survey-comment';
9+
export { IASurveyVote } from './src/survey/questions/ia-survey-vote';
10+
export { IASurveyExtra } from './src/survey/questions/ia-survey-extra';
11+
export {
12+
IASurveyQuestionInterface,
13+
IASurveyQuestionResponse,
14+
} from './src/survey/models';

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,30 @@
2222
},
2323
"types": "dist/index.d.ts",
2424
"dependencies": {
25+
"@internetarchive/ia-styles": "^1.0.0",
2526
"@internetarchive/recaptcha-manager": "^0.1.1",
2627
"@internetarchive/result-type": "^0.0.1",
2728
"@internetarchive/shared-resize-observer": "^0.2.0",
29+
"@lit/localize": "^0.11.4",
2830
"lit": "^2.2.7"
2931
},
3032
"devDependencies": {
3133
"@open-wc/eslint-config": "^7.0.0",
32-
"@open-wc/testing": "^3.0.3",
33-
"@typescript-eslint/eslint-plugin": "^5.3.1",
34-
"@typescript-eslint/parser": "^5.3.1",
35-
"@web/dev-server": "^0.1.28",
36-
"@web/test-runner": "^0.13.22",
34+
"@open-wc/testing": "^4.0.0",
35+
"@types/mocha": "^10.0.10",
36+
"@types/node": "^22.15.29",
37+
"@typescript-eslint/eslint-plugin": "^6.21.0",
38+
"@typescript-eslint/parser": "^6.21.0",
39+
"@web/dev-server": "^0.4.6",
40+
"@web/test-runner": "^0.20.0",
3741
"concurrently": "^6.3.0",
3842
"eslint": "^8.2.0",
3943
"eslint-config-prettier": "^8.3.0",
4044
"eslint-plugin-import": "^2.25.3",
4145
"eslint-plugin-lit-a11y": "^2.2.0",
4246
"eslint-plugin-wc": "^1.3.2",
4347
"husky": "^7.0.0",
44-
"madge": "^5.0.1",
48+
"madge": "^8.0.0",
4549
"prettier": "^2.4.1",
4650
"sinon": "^12.0.1",
4751
"tslib": "^2.3.1",
@@ -64,6 +68,10 @@
6468
},
6569
"rules": {
6670
"no-unused-vars": "off",
71+
"no-param-reassign": [
72+
"error",
73+
{ "props": false }
74+
],
6775
"@typescript-eslint/no-unused-vars": [
6876
"error"
6977
],

src/feature-feedback-service.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
import type { Result } from '@internetarchive/result-type';
22
import { Vote } from './models';
3+
import { IASurveyQuestionResponse } from './survey/models';
34

45
export interface FeatureFeedbackServiceInterface {
6+
/**
7+
* Submits a feedback entry to the configured service URL.
8+
* @param options Details about the survey:
9+
* - `featureIdentifier`: Internal identifier for the feature being rated
10+
* - `vote`: The up/down rating for the feature
11+
* - `comments`: (Optional) A text comment about the feature
12+
* - `recaptchaToken`: Token generated by the ReCAPTCHA service
13+
*/
514
submitFeedback(options: {
615
featureIdentifier: string;
716
vote: Vote;
817
comments?: string;
918
recaptchaToken: string;
1019
}): Promise<Result<boolean, Error>>;
20+
21+
/**
22+
* Submits a set of survey responses to the configured service URL.
23+
* @param options Details about the survey:
24+
* - `surveyIdentifier`: Internal identifier of the survey to be submitted
25+
* - `responses`: List of responses to the survey questions
26+
* - `recaptchaToken`: Token generated by the ReCAPTCHA service
27+
*/
28+
submitSurvey(options: {
29+
surveyIdentifier: string;
30+
responses: IASurveyQuestionResponse[];
31+
recaptchaToken: string;
32+
}): Promise<Result<boolean, Error>>;
1133
}
1234

1335
export class FeatureFeedbackService implements FeatureFeedbackServiceInterface {
@@ -17,6 +39,9 @@ export class FeatureFeedbackService implements FeatureFeedbackServiceInterface {
1739
this.serviceUrl = options.serviceUrl;
1840
}
1941

42+
/**
43+
* @inheritdoc
44+
*/
2045
async submitFeedback(options: {
2146
featureIdentifier: string;
2247
vote: Vote;
@@ -50,4 +75,48 @@ export class FeatureFeedbackService implements FeatureFeedbackServiceInterface {
5075
};
5176
}
5277
}
78+
79+
/**
80+
* @inheritdoc
81+
*/
82+
async submitSurvey(options: {
83+
surveyIdentifier: string;
84+
responses: IASurveyQuestionResponse[];
85+
recaptchaToken: string;
86+
}): Promise<Result<boolean, Error>> {
87+
const url = new URL(this.serviceUrl);
88+
url.searchParams.append('surveyId', options.surveyIdentifier);
89+
url.searchParams.append('token', options.recaptchaToken);
90+
const body = JSON.stringify({
91+
surveyResponses: options.responses.map(resp => ({
92+
name: resp.name,
93+
...(resp.rating ? { rating: resp.rating } : {}),
94+
...(resp.comment ? { comment: resp.comment } : {}),
95+
})),
96+
});
97+
98+
try {
99+
const response = await fetch(url.href, {
100+
method: 'POST',
101+
headers: { 'Content-Type': 'application/json' },
102+
body,
103+
});
104+
const json = await response.json();
105+
return json as Result<boolean, Error>;
106+
} catch (error) {
107+
let err: Error;
108+
if (error instanceof Error) {
109+
err = error;
110+
} else if (typeof error === 'string') {
111+
err = new Error(error);
112+
} else {
113+
err = new Error('Unknown error');
114+
}
115+
116+
return {
117+
success: false,
118+
error: err,
119+
};
120+
}
121+
}
53122
}

0 commit comments

Comments
 (0)