pushfeedback 0.1.83 → 0.1.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,36 @@
1
1
  import { h } from '@stencil/core';
2
+ import { sanitizeHtml } from '../../utils/sanitize-html';
2
3
  export class FeedbackModal {
3
4
  constructor() {
5
+ // Track the history entry pushed while the modal is open
6
+ this.historyEntryPushed = false;
7
+ // Element that had focus before the modal opened, so it can be restored on close
8
+ this.previouslyFocused = null;
9
+ this.handleKeyDown = (event) => {
10
+ // The canvas editor renders its own overlay and handles its own keys
11
+ if (!this.showModal || this.embedded || this.showCanvasEditor) {
12
+ return;
13
+ }
14
+ if (event.key === 'Escape' && this.escapeClose) {
15
+ event.preventDefault();
16
+ // Escape is an accidental-dismissal path like clicking outside, so keep
17
+ // whatever the user already typed
18
+ this.doClose(!this.formSuccess);
19
+ return;
20
+ }
21
+ if (event.key === 'Tab') {
22
+ this.trapFocus(event);
23
+ }
24
+ };
25
+ this.handlePopState = () => {
26
+ // Back button pressed while the modal is open: close it and consume our entry
27
+ if (this.historyEntryPushed) {
28
+ this.historyEntryPushed = false;
29
+ if (this.showModal) {
30
+ this.close();
31
+ }
32
+ }
33
+ };
4
34
  this.onScrollDebounced = () => {
5
35
  clearTimeout(this.scrollTimeout);
6
36
  this.scrollTimeout = setTimeout(() => {
@@ -35,12 +65,16 @@ export class FeedbackModal {
35
65
  }
36
66
  }
37
67
  const body = Object.assign({ url: window.location.href, message: this.formMessage, email: this.formEmail, project: this.project, screenshot: this.encodedScreenshot, rating: this.selectedRating, ratingMode: this.ratingMode, metadata: this.metadata, verification: this.formVerification, session: localStorage.getItem('pushfeedback_sessionid') || '' }, (recaptchaToken && { recaptchaToken }));
38
- const res = await fetch('https://app.pushfeedback.com/api/feedback/', {
68
+ const headers = {
69
+ 'Content-Type': 'application/json',
70
+ };
71
+ if (this.apiKey) {
72
+ headers['Authorization'] = `Api-Key ${this.apiKey}`;
73
+ }
74
+ const res = await fetch('https://app.pushfeedback.com/api/v1/feedback/', {
39
75
  method: 'POST',
40
76
  body: JSON.stringify(body),
41
- headers: {
42
- 'Content-Type': 'application/json',
43
- },
77
+ headers,
44
78
  });
45
79
  if (res.status === 201) {
46
80
  const feedback_with_id = Object.assign(Object.assign({}, body), { id: await res.json() });
@@ -85,14 +119,22 @@ export class FeedbackModal {
85
119
  }
86
120
  };
87
121
  this.close = () => {
122
+ this.doClose(false);
123
+ };
124
+ // Closing by clicking outside keeps the typed input, so an accidental
125
+ // click does not throw away a half-written message or screenshot
126
+ this.handleOverlayClick = () => {
127
+ this.doClose(!this.formSuccess);
128
+ };
129
+ this.doClose = (preserveInput) => {
88
130
  this.isAnimating = false;
131
+ this.popHistoryEntry();
89
132
  setTimeout(() => {
90
133
  this.sending = false;
91
134
  this.showModal = false;
92
135
  this.showScreenshotMode = false;
93
136
  this.showScreenshotTopBar = false;
94
137
  this.hasSelectedElement = false;
95
- this.encodedScreenshot = null;
96
138
  // Remove highlight from ALL selected elements
97
139
  document.querySelectorAll('.feedback-modal-element-selected').forEach((el) => {
98
140
  el.classList.remove('feedback-modal-element-selected');
@@ -101,9 +143,19 @@ export class FeedbackModal {
101
143
  this.formSuccess = false;
102
144
  this.formError = false;
103
145
  this.formErrorStatus = 500;
104
- this.formMessage = '';
105
- this.formEmail = '';
146
+ if (!preserveInput) {
147
+ this.encodedScreenshot = null;
148
+ this.formMessage = '';
149
+ this.formEmail = '';
150
+ // The rating and the privacy checkbox are part of the same in-progress
151
+ // input. Leaving the rating set meant reopening the modal showed the
152
+ // previous session's choice still highlighted, and submitting then sent
153
+ // that stale rating instead of one chosen for this submission.
154
+ this.selectedRating = this.initialRating();
155
+ this.isPrivacyChecked = false;
156
+ }
106
157
  this.resetOverflow();
158
+ this.restoreFocus();
107
159
  }, 200);
108
160
  };
109
161
  // Handle screenshot events from canvas editor
@@ -113,12 +165,16 @@ export class FeedbackModal {
113
165
  this.takingScreenshot = false;
114
166
  this.showCanvasEditor = false;
115
167
  this.autoStartCapture = false;
168
+ this.focusModal();
116
169
  };
117
170
  this.handleScreenshotCancelled = () => {
118
171
  this.showModal = true;
119
172
  this.takingScreenshot = false;
120
173
  this.showCanvasEditor = false;
121
174
  this.autoStartCapture = false;
175
+ // Focus was inside the editor that just went away, so bring it back to the
176
+ // form rather than letting it fall to the top of the page
177
+ this.focusModal();
122
178
  };
123
179
  this.handleScreenshotError = (event) => {
124
180
  console.error('Screenshot error:', event.detail.error);
@@ -130,6 +186,7 @@ export class FeedbackModal {
130
186
  this.takingScreenshot = false;
131
187
  this.showCanvasEditor = false;
132
188
  this.autoStartCapture = false;
189
+ this.focusModal();
133
190
  // Auto-hide error after 8 seconds
134
191
  setTimeout(() => {
135
192
  this.showScreenshotError = false;
@@ -171,6 +228,7 @@ export class FeedbackModal {
171
228
  this.screenshotError = '';
172
229
  this.showCanvasEditor = false;
173
230
  this.autoStartCapture = false;
231
+ this.apiKey = '';
174
232
  this.customFont = false;
175
233
  this.emailAddress = '';
176
234
  this.hideEmail = false;
@@ -188,8 +246,12 @@ export class FeedbackModal {
188
246
  this.metadata = undefined;
189
247
  this.fetchData = true;
190
248
  this.embedded = false;
249
+ this.clickOutsideClose = true;
250
+ this.historyClose = true;
251
+ this.escapeClose = true;
191
252
  this.emailPlaceholder = 'Email address (optional)';
192
253
  this.errorMessage = 'Please try again later.';
254
+ this.errorMessage401 = 'This project requires a valid API key to submit feedback. Check the api-key attribute.';
193
255
  this.errorMessage403 = 'The request URL does not match the one defined in PushFeedback for this project.';
194
256
  this.errorMessage404 = 'We could not find the provided project ID in PushFeedback.';
195
257
  this.messagePlaceholder = 'Share your thoughts...';
@@ -202,6 +264,10 @@ export class FeedbackModal {
202
264
  this.ratingPlaceholder = 'Was this page helpful?';
203
265
  this.ratingStarsPlaceholder = 'How would you rate this page?';
204
266
  this.sendButtonText = 'Send';
267
+ this.closeButtonLabel = 'Close feedback form';
268
+ this.ratingPositiveLabel = 'Yes';
269
+ this.ratingNegativeLabel = 'No';
270
+ this.ratingStarLabel = 'Rate {rating} out of 5';
205
271
  this.successMessage = '';
206
272
  this.recaptchaText = 'This form is protected by reCAPTCHA and the Google <a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a> and <a href="https://policies.google.com/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a> apply.';
207
273
  this.screenshotAttachedText = 'Screenshot attached';
@@ -222,16 +288,98 @@ export class FeedbackModal {
222
288
  this.screenshotErrorBrowserNotSupported = 'Your browser does not support screen capture. Please use a browser like Chrome, Firefox, or Safari on desktop.';
223
289
  this.screenshotErrorUnexpected = 'An unexpected error occurred. Please try again.';
224
290
  }
291
+ connectedCallback() {
292
+ window.addEventListener('popstate', this.handlePopState);
293
+ document.addEventListener('keydown', this.handleKeyDown);
294
+ }
295
+ disconnectedCallback() {
296
+ window.removeEventListener('popstate', this.handlePopState);
297
+ document.removeEventListener('keydown', this.handleKeyDown);
298
+ }
299
+ // Keeps Tab cycling inside the dialog while it is open
300
+ trapFocus(event) {
301
+ var _a, _b;
302
+ const focusable = this.getFocusableElements();
303
+ if (focusable.length === 0) {
304
+ return;
305
+ }
306
+ const first = focusable[0];
307
+ const last = focusable[focusable.length - 1];
308
+ const active = (_a = this.modalContent) === null || _a === void 0 ? void 0 : _a.getRootNode();
309
+ const current = active === null || active === void 0 ? void 0 : active.activeElement;
310
+ if (current === this.modalContent) {
311
+ // Focus is on the dialog container itself (where it lands on open).
312
+ // Tab falls through to the first control natively; Shift+Tab would
313
+ // escape the dialog, so wrap it to the last one.
314
+ if (event.shiftKey) {
315
+ event.preventDefault();
316
+ last.focus();
317
+ }
318
+ }
319
+ else if (event.shiftKey && current === first) {
320
+ event.preventDefault();
321
+ last.focus();
322
+ }
323
+ else if (!event.shiftKey && current === last) {
324
+ event.preventDefault();
325
+ first.focus();
326
+ }
327
+ else if (!current || !((_b = this.modalContent) === null || _b === void 0 ? void 0 : _b.contains(current))) {
328
+ // Focus escaped the dialog (or never entered it) - pull it back in
329
+ event.preventDefault();
330
+ first.focus();
331
+ }
332
+ }
333
+ // offsetParent is not usable here: it is null for `position: fixed` elements
334
+ // even when they are perfectly visible, and it is non-null for
335
+ // `visibility: hidden` ones, which cannot be focused. getClientRects covers
336
+ // display:none and detached nodes, so visibility is the only extra check.
337
+ isFocusable(el) {
338
+ if (el.hidden || el.getClientRects().length === 0) {
339
+ return false;
340
+ }
341
+ return getComputedStyle(el).visibility !== 'hidden';
342
+ }
343
+ getFocusableElements() {
344
+ if (!this.modalContent) {
345
+ return [];
346
+ }
347
+ const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
348
+ return Array.from(this.modalContent.querySelectorAll(selector)).filter((el) => this.isFocusable(el));
349
+ }
350
+ // Moves focus to the dialog container once it has rendered, so screen readers
351
+ // announce the dialog title rather than jumping straight to the first control
352
+ focusModal() {
353
+ requestAnimationFrame(() => {
354
+ if (this.modalContent) {
355
+ this.modalContent.focus();
356
+ }
357
+ });
358
+ }
359
+ restoreFocus() {
360
+ const target = this.previouslyFocused;
361
+ this.previouslyFocused = null;
362
+ if (target && typeof target.focus === 'function' && target.isConnected) {
363
+ target.focus();
364
+ }
365
+ }
366
+ // The rating the widget starts from. Closing restores this rather than
367
+ // blanking to neutral, so a consumer's configured `rating` default survives
368
+ // the modal being closed and reopened.
369
+ initialRating() {
370
+ if (this.ratingMode == 'thumbs' && this.rating == 0) {
371
+ return 5;
372
+ }
373
+ if (this.rating) {
374
+ return this.rating;
375
+ }
376
+ return -1;
377
+ }
225
378
  componentWillLoad() {
226
379
  if (this.fetchData)
227
380
  this.fetchProjectData();
228
381
  this.formEmail = this.emailAddress;
229
- if (this.rating) {
230
- this.selectedRating = this.rating;
231
- }
232
- if (this.ratingMode == 'thumbs' && this.rating == 0) {
233
- this.selectedRating = 5;
234
- }
382
+ this.selectedRating = this.initialRating();
235
383
  // Automatically show modal when embedded
236
384
  if (this.embedded) {
237
385
  this.showModal = true;
@@ -239,7 +387,9 @@ export class FeedbackModal {
239
387
  }
240
388
  async fetchProjectData() {
241
389
  try {
242
- const response = await fetch('https://app.pushfeedback.com/api/projects/' + this.project + '/');
390
+ // Public config endpoint: never send the API key here, it is only
391
+ // needed (and only allowed to travel) on the feedback POST
392
+ const response = await fetch('https://app.pushfeedback.com/api/v1/projects/' + this.project + '/');
243
393
  const data = await response.json();
244
394
  this.whitelabel = data.whitelabel;
245
395
  this.recaptchaEnabled = data.recaptcha_enabled || false;
@@ -312,6 +462,12 @@ export class FeedbackModal {
312
462
  handleEmailInput(event) {
313
463
  this.formEmail = event.target.value;
314
464
  }
465
+ popHistoryEntry() {
466
+ if (this.historyEntryPushed) {
467
+ this.historyEntryPushed = false;
468
+ history.back();
469
+ }
470
+ }
315
471
  handleCheckboxChange(event) {
316
472
  this.isPrivacyChecked = event.target.checked;
317
473
  }
@@ -322,28 +478,28 @@ export class FeedbackModal {
322
478
  this.selectedRating = newRating;
323
479
  }
324
480
  render() {
325
- return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''} ${this.embedded ? 'feedback-modal-wrapper--embedded' : ''}`, part: "wrapper" }, this.showCanvasEditor && (h("canvas-editor", { ref: (el) => (this.canvasEditorRef = el), exportparts: "overlay: canvas-editor-overlay, modal: canvas-editor-modal, header: canvas-editor-header, title: canvas-editor-title, toolbar: canvas-editor-toolbar, tool-button: canvas-editor-tool-button, cancel-button: canvas-editor-cancel-button, save-button: canvas-editor-save-button, content: canvas-editor-content, canvas: canvas-editor-canvas", "canvas-editor-title": this.screenshotEditorTitle, "canvas-editor-cancel-text": this.screenshotEditorCancelText, "canvas-editor-save-text": this.screenshotEditorSaveText, "screenshot-taking-text": this.screenshotTakingText, "screenshot-attached-text": this.screenshotAttachedText, "screenshot-button-text": this.screenshotButtonText, "auto-start-screenshot": this.autoStartCapture, "existing-screenshot": this.encodedScreenshot || '', "edit-text-button-text": this.screenshotEditTextButtonText, "size-label-text": this.screenshotSizeLabelText, "border-label-text": this.screenshotBorderLabelText, "edit-text-prompt-text": this.screenshotEditTextPromptText, "screenshot-error-general": this.screenshotErrorGeneral, "screenshot-error-permission": this.screenshotErrorPermission, "screenshot-error-not-supported": this.screenshotErrorNotSupported, "screenshot-error-not-found": this.screenshotErrorNotFound, "screenshot-error-cancelled": this.screenshotErrorCancelled, "screenshot-error-browser-not-supported": this.screenshotErrorBrowserNotSupported, "screenshot-error-unexpected": this.screenshotErrorUnexpected, onScreenshotReady: this.handleScreenshotReady, onScreenshotCancelled: this.handleScreenshotCancelled, onScreenshotFailed: this.handleScreenshotError })), this.showScreenshotError && (h("div", { class: "screenshot-error-notification" }, h("div", { class: "screenshot-error-content" }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), h("line", { x1: "9", y1: "9", x2: "15", y2: "15" })), h("span", null, this.screenshotError), h("button", { class: "error-close-btn", onClick: () => (this.showScreenshotError = false), title: "Close" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))))), this.showModal && !this.embedded && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}`, part: "overlay" })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''} ${this.embedded ? 'feedback-modal-content--embedded' : ''}`, part: "modal", ref: (el) => (this.modalContent = el) }, h("div", { class: `feedback-modal-header ${(this.formSuccess && !this.successMessage) || (this.formError && !this.errorMessage)
481
+ return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''} ${this.embedded ? 'feedback-modal-wrapper--embedded' : ''}`, part: "wrapper" }, this.showCanvasEditor && (h("canvas-editor", { ref: (el) => (this.canvasEditorRef = el), exportparts: "overlay: canvas-editor-overlay, modal: canvas-editor-modal, header: canvas-editor-header, title: canvas-editor-title, toolbar: canvas-editor-toolbar, tool-button: canvas-editor-tool-button, cancel-button: canvas-editor-cancel-button, save-button: canvas-editor-save-button, content: canvas-editor-content, canvas: canvas-editor-canvas", "canvas-editor-title": this.screenshotEditorTitle, "canvas-editor-cancel-text": this.screenshotEditorCancelText, "canvas-editor-save-text": this.screenshotEditorSaveText, "screenshot-taking-text": this.screenshotTakingText, "screenshot-attached-text": this.screenshotAttachedText, "screenshot-button-text": this.screenshotButtonText, "escape-close": this.escapeClose ? 'true' : 'false', "click-outside-close": this.clickOutsideClose ? 'true' : 'false', "auto-start-screenshot": this.autoStartCapture, "existing-screenshot": this.encodedScreenshot || '', "edit-text-button-text": this.screenshotEditTextButtonText, "size-label-text": this.screenshotSizeLabelText, "border-label-text": this.screenshotBorderLabelText, "edit-text-prompt-text": this.screenshotEditTextPromptText, "screenshot-error-general": this.screenshotErrorGeneral, "screenshot-error-permission": this.screenshotErrorPermission, "screenshot-error-not-supported": this.screenshotErrorNotSupported, "screenshot-error-not-found": this.screenshotErrorNotFound, "screenshot-error-cancelled": this.screenshotErrorCancelled, "screenshot-error-browser-not-supported": this.screenshotErrorBrowserNotSupported, "screenshot-error-unexpected": this.screenshotErrorUnexpected, onScreenshotReady: this.handleScreenshotReady, onScreenshotCancelled: this.handleScreenshotCancelled, onScreenshotFailed: this.handleScreenshotError })), this.showScreenshotError && (h("div", { class: "screenshot-error-notification" }, h("div", { class: "screenshot-error-content" }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), h("line", { x1: "9", y1: "9", x2: "15", y2: "15" })), h("span", null, this.screenshotError), h("button", { class: "error-close-btn", onClick: () => (this.showScreenshotError = false), title: "Close" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))))), this.showModal && !this.embedded && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}`, part: "overlay", "aria-hidden": "true", onClick: this.clickOutsideClose ? this.handleOverlayClick : undefined })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''} ${this.embedded ? 'feedback-modal-content--embedded' : ''}`, part: "modal", role: this.embedded ? 'region' : 'dialog', "aria-modal": this.embedded ? undefined : 'true', "aria-labelledby": "feedback-modal-title", tabindex: "-1", ref: (el) => (this.modalContent = el) }, h("div", { class: `feedback-modal-header ${(this.formSuccess && !this.successMessage) || (this.formError && !this.errorMessage)
326
482
  ? 'feedback-modal-header--no-content'
327
- : ''}`, part: "header" }, h("div", { class: "feedback-modal-header-content" }, !this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-header-text" }, h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitle), !this.whitelabel && (h("span", { class: "feedback-modal-powered-by", part: "powered-by" }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback"))))) : this.formSuccess ? (h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitleSuccess)) : (h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitleError))), !this.embedded && (h("button", { class: "feedback-modal-close", part: "close-button", onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))), h("div", { class: "feedback-modal-body", part: "body" }, !this.formSuccess && !this.formError ? (h("form", { part: "form", onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating", part: "rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs", part: "rating-buttons" }, h("button", { title: "Yes", class: `feedback-modal-rating-button feedback-modal-rating-button--positive ${this.selectedRating === 1
483
+ : ''}`, part: "header" }, h("div", { class: "feedback-modal-header-content" }, !this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-header-text" }, h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitle), !this.whitelabel && (h("span", { class: "feedback-modal-powered-by", part: "powered-by" }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback"))))) : this.formSuccess ? (h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitleSuccess)) : (h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitleError))), !this.embedded && (h("button", { class: "feedback-modal-close", part: "close-button", type: "button", "aria-label": this.closeButtonLabel, onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))), h("div", { class: "feedback-modal-body", part: "body" }, !this.formSuccess && !this.formError ? (h("form", { part: "form", onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating", part: "rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs", part: "rating-buttons" }, h("button", { type: "button", title: this.ratingPositiveLabel, "aria-label": this.ratingPositiveLabel, "aria-pressed": this.selectedRating === 1 ? 'true' : 'false', class: `feedback-modal-rating-button feedback-modal-rating-button--positive ${this.selectedRating === 1
328
484
  ? 'feedback-modal-rating-button--selected'
329
485
  : ''}`, part: "rating-button rating-button-positive", onClick: (event) => {
330
486
  event.preventDefault();
331
487
  this.handleRatingChange(1);
332
- } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }))), h("button", { title: "No", class: `feedback-modal-rating-button feedback-modal-rating-button--negative ${this.selectedRating === 5
488
+ } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }))), h("button", { type: "button", title: this.ratingNegativeLabel, "aria-label": this.ratingNegativeLabel, "aria-pressed": this.selectedRating === 5 ? 'true' : 'false', class: `feedback-modal-rating-button feedback-modal-rating-button--negative ${this.selectedRating === 5
333
489
  ? 'feedback-modal-rating-button--selected'
334
490
  : ''}`, part: "rating-button rating-button-negative", onClick: (event) => {
335
491
  event.preventDefault();
336
492
  this.handleRatingChange(5);
337
- } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" })))))) : (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingStarsPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--stars", part: "rating-buttons" }, [1, 2, 3, 4, 5].map((rating) => (h("button", { key: rating, class: `feedback-modal-rating-button ${this.selectedRating >= rating
493
+ } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" })))))) : (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingStarsPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--stars", part: "rating-buttons" }, [1, 2, 3, 4, 5].map((rating) => (h("button", { key: rating, type: "button", "aria-label": this.ratingStarLabel.replace('{rating}', String(rating)), "aria-pressed": this.selectedRating === rating ? 'true' : 'false', class: `feedback-modal-rating-button ${this.selectedRating >= rating
338
494
  ? 'feedback-modal-rating-button--selected'
339
495
  : ''}`, part: "rating-button", onClick: (event) => {
340
496
  event.preventDefault();
341
497
  this.handleRatingChange(rating);
342
- } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { part: "message-input", placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { part: "email-input", placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy", part: "privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: this.privacyPolicyText }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}`, part: "buttons" }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, part: "screenshot-button", onClick: this.openScreenShot, disabled: this.sending || this.takingScreenshot }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openCanvasEditor }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && !this.takingScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.takingScreenshot && (h("div", { class: "screenshot-loading" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather-loader" }, h("line", { x1: "12", y1: "2", x2: "12", y2: "6" }), h("line", { x1: "12", y1: "18", x2: "12", y2: "22" }), h("line", { x1: "4.93", y1: "4.93", x2: "7.76", y2: "7.76" }), h("line", { x1: "16.24", y1: "16.24", x2: "19.07", y2: "19.07" }), h("line", { x1: "2", y1: "12", x2: "6", y2: "12" }), h("line", { x1: "18", y1: "12", x2: "22", y2: "12" }), h("line", { x1: "4.93", y1: "19.07", x2: "7.76", y2: "16.24" }), h("line", { x1: "16.24", y1: "7.76", x2: "19.07", y2: "4.93" })))), this.takingScreenshot
498
+ } }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { part: "message-input", placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { part: "email-input", placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy", part: "privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: sanitizeHtml(this.privacyPolicyText) }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}`, part: "buttons" }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, part: "screenshot-button", onClick: this.openScreenShot, disabled: this.sending || this.takingScreenshot }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openCanvasEditor }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && !this.takingScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.takingScreenshot && (h("div", { class: "screenshot-loading" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather-loader" }, h("line", { x1: "12", y1: "2", x2: "12", y2: "6" }), h("line", { x1: "12", y1: "18", x2: "12", y2: "22" }), h("line", { x1: "4.93", y1: "4.93", x2: "7.76", y2: "7.76" }), h("line", { x1: "16.24", y1: "16.24", x2: "19.07", y2: "19.07" }), h("line", { x1: "2", y1: "12", x2: "6", y2: "12" }), h("line", { x1: "18", y1: "12", x2: "22", y2: "12" }), h("line", { x1: "4.93", y1: "19.07", x2: "7.76", y2: "16.24" }), h("line", { x1: "16.24", y1: "7.76", x2: "19.07", y2: "4.93" })))), this.takingScreenshot
343
499
  ? this.screenshotTakingText
344
500
  : this.encodedScreenshot
345
501
  ? this.screenshotAttachedText
346
- : this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", part: "submit-button", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message", part: "success-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage)) : (h("span", null))), !this.formSuccess && !this.formError && (this.recaptchaEnabled || this.footerText) && (h("div", { class: "feedback-modal-footer", part: "footer" }, h("div", { class: "feedback-footer-combined" }, this.recaptchaEnabled && h("span", { innerHTML: this.recaptchaText }), this.recaptchaEnabled && this.footerText && ' ', this.footerText && h("span", { innerHTML: this.footerText }))))))));
502
+ : this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", part: "submit-button", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message", part: "success-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 401 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage401)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage)) : (h("span", null))), !this.formSuccess && !this.formError && (this.recaptchaEnabled || this.footerText) && (h("div", { class: "feedback-modal-footer", part: "footer" }, h("div", { class: "feedback-footer-combined" }, this.recaptchaEnabled && (h("span", { innerHTML: sanitizeHtml(this.recaptchaText) })), this.recaptchaEnabled && this.footerText && ' ', this.footerText && h("span", { innerHTML: sanitizeHtml(this.footerText) }))))))));
347
503
  }
348
504
  componentDidRender() {
349
505
  if (this.showModal) {
@@ -353,7 +509,17 @@ export class FeedbackModal {
353
509
  }
354
510
  }
355
511
  async openModal() {
512
+ // Remember the trigger so focus can return there when the modal closes
513
+ if (!this.embedded) {
514
+ const active = document.activeElement;
515
+ this.previouslyFocused = active && active !== document.body ? active : null;
516
+ }
356
517
  this.showModal = true;
518
+ this.focusModal();
519
+ if (this.historyClose && !this.embedded && !this.historyEntryPushed) {
520
+ history.pushState({ feedbackModal: true }, '');
521
+ this.historyEntryPushed = true;
522
+ }
357
523
  requestAnimationFrame(() => {
358
524
  requestAnimationFrame(() => {
359
525
  this.isAnimating = true;
@@ -374,6 +540,24 @@ export class FeedbackModal {
374
540
  }
375
541
  static get properties() {
376
542
  return {
543
+ "apiKey": {
544
+ "type": "string",
545
+ "mutable": false,
546
+ "complexType": {
547
+ "original": "string",
548
+ "resolved": "string",
549
+ "references": {}
550
+ },
551
+ "required": false,
552
+ "optional": false,
553
+ "docs": {
554
+ "tags": [],
555
+ "text": "Optional API key sent with feedback submissions, for projects that\nrequire one. Sent as `Authorization: Api-Key <key>` on the feedback POST\nrequest only, never on the public config request. Only use keys scoped to\n`feedback:submit`: like any credential used by an embedded widget, the\nvalue is part of the page and readable by any script running on it, so it\nprovides server-side scoping and traceability rather than client-side\nsecrecy. Intended for internal or access-controlled pages."
556
+ },
557
+ "attribute": "api-key",
558
+ "reflect": false,
559
+ "defaultValue": "''"
560
+ },
377
561
  "customFont": {
378
562
  "type": "boolean",
379
563
  "mutable": false,
@@ -678,6 +862,60 @@ export class FeedbackModal {
678
862
  "reflect": false,
679
863
  "defaultValue": "false"
680
864
  },
865
+ "clickOutsideClose": {
866
+ "type": "boolean",
867
+ "mutable": false,
868
+ "complexType": {
869
+ "original": "boolean",
870
+ "resolved": "boolean",
871
+ "references": {}
872
+ },
873
+ "required": false,
874
+ "optional": false,
875
+ "docs": {
876
+ "tags": [],
877
+ "text": ""
878
+ },
879
+ "attribute": "click-outside-close",
880
+ "reflect": false,
881
+ "defaultValue": "true"
882
+ },
883
+ "historyClose": {
884
+ "type": "boolean",
885
+ "mutable": false,
886
+ "complexType": {
887
+ "original": "boolean",
888
+ "resolved": "boolean",
889
+ "references": {}
890
+ },
891
+ "required": false,
892
+ "optional": false,
893
+ "docs": {
894
+ "tags": [],
895
+ "text": ""
896
+ },
897
+ "attribute": "history-close",
898
+ "reflect": false,
899
+ "defaultValue": "true"
900
+ },
901
+ "escapeClose": {
902
+ "type": "boolean",
903
+ "mutable": false,
904
+ "complexType": {
905
+ "original": "boolean",
906
+ "resolved": "boolean",
907
+ "references": {}
908
+ },
909
+ "required": false,
910
+ "optional": false,
911
+ "docs": {
912
+ "tags": [],
913
+ "text": ""
914
+ },
915
+ "attribute": "escape-close",
916
+ "reflect": false,
917
+ "defaultValue": "true"
918
+ },
681
919
  "emailPlaceholder": {
682
920
  "type": "string",
683
921
  "mutable": false,
@@ -714,6 +952,24 @@ export class FeedbackModal {
714
952
  "reflect": false,
715
953
  "defaultValue": "'Please try again later.'"
716
954
  },
955
+ "errorMessage401": {
956
+ "type": "string",
957
+ "mutable": false,
958
+ "complexType": {
959
+ "original": "string",
960
+ "resolved": "string",
961
+ "references": {}
962
+ },
963
+ "required": false,
964
+ "optional": false,
965
+ "docs": {
966
+ "tags": [],
967
+ "text": ""
968
+ },
969
+ "attribute": "error-message-4-0-1",
970
+ "reflect": false,
971
+ "defaultValue": "'This project requires a valid API key to submit feedback. Check the api-key attribute.'"
972
+ },
717
973
  "errorMessage403": {
718
974
  "type": "string",
719
975
  "mutable": false,
@@ -930,6 +1186,78 @@ export class FeedbackModal {
930
1186
  "reflect": false,
931
1187
  "defaultValue": "'Send'"
932
1188
  },
1189
+ "closeButtonLabel": {
1190
+ "type": "string",
1191
+ "mutable": false,
1192
+ "complexType": {
1193
+ "original": "string",
1194
+ "resolved": "string",
1195
+ "references": {}
1196
+ },
1197
+ "required": false,
1198
+ "optional": false,
1199
+ "docs": {
1200
+ "tags": [],
1201
+ "text": ""
1202
+ },
1203
+ "attribute": "close-button-label",
1204
+ "reflect": false,
1205
+ "defaultValue": "'Close feedback form'"
1206
+ },
1207
+ "ratingPositiveLabel": {
1208
+ "type": "string",
1209
+ "mutable": false,
1210
+ "complexType": {
1211
+ "original": "string",
1212
+ "resolved": "string",
1213
+ "references": {}
1214
+ },
1215
+ "required": false,
1216
+ "optional": false,
1217
+ "docs": {
1218
+ "tags": [],
1219
+ "text": ""
1220
+ },
1221
+ "attribute": "rating-positive-label",
1222
+ "reflect": false,
1223
+ "defaultValue": "'Yes'"
1224
+ },
1225
+ "ratingNegativeLabel": {
1226
+ "type": "string",
1227
+ "mutable": false,
1228
+ "complexType": {
1229
+ "original": "string",
1230
+ "resolved": "string",
1231
+ "references": {}
1232
+ },
1233
+ "required": false,
1234
+ "optional": false,
1235
+ "docs": {
1236
+ "tags": [],
1237
+ "text": ""
1238
+ },
1239
+ "attribute": "rating-negative-label",
1240
+ "reflect": false,
1241
+ "defaultValue": "'No'"
1242
+ },
1243
+ "ratingStarLabel": {
1244
+ "type": "string",
1245
+ "mutable": false,
1246
+ "complexType": {
1247
+ "original": "string",
1248
+ "resolved": "string",
1249
+ "references": {}
1250
+ },
1251
+ "required": false,
1252
+ "optional": false,
1253
+ "docs": {
1254
+ "tags": [],
1255
+ "text": ""
1256
+ },
1257
+ "attribute": "rating-star-label",
1258
+ "reflect": false,
1259
+ "defaultValue": "'Rate {rating} out of 5'"
1260
+ },
933
1261
  "successMessage": {
934
1262
  "type": "string",
935
1263
  "mutable": false,
@@ -1340,6 +1668,9 @@ export class FeedbackModal {
1340
1668
  "references": {
1341
1669
  "Promise": {
1342
1670
  "location": "global"
1671
+ },
1672
+ "HTMLElement": {
1673
+ "location": "global"
1343
1674
  }
1344
1675
  },
1345
1676
  "return": "Promise<void>"