Skip to content

Commit 170dcc5

Browse files
authored
feat(feedback): Add openDialog and closeDialog onto integration interface (#9464)
* Adds `openDialog` and `closeDialog` public methods on the Feedback integration class * Rename `hideDialog` -> `closeDialog` (for Widget) * Rename type `Widget` -> `FeedbackWidget` * Refactor internal logic when creating widgets
1 parent aa27ff0 commit 170dcc5

File tree

5 files changed

+256
-37
lines changed

5 files changed

+256
-37
lines changed

packages/feedback/src/integration.ts

+81-21
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
SUBMIT_BUTTON_LABEL,
1717
SUCCESS_MESSAGE_TEXT,
1818
} from './constants';
19-
import type { FeedbackInternalOptions, OptionalFeedbackConfiguration, Widget } from './types';
19+
import type { FeedbackInternalOptions, FeedbackWidget, OptionalFeedbackConfiguration } from './types';
2020
import { mergeOptions } from './util/mergeOptions';
2121
import { createActorStyles } from './widget/Actor.css';
2222
import { createShadowHost } from './widget/createShadowHost';
@@ -48,12 +48,12 @@ export class Feedback implements Integration {
4848
/**
4949
* Reference to widget element that is created when autoInject is true
5050
*/
51-
private _widget: Widget | null;
51+
private _widget: FeedbackWidget | null;
5252

5353
/**
5454
* List of all widgets that are created from the integration
5555
*/
56-
private _widgets: Set<Widget>;
56+
private _widgets: Set<FeedbackWidget>;
5757

5858
/**
5959
* Reference to the host element where widget is inserted
@@ -166,15 +166,7 @@ export class Feedback implements Integration {
166166
}
167167

168168
try {
169-
// TODO: This is only here for hot reloading
170-
if (this._host) {
171-
this.remove();
172-
}
173-
const existingFeedback = doc.querySelector(`#${this.options.id}`);
174-
if (existingFeedback) {
175-
existingFeedback.remove();
176-
}
177-
// TODO: End hotloading
169+
this._cleanupWidgetIfExists();
178170

179171
const { autoInject } = this.options;
180172

@@ -183,20 +175,49 @@ export class Feedback implements Integration {
183175
return;
184176
}
185177

186-
this._widget = this._createWidget(this.options);
178+
this._createWidget(this.options);
187179
} catch (err) {
188180
__DEBUG_BUILD__ && logger.error(err);
189181
}
190182
}
191183

184+
/**
185+
* Allows user to open the dialog box. Creates a new widget if
186+
* `autoInject` was false, otherwise re-uses the default widget that was
187+
* created during initialization of the integration.
188+
*/
189+
public openDialog(): void {
190+
if (!this._widget) {
191+
this._createWidget({ ...this.options, shouldCreateActor: false });
192+
}
193+
194+
if (!this._widget) {
195+
return;
196+
}
197+
198+
this._widget.openDialog();
199+
}
200+
201+
/**
202+
* Closes the dialog for the default widget, if it exists
203+
*/
204+
public closeDialog(): void {
205+
if (!this._widget) {
206+
// Nothing to do if widget does not exist
207+
return;
208+
}
209+
210+
this._widget.closeDialog();
211+
}
212+
192213
/**
193214
* Adds click listener to attached element to open a feedback dialog
194215
*/
195-
public attachTo(el: Element | string, optionOverrides: OptionalFeedbackConfiguration): Widget | null {
216+
public attachTo(el: Element | string, optionOverrides?: OptionalFeedbackConfiguration): FeedbackWidget | null {
196217
try {
197-
const options = mergeOptions(this.options, optionOverrides);
218+
const options = mergeOptions(this.options, optionOverrides || {});
198219

199-
return this._ensureShadowHost<Widget | null>(options, ({ shadow }) => {
220+
return this._ensureShadowHost<FeedbackWidget | null>(options, ({ shadow }) => {
200221
const targetEl =
201222
typeof el === 'string' ? doc.querySelector(el) : typeof el.addEventListener === 'function' ? el : null;
202223

@@ -207,6 +228,11 @@ export class Feedback implements Integration {
207228

208229
const widget = createWidget({ shadow, options, attachTo: targetEl });
209230
this._widgets.add(widget);
231+
232+
if (!this._widget) {
233+
this._widget = widget;
234+
}
235+
210236
return widget;
211237
});
212238
} catch (err) {
@@ -218,9 +244,11 @@ export class Feedback implements Integration {
218244
/**
219245
* Creates a new widget. Accepts partial options to override any options passed to constructor.
220246
*/
221-
public createWidget(optionOverrides: OptionalFeedbackConfiguration): Widget | null {
247+
public createWidget(
248+
optionOverrides?: OptionalFeedbackConfiguration & { shouldCreateActor?: boolean },
249+
): FeedbackWidget | null {
222250
try {
223-
return this._createWidget(mergeOptions(this.options, optionOverrides));
251+
return this._createWidget(mergeOptions(this.options, optionOverrides || {}));
224252
} catch (err) {
225253
__DEBUG_BUILD__ && logger.error(err);
226254
return null;
@@ -230,7 +258,7 @@ export class Feedback implements Integration {
230258
/**
231259
* Removes a single widget
232260
*/
233-
public removeWidget(widget: Widget | null | undefined): boolean {
261+
public removeWidget(widget: FeedbackWidget | null | undefined): boolean {
234262
if (!widget) {
235263
return false;
236264
}
@@ -240,6 +268,12 @@ export class Feedback implements Integration {
240268
widget.removeActor();
241269
widget.removeDialog();
242270
this._widgets.delete(widget);
271+
272+
if (this._widget === widget) {
273+
// TODO: is more clean-up needed? e.g. call remove()
274+
this._widget = null;
275+
}
276+
243277
return true;
244278
}
245279
} catch (err) {
@@ -249,6 +283,13 @@ export class Feedback implements Integration {
249283
return false;
250284
}
251285

286+
/**
287+
* Returns the default (first-created) widget
288+
*/
289+
public getWidget(): FeedbackWidget | null {
290+
return this._widget;
291+
}
292+
252293
/**
253294
* Removes the Feedback integration (including host, shadow DOM, and all widgets)
254295
*/
@@ -270,11 +311,25 @@ export class Feedback implements Integration {
270311
this._hasInsertedActorStyles = false;
271312
}
272313

314+
/**
315+
* Clean-up the widget if it already exists in the DOM. This shouldn't happen
316+
* in prod, but can happen in development with hot module reloading.
317+
*/
318+
protected _cleanupWidgetIfExists(): void {
319+
if (this._host) {
320+
this.remove();
321+
}
322+
const existingFeedback = doc.querySelector(`#${this.options.id}`);
323+
if (existingFeedback) {
324+
existingFeedback.remove();
325+
}
326+
}
327+
273328
/**
274329
* Creates a new widget, after ensuring shadow DOM exists
275330
*/
276-
protected _createWidget(options: FeedbackInternalOptions): Widget | null {
277-
return this._ensureShadowHost<Widget>(options, ({ shadow }) => {
331+
protected _createWidget(options: FeedbackInternalOptions & { shouldCreateActor?: boolean }): FeedbackWidget | null {
332+
return this._ensureShadowHost<FeedbackWidget>(options, ({ shadow }) => {
278333
const widget = createWidget({ shadow, options });
279334

280335
if (!this._hasInsertedActorStyles && widget.actor) {
@@ -283,6 +338,11 @@ export class Feedback implements Integration {
283338
}
284339

285340
this._widgets.add(widget);
341+
342+
if (!this._widget) {
343+
this._widget = widget;
344+
}
345+
286346
return widget;
287347
});
288348
}

packages/feedback/src/types/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ export interface FeedbackComponent<T extends HTMLElement> {
341341
* - dialog + feedback form
342342
* - shadow root?
343343
*/
344-
export interface Widget {
344+
export interface FeedbackWidget {
345345
actor: ActorComponent | undefined;
346346
dialog: DialogComponent | undefined;
347347

@@ -350,6 +350,6 @@ export interface Widget {
350350
removeActor: () => void;
351351

352352
openDialog: () => void;
353-
hideDialog: () => void;
353+
closeDialog: () => void;
354354
removeDialog: () => void;
355355
}

packages/feedback/src/widget/createWidget.ts

+31-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getCurrentHub } from '@sentry/core';
22
import { logger } from '@sentry/utils';
33

4-
import type { FeedbackFormData, FeedbackInternalOptions, Widget } from '../types';
4+
import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types';
55
import { handleFeedbackSubmit } from '../util/handleFeedbackSubmit';
66
import type { ActorComponent } from './Actor';
77
import { Actor } from './Actor';
@@ -10,15 +10,35 @@ import { Dialog } from './Dialog';
1010
import { SuccessMessage } from './SuccessMessage';
1111

1212
interface CreateWidgetParams {
13+
/**
14+
* Shadow DOM to append to
15+
*/
1316
shadow: ShadowRoot;
14-
options: FeedbackInternalOptions;
17+
18+
/**
19+
* Feedback integration options
20+
*/
21+
options: FeedbackInternalOptions & { shouldCreateActor?: boolean };
22+
23+
/**
24+
* An element to attach to, that when clicked, will open a dialog
25+
*/
1526
attachTo?: Element;
27+
28+
/**
29+
* If false, will not create an actor
30+
*/
31+
shouldCreateActor?: boolean;
1632
}
1733

1834
/**
1935
* Creates a new widget. Returns public methods that control widget behavior.
2036
*/
21-
export function createWidget({ shadow, options, attachTo }: CreateWidgetParams): Widget {
37+
export function createWidget({
38+
shadow,
39+
options: { shouldCreateActor = true, ...options },
40+
attachTo,
41+
}: CreateWidgetParams): FeedbackWidget {
2242
let actor: ActorComponent | undefined;
2343
let dialog: DialogComponent | undefined;
2444
let isDialogOpen: boolean = false;
@@ -159,7 +179,7 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams):
159179
}
160180
},
161181
onCancel: () => {
162-
hideDialog();
182+
closeDialog();
163183
showActor();
164184
},
165185
onSubmit: _handleFeedbackSubmit,
@@ -184,9 +204,9 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams):
184204
}
185205

186206
/**
187-
* Hides the dialog
207+
* Closes the dialog
188208
*/
189-
function hideDialog(): void {
209+
function closeDialog(): void {
190210
if (dialog) {
191211
dialog.close();
192212
isDialogOpen = false;
@@ -202,7 +222,7 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams):
202222
*/
203223
function removeDialog(): void {
204224
if (dialog) {
205-
hideDialog();
225+
closeDialog();
206226
const dialogEl = dialog.el;
207227
dialogEl && dialogEl.remove();
208228
dialog = undefined;
@@ -226,11 +246,11 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams):
226246
}
227247
}
228248

229-
if (!attachTo) {
249+
if (attachTo) {
250+
attachTo.addEventListener('click', handleActorClick);
251+
} else if (shouldCreateActor) {
230252
actor = Actor({ buttonLabel: options.buttonLabel, onClick: handleActorClick });
231253
actor.el && shadow.appendChild(actor.el);
232-
} else {
233-
attachTo.addEventListener('click', handleActorClick);
234254
}
235255

236256
return {
@@ -246,7 +266,7 @@ export function createWidget({ shadow, options, attachTo }: CreateWidgetParams):
246266
removeActor,
247267

248268
openDialog,
249-
hideDialog,
269+
closeDialog,
250270
removeDialog,
251271
};
252272
}

0 commit comments

Comments
 (0)