Skip to content

Commit 48cd500

Browse files
committed
cleanup callbacks and rendering
1 parent 8db78b0 commit 48cd500

File tree

18 files changed

+351
-402
lines changed

18 files changed

+351
-402
lines changed

.size-limit.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ module.exports = [
6161
gzip: true,
6262
limit: '50 KB',
6363
},
64+
{
65+
name: '@sentry/browser (incl. feedback2Integration) - Webpack (gzipped)',
66+
path: 'packages/browser/build/npm/esm/index.js',
67+
import: '{ init, feedback2Integration }',
68+
gzip: true,
69+
limit: '50 KB',
70+
},
71+
{
72+
name: '@sentry/browser (incl. feedback2ModalIntegration) - Webpack (gzipped)',
73+
path: 'packages/browser/build/npm/esm/index.js',
74+
import: '{ init, feedback2Integration, feedback2ModalIntegration }',
75+
gzip: true,
76+
limit: '50 KB',
77+
},
78+
{
79+
name: '@sentry/browser (incl. feedback2ScreenshotIntegration) - Webpack (gzipped)',
80+
path: 'packages/browser/build/npm/esm/index.js',
81+
import: '{ init, feedback2Integration, feedback2ModalIntegration, feedback2ScreenshotIntegration }',
82+
gzip: true,
83+
limit: '50 KB',
84+
},
6485
{
6586
name: '@sentry/browser (incl. sendFeedback) - Webpack (gzipped)',
6687
path: 'packages/browser/build/npm/esm/index.js',

packages/feedback2/src/core/components/Actor.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,21 @@ import { FeedbackIcon } from './FeedbackIcon';
44

55
export interface ActorProps {
66
buttonLabel: string;
7+
shadow: ShadowRoot;
78
}
89

910
export interface ActorComponent {
10-
/**
11-
* The button element itself
12-
*/
13-
el: HTMLButtonElement;
11+
el: HTMLElement;
1412

15-
/**
16-
* The style element for this component
17-
*/
18-
style: HTMLStyleElement;
13+
appendToDom: () => void;
14+
15+
removeFromDom: () => void;
1916
}
2017

2118
/**
2219
* The sentry-provided button to open the feedback modal
2320
*/
24-
export function Actor({ buttonLabel }: ActorProps): ActorComponent {
21+
export function Actor({ buttonLabel, shadow }: ActorProps): ActorComponent {
2522
const el = DOCUMENT.createElement('button');
2623
el.type = 'button';
2724
el.className = 'widget__actor';
@@ -38,11 +35,14 @@ export function Actor({ buttonLabel }: ActorProps): ActorComponent {
3835
const style = createActorStyles();
3936

4037
return {
41-
get el() {
42-
return el;
38+
el,
39+
appendToDom(): void {
40+
shadow.appendChild(style);
41+
shadow.appendChild(el);
4342
},
44-
get style() {
45-
return style;
43+
removeFromDom(): void {
44+
shadow.removeChild(el);
45+
shadow.removeChild(style);
4646
},
4747
};
4848
}

packages/feedback2/src/core/integration.ts

Lines changed: 76 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,27 @@ import {
1616
SUBMIT_BUTTON_LABEL,
1717
SUCCESS_MESSAGE_TEXT,
1818
} from '../constants';
19-
import type { DialogComponent } from '../modal/components/Dialog';
2019
import type { IFeedback2ModalIntegration } from '../modal/integration';
2120
import type { IFeedback2ScreenshotIntegration } from '../screenshot/integration';
22-
import type { FeedbackInternalOptions, OptionalFeedbackConfiguration } from '../types';
21+
import type {
22+
Dialog,
23+
FeedbackInternalOptions,
24+
OptionalFeedbackConfiguration,
25+
OverrideFeedbackConfiguration,
26+
} from '../types';
2327
import { DEBUG_BUILD } from '../util/debug-build';
2428
import { mergeOptions } from '../util/mergeOptions';
2529
import { Actor } from './components/Actor';
2630
import { createMainStyles } from './createMainStyles';
2731
import { sendFeedback } from './sendFeedback';
2832

33+
type Unsubscribe = () => void;
34+
2935
interface PublicFeedback2Integration {
36+
attachTo: (el: Element | string, optionOverrides: OverrideFeedbackConfiguration) => () => void;
37+
createWidget: (optionOverrides: OverrideFeedbackConfiguration & { shouldCreateActor?: boolean }) => Promise<Dialog>;
38+
getWidget: () => Dialog | null;
3039
remove: () => void;
31-
attachTo: (el: Element | string, optionOverrides: OptionalFeedbackConfiguration) => () => void;
32-
createWidget: (
33-
optionOverrides: OptionalFeedbackConfiguration & { shouldCreateActor?: boolean },
34-
) => Promise<DialogComponent>;
35-
getWidget: () => DialogComponent | null;
3640
openDialog: () => void;
3741
closeDialog: () => void;
3842
removeWidget: () => void;
@@ -119,72 +123,33 @@ export const _feedback2Integration = (({
119123
onFormSubmitted,
120124
};
121125

122-
let _host: HTMLElement | null = null;
123126
let _shadow: ShadowRoot | null = null;
124-
let _dialog: DialogComponent | null = null;
125-
126-
/**
127-
* Get the dom root, where DOM nodes will be appended into
128-
*/
129-
const _getHost = (options: FeedbackInternalOptions): HTMLElement => {
130-
if (!_host) {
131-
const { id, colorScheme } = options;
132-
133-
const host = DOCUMENT.createElement('div');
134-
_host = host;
135-
host.id = String(id);
136-
host.dataset.sentryFeedbackColorscheme = colorScheme;
137-
DOCUMENT.body.appendChild(_host);
138-
}
139-
return _host;
140-
};
127+
let _subscriptions: Unsubscribe[] = [];
141128

142129
/**
143130
* Get the shadow root where we will append css
144131
*/
145-
const _getShadow = (options: FeedbackInternalOptions): ShadowRoot => {
132+
const _createShadow = (options: FeedbackInternalOptions): ShadowRoot => {
146133
if (!_shadow) {
147-
const host = _getHost(options);
134+
const host = DOCUMENT.createElement('div');
135+
host.id = String(options.id);
136+
DOCUMENT.body.appendChild(host);
148137

149-
const { colorScheme, themeDark, themeLight } = options;
150-
const shadow = host.attachShadow({ mode: 'open' });
151-
shadow.appendChild(
152-
// TODO: inject main styles as part of actor and dialog styles
153-
// therefore each render root can have it's own theme
154-
// err, everything can just have it's own shadowroot...
155-
createMainStyles(colorScheme, {
156-
themeDark,
157-
themeLight,
158-
}),
159-
);
160-
_shadow = shadow;
138+
_shadow = host.attachShadow({ mode: 'open' });
139+
_shadow.appendChild(createMainStyles(options.colorScheme, options));
161140
}
162-
163-
return _shadow;
141+
return _shadow as ShadowRoot;
164142
};
165143

166-
const _loadAndRenderDialog = async (options: FeedbackInternalOptions): Promise<DialogComponent> => {
167-
if (_dialog) {
168-
return _dialog;
169-
}
170-
144+
const _loadAndRenderDialog = async (options: FeedbackInternalOptions): Promise<Dialog> => {
171145
const client = getClient(); // TODO: getClient<BrowserClient>()
172146
if (!client) {
173147
throw new Error('Sentry Client is not initialized correctly');
174148
}
175149
const modalIntegration = client.getIntegrationByName<IFeedback2ModalIntegration>('Feedback2Modal');
176150
const screenshotIntegration = client.getIntegrationByName<IFeedback2ScreenshotIntegration>('Feedback2Screenshot');
177151

178-
// Disable this because the site could have multiple feedback buttons, not all of them need to have screenshots enabled.
179-
// Must be a better way...
180-
//
181-
// if (showScreenshot === false && screenshotIntegration) {
182-
// // Warn the user that they loaded too much and explicitly asked for screen shots to be off
183-
// console.log('WARNING: Feedback2Screenshot is bundled but not rendered.'); // eslint-disable-line no-console
184-
// }
185-
186152
// START TEMP: Error messages
187-
console.log('ensureRenderer:', { modalIntegration, showScreenshot, screenshotIntegration }); // eslint-disable-line no-console
188153
if (!modalIntegration && showScreenshot && !screenshotIntegration) {
189154
throw new Error('Async loading of Feedback Modal & Screenshot integrations is not yet implemented');
190155
} else if (!modalIntegration) {
@@ -203,21 +168,16 @@ export const _feedback2Integration = (({
203168
throw new Error('Not implemented yet');
204169
}
205170

206-
const dialog = modalIntegration.createDialog({
207-
shadow: _getShadow(options),
208-
sendFeedback,
171+
return modalIntegration.createDialog({
209172
options,
210-
onDone: () => {
211-
_dialog = null;
212-
},
213173
screenshotIntegration,
174+
sendFeedback,
175+
shadow: _createShadow(options),
214176
});
215-
_dialog = dialog;
216-
return dialog;
217177
};
218178

219-
const attachTo = (el: Element | string, optionOverrides: OptionalFeedbackConfiguration = {}): (() => void) => {
220-
const options = mergeOptions(_options, optionOverrides);
179+
const attachTo = (el: Element | string, optionOverrides: OverrideFeedbackConfiguration = {}): Unsubscribe => {
180+
const mergedOptions = mergeOptions(_options, optionOverrides);
221181

222182
const targetEl =
223183
typeof el === 'string' ? DOCUMENT.querySelector(el) : typeof el.addEventListener === 'function' ? el : null;
@@ -227,14 +187,52 @@ export const _feedback2Integration = (({
227187
throw new Error('Unable to attach to target element');
228188
}
229189

190+
let dialog: Dialog | null = null;
230191
const handleClick = async (): Promise<void> => {
231-
const dialog = await _loadAndRenderDialog(options);
192+
if (!dialog) {
193+
dialog = await _loadAndRenderDialog({
194+
...mergedOptions,
195+
onFormClose: () => {
196+
dialog && dialog.close();
197+
mergedOptions.onFormClose && mergedOptions.onFormClose();
198+
},
199+
onFormSubmitted: () => {
200+
dialog && dialog.removeFromDom();
201+
mergedOptions.onFormSubmitted && mergedOptions.onFormSubmitted();
202+
},
203+
});
204+
}
205+
dialog.appendToDom();
232206
dialog.open();
233207
};
234208
targetEl.addEventListener('click', handleClick);
235-
return () => {
209+
const unsubscribe = (): void => {
210+
_subscriptions = _subscriptions.filter(sub => sub !== unsubscribe);
211+
dialog && dialog.removeFromDom();
212+
dialog = null;
236213
targetEl.removeEventListener('click', handleClick);
237214
};
215+
_subscriptions.push(unsubscribe);
216+
return unsubscribe;
217+
};
218+
219+
const autoInjectActor = (): void => {
220+
const shadow = _createShadow(_options);
221+
const actor = Actor({ buttonLabel: _options.buttonLabel, shadow });
222+
const mergedOptions = mergeOptions(_options, {
223+
onFormOpen() {
224+
actor.removeFromDom();
225+
},
226+
onFormClose() {
227+
actor.appendToDom();
228+
},
229+
onFormSubmitted() {
230+
actor.appendToDom();
231+
},
232+
});
233+
attachTo(actor.el, mergedOptions);
234+
235+
actor.appendToDom();
238236
};
239237

240238
return {
@@ -244,40 +242,7 @@ export const _feedback2Integration = (({
244242
return;
245243
}
246244

247-
const shadow = _getShadow(_options);
248-
const actor = Actor({ buttonLabel: _options.buttonLabel });
249-
const insertActor = (): void => {
250-
shadow.appendChild(actor.style);
251-
shadow.appendChild(actor.el);
252-
};
253-
attachTo(actor.el, {
254-
onFormOpen() {
255-
shadow.removeChild(actor.el);
256-
shadow.removeChild(actor.style);
257-
_options.onFormOpen && _options.onFormOpen();
258-
},
259-
onFormClose() {
260-
insertActor();
261-
_options.onFormClose && _options.onFormClose();
262-
},
263-
onFormSubmitted() {
264-
insertActor();
265-
_options.onFormSubmitted && _options.onFormSubmitted();
266-
},
267-
});
268-
269-
insertActor();
270-
},
271-
272-
/**
273-
* Removes the Feedback integration (including host, shadow DOM, and all widgets)
274-
*/
275-
remove(): void {
276-
if (_host) {
277-
_host.remove();
278-
}
279-
_host = null;
280-
_shadow = null;
245+
autoInjectActor();
281246
},
282247

283248
/**
@@ -290,46 +255,21 @@ export const _feedback2Integration = (({
290255
/**
291256
* Creates a new widget. Accepts partial options to override any options passed to constructor.
292257
*/
293-
createWidget(
294-
optionOverrides: OptionalFeedbackConfiguration & { shouldCreateActor?: boolean } = {},
295-
): Promise<DialogComponent> {
296-
const options = mergeOptions(_options, optionOverrides);
297-
298-
return _loadAndRenderDialog(options);
258+
async createWidget(optionOverrides: OverrideFeedbackConfiguration = {}): Promise<Dialog> {
259+
return _loadAndRenderDialog(mergeOptions(_options, optionOverrides));
299260
},
300261

301262
/**
302-
* Returns the default widget, if it exists
303-
*/
304-
getWidget(): DialogComponent | null {
305-
return _dialog;
306-
},
307-
308-
/**
309-
* Allows user to open the dialog box. Creates a new widget if
310-
* `autoInject` was false, otherwise re-uses the default widget that was
311-
* created during initialization of the integration.
312-
*/
313-
openDialog(): void {
314-
_dialog && _dialog.open();
315-
},
316-
317-
/**
318-
* Closes the dialog for the default widget, if it exists
319-
*/
320-
closeDialog(): void {
321-
_dialog && _dialog.close();
322-
},
323-
324-
/**
325-
* Removes the rendered widget, if it exists
263+
* Removes the Feedback integration (including host, shadow DOM, and all widgets)
326264
*/
327-
removeWidget(): void {
328-
if (_shadow && _dialog) {
329-
_shadow.removeChild(_dialog.el);
330-
_shadow.removeChild(_dialog.style);
265+
remove(): void {
266+
if (_shadow) {
267+
_shadow.parentElement && _shadow.parentElement.remove();
268+
_shadow = null;
331269
}
332-
_dialog = null;
270+
// Remove any lingering subscriptions
271+
_subscriptions.forEach(sub => sub());
272+
_subscriptions = [];
333273
},
334274
};
335275
}) satisfies IntegrationFn;

packages/feedback2/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ export {
77

88
export {
99
// eslint-disable-next-line deprecation/deprecation
10-
Feedback2Modal,
1110
feedback2ModalIntegration,
1211
} from './modal/integration';
1312

1413
export {
1514
// eslint-disable-next-line deprecation/deprecation
16-
Feedback2Screenshot,
1715
feedback2ScreenshotIntegration,
1816
} from './screenshot/integration';
1917

2018
export type { OptionalFeedbackConfiguration } from './types';
19+
20+
export type { IFeedback2Integration } from './core/integration';

0 commit comments

Comments
 (0)