Skip to content

Commit ab39cfc

Browse files
Merge pull request #2838 from elastic/scottybollinger/feedback-widget
Add feedback widget to docs sidebar
2 parents 6819c1d + eadf49c commit ab39cfc

File tree

6 files changed

+857
-8
lines changed

6 files changed

+857
-8
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { h, Component } from '../../../../../../node_modules/preact';
2+
3+
const FEEDBACK_URL = 'https://docs.elastic.co/api/feedback'
4+
const MAX_COMMENT_LENGTH = 1000;
5+
6+
export default class FeedbackModal extends Component {
7+
constructor(props) {
8+
super(props);
9+
this.state = {
10+
comment: '',
11+
modalClosed: false,
12+
isLoading: false,
13+
hasError: false,
14+
};
15+
this.onEscape = this.onEscape.bind(this);
16+
this.resetState = this.resetState.bind(this);
17+
this.submitFeedback = this.submitFeedback.bind(this);
18+
}
19+
20+
onEscape(event) {
21+
if (event.key === 'Escape') {
22+
this.resetState();
23+
}
24+
}
25+
26+
resetState() {
27+
this.setState({ modalClosed: true });
28+
document.querySelectorAll('.isPressed').forEach((el) => {
29+
el.classList.remove('isPressed');
30+
});
31+
}
32+
33+
submitFeedback() {
34+
this.setState({ isLoading: true });
35+
fetch(FEEDBACK_URL, {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
body: JSON.stringify({
41+
comment: this.state.comment,
42+
feedback: this.props.isLiked ? 'liked' : 'disliked',
43+
url: window.location.href,
44+
}),
45+
})
46+
.then((response) => response.json())
47+
.then(() => {
48+
this.setState({ modalClosed: true })
49+
document.getElementById('feedbackSuccess').classList.remove('hidden')
50+
document.querySelectorAll('.feedbackButton').forEach((el) => {
51+
el.disabled = true
52+
})
53+
})
54+
.catch((error) => {
55+
this.setState({ isLoading: false, hasError: true });
56+
console.error('Error:', error);
57+
});
58+
59+
}
60+
61+
componentDidMount() {
62+
document.addEventListener('keydown', this.onEscape, false);
63+
}
64+
65+
componentWillUnmount() {
66+
document.removeEventListener('keydown', this.onEscape, false);
67+
}
68+
69+
render(props, state) {
70+
const { isLiked } = props;
71+
const { modalClosed, isLoading, hasError, comment } = state;
72+
const maxCommentLengthReached = comment.length > MAX_COMMENT_LENGTH;
73+
const sendDisabled = isLoading || maxCommentLengthReached;
74+
75+
if (modalClosed) {
76+
return null;
77+
}
78+
79+
return (
80+
<div
81+
data-relative-to-header="above"
82+
id="feedbackModal"
83+
>
84+
<div
85+
data-focus-guard="true"
86+
tabindex="0"
87+
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
88+
></div>
89+
<div data-focus-lock-disabled="false">
90+
<div className="feedbackModalContent" tabindex="0">
91+
<button
92+
className="closeIcon"
93+
type="button"
94+
aria-label="Closes this modal window"
95+
onClick={this.resetState}
96+
disabled={isLoading}
97+
>
98+
<svg
99+
xmlns="http://www.w3.org/2000/svg"
100+
width="16"
101+
height="16"
102+
viewBox="0 0 16 16"
103+
role="img"
104+
data-icon-type="cross"
105+
data-is-loaded="true"
106+
aria-hidden="true"
107+
>
108+
<path d="M7.293 8 3.146 3.854a.5.5 0 1 1 .708-.708L8 7.293l4.146-4.147a.5.5 0 0 1 .708.708L8.707 8l4.147 4.146a.5.5 0 0 1-.708.708L8 8.707l-4.146 4.147a.5.5 0 0 1-.708-.708L7.293 8Z"></path>
109+
</svg>
110+
</button>
111+
<div className="feedbackModalHeader">
112+
<h2>Send us your feedback</h2>
113+
</div>
114+
<div className="feedbackModalBody">
115+
<div className="feedbackModalBodyOverflow">
116+
<div>
117+
Thank you for helping us improve Elastic documentation.
118+
</div>
119+
<div className="spacer"></div>
120+
<div className="feedbackForm">
121+
<div className="feedbackFormRow">
122+
<div className="feedbackFormRow__labelWrapper">
123+
<label
124+
className="feedbackFormLabel"
125+
id="feedbackLabel"
126+
for="feedbackComment"
127+
>
128+
Additional comment (optional)
129+
</label>
130+
</div>
131+
<div className="feedbackFormRow__fieldWrapper">
132+
<div className="feedbackFormControlLayout">
133+
<div className="feedbackFormControlLayout__childrenWrapper">
134+
<textarea
135+
className="feedbackTextArea"
136+
rows="6"
137+
id="feedbackComment"
138+
disabled={isLoading}
139+
onKeyUp={(e) =>
140+
this.setState({ comment: e.target.value })
141+
}
142+
></textarea>
143+
{maxCommentLengthReached && (
144+
<div className="feedbackFormError">
145+
Max comment length of {MAX_COMMENT_LENGTH}{' '}
146+
characters reached.
147+
<br />
148+
<br />
149+
Character count: {comment.length}
150+
</div>
151+
)}
152+
{hasError && (
153+
<div className="feedbackFormError">
154+
There was a problem submitting your feedback.
155+
<br />
156+
<br />
157+
Please try again.
158+
</div>
159+
)}
160+
</div>
161+
</div>
162+
</div>
163+
</div>
164+
</div>
165+
</div>
166+
</div>
167+
<div
168+
className={`feedbackModalFooter ${isLoading ? 'loading' : ''}`}
169+
>
170+
<button
171+
className="feedbackButton cancelButton"
172+
type="button"
173+
onClick={this.resetState}
174+
disabled={isLoading}
175+
>
176+
<span className="feedbackButtonContent">
177+
<span>Cancel</span>
178+
</span>
179+
</button>
180+
<button
181+
type="button"
182+
disabled={sendDisabled}
183+
className={`feedbackButton sendButton ${
184+
isLiked ? 'like' : 'dislike'
185+
}`}
186+
onClick={this.submitFeedback}
187+
>
188+
<span className="loadingContent">
189+
<span
190+
class="loadingSpinner"
191+
role="progressbar"
192+
aria-label="Loading"
193+
style="border-color: rgb(0, 119, 204) currentcolor currentcolor;"
194+
></span>
195+
<span>Sending...</span>
196+
</span>
197+
<span className="feedbackButtonContent">
198+
<span>Send</span>
199+
<svg
200+
xmlns="http://www.w3.org/2000/svg"
201+
width="24"
202+
height="24"
203+
viewBox="0 0 24 24"
204+
className="sendIcon like"
205+
role="img"
206+
aria-hidden="true"
207+
>
208+
<path d="M9 21h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2c0-1.1-.9-2-2-2h-6.31l.95-4.57l.03-.32c0-.41-.17-.79-.44-1.06L14.17 1L7.58 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2zM9 9l4.34-4.34L12 10h9v2l-3 7H9V9zM1 9h4v12H1z"></path>
209+
</svg>
210+
<svg
211+
xmlns="http://www.w3.org/2000/svg"
212+
width="24"
213+
height="24"
214+
viewBox="0 0 24 24"
215+
className="sendIcon dislike"
216+
role="img"
217+
aria-hidden="true"
218+
>
219+
<path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57l-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm0 12l-4.34 4.34L12 14H3v-2l3-7h9v10zm4-12h4v12h-4z"></path>
220+
</svg>
221+
</span>
222+
</button>
223+
</div>
224+
</div>
225+
</div>
226+
<div
227+
data-focus-guard="true"
228+
tabindex="0"
229+
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
230+
></div>
231+
</div>
232+
);
233+
}
234+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { h, Component } from '../../../../../../node_modules/preact';
2+
3+
export default class FeedbackWidget extends Component {
4+
render() {
5+
return (
6+
<div>
7+
<div id="feedbackWidget">
8+
Was this helpful?
9+
<span className="docHorizontalSpacer"></span>
10+
<fieldset className="buttonGroup">
11+
<legend className="screenReaderOnly">Feedback</legend>
12+
<div className="buttonGroup">
13+
<button
14+
aria-pressed="false"
15+
id="feedbackLiked"
16+
type="button"
17+
className="feedbackButton feedbackLiked"
18+
title="Like"
19+
>
20+
<span className="feedbackButtonContent">
21+
<svg
22+
xmlns="http://www.w3.org/2000/svg"
23+
width="24"
24+
height="24"
25+
viewBox="0 0 24 24"
26+
className="feedbackIcon unpressed"
27+
role="img"
28+
aria-hidden="true"
29+
>
30+
<path d="M9 21h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2c0-1.1-.9-2-2-2h-6.31l.95-4.57l.03-.32c0-.41-.17-.79-.44-1.06L14.17 1L7.58 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2zM9 9l4.34-4.34L12 10h9v2l-3 7H9V9zM1 9h4v12H1z"></path>
31+
</svg>
32+
<svg
33+
xmlns="http://www.w3.org/2000/svg"
34+
width="24"
35+
height="24"
36+
viewBox="0 0 24 24"
37+
className="feedbackIcon pressed"
38+
role="img"
39+
data-is-loaded="true"
40+
aria-hidden="true"
41+
>
42+
<path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57l.03-.32c0-.41-.17-.79-.44-1.06L14.17 1L7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"></path>
43+
</svg>
44+
<span className="screenReaderOnly" data-text="Like">
45+
Like
46+
</span>
47+
</span>
48+
</button>
49+
<button
50+
aria-pressed="false"
51+
id="feedbackDisliked"
52+
type="button"
53+
className="feedbackButton feedbackDisliked"
54+
title="Dislike"
55+
>
56+
<span className="feedbackButtonContent">
57+
<svg
58+
xmlns="http://www.w3.org/2000/svg"
59+
width="24"
60+
height="24"
61+
viewBox="0 0 24 24"
62+
className="feedbackIcon unpressed"
63+
role="img"
64+
aria-hidden="true"
65+
>
66+
<path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57l-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm0 12l-4.34 4.34L12 14H3v-2l3-7h9v10zm4-12h4v12h-4z"></path>
67+
</svg>
68+
<svg
69+
xmlns="http://www.w3.org/2000/svg"
70+
width="24"
71+
height="24"
72+
viewBox="0 0 24 24"
73+
className="feedbackIcon pressed"
74+
role="img"
75+
data-is-loaded="true"
76+
aria-hidden="true"
77+
>
78+
<path d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57l-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z"></path>
79+
</svg>
80+
<span className="screenReaderOnly" data-text="Dislike">
81+
Dislike
82+
</span>
83+
</span>
84+
</button>
85+
</div>
86+
</fieldset>
87+
</div>
88+
<div id="feedbackSuccess" className="hidden">
89+
Thank you for your feedback.
90+
</div>
91+
</div>
92+
);
93+
}
94+
}

resources/web/docs_js/index.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import AlternativeSwitcher from "./components/alternative_switcher";
22
import ConsoleWidget from "./components/console_widget";
3+
import FeedbackModal from './components/feedback_modal';
4+
import FeedbackWidget from './components/feedback_widget';
35
import Modal from "./components/modal";
46
import mount from "./components/mount";
57
import {switchTabs} from "./components/tabbed_widget";
@@ -89,6 +91,15 @@ export function init_console_widgets() {
8991
});
9092
}
9193

94+
export function init_feedback_widget() {
95+
mount($('#feedbackWidgetContainer'), FeedbackWidget);
96+
$('.feedbackButton').click(function () {
97+
const isLiked = $(this).hasClass('feedbackLiked');
98+
$(this).addClass('isPressed');
99+
mount($('#feedbackModalContainer'), FeedbackModal, { isLiked: isLiked });
100+
});
101+
}
102+
92103
export function init_sense_widgets() {
93104
$('div.sense_widget').each(function() {
94105
const div = $(this),
@@ -332,8 +343,8 @@ $(function() {
332343
}
333344
})
334345
// Bold the item in the popover that represents
335-
// the current book
336-
const currentBookTitle = dropDownAnchor.text()
346+
// the current book
347+
const currentBookTitle = dropDownAnchor.text()
337348
const items = dropDownContent.find("li")
338349
items.each(function(i) {
339350
if (items[i].innerText === currentBookTitle) {
@@ -365,6 +376,7 @@ $(function() {
365376
init_sense_widgets();
366377
init_console_widgets();
367378
init_kibana_widgets();
379+
init_feedback_widget();
368380
$("div.ess_widget").each(function() {
369381
const div = $(this),
370382
snippet = div.attr('data-snippet'),

0 commit comments

Comments
 (0)