Skip to content

Commit 1fc051f

Browse files
committed
ref: move Actor into sep file
1 parent 605abe7 commit 1fc051f

File tree

3 files changed

+174
-76
lines changed

3 files changed

+174
-76
lines changed

packages/feedback/src/index.ts

Lines changed: 106 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { getCurrentHub } from '@sentry/core';
1+
// import { getCurrentHub } from '@sentry/core';
22
import type { Integration } from '@sentry/types';
33
import { isNodeEnv } from '@sentry/utils';
44

55
import type { FeedbackConfigurationWithDefaults } from './types';
66
import { sendFeedbackRequest } from './util/sendFeedbackRequest';
7+
import { Actor } from './widget/Actor';
78
import { createActorStyles } from './widget/Actor.css';
89
import { Dialog } from './widget/Dialog';
910
import { createDialogStyles } from './widget/Dialog.css';
10-
import { Icon } from './widget/Icon';
1111

1212
export { sendFeedbackRequest };
1313

@@ -50,13 +50,31 @@ export class Feedback implements Integration {
5050
*/
5151
public name: string;
5252

53+
/**
54+
* Feedback configuration options
55+
*/
5356
public options: FeedbackConfigurationWithDefaults;
5457

55-
private actor: HTMLButtonElement | null = null;
56-
private dialog: ReturnType<typeof Dialog> | null = null;
57-
private host: HTMLDivElement | null = null;
58-
private shadow: ShadowRoot | null = null;
59-
private isDialogOpen: boolean = false;
58+
/**
59+
* Reference to widget actor element (button that opens dialog).
60+
*/
61+
private _actor: ReturnType<typeof Actor> | null;
62+
/**
63+
* Reference to dialog element
64+
*/
65+
private _dialog: ReturnType<typeof Dialog> | null;
66+
/**
67+
* Reference to the host element where widget is inserted
68+
*/
69+
private _host: HTMLDivElement | null;
70+
/**
71+
* Refernce to Shadow DOM root
72+
*/
73+
private _shadow: ShadowRoot | null;
74+
/**
75+
* State property to track if dialog is currently open
76+
*/
77+
private _isDialogOpen: boolean;
6078

6179
public constructor({
6280
showEmail = true,
@@ -80,7 +98,14 @@ export class Feedback implements Integration {
8098
namePlaceholder = 'Your Name',
8199
nameLabel = 'Name',
82100
}: Partial<FeedbackConfigurationWithDefaults> = {}) {
101+
// Initializations
83102
this.name = Feedback.id;
103+
this._actor = null;
104+
this._dialog = null;
105+
this._host = null;
106+
this._shadow = null;
107+
this._isDialogOpen = false;
108+
84109
this.options = {
85110
isAnonymous,
86111
isEmailRequired,
@@ -117,12 +142,54 @@ export class Feedback implements Integration {
117142
this._injectWidget();
118143
}
119144

145+
/**
146+
* Removes the Feedback widget
147+
*/
148+
public remove(): void {
149+
if (this._host) {
150+
this._host.remove();
151+
}
152+
}
153+
154+
/**
155+
* Opens the Feedback dialog form
156+
*/
157+
public openDialog(): void {
158+
if (this._dialog) {
159+
this._dialog.openDialog();
160+
return;
161+
}
162+
163+
if (!this._shadow) {
164+
this._shadow = this._createShadowHost();
165+
}
166+
167+
this._shadow.appendChild(createDialogStyles(document, THEME));
168+
this._dialog = Dialog({ onCancel: this.closeDialog, options: this.options });
169+
this._shadow.appendChild(this._dialog.$el);
170+
this._actor && this._actor.hide();
171+
}
172+
173+
/**
174+
* Closes the dialog
175+
*/
176+
public closeDialog = (): void => {
177+
if (this._dialog) {
178+
this._dialog.closeDialog();
179+
}
180+
181+
// TODO: if has default actor, show the button
182+
if (this._actor) {
183+
this._actor.show();
184+
}
185+
};
186+
120187
/**
121188
*
122189
*/
123-
protected _injectWidget() {
190+
protected _injectWidget(): void {
124191
// TODO: This is only here for hot reloading
125-
if (this.host) {
192+
if (this._host) {
126193
this.remove();
127194
}
128195
const existingFeedback = document.querySelector('#sentry-feedback');
@@ -132,94 +199,59 @@ export class Feedback implements Integration {
132199

133200
// TODO: End hotloading
134201

135-
this.createWidgetButton();
202+
this._shadow = this._createShadowHost();
203+
this._createWidgetActor();
136204

137-
if (!this.host) {
205+
if (!this._host) {
138206
return;
139207
}
140208

141-
document.body.appendChild(this.host);
142-
}
143-
144-
/**
145-
* Removes the Feedback widget
146-
*/
147-
public remove() {
148-
if (this.host) {
149-
this.host.remove();
150-
}
209+
document.body.appendChild(this._host);
151210
}
152211

153212
/**
154-
*
213+
* Creates the host element of widget's shadow DOM
155214
*/
156-
protected createWidgetButton() {
215+
protected _createShadowHost(): ShadowRoot {
157216
// Create the host
158-
this.host = document.createElement('div');
159-
this.host.id = 'sentry-feedback';
160-
this.shadow = this.host.attachShadow({ mode: 'open' });
161-
162-
this.shadow.appendChild(createActorStyles(document, THEME));
163-
164-
const actorButton = document.createElement('button');
165-
actorButton.type = 'button';
166-
actorButton.className = 'widget-actor';
167-
actorButton.ariaLabel = this.options.buttonLabel;
168-
const buttonTextEl = document.createElement('span');
169-
buttonTextEl.className = 'widget-actor-text';
170-
buttonTextEl.textContent = this.options.buttonLabel;
171-
this.shadow.appendChild(actorButton);
172-
173-
actorButton.appendChild(Icon({ color: THEME.light.foreground }));
174-
actorButton.appendChild(buttonTextEl);
175-
176-
actorButton.addEventListener('click', this.handleActorClick.bind(this));
177-
this.actor = actorButton;
178-
}
179-
180-
/**
181-
*
182-
*/
183-
protected handleActorClick() {
184-
console.log('button clicked');
185-
186-
// Open dialog
187-
if (!this.isDialogOpen) {
188-
this.openDialog();
189-
}
217+
this._host = document.createElement('div');
218+
this._host.id = 'sentry-feedback';
190219

191-
// Hide actor button
192-
if (this.actor) {
193-
this.actor.classList.add('hidden');
194-
}
220+
// Create the shadow root
221+
return this._host.attachShadow({ mode: 'open' });
195222
}
196223

197224
/**
198-
* Opens the Feedback dialog form
225+
* Creates the host element of our shadow DOM as well as the actor
199226
*/
200-
public openDialog() {
201-
if (this.dialog) {
202-
this.dialog.openDialog();
227+
protected _createWidgetActor(): void {
228+
if (!this._shadow) {
229+
// This shouldn't happen... we could call `_createShadowHost` if this is the case?
203230
return;
204231
}
205232

206-
this.shadow?.appendChild(createDialogStyles(document, THEME));
207-
this.dialog = Dialog({ onCancel: this.closeDialog, options: this.options });
208-
this.shadow?.appendChild(this.dialog.$el);
233+
// Insert styles for actor
234+
this._shadow.appendChild(createActorStyles(document, THEME));
235+
236+
// Create Actor component
237+
this._actor = Actor({ options: this.options, theme: THEME, onClick: this._handleActorClick });
238+
239+
this._shadow.appendChild(this._actor.$el);
209240
}
210241

211242
/**
212-
* Closes the dialog
243+
* Handles when the actor is clicked, opens the dialog modal and calls any
244+
* callbacks.
213245
*/
214-
public closeDialog = () => {
215-
if (this.dialog) {
216-
this.dialog.closeDialog();
246+
protected _handleActorClick = (): void => {
247+
// Open dialog
248+
if (!this._isDialogOpen) {
249+
this.openDialog();
217250
}
218251

219-
// TODO: if has default actor, show the button
220-
221-
if (this.actor) {
222-
this.actor.classList.remove('hidden');
252+
// Hide actor button
253+
if (this._actor) {
254+
this._actor.classList.add('hidden');
223255
}
224256
};
225257
}

packages/feedback/src/widget/Actor.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { FeedbackConfigurationWithDefaults, FeedbackTheme } from '../types';
2+
import { Icon } from './Icon';
3+
import { createElement as h } from './util/createElement';
4+
5+
interface Props {
6+
options: FeedbackConfigurationWithDefaults;
7+
theme: FeedbackTheme;
8+
onClick?: (e: MouseEvent) => void;
9+
}
10+
11+
interface ActorReturn {
12+
$el: HTMLButtonElement;
13+
/**
14+
* Shows the actor element
15+
*/
16+
show: () => void;
17+
/**
18+
* Hides the actor element
19+
*/
20+
hide: () => void;
21+
}
22+
23+
/**
24+
*
25+
*/
26+
export function Actor({ options, theme, onClick }: Props): ActorReturn {
27+
function _handleClick(e: MouseEvent) {
28+
if (typeof onClick === 'function') {
29+
onClick(e);
30+
}
31+
}
32+
const $el = h(
33+
'button',
34+
{
35+
type: 'button',
36+
className: 'widget__actor',
37+
ariaLabel: options.buttonLabel,
38+
},
39+
Icon({ color: theme.light.foreground }),
40+
h(
41+
'span',
42+
{
43+
className: 'widget__actor__text',
44+
},
45+
options.buttonLabel,
46+
),
47+
);
48+
49+
$el.addEventListener('click', _handleClick);
50+
51+
return {
52+
$el,
53+
show: (): void => {
54+
$el.classList.remove('widget__actor--hidden');
55+
},
56+
hide: (): void => {
57+
$el.classList.add('widget__actor--hidden');
58+
},
59+
};
60+
}

packages/feedback/src/widget/Icon.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ interface Props {
55
color: string;
66
}
77

8+
interface IconReturn {
9+
$el: SVGElement;
10+
}
11+
812
function setAttributes<T extends SVGElement>(el: T, attributes: Record<string, string>): T {
913
Object.entries(attributes).forEach(([key, val]) => {
1014
el.setAttributeNS(null, key, val);
@@ -15,7 +19,7 @@ function setAttributes<T extends SVGElement>(el: T, attributes: Record<string, s
1519
/**
1620
* Feedback Icon
1721
*/
18-
export function Icon({ color }: Props): SVGElement {
22+
export function Icon({ color }: Props): IconReturn {
1923
const svg = setAttributes(document.createElementNS(XMLNS, 'svg'), {
2024
width: `${SIZE}`,
2125
height: `${SIZE}`,
@@ -51,5 +55,7 @@ export function Icon({ color }: Props): SVGElement {
5155

5256
svg.appendChild(speakerDefs).appendChild(speakerClipPathDef).appendChild(speakerRect);
5357

54-
return svg;
58+
return {
59+
$el: svg,
60+
};
5561
}

0 commit comments

Comments
 (0)