Skip to content

Add feedback widget to docs sidebar #2838

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

Merged
merged 3 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 234 additions & 0 deletions resources/web/docs_js/components/feedback_modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { h, Component } from '../../../../../../node_modules/preact';

const FEEDBACK_URL = 'https://docs.elastic.co/api/feedback'
const MAX_COMMENT_LENGTH = 1000;

export default class FeedbackModal extends Component {
constructor(props) {
super(props);
this.state = {
comment: '',
modalClosed: false,
isLoading: false,
hasError: false,
};
this.onEscape = this.onEscape.bind(this);
this.resetState = this.resetState.bind(this);
this.submitFeedback = this.submitFeedback.bind(this);
}

onEscape(event) {
if (event.key === 'Escape') {
this.resetState();
}
}

resetState() {
this.setState({ modalClosed: true });
document.querySelectorAll('.isPressed').forEach((el) => {
el.classList.remove('isPressed');
});
}

submitFeedback() {
this.setState({ isLoading: true });
fetch(FEEDBACK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
comment: this.state.comment,
feedback: this.props.isLiked ? 'liked' : 'disliked',
url: window.location.href,
}),
})
.then((response) => response.json())
.then(() => {
this.setState({ modalClosed: true })
document.getElementById('feedbackSuccess').classList.remove('hidden')
document.querySelectorAll('.feedbackButton').forEach((el) => {
el.disabled = true
})
})
.catch((error) => {
this.setState({ isLoading: false, hasError: true });
console.error('Error:', error);
});

}

componentDidMount() {
document.addEventListener('keydown', this.onEscape, false);
}

componentWillUnmount() {
document.removeEventListener('keydown', this.onEscape, false);
}

render(props, state) {
const { isLiked } = props;
const { modalClosed, isLoading, hasError, comment } = state;
const maxCommentLengthReached = comment.length > MAX_COMMENT_LENGTH;
const sendDisabled = isLoading || maxCommentLengthReached;

if (modalClosed) {
return null;
}

return (
<div
data-relative-to-header="above"
id="feedbackModal"
>
<div
data-focus-guard="true"
tabindex="0"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
></div>
<div data-focus-lock-disabled="false">
<div className="feedbackModalContent" tabindex="0">
<button
className="closeIcon"
type="button"
aria-label="Closes this modal window"
onClick={this.resetState}
disabled={isLoading}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
role="img"
data-icon-type="cross"
data-is-loaded="true"
aria-hidden="true"
>
<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>
</svg>
</button>
<div className="feedbackModalHeader">
<h2>Send us your feedback</h2>
</div>
<div className="feedbackModalBody">
<div className="feedbackModalBodyOverflow">
<div>
Thank you for helping us improve Elastic documentation.
</div>
<div className="spacer"></div>
<div className="feedbackForm">
<div className="feedbackFormRow">
<div className="feedbackFormRow__labelWrapper">
<label
className="feedbackFormLabel"
id="feedbackLabel"
for="feedbackComment"
>
Additional comment (optional)
</label>
</div>
<div className="feedbackFormRow__fieldWrapper">
<div className="feedbackFormControlLayout">
<div className="feedbackFormControlLayout__childrenWrapper">
<textarea
className="feedbackTextArea"
rows="6"
id="feedbackComment"
disabled={isLoading}
onKeyUp={(e) =>
this.setState({ comment: e.target.value })
}
></textarea>
{maxCommentLengthReached && (
<div className="feedbackFormError">
Max comment length of {MAX_COMMENT_LENGTH}{' '}
characters reached.
<br />
<br />
Character count: {comment.length}
</div>
)}
{hasError && (
<div className="feedbackFormError">
There was a problem submitting your feedback.
<br />
<br />
Please try again.
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
className={`feedbackModalFooter ${isLoading ? 'loading' : ''}`}
>
<button
className="feedbackButton cancelButton"
type="button"
onClick={this.resetState}
disabled={isLoading}
>
<span className="feedbackButtonContent">
<span>Cancel</span>
</span>
</button>
<button
type="button"
disabled={sendDisabled}
className={`feedbackButton sendButton ${
isLiked ? 'like' : 'dislike'
}`}
onClick={this.submitFeedback}
>
<span className="loadingContent">
<span
class="loadingSpinner"
role="progressbar"
aria-label="Loading"
style="border-color: rgb(0, 119, 204) currentcolor currentcolor;"
></span>
<span>Sending...</span>
</span>
<span className="feedbackButtonContent">
<span>Send</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="sendIcon like"
role="img"
aria-hidden="true"
>
<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>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="sendIcon dislike"
role="img"
aria-hidden="true"
>
<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>
</svg>
</span>
</button>
</div>
</div>
</div>
<div
data-focus-guard="true"
tabindex="0"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
></div>
</div>
);
}
}
94 changes: 94 additions & 0 deletions resources/web/docs_js/components/feedback_widget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { h, Component } from '../../../../../../node_modules/preact';

export default class FeedbackWidget extends Component {
render() {
return (
<div>
<div id="feedbackWidget">
Was this helpful?
<span className="docHorizontalSpacer"></span>
<fieldset className="buttonGroup">
<legend className="screenReaderOnly">Feedback</legend>
<div className="buttonGroup">
<button
aria-pressed="false"
id="feedbackLiked"
type="button"
className="feedbackButton feedbackLiked"
title="Like"
>
<span className="feedbackButtonContent">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="feedbackIcon unpressed"
role="img"
aria-hidden="true"
>
<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>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="feedbackIcon pressed"
role="img"
data-is-loaded="true"
aria-hidden="true"
>
<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>
</svg>
<span className="screenReaderOnly" data-text="Like">
Like
</span>
</span>
</button>
<button
aria-pressed="false"
id="feedbackDisliked"
type="button"
className="feedbackButton feedbackDisliked"
title="Dislike"
>
<span className="feedbackButtonContent">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="feedbackIcon unpressed"
role="img"
aria-hidden="true"
>
<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>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="feedbackIcon pressed"
role="img"
data-is-loaded="true"
aria-hidden="true"
>
<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>
</svg>
<span className="screenReaderOnly" data-text="Dislike">
Dislike
</span>
</span>
</button>
</div>
</fieldset>
</div>
<div id="feedbackSuccess" className="hidden">
Thank you for your feedback.
</div>
</div>
);
}
}
16 changes: 14 additions & 2 deletions resources/web/docs_js/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import AlternativeSwitcher from "./components/alternative_switcher";
import ConsoleWidget from "./components/console_widget";
import FeedbackModal from './components/feedback_modal';
import FeedbackWidget from './components/feedback_widget';
import Modal from "./components/modal";
import mount from "./components/mount";
import {switchTabs} from "./components/tabbed_widget";
Expand Down Expand Up @@ -89,6 +91,15 @@ export function init_console_widgets() {
});
}

export function init_feedback_widget() {
mount($('#feedbackWidgetContainer'), FeedbackWidget);
$('.feedbackButton').click(function () {
const isLiked = $(this).hasClass('feedbackLiked');
$(this).addClass('isPressed');
mount($('#feedbackModalContainer'), FeedbackModal, { isLiked: isLiked });
});
}

export function init_sense_widgets() {
$('div.sense_widget').each(function() {
const div = $(this),
Expand Down Expand Up @@ -332,8 +343,8 @@ $(function() {
}
})
// Bold the item in the popover that represents
// the current book
const currentBookTitle = dropDownAnchor.text()
// the current book
const currentBookTitle = dropDownAnchor.text()
const items = dropDownContent.find("li")
items.each(function(i) {
if (items[i].innerText === currentBookTitle) {
Expand Down Expand Up @@ -365,6 +376,7 @@ $(function() {
init_sense_widgets();
init_console_widgets();
init_kibana_widgets();
init_feedback_widget();
$("div.ess_widget").each(function() {
const div = $(this),
snippet = div.attr('data-snippet'),
Expand Down
Loading