From 64617dfe861d3631ac7a9477fae98990edf59cf8 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Wed, 1 Nov 2023 12:00:40 -0400 Subject: [PATCH 01/59] docs(feedback): Add more themeables + improve/fix inconsistencies * Adds themeable options for submit/cancel/input * Make css vars + theme vars consistent * Fix incorrect defaults --- packages/feedback/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/feedback/README.md b/packages/feedback/README.md index 56be5e23ee7c..c99e38c8d8a6 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -93,7 +93,7 @@ Most text that you see in the default Feedback widget can be customized. | `formTitle` | `Report a Bug` | The title at the top of the feedback form dialog. | | `nameLabel` | `Name` | The label of the name input field. | | `namePlaceholder` | `Your Name` | The placeholder for the name input field. | -| `emailLabel` | `Email` | The label of the email input field. || +| `emailLabel` | `Email` | The label of the email input field. | | `emailPlaceholder` | `your.email@example.org` | The placeholder for the email input field. | | `messageLabel` | `Description` | The label for the feedback description input field. | | `messagePlaceholder` | `What's the bug? What did you expect?` | The placeholder for the feedback description input field. | From 493992ce864be32b8a06f9e0d0ade884a1679932 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 6 Oct 2023 16:41:28 -0400 Subject: [PATCH 02/59] wip: initial widget api --- packages/feedback/src/index.ts | 554 ++++++++++++++++++++++++++++++++- 1 file changed, 553 insertions(+), 1 deletion(-) diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts index 834e9dcce670..e587b1751c4e 100644 --- a/packages/feedback/src/index.ts +++ b/packages/feedback/src/index.ts @@ -1 +1,553 @@ -export { sendFeedbackRequest } from './util/sendFeedbackRequest'; +import type { BrowserClient, Replay } from '@sentry/browser'; +import { getCurrentHub } from '@sentry/core'; +import type { Integration } from '@sentry/types'; +import { isNodeEnv } from '@sentry/utils'; + +import type {FeedbackConfigurationWithDefaults} from './types'; +import { sendFeedbackRequest } from './util/sendFeedbackRequest'; +import {Form} from './widget/Form'; +import {Icon} from './widget/Icon'; + +export {sendFeedbackRequest}; + +type ElectronProcess = { type?: string }; + +// Electron renderers with nodeIntegration enabled are detected as Node.js so we specifically test for them +function isElectronNodeRenderer(): boolean { + return typeof process !== 'undefined' && (process as ElectronProcess).type === 'renderer'; +} +/** + * Returns true if we are in the browser. + */ +function isBrowser(): boolean { + // eslint-disable-next-line no-restricted-globals + return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); +} + +function retrieveStringValue(formData: FormData, key: string) { + const value = formData.get(key); + if (typeof value === 'string') { + return value.trim(); + } + return ''; +} + +const THEME = { + light: { + foreground: '#2B2233', + }, + dark: { + foreground: '#EBE6EF', + }, +}; + +/** + * + */ +export class Feedback implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Feedback'; + + /** + * @inheritDoc + */ + public name: string; + + public options: FeedbackConfigurationWithDefaults; + + private actor: HTMLButtonElement | null = null; + private dialog: HTMLDialogElement | null = null; + private host: HTMLDivElement | null = null; + private shadow: ShadowRoot | null = null; + private isDialogOpen: boolean = false; + + public constructor({ + showEmail = true, + showName = true, + useSentryUser = { + email: 'email', + name: 'username', + }, + isAnonymous = true, + isEmailRequired = false, + isNameRequired = false, + + buttonLabel = 'Report a Bug', + cancelButtonLabel = 'Cancel', + submitButtonLabel = 'Send Bug Report', + formTitle = 'Report a Bug', + emailPlaceholder = 'your.email@example.org', + emailLabel = 'Email', + messagePlaceholder = 'What\'s the bug? What did you expect?', + messageLabel = 'Description', + namePlaceholder = 'Your Name', + nameLabel = 'Name', + }: Partial = {}) { + this.name = Feedback.id; + this.options = { + isAnonymous, + isEmailRequired, + isNameRequired, + showEmail, + showName, + useSentryUser, + + buttonLabel, + cancelButtonLabel, + submitButtonLabel, + formTitle, + emailLabel, + emailPlaceholder, + messageLabel, + messagePlaceholder, + nameLabel, + namePlaceholder, + }; + + // TOOD: temp for testing; + this.setupOnce(); + } + + /** If replay has already been initialized */ + /** + * Setup and initialize replay container + */ + public setupOnce(): void { + if (!isBrowser()) { + return; + } + + + this.injectWidget() + } + + /** + * + */ + protected injectWidget() { + console.log('injectWidget') + + // TODO: This is only here for hot reloading + if (this.host) { + console.log('host already exists'); + this.remove(); + } + const existingFeedback = document.querySelector('#sentry-feedback'); + if (existingFeedback) { + console.log('existingFeedback') + existingFeedback.remove(); + } + + // TODO: End hotloading + + this.createWidgetButton(); + + if (!this.host) { + return; + } + + document.body.appendChild(this.host); + } + + /** + * Removes the Feedback widget + */ + public remove() { + if (this.host) { + this.host.remove(); + } + } + + /** + * + */ + protected createWidgetButton() { + // Create the host + this.host = document.createElement('div'); + this.host.id = 'sentry-feedback'; + this.shadow = this.host.attachShadow({ mode: 'open' }); + + const style = document.createElement('style'); + style.textContent = ` + :host { + position: fixed; + right: 1rem; + bottom: 1rem; + font-family: 'Helvetica Neue', Arial, sans-serif; + --bg-color: #fff; + --bg-hover-color: #f6f6f7; + --fg-color: ${THEME.light.foreground}; + --border: 1.5px solid rgba(41, 35, 47, 0.13); + --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); + } + + .__sntry_fdbk_dark:host { + --bg-color: #29232f; + --bg-hover-color: #352f3b; + --fg-color: ${THEME.dark.foreground}; + --border: 1.5px solid rgba(235, 230, 239, 0.15); + --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); + } + + .widget-actor { + line-height: 25px; + + display: flex; + align-items: center; + gap: 8px; + + border-radius: 12px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + padding: 12px 16px; + text-decoration: none; + z-index: 9000; + + color: var(--fg-color); + background-color: var(--bg-color); + border: var(--border); + box-shadow: var(--box-shadow); + opacity: 1; + transition: opacity 0.1s ease-in-out; + } + + .widget-actor:hover { + background-color: var(--bg-hover-color); + } + + .widget-actor svg { + width: 16px; + height: 16px; + } + + .widget-actor.hidden { + opacity: 0; + pointer-events: none; + visibility: hidden; + } + + .widget-actor-text { + } +`; + this.shadow.appendChild(style); + + const actorButton = document.createElement('button'); + actorButton.type = 'button'; + actorButton.className = 'widget-actor'; + actorButton.ariaLabel = this.options.buttonLabel; + const buttonTextEl = document.createElement('span'); + buttonTextEl.className = 'widget-actor-text'; + buttonTextEl.textContent = this.options.buttonLabel; + this.shadow.appendChild(actorButton); + + actorButton.appendChild(Icon({color: THEME.light.foreground})); + actorButton.appendChild(buttonTextEl); + + + + actorButton.addEventListener('click', this.handleActorClick.bind(this)) + this.actor = actorButton; + } + + /** + * + */ + protected handleActorClick() { + console.log('button clicked'); + + // Open dialog + if (!this.isDialogOpen) { + this.openDialog(); + } + + // Hide actor button + if (this.actor) { + this.actor.classList.add('hidden'); + } + } + + /** + * Opens the Feedback dialog form + */ + public openDialog() { + if (this.dialog) { + this.dialog.open = true; + return; + } + + const style = document.createElement('style'); + style.textContent = ` +#feedbackDialog { + --bg-color: #fff; + --bg-hover-color: #f0f0f0; + --fg-color: #000; + --border: 1.5px solid rgba(41, 35, 47, 0.13); + --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); + + &.__sntry_fdbk_dark { + --bg-color: #29232f; + --bg-hover-color: #3a3540; + --fg-color: #ebe6ef; + --border: 1.5px solid rgba(235, 230, 239, 0.15); + --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); + } + + line-height: 25px; + background-color: rgba(0, 0, 0, 0.05); + border: none; + position: fixed; + inset: 0; + z-index: 10000; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + opacity: 1; + transition: opacity 0.2s ease-in-out; +} +#feedbackDialog:not([open]) { + opacity: 0; + pointer-events: none; + visibility: hidden; +} + +.feedback-content { + position: fixed; + right: 1rem; + bottom: 1rem; + + border: var(--border); + padding: 24px; + border-radius: 20px; + background-color: var(--bg-color); + color: var(--fg-color); + + width: 320px; + max-width: 100%; + max-height: calc(100% - 2rem); + display: flex; + flex-direction: column; + box-shadow: + 0 0 0 1px rgba(0, 0, 0, 0.05), + 0 4px 16px rgba(0, 0, 0, 0.2); + transition: transform 0.2s ease-in-out; + transform: translate(0, 0) scale(1); + dialog:not([open]) & { + transform: translate(0, -16px) scale(0.98); + } +} + +.feedback-header { + font-size: 20px; + font-weight: 600; + padding: 0; + margin: 0; + margin-bottom: 16px; +} + +.error { + color: red; + margin-bottom: 16px; +} + +.form { + display: grid; + overflow: auto; + flex-direction: column; + gap: 16px; + padding: 0; +} + +.form__label { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0px; +} + +.form__input { + font-family: inherit; + line-height: inherit; + box-sizing: border-box; + border: var(--border); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + padding: 6px 12px; + &:focus { + border-color: rgba(108, 95, 199, 1); + } +} + +.form__input--textarea { + font-family: inherit; + resize: vertical; +} + +.btn-group { + display: grid; + gap: 8px; + margin-top: 8px; +} + +.btn { + line-height: inherit; + border: var(--border); + border-radius: 6px; + cursor: pointer; + font-size: 14px; + font-weight: 600; + padding: 6px 16px; + + &[disabled] { + opacity: 0.6; + pointer-events: none; + } +} + +.btn--primary { + background-color: rgba(108, 95, 199, 1); + border-color: rgba(108, 95, 199, 1); + color: #fff; + &:hover { + background-color: rgba(88, 74, 192, 1); + } +} + +.btn--default { + background-color: transparent; + color: var(--fg-color); + font-weight: 500; + &:hover { + background-color: var(--bg-accent-color); + } +} +`; + this.shadow?.appendChild(style); + + this.dialog = document.createElement('dialog'); + this.dialog.id = 'feedbackDialog'; + this.dialog.open = true; + + + const user = getCurrentHub().getScope()?.getUser(); + + const contentDiv = document.createElement('div'); + contentDiv.className = 'feedback-content'; + const header = document.createElement('h2'); + header.className = 'feedback-header'; + header.textContent = this.options.formTitle; + + const formEl = document.createElement('form'); + formEl.className = 'form'; + const nameEl = document.createElement('input'); + nameEl.id = 'name' + nameEl.type = 'text'; // TODO can be hidden + nameEl.name = 'name'; + nameEl.className = 'input'; + nameEl.placeholder = this.options.namePlaceholder; + nameEl.ariaHidden = 'false'; // TODO can be hidden + + + const emailEl = document.createElement('input'); + emailEl.type = 'text'; // TODO can be hidden + emailEl.id = 'email'; + emailEl.name = 'email'; + emailEl.className = 'input'; + emailEl.placeholder = this.options.emailPlaceholder; + emailEl.ariaHidden = 'false'; // TODO can be hidden + + const nameLabel = document.createElement('label'); + nameLabel.htmlFor = 'name'; + nameLabel.className = 'label'; + nameLabel.append(this.options.nameLabel, nameEl); + + const emailLabel = document.createElement('label'); + emailLabel.htmlFor = 'email'; + emailLabel.className = 'label'; + emailLabel.append(this.options.emailLabel, emailEl); + + const descLabel = document.createElement('label') + descLabel.htmlFor = 'feedback-message'; + descLabel.className = 'label' + const descLabelWrapper = document.createElement('span'); + descLabelWrapper.textContent = 'Description'; // TODO should be option + + const messageEl = document.createElement('textarea'); + messageEl.className = 'input'; + messageEl.autofocus = true; + messageEl.rows = 5; + messageEl.id = 'feedback-message' + messageEl.name = 'message'; + messageEl.placeholder = this.options.messagePlaceholder + const buttonGroup = document.createElement('div'); + buttonGroup.className = 'btn-group'; + + const submitEl = document.createElement('button'); + submitEl.className = 'btn btn-primary'; + submitEl.type = 'submit'; + submitEl.textContent = this.options.submitButtonLabel; + submitEl.disabled = true; + submitEl.ariaDisabled = 'disabled'; + + const cancelEl = document.createElement('button'); + cancelEl.className = 'btn btn-default'; + cancelEl.type = 'button'; + cancelEl.textContent = this.options.cancelButtonLabel; + + this.dialog.addEventListener('click', this.closeDialog); + contentDiv.addEventListener('click', (e) => { + // Stop event propagation so clicks on content modal do not propagate to dialog (which will close dialog) + e.stopPropagation(); + }) + + cancelEl.addEventListener('click', this.closeDialog); + messageEl.addEventListener('keyup', (e) => { + if (!(e.currentTarget instanceof HTMLTextAreaElement)) { + return; + } + + if (e.currentTarget.value) { + submitEl.ariaDisabled = 'false'; + submitEl.disabled = false; + } else { + submitEl.ariaDisabled = 'true'; + submitEl.disabled = true; + } + }) + + buttonGroup.append(submitEl, cancelEl); + descLabel.append(descLabelWrapper, messageEl); + formEl.append(nameLabel, emailLabel, descLabel, buttonGroup); + + const userKey = this.options.useSentryUser; + + const {$form} = Form({ + defaultName: userKey && user?.[userKey.name] || '', + defaultEmail: userKey && user?.[userKey.email] || '', + options: this.options}) + contentDiv.append(header, $form) + + this.dialog.appendChild(contentDiv) + this.shadow?.appendChild(this.dialog) + } + + /** + * Closes the dialog + */ + public closeDialog = () => { + if (this.dialog) { + this.dialog.open = false; + } + + // TODO: if has default actor, show the button + + if (this.actor) { + this.actor.classList.remove('hidden'); + } + } +} From 9f81fda72a0df6ebc39c6c94cde40b467f0d073a Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 6 Oct 2023 16:47:13 -0400 Subject: [PATCH 03/59] wip: refactor to functions --- packages/feedback/package.json | 3 +- packages/feedback/src/index.ts | 76 +------- packages/feedback/src/sendFeedback.ts | 36 ++++ packages/feedback/src/types/index.ts | 78 ++++++++ packages/feedback/src/widget/Form.ts | 177 ++++++++++++++++++ packages/feedback/src/widget/Icon.ts | 57 ++++++ .../feedback/src/widget/util/createElement.ts | 54 ++++++ yarn.lock | 31 +++ 8 files changed, 436 insertions(+), 76 deletions(-) create mode 100644 packages/feedback/src/sendFeedback.ts create mode 100644 packages/feedback/src/widget/Form.ts create mode 100644 packages/feedback/src/widget/Icon.ts create mode 100644 packages/feedback/src/widget/util/createElement.ts diff --git a/packages/feedback/package.json b/packages/feedback/package.json index f0d2927d2dd3..b4ed531f44b9 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -23,6 +23,7 @@ "access": "public" }, "dependencies": { + "@sentry/browser": "7.70.0", "@sentry/core": "7.70.0", "@sentry/types": "7.70.0", "@sentry/utils": "7.70.0" @@ -51,7 +52,7 @@ "lint:prettier": "prettier --check \"{src,test,scripts}/**/*.ts\"", "test": "jest", "test:watch": "jest --watch", - "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push" + "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push --sig" }, "volta": { "extends": "../../package.json" diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts index e587b1751c4e..07fa1e691b60 100644 --- a/packages/feedback/src/index.ts +++ b/packages/feedback/src/index.ts @@ -442,87 +442,13 @@ export class Feedback implements Integration { header.className = 'feedback-header'; header.textContent = this.options.formTitle; - const formEl = document.createElement('form'); - formEl.className = 'form'; - const nameEl = document.createElement('input'); - nameEl.id = 'name' - nameEl.type = 'text'; // TODO can be hidden - nameEl.name = 'name'; - nameEl.className = 'input'; - nameEl.placeholder = this.options.namePlaceholder; - nameEl.ariaHidden = 'false'; // TODO can be hidden - - - const emailEl = document.createElement('input'); - emailEl.type = 'text'; // TODO can be hidden - emailEl.id = 'email'; - emailEl.name = 'email'; - emailEl.className = 'input'; - emailEl.placeholder = this.options.emailPlaceholder; - emailEl.ariaHidden = 'false'; // TODO can be hidden - - const nameLabel = document.createElement('label'); - nameLabel.htmlFor = 'name'; - nameLabel.className = 'label'; - nameLabel.append(this.options.nameLabel, nameEl); - - const emailLabel = document.createElement('label'); - emailLabel.htmlFor = 'email'; - emailLabel.className = 'label'; - emailLabel.append(this.options.emailLabel, emailEl); - - const descLabel = document.createElement('label') - descLabel.htmlFor = 'feedback-message'; - descLabel.className = 'label' - const descLabelWrapper = document.createElement('span'); - descLabelWrapper.textContent = 'Description'; // TODO should be option - - const messageEl = document.createElement('textarea'); - messageEl.className = 'input'; - messageEl.autofocus = true; - messageEl.rows = 5; - messageEl.id = 'feedback-message' - messageEl.name = 'message'; - messageEl.placeholder = this.options.messagePlaceholder - const buttonGroup = document.createElement('div'); - buttonGroup.className = 'btn-group'; - - const submitEl = document.createElement('button'); - submitEl.className = 'btn btn-primary'; - submitEl.type = 'submit'; - submitEl.textContent = this.options.submitButtonLabel; - submitEl.disabled = true; - submitEl.ariaDisabled = 'disabled'; - - const cancelEl = document.createElement('button'); - cancelEl.className = 'btn btn-default'; - cancelEl.type = 'button'; - cancelEl.textContent = this.options.cancelButtonLabel; - this.dialog.addEventListener('click', this.closeDialog); contentDiv.addEventListener('click', (e) => { // Stop event propagation so clicks on content modal do not propagate to dialog (which will close dialog) e.stopPropagation(); }) - cancelEl.addEventListener('click', this.closeDialog); - messageEl.addEventListener('keyup', (e) => { - if (!(e.currentTarget instanceof HTMLTextAreaElement)) { - return; - } - - if (e.currentTarget.value) { - submitEl.ariaDisabled = 'false'; - submitEl.disabled = false; - } else { - submitEl.ariaDisabled = 'true'; - submitEl.disabled = true; - } - }) - - buttonGroup.append(submitEl, cancelEl); - descLabel.append(descLabelWrapper, messageEl); - formEl.append(nameLabel, emailLabel, descLabel, buttonGroup); + // cancelEl.addEventListener('click', this.closeDialog); const userKey = this.options.useSentryUser; diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts new file mode 100644 index 000000000000..345f2b20812f --- /dev/null +++ b/packages/feedback/src/sendFeedback.ts @@ -0,0 +1,36 @@ +import type { BrowserClient, Replay } from '@sentry/browser'; +import { getCurrentHub } from '@sentry/core'; + +import { sendFeedbackRequest } from './util/sendFeedbackRequest'; + +interface SendFeedbackParams { + message: string; + name?: string; + email?: string; + url?: string; +} + +interface SendFeedbackOptions { + includeReplay?: boolean; +} + +/** + * Public API to send a Feedback item to Sentry + */ +export function sendFeedback({name, email, message, url = document.location.href}: SendFeedbackParams, {includeReplay = true}: SendFeedbackOptions = {}) { + const replay = includeReplay ? getCurrentHub()?.getClient()?.getIntegrationById('Replay') as Replay | undefined : undefined; + + // Prepare session replay + replay?.flush(); + const replayId = replay?.getReplayId(); + + return sendFeedbackRequest({ + feedback: { + name, + email, + message, + url, + replay_id: replayId, + } + }); +} diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index 01a12814c88b..0a6d5d70878d 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -27,3 +27,81 @@ export interface SendFeedbackData { name?: string; }; } + +export interface FeedbackConfigurationWithDefaults { + /** + * If true, will not collect user data (email/name). + */ + isAnonymous: boolean; + + /** + * Should the email field be required + */ + isEmailRequired: boolean; + + /** + * Should the name field be required + */ + isNameRequired: boolean; + + /** + * Should the email input field be visible? + */ + showEmail: boolean; + /** + * Should the name input field be visible? + */ + showName: boolean; + + /** + * Fill in email/name input fields with Sentry user context if it exists. + * The value of the email/name keys represent the properties of your user context. + */ + useSentryUser: { + email: string; + name: string; + }; + + // * Text customization * // + /** + * The label for the Feedback widget button that opens the dialog + */ + buttonLabel: string; + /** + * The label for the Feedback form cancel button that closes dialog + */ + cancelButtonLabel: string; + /** + * The label for the Feedback form submit button that sends feedback + */ + submitButtonLabel: string; + /** + * The title of the Feedback form + */ + formTitle: string; + /** + * Label for the email input + */ + emailLabel: string; + /** + * Placeholder text for Feedback email input + */ + emailPlaceholder: string; + /** + * Label for the message input + */ + messageLabel: string; + /** + * Placeholder text for Feedback message input + */ + messagePlaceholder: string; + /** + * Label for the name input + */ + nameLabel: string; + /** + * Placeholder text for Feedback name input + */ + namePlaceholder: string; + // * End of text customization * // +} diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts new file mode 100644 index 000000000000..af54281be803 --- /dev/null +++ b/packages/feedback/src/widget/Form.ts @@ -0,0 +1,177 @@ +import {sendFeedback} from '../sendFeedback'; +import type { FeedbackConfigurationWithDefaults } from '../types'; +import { createElement as h } from './util/createElement'; + +interface Props { + defaultName: string; + defaultEmail: string, + options: FeedbackConfigurationWithDefaults, +} + +function retrieveStringValue(formData: FormData, key: string) { + const value = formData.get(key); + if (typeof value === 'string') { + return value.trim(); + } + return ''; +} + +/** + * Creates the form element + */ +export function Form({defaultName, defaultEmail, options}: Props) { + const {$el: $submit, setDisabled: setSubmitDisabled, setEnabled: setSubmitEnabled} = SubmitButton({ + label: options.submitButtonLabel, + }); + + + async function handleSubmit(e: Event) { + e.preventDefault(); + console.log('form submitted'); + if (!(e.target instanceof HTMLFormElement)) { + return; + } + + try { + const formData = new FormData(e.target as HTMLFormElement); + const feedback = { + name: retrieveStringValue(formData, 'name'), + email: retrieveStringValue(formData, 'email'), + message: retrieveStringValue(formData, 'message'), + }; + + try { + setSubmitDisabled(); + const resp = await sendFeedback(feedback); + + console.log({resp}) + if (!resp) { + setSubmitEnabled(); + } + } catch(err) { + setSubmitEnabled(); + } + } catch(err) { + } + } + + const $name = h('input', { + id: 'name', + type: 'text', // TODO can be hidden + ariaHidden: 'false', + name: 'name', + className: 'form__input', + placeholder: options.namePlaceholder, + value: defaultName, + }) + + const $email = h('input', { + id: 'email', + type: 'text', // TODO can be hidden + ariaHidden: 'false', + name: 'email', + className: 'form__input', + placeholder: options.emailPlaceholder, + value: defaultEmail, + }) + + const $message = h('textarea', { + id: 'message', + autoFocus: 'true', + rows: '5', + name: 'message', + className: 'form__input form__input--textarea', + placeholder: options.messagePlaceholder, + onKeyup: (e) => { + if (!(e.currentTarget instanceof HTMLTextAreaElement)) { + return; + } + + if (e.currentTarget.value) { + setSubmitEnabled(); + } else { + setSubmitDisabled(); + } + } + }) + + // h('button', { + // type: 'submit', + // className: 'btn btn--primary', + // disabled: true, + // ariaDisabled: 'disabled', + // }, options.submitButtonLabel) + // + const $cancel = h('button', { + type: 'button', + className: 'btn btn--default', + }, options.cancelButtonLabel) + + const $form = h('form', { + className: 'form', + onSubmit: handleSubmit, + }, [ + + h('label', { + htmlFor: 'name', + className: 'form__label', + }, [ + options.nameLabel, + $name + ]), + + h('label', { + htmlFor: 'email', + className: 'form__label', + }, [ + options.emailLabel, + $email + ]), + + + h('label', { + htmlFor: 'message', + className: 'form__label', + }, [ + options.messageLabel, + $message + ]), + + h('div', { + className: 'btn-group', + }, [ + $submit, + $cancel, + ]) + ]) + + return { + $form, + } +} + +interface SubmitButtonProps { + label: string; +} + +function SubmitButton({label}: SubmitButtonProps) { + const $el = h('button', { + type: 'submit', + className: 'btn btn--primary', + disabled: true, + ariaDisabled: 'disabled', + }, label) + + return { + $el, + setDisabled: () => { + $el.disabled = true; + $el.ariaDisabled= 'disabled'; + }, + setEnabled: () => { + $el.disabled = false; + $el.ariaDisabled = 'false'; + $el.removeAttribute('ariaDisabled'); + } + } +} diff --git a/packages/feedback/src/widget/Icon.ts b/packages/feedback/src/widget/Icon.ts new file mode 100644 index 000000000000..a7e42d93e15c --- /dev/null +++ b/packages/feedback/src/widget/Icon.ts @@ -0,0 +1,57 @@ +const SIZE = 20; +const XMLNS = 'http://www.w3.org/2000/svg'; + +interface Props { + color: string; +} + +function setAttributes(el: SVGElement, attributes: Record) { + Object.entries(attributes).forEach(([key, val]) => { + el.setAttributeNS(null, key, val) + }) + return el; +} + +/** + * Feedback Icon + */ +export function Icon({color}: Props) { + const svg = setAttributes(document.createElementNS(XMLNS, 'svg'), { + width: `${ SIZE }`, + height: `${ SIZE }`, + viewBox: `0 0 ${SIZE} ${SIZE}`, + fill: 'none', + }) + + const g = setAttributes( document.createElementNS(XMLNS, 'g'), { + clipPath: 'url(#clip0_57_80)' + }) + + const path = setAttributes( document.createElementNS(XMLNS, 'path'), { + ['fill-rule']: 'evenodd', + ['clip-rule']: 'evenodd', + d: 'M15.6622 15H12.3997C12.2129 14.9959 12.031 14.9396 11.8747 14.8375L8.04965 12.2H7.49956V19.1C7.4875 19.3348 7.3888 19.5568 7.22256 19.723C7.05632 19.8892 6.83435 19.9879 6.59956 20H2.04956C1.80193 19.9968 1.56535 19.8969 1.39023 19.7218C1.21511 19.5467 1.1153 19.3101 1.11206 19.0625V12.2H0.949652C0.824431 12.2017 0.700142 12.1783 0.584123 12.1311C0.468104 12.084 0.362708 12.014 0.274155 11.9255C0.185602 11.8369 0.115689 11.7315 0.0685419 11.6155C0.0213952 11.4995 -0.00202913 11.3752 -0.00034808 11.25V3.75C-0.00900498 3.62067 0.0092504 3.49095 0.0532651 3.36904C0.0972798 3.24712 0.166097 3.13566 0.255372 3.04168C0.344646 2.94771 0.452437 2.87327 0.571937 2.82307C0.691437 2.77286 0.82005 2.74798 0.949652 2.75H8.04965L11.8747 0.1625C12.031 0.0603649 12.2129 0.00407221 12.3997 0H15.6622C15.9098 0.00323746 16.1464 0.103049 16.3215 0.278167C16.4966 0.453286 16.5964 0.689866 16.5997 0.9375V3.25269C17.3969 3.42959 18.1345 3.83026 18.7211 4.41679C19.5322 5.22788 19.9878 6.32796 19.9878 7.47502C19.9878 8.62209 19.5322 9.72217 18.7211 10.5333C18.1345 11.1198 17.3969 11.5205 16.5997 11.6974V14.0125C16.6047 14.1393 16.5842 14.2659 16.5395 14.3847C16.4948 14.5035 16.4268 14.6121 16.3394 14.7042C16.252 14.7962 16.147 14.8698 16.0307 14.9206C15.9144 14.9714 15.7891 14.9984 15.6622 15ZM1.89695 10.325H1.88715V4.625H8.33715C8.52423 4.62301 8.70666 4.56654 8.86215 4.4625L12.6872 1.875H14.7247V13.125H12.6872L8.86215 10.4875C8.70666 10.3835 8.52423 10.327 8.33715 10.325H2.20217C2.15205 10.3167 2.10102 10.3125 2.04956 10.3125C1.9981 10.3125 1.94708 10.3167 1.89695 10.325ZM2.98706 12.2V18.1625H5.66206V12.2H2.98706ZM16.5997 9.93612V5.01393C16.6536 5.02355 16.7072 5.03495 16.7605 5.04814C17.1202 5.13709 17.4556 5.30487 17.7425 5.53934C18.0293 5.77381 18.2605 6.06912 18.4192 6.40389C18.578 6.73866 18.6603 7.10452 18.6603 7.47502C18.6603 7.84552 18.578 8.21139 18.4192 8.54616C18.2605 8.88093 18.0293 9.17624 17.7425 9.41071C17.4556 9.64518 17.1202 9.81296 16.7605 9.90191C16.7072 9.91509 16.6536 9.9265 16.5997 9.93612Z', + fill: color, + }); + svg.appendChild(g).appendChild(path); + + const speakerDefs = document.createElementNS(XMLNS, 'defs'); + const speakerClipPathDef = setAttributes( + document.createElementNS(XMLNS, 'clipPath'), + { + id: 'clip0_57_80', + }); + + const speakerRect = setAttributes(document.createElementNS(XMLNS, 'rect'), { + width: `${SIZE}`, + height: `${SIZE}`, + fill: 'white', + }); + + speakerClipPathDef.appendChild(speakerRect); + speakerDefs.appendChild(speakerClipPathDef); + + svg.appendChild(speakerDefs).appendChild(speakerClipPathDef).appendChild(speakerRect);; + + return svg; +} diff --git a/packages/feedback/src/widget/util/createElement.ts b/packages/feedback/src/widget/util/createElement.ts new file mode 100644 index 000000000000..cbebacbec6ae --- /dev/null +++ b/packages/feedback/src/widget/util/createElement.ts @@ -0,0 +1,54 @@ + /** + * + */ + export function createElement(tagName: K, attributes: {[key: string]: string|boolean|EventListenerOrEventListenerObject}| null, ...children: any): HTMLElementTagNameMap[K] { + const element = document.createElement(tagName); + + if (attributes) { + Object.entries(attributes).forEach(([attribute, attributeValue]) => { + if (attribute === 'className' && typeof attributeValue === 'string') { // JSX does not allow class as a valid name + element.setAttribute('class', attributeValue); + } else if (typeof attributeValue === 'boolean' && attributeValue) { + element.setAttribute(attribute, ''); + } else if (typeof attributeValue === 'string'){ + element.setAttribute(attribute, attributeValue); + } else if (attribute.startsWith('on') && typeof attributeValue === 'function') { + element.addEventListener(attribute.substring(2).toLowerCase(), attributeValue); + } + }) + } + Object.values(children).forEach(child => appendChild(element, child)) + + return element; + } + + function appendChild(parent: Node, child: any) { + if (typeof child === 'undefined' || child === null) { + return; + } + + if (Array.isArray(child)) { + for (const value of child) { + appendChild(parent, value); + } + } else if (typeof child === 'string') { + parent.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + parent.appendChild(child); + } else { + parent.appendChild(document.createTextNode(String(child))); + } + } + +// export function createElement(tagName: keyof HTMLElementTagNameMap, attributes: Record, ...children: HTMLElement[]) { +// const el = document.createElement(tagName) +// Object.entries(attributes).forEach(([key, val]) => el[key] = val); +// children.forEach(child => el.appendChild(child)); +// return el; +// } + +// const form = createElement('form'); + + // c('form', {}, + // c('input', {}), + // ) diff --git a/yarn.lock b/yarn.lock index 96b2d9b8099c..d79a6a5a37f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5039,6 +5039,28 @@ fflate "^0.4.4" mitt "^3.0.0" +"@sentry-internal/tracing@7.70.0": + version "7.70.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.70.0.tgz#00fd30426a6d4737385004434a39cf60736beafc" + integrity sha512-SpbE6wZhs6QwG2ORWCt8r28o1T949qkWx/KeRTCdK4Ub95PQ3Y3DgnqD8Wz//3q50Wt6EZDEibmz4t067g6PPg== + dependencies: + "@sentry/core" "7.70.0" + "@sentry/types" "7.70.0" + "@sentry/utils" "7.70.0" + tslib "^2.4.1 || ^1.9.3" + +"@sentry/browser@7.70.0": + version "7.70.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.70.0.tgz#e284999843bebc5bccd2c7b247f01aa048518f5c" + integrity sha512-PB+IP49/TLcnDHCj9eJ5tcHE0pzXg23wBakmF3KGMSd5nxEbUvmOsaFPZcgUUlL9JlU3v1Y40We7HdPStrY6oA== + dependencies: + "@sentry-internal/tracing" "7.70.0" + "@sentry/core" "7.70.0" + "@sentry/replay" "7.70.0" + "@sentry/types" "7.70.0" + "@sentry/utils" "7.70.0" + tslib "^2.4.1 || ^1.9.3" + "@sentry/bundler-plugin-core@0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.1.tgz#6c6a2ff3cdc98cd0ff1c30c59408cee9f067adf2" @@ -5111,6 +5133,15 @@ "@sentry/utils" "7.70.0" tslib "^2.4.1 || ^1.9.3" +"@sentry/replay@7.70.0": + version "7.70.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.70.0.tgz#fd0c75cb0d632e15c8d270af33cb157d328399e8" + integrity sha512-XjnyE6ORREz9kBWWHdXaIjS9P2Wo7uEw+y23vfLQwzV0Nx3xJ+FG4dwf8onyIoeCZDKbz7cqQIbugU1gkgUtZw== + dependencies: + "@sentry/core" "7.70.0" + "@sentry/types" "7.70.0" + "@sentry/utils" "7.70.0" + "@sentry/types@7.70.0": version "7.70.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.70.0.tgz#c7b533bb18144e3b020550b38cf4812c32d05ffe" From 916bb622733c2ec3f62e1b218b97b1ec86f73791 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 10 Oct 2023 11:40:14 -0400 Subject: [PATCH 04/59] lint --- packages/feedback/src/index.ts | 86 ++---- packages/feedback/src/sendFeedback.ts | 34 ++- packages/feedback/src/types/index.ts | 6 + packages/feedback/src/widget/Dialog.ts | 101 +++++++ packages/feedback/src/widget/Form.ts | 278 +++++++++++------- packages/feedback/src/widget/Icon.ts | 84 +++--- .../feedback/src/widget/util/createElement.ts | 93 +++--- 7 files changed, 398 insertions(+), 284 deletions(-) create mode 100644 packages/feedback/src/widget/Dialog.ts diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts index 07fa1e691b60..c7cad754288b 100644 --- a/packages/feedback/src/index.ts +++ b/packages/feedback/src/index.ts @@ -1,14 +1,13 @@ -import type { BrowserClient, Replay } from '@sentry/browser'; import { getCurrentHub } from '@sentry/core'; import type { Integration } from '@sentry/types'; import { isNodeEnv } from '@sentry/utils'; -import type {FeedbackConfigurationWithDefaults} from './types'; +import type { FeedbackConfigurationWithDefaults } from './types'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; -import {Form} from './widget/Form'; -import {Icon} from './widget/Icon'; +import { Dialog } from './widget/Dialog'; +import { Icon } from './widget/Icon'; -export {sendFeedbackRequest}; +export { sendFeedbackRequest }; type ElectronProcess = { type?: string }; @@ -24,14 +23,6 @@ function isBrowser(): boolean { return typeof window !== 'undefined' && (!isNodeEnv() || isElectronNodeRenderer()); } -function retrieveStringValue(formData: FormData, key: string) { - const value = formData.get(key); - if (typeof value === 'string') { - return value.trim(); - } - return ''; -} - const THEME = { light: { foreground: '#2B2233', @@ -58,7 +49,7 @@ export class Feedback implements Integration { public options: FeedbackConfigurationWithDefaults; private actor: HTMLButtonElement | null = null; - private dialog: HTMLDialogElement | null = null; + private dialog: ReturnType | null = null; private host: HTMLDivElement | null = null; private shadow: ShadowRoot | null = null; private isDialogOpen: boolean = false; @@ -80,7 +71,7 @@ export class Feedback implements Integration { formTitle = 'Report a Bug', emailPlaceholder = 'your.email@example.org', emailLabel = 'Email', - messagePlaceholder = 'What\'s the bug? What did you expect?', + messagePlaceholder = "What's the bug? What did you expect?", messageLabel = 'Description', namePlaceholder = 'Your Name', nameLabel = 'Name', @@ -119,24 +110,19 @@ export class Feedback implements Integration { return; } - - this.injectWidget() + this._injectWidget(); } /** * */ - protected injectWidget() { - console.log('injectWidget') - + protected _injectWidget() { // TODO: This is only here for hot reloading if (this.host) { - console.log('host already exists'); this.remove(); } const existingFeedback = document.querySelector('#sentry-feedback'); if (existingFeedback) { - console.log('existingFeedback') existingFeedback.remove(); } @@ -237,18 +223,16 @@ export class Feedback implements Integration { const actorButton = document.createElement('button'); actorButton.type = 'button'; actorButton.className = 'widget-actor'; - actorButton.ariaLabel = this.options.buttonLabel; + actorButton.ariaLabel = this.options.buttonLabel; const buttonTextEl = document.createElement('span'); buttonTextEl.className = 'widget-actor-text'; buttonTextEl.textContent = this.options.buttonLabel; this.shadow.appendChild(actorButton); - actorButton.appendChild(Icon({color: THEME.light.foreground})); + actorButton.appendChild(Icon({ color: THEME.light.foreground })); actorButton.appendChild(buttonTextEl); - - - actorButton.addEventListener('click', this.handleActorClick.bind(this)) + actorButton.addEventListener('click', this.handleActorClick.bind(this)); this.actor = actorButton; } @@ -274,13 +258,13 @@ export class Feedback implements Integration { */ public openDialog() { if (this.dialog) { - this.dialog.open = true; + this.dialog.openDialog(); return; } const style = document.createElement('style'); style.textContent = ` -#feedbackDialog { +.dialog { --bg-color: #fff; --bg-hover-color: #f0f0f0; --fg-color: #000; @@ -309,13 +293,13 @@ export class Feedback implements Integration { opacity: 1; transition: opacity 0.2s ease-in-out; } -#feedbackDialog:not([open]) { +.dialog:not([open]) { opacity: 0; pointer-events: none; visibility: hidden; } -.feedback-content { +.dialog__content { position: fixed; right: 1rem; bottom: 1rem; @@ -341,7 +325,7 @@ export class Feedback implements Integration { } } -.feedback-header { +.dialog__header { font-size: 20px; font-weight: 600; padding: 0; @@ -428,38 +412,8 @@ export class Feedback implements Integration { } `; this.shadow?.appendChild(style); - - this.dialog = document.createElement('dialog'); - this.dialog.id = 'feedbackDialog'; - this.dialog.open = true; - - - const user = getCurrentHub().getScope()?.getUser(); - - const contentDiv = document.createElement('div'); - contentDiv.className = 'feedback-content'; - const header = document.createElement('h2'); - header.className = 'feedback-header'; - header.textContent = this.options.formTitle; - - this.dialog.addEventListener('click', this.closeDialog); - contentDiv.addEventListener('click', (e) => { - // Stop event propagation so clicks on content modal do not propagate to dialog (which will close dialog) - e.stopPropagation(); - }) - - // cancelEl.addEventListener('click', this.closeDialog); - - const userKey = this.options.useSentryUser; - - const {$form} = Form({ - defaultName: userKey && user?.[userKey.name] || '', - defaultEmail: userKey && user?.[userKey.email] || '', - options: this.options}) - contentDiv.append(header, $form) - - this.dialog.appendChild(contentDiv) - this.shadow?.appendChild(this.dialog) + this.dialog = Dialog({ onCancel: this.closeDialog, options: this.options }); + this.shadow?.appendChild(this.dialog.$el); } /** @@ -467,7 +421,7 @@ export class Feedback implements Integration { */ public closeDialog = () => { if (this.dialog) { - this.dialog.open = false; + this.dialog.closeDialog(); } // TODO: if has default actor, show the button @@ -475,5 +429,5 @@ export class Feedback implements Integration { if (this.actor) { this.actor.classList.remove('hidden'); } - } + }; } diff --git a/packages/feedback/src/sendFeedback.ts b/packages/feedback/src/sendFeedback.ts index 345f2b20812f..e04376f94402 100644 --- a/packages/feedback/src/sendFeedback.ts +++ b/packages/feedback/src/sendFeedback.ts @@ -1,5 +1,6 @@ import type { BrowserClient, Replay } from '@sentry/browser'; import { getCurrentHub } from '@sentry/core'; +import { GLOBAL_OBJ } from '@sentry/utils'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; @@ -17,20 +18,25 @@ interface SendFeedbackOptions { /** * Public API to send a Feedback item to Sentry */ -export function sendFeedback({name, email, message, url = document.location.href}: SendFeedbackParams, {includeReplay = true}: SendFeedbackOptions = {}) { - const replay = includeReplay ? getCurrentHub()?.getClient()?.getIntegrationById('Replay') as Replay | undefined : undefined; +export function sendFeedback( + { name, email, message, url = document.location.href }: SendFeedbackParams, + { includeReplay = true }: SendFeedbackOptions = {}, +) { + const replay = includeReplay + ? (getCurrentHub()?.getClient()?.getIntegrationById('Replay') as Replay | undefined) + : undefined; - // Prepare session replay - replay?.flush(); - const replayId = replay?.getReplayId(); + // Prepare session replay + replay?.flush(); + const replayId = replay?.getReplayId(); - return sendFeedbackRequest({ - feedback: { - name, - email, - message, - url, - replay_id: replayId, - } - }); + return sendFeedbackRequest({ + feedback: { + name, + email, + message, + url, + replay_id: replayId, + }, + }); } diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index 0a6d5d70878d..c4a1001a5d54 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -28,6 +28,12 @@ export interface SendFeedbackData { }; } +export interface FeedbackFormData { + message: string; + email?: string; + name?: string; +} + export interface FeedbackConfigurationWithDefaults { /** * If true, will not collect user data (email/name). diff --git a/packages/feedback/src/widget/Dialog.ts b/packages/feedback/src/widget/Dialog.ts new file mode 100644 index 000000000000..b5af02c5e443 --- /dev/null +++ b/packages/feedback/src/widget/Dialog.ts @@ -0,0 +1,101 @@ +import { getCurrentHub } from '@sentry/core'; + +import type { FeedbackConfigurationWithDefaults, FeedbackFormData } from '../types'; +import { Form } from './Form'; +import { createElement as h } from './util/createElement'; + +interface DialogProps { + onCancel?: (e: Event) => void; + onSubmit?: (feedback: FeedbackFormData) => void; + options: FeedbackConfigurationWithDefaults; +} + +interface DialogReturn { + $el: HTMLDialogElement; + setSubmitDisabled: () => void; + setSubmitEnabled: () => void; + removeDialog: () => void; + openDialog: () => void; + closeDialog: () => void; +} + +/** + * Feedback dialog component that has the form + */ +export function Dialog({ onCancel, onSubmit, options }: DialogProps): DialogReturn { + let $el: HTMLDialogElement | null = null; + + /** + * + */ + function closeDialog() { + if ($el) { + $el.open = false; + } + } + + /** + * + */ + function removeDialog() { + if ($el) { + $el.remove(); + $el = null; + } + } + + /** + * + */ + function openDialog() { + if ($el) { + $el.open = true; + } + } + + const userKey = options.useSentryUser; + const user = getCurrentHub().getScope()?.getUser(); + + const { + $el: $form, + setSubmitEnabled, + setSubmitDisabled, + } = Form({ + defaultName: (userKey && user && user[userKey.name]) || '', + defaultEmail: (userKey && user && user[userKey.email]) || '', + options, + onSubmit, + onCancel, + }); + + $el = h( + 'dialog', + { + id: 'feedback-dialog', + className: 'dialog', + open: true, + onClick: closeDialog, + }, + h( + 'div', + { + className: 'dialog__content', + onClick: e => { + // Stop event propagation so clicks on content modal do not propagate to dialog (which will close dialog) + e.stopPropagation(); + }, + }, + h('h2', { className: 'dialog__header' }, options.formTitle), + $form, + ), + ); + + return { + $el, + setSubmitDisabled, + setSubmitEnabled, + removeDialog, + openDialog, + closeDialog, + }; +} diff --git a/packages/feedback/src/widget/Form.ts b/packages/feedback/src/widget/Form.ts index af54281be803..abec800be28b 100644 --- a/packages/feedback/src/widget/Form.ts +++ b/packages/feedback/src/widget/Form.ts @@ -1,14 +1,21 @@ -import {sendFeedback} from '../sendFeedback'; -import type { FeedbackConfigurationWithDefaults } from '../types'; +import type { FeedbackConfigurationWithDefaults, FeedbackFormData } from '../types'; import { createElement as h } from './util/createElement'; interface Props { defaultName: string; - defaultEmail: string, - options: FeedbackConfigurationWithDefaults, + defaultEmail: string; + options: FeedbackConfigurationWithDefaults; + onCancel?: (e: Event) => void; + onSubmit?: (feedback: FeedbackFormData) => void; } -function retrieveStringValue(formData: FormData, key: string) { +interface FormReturn { + $el: HTMLFormElement; + setSubmitDisabled: () => void; + setSubmitEnabled: () => void; +} + +function retrieveStringValue(formData: FormData, key: string): string { const value = formData.get(key); if (typeof value === 'string') { return value.trim(); @@ -19,12 +26,15 @@ function retrieveStringValue(formData: FormData, key: string) { /** * Creates the form element */ -export function Form({defaultName, defaultEmail, options}: Props) { - const {$el: $submit, setDisabled: setSubmitDisabled, setEnabled: setSubmitEnabled} = SubmitButton({ +export function Form({ defaultName, defaultEmail, onCancel, onSubmit, options }: Props): FormReturn { + const { + $el: $submit, + setDisabled: setSubmitDisabled, + setEnabled: setSubmitEnabled, + } = SubmitButton({ label: options.submitButtonLabel, }); - async function handleSubmit(e: Event) { e.preventDefault(); console.log('form submitted'); @@ -33,67 +43,76 @@ export function Form({defaultName, defaultEmail, options}: Props) { } try { - const formData = new FormData(e.target as HTMLFormElement); - const feedback = { - name: retrieveStringValue(formData, 'name'), - email: retrieveStringValue(formData, 'email'), - message: retrieveStringValue(formData, 'message'), - }; - - try { - setSubmitDisabled(); - const resp = await sendFeedback(feedback); - - console.log({resp}) - if (!resp) { - setSubmitEnabled(); - } - } catch(err) { - setSubmitEnabled(); + if (typeof onSubmit === 'function') { + const formData = new FormData(e.target as HTMLFormElement); + const feedback = { + name: retrieveStringValue(formData, 'name'), + email: retrieveStringValue(formData, 'email'), + message: retrieveStringValue(formData, 'message'), + }; + + onSubmit(feedback); } - } catch(err) { + + // try { + // setSubmitDisabled(); + // const resp = await sendFeedback(feedback); + // + // console.log({resp}) + // + // if (!resp) { + // // Errored + // setSubmitEnabled(); + // return; + // } + // // Success + // } catch(err) { + // setSubmitEnabled(); + // } + } catch { + // pass } } const $name = h('input', { - id: 'name', - type: 'text', // TODO can be hidden - ariaHidden: 'false', - name: 'name', - className: 'form__input', - placeholder: options.namePlaceholder, - value: defaultName, - }) + id: 'name', + type: 'text', // TODO can be hidden + ariaHidden: 'false', + name: 'name', + className: 'form__input', + placeholder: options.namePlaceholder, + value: defaultName, + }); const $email = h('input', { - id: 'email', - type: 'text', // TODO can be hidden - ariaHidden: 'false', - name: 'email', - className: 'form__input', - placeholder: options.emailPlaceholder, - value: defaultEmail, - }) + id: 'email', + type: 'text', // TODO can be hidden + ariaHidden: 'false', + name: 'email', + className: 'form__input', + placeholder: options.emailPlaceholder, + value: defaultEmail, + }); const $message = h('textarea', { - id: 'message', - autoFocus: 'true', - rows: '5', - name: 'message', - className: 'form__input form__input--textarea', - placeholder: options.messagePlaceholder, - onKeyup: (e) => { - if (!(e.currentTarget instanceof HTMLTextAreaElement)) { - return; - } + id: 'message', + autoFocus: 'true', + rows: '5', + name: 'message', + className: 'form__input form__input--textarea', + placeholder: options.messagePlaceholder, + onKeyup: e => { + if (!(e.currentTarget instanceof HTMLTextAreaElement)) { + return; + } - if (e.currentTarget.value) { - setSubmitEnabled(); - } else { - setSubmitDisabled(); - } + if (e.currentTarget.value) { + setSubmitEnabled(); + } else { + setSubmitDisabled(); } - }) + }, + }); // h('button', { // type: 'submit', @@ -102,76 +121,111 @@ export function Form({defaultName, defaultEmail, options}: Props) { // ariaDisabled: 'disabled', // }, options.submitButtonLabel) // - const $cancel = h('button', { - type: 'button', - className: 'btn btn--default', - }, options.cancelButtonLabel) - - const $form = h('form', { - className: 'form', - onSubmit: handleSubmit, - }, [ - - h('label', { - htmlFor: 'name', - className: 'form__label', - }, [ - options.nameLabel, - $name - ]), - - h('label', { - htmlFor: 'email', - className: 'form__label', - }, [ - options.emailLabel, - $email - ]), - - - h('label', { - htmlFor: 'message', - className: 'form__label', - }, [ - options.messageLabel, - $message - ]), - - h('div', { - className: 'btn-group', - }, [ - $submit, - $cancel, - ]) - ]) + const $cancel = h( + 'button', + { + type: 'button', + className: 'btn btn--default', + onClick: e => { + if (typeof onCancel === 'function') { + onCancel(e); + } + }, + }, + options.cancelButtonLabel, + ); + + const $form = h( + 'form', + { + className: 'form', + onSubmit: handleSubmit, + }, + [ + h( + 'label', + { + htmlFor: 'name', + className: 'form__label', + }, + [options.nameLabel, $name], + ), + + h( + 'label', + { + htmlFor: 'email', + className: 'form__label', + }, + [options.emailLabel, $email], + ), + + h( + 'label', + { + htmlFor: 'message', + className: 'form__label', + }, + [options.messageLabel, $message], + ), + + h( + 'div', + { + className: 'btn-group', + }, + [$submit, $cancel], + ), + ], + ); return { - $form, - } + $el: $form, + setSubmitDisabled, + setSubmitEnabled, + }; } interface SubmitButtonProps { label: string; } -function SubmitButton({label}: SubmitButtonProps) { - const $el = h('button', { - type: 'submit', - className: 'btn btn--primary', - disabled: true, - ariaDisabled: 'disabled', - }, label) +interface SubmitReturn { + $el: HTMLButtonElement; + + /** + * Disables the submit button + */ + setDisabled: () => void; + + /** + * Enables the submit button + */ + setEnabled: () => void; +} + +function SubmitButton({ label }: SubmitButtonProps): SubmitReturn { + const $el = h( + 'button', + { + type: 'submit', + className: 'btn btn--primary', + disabled: true, + ariaDisabled: 'disabled', + }, + label, + ); return { $el, setDisabled: () => { $el.disabled = true; - $el.ariaDisabled= 'disabled'; + $el.ariaDisabled = 'disabled'; }, setEnabled: () => { $el.disabled = false; $el.ariaDisabled = 'false'; $el.removeAttribute('ariaDisabled'); - } - } + }, + }; } diff --git a/packages/feedback/src/widget/Icon.ts b/packages/feedback/src/widget/Icon.ts index a7e42d93e15c..52caae228f28 100644 --- a/packages/feedback/src/widget/Icon.ts +++ b/packages/feedback/src/widget/Icon.ts @@ -1,57 +1,55 @@ const SIZE = 20; -const XMLNS = 'http://www.w3.org/2000/svg'; +const XMLNS = 'http://www.w3.org/2000/svg'; interface Props { color: string; } -function setAttributes(el: SVGElement, attributes: Record) { +function setAttributes(el: T, attributes: Record): T { Object.entries(attributes).forEach(([key, val]) => { - el.setAttributeNS(null, key, val) - }) + el.setAttributeNS(null, key, val); + }); return el; } /** * Feedback Icon */ -export function Icon({color}: Props) { - const svg = setAttributes(document.createElementNS(XMLNS, 'svg'), { - width: `${ SIZE }`, - height: `${ SIZE }`, - viewBox: `0 0 ${SIZE} ${SIZE}`, - fill: 'none', - }) - - const g = setAttributes( document.createElementNS(XMLNS, 'g'), { - clipPath: 'url(#clip0_57_80)' - }) - - const path = setAttributes( document.createElementNS(XMLNS, 'path'), { - ['fill-rule']: 'evenodd', - ['clip-rule']: 'evenodd', - d: 'M15.6622 15H12.3997C12.2129 14.9959 12.031 14.9396 11.8747 14.8375L8.04965 12.2H7.49956V19.1C7.4875 19.3348 7.3888 19.5568 7.22256 19.723C7.05632 19.8892 6.83435 19.9879 6.59956 20H2.04956C1.80193 19.9968 1.56535 19.8969 1.39023 19.7218C1.21511 19.5467 1.1153 19.3101 1.11206 19.0625V12.2H0.949652C0.824431 12.2017 0.700142 12.1783 0.584123 12.1311C0.468104 12.084 0.362708 12.014 0.274155 11.9255C0.185602 11.8369 0.115689 11.7315 0.0685419 11.6155C0.0213952 11.4995 -0.00202913 11.3752 -0.00034808 11.25V3.75C-0.00900498 3.62067 0.0092504 3.49095 0.0532651 3.36904C0.0972798 3.24712 0.166097 3.13566 0.255372 3.04168C0.344646 2.94771 0.452437 2.87327 0.571937 2.82307C0.691437 2.77286 0.82005 2.74798 0.949652 2.75H8.04965L11.8747 0.1625C12.031 0.0603649 12.2129 0.00407221 12.3997 0H15.6622C15.9098 0.00323746 16.1464 0.103049 16.3215 0.278167C16.4966 0.453286 16.5964 0.689866 16.5997 0.9375V3.25269C17.3969 3.42959 18.1345 3.83026 18.7211 4.41679C19.5322 5.22788 19.9878 6.32796 19.9878 7.47502C19.9878 8.62209 19.5322 9.72217 18.7211 10.5333C18.1345 11.1198 17.3969 11.5205 16.5997 11.6974V14.0125C16.6047 14.1393 16.5842 14.2659 16.5395 14.3847C16.4948 14.5035 16.4268 14.6121 16.3394 14.7042C16.252 14.7962 16.147 14.8698 16.0307 14.9206C15.9144 14.9714 15.7891 14.9984 15.6622 15ZM1.89695 10.325H1.88715V4.625H8.33715C8.52423 4.62301 8.70666 4.56654 8.86215 4.4625L12.6872 1.875H14.7247V13.125H12.6872L8.86215 10.4875C8.70666 10.3835 8.52423 10.327 8.33715 10.325H2.20217C2.15205 10.3167 2.10102 10.3125 2.04956 10.3125C1.9981 10.3125 1.94708 10.3167 1.89695 10.325ZM2.98706 12.2V18.1625H5.66206V12.2H2.98706ZM16.5997 9.93612V5.01393C16.6536 5.02355 16.7072 5.03495 16.7605 5.04814C17.1202 5.13709 17.4556 5.30487 17.7425 5.53934C18.0293 5.77381 18.2605 6.06912 18.4192 6.40389C18.578 6.73866 18.6603 7.10452 18.6603 7.47502C18.6603 7.84552 18.578 8.21139 18.4192 8.54616C18.2605 8.88093 18.0293 9.17624 17.7425 9.41071C17.4556 9.64518 17.1202 9.81296 16.7605 9.90191C16.7072 9.91509 16.6536 9.9265 16.5997 9.93612Z', - fill: color, - }); - svg.appendChild(g).appendChild(path); - - const speakerDefs = document.createElementNS(XMLNS, 'defs'); - const speakerClipPathDef = setAttributes( - document.createElementNS(XMLNS, 'clipPath'), - { - id: 'clip0_57_80', - }); - - const speakerRect = setAttributes(document.createElementNS(XMLNS, 'rect'), { - width: `${SIZE}`, - height: `${SIZE}`, - fill: 'white', - }); - - speakerClipPathDef.appendChild(speakerRect); - speakerDefs.appendChild(speakerClipPathDef); - - svg.appendChild(speakerDefs).appendChild(speakerClipPathDef).appendChild(speakerRect);; - - return svg; +export function Icon({ color }: Props): SVGElement { + const svg = setAttributes(document.createElementNS(XMLNS, 'svg'), { + width: `${SIZE}`, + height: `${SIZE}`, + viewBox: `0 0 ${SIZE} ${SIZE}`, + fill: 'none', + }); + + const g = setAttributes(document.createElementNS(XMLNS, 'g'), { + clipPath: 'url(#clip0_57_80)', + }); + + const path = setAttributes(document.createElementNS(XMLNS, 'path'), { + ['fill-rule']: 'evenodd', + ['clip-rule']: 'evenodd', + d: 'M15.6622 15H12.3997C12.2129 14.9959 12.031 14.9396 11.8747 14.8375L8.04965 12.2H7.49956V19.1C7.4875 19.3348 7.3888 19.5568 7.22256 19.723C7.05632 19.8892 6.83435 19.9879 6.59956 20H2.04956C1.80193 19.9968 1.56535 19.8969 1.39023 19.7218C1.21511 19.5467 1.1153 19.3101 1.11206 19.0625V12.2H0.949652C0.824431 12.2017 0.700142 12.1783 0.584123 12.1311C0.468104 12.084 0.362708 12.014 0.274155 11.9255C0.185602 11.8369 0.115689 11.7315 0.0685419 11.6155C0.0213952 11.4995 -0.00202913 11.3752 -0.00034808 11.25V3.75C-0.00900498 3.62067 0.0092504 3.49095 0.0532651 3.36904C0.0972798 3.24712 0.166097 3.13566 0.255372 3.04168C0.344646 2.94771 0.452437 2.87327 0.571937 2.82307C0.691437 2.77286 0.82005 2.74798 0.949652 2.75H8.04965L11.8747 0.1625C12.031 0.0603649 12.2129 0.00407221 12.3997 0H15.6622C15.9098 0.00323746 16.1464 0.103049 16.3215 0.278167C16.4966 0.453286 16.5964 0.689866 16.5997 0.9375V3.25269C17.3969 3.42959 18.1345 3.83026 18.7211 4.41679C19.5322 5.22788 19.9878 6.32796 19.9878 7.47502C19.9878 8.62209 19.5322 9.72217 18.7211 10.5333C18.1345 11.1198 17.3969 11.5205 16.5997 11.6974V14.0125C16.6047 14.1393 16.5842 14.2659 16.5395 14.3847C16.4948 14.5035 16.4268 14.6121 16.3394 14.7042C16.252 14.7962 16.147 14.8698 16.0307 14.9206C15.9144 14.9714 15.7891 14.9984 15.6622 15ZM1.89695 10.325H1.88715V4.625H8.33715C8.52423 4.62301 8.70666 4.56654 8.86215 4.4625L12.6872 1.875H14.7247V13.125H12.6872L8.86215 10.4875C8.70666 10.3835 8.52423 10.327 8.33715 10.325H2.20217C2.15205 10.3167 2.10102 10.3125 2.04956 10.3125C1.9981 10.3125 1.94708 10.3167 1.89695 10.325ZM2.98706 12.2V18.1625H5.66206V12.2H2.98706ZM16.5997 9.93612V5.01393C16.6536 5.02355 16.7072 5.03495 16.7605 5.04814C17.1202 5.13709 17.4556 5.30487 17.7425 5.53934C18.0293 5.77381 18.2605 6.06912 18.4192 6.40389C18.578 6.73866 18.6603 7.10452 18.6603 7.47502C18.6603 7.84552 18.578 8.21139 18.4192 8.54616C18.2605 8.88093 18.0293 9.17624 17.7425 9.41071C17.4556 9.64518 17.1202 9.81296 16.7605 9.90191C16.7072 9.91509 16.6536 9.9265 16.5997 9.93612Z', + fill: color, + }); + svg.appendChild(g).appendChild(path); + + const speakerDefs = document.createElementNS(XMLNS, 'defs'); + const speakerClipPathDef = setAttributes(document.createElementNS(XMLNS, 'clipPath'), { + id: 'clip0_57_80', + }); + + const speakerRect = setAttributes(document.createElementNS(XMLNS, 'rect'), { + width: `${SIZE}`, + height: `${SIZE}`, + fill: 'white', + }); + + speakerClipPathDef.appendChild(speakerRect); + speakerDefs.appendChild(speakerClipPathDef); + + svg.appendChild(speakerDefs).appendChild(speakerClipPathDef).appendChild(speakerRect); + + return svg; } diff --git a/packages/feedback/src/widget/util/createElement.ts b/packages/feedback/src/widget/util/createElement.ts index cbebacbec6ae..3f5866ed8c26 100644 --- a/packages/feedback/src/widget/util/createElement.ts +++ b/packages/feedback/src/widget/util/createElement.ts @@ -1,54 +1,49 @@ - /** - * - */ - export function createElement(tagName: K, attributes: {[key: string]: string|boolean|EventListenerOrEventListenerObject}| null, ...children: any): HTMLElementTagNameMap[K] { - const element = document.createElement(tagName); +/** + * Helper function to create an element. Could be used as a JSX factory + * (i.e. React-like syntax). + */ +export function createElement( + tagName: K, + attributes: { [key: string]: string | boolean | EventListenerOrEventListenerObject } | null, + ...children: any +): HTMLElementTagNameMap[K] { + const element = document.createElement(tagName); - if (attributes) { - Object.entries(attributes).forEach(([attribute, attributeValue]) => { - if (attribute === 'className' && typeof attributeValue === 'string') { // JSX does not allow class as a valid name - element.setAttribute('class', attributeValue); - } else if (typeof attributeValue === 'boolean' && attributeValue) { - element.setAttribute(attribute, ''); - } else if (typeof attributeValue === 'string'){ - element.setAttribute(attribute, attributeValue); - } else if (attribute.startsWith('on') && typeof attributeValue === 'function') { - element.addEventListener(attribute.substring(2).toLowerCase(), attributeValue); - } - }) - } - Object.values(children).forEach(child => appendChild(element, child)) + if (attributes) { + Object.entries(attributes).forEach(([attribute, attributeValue]) => { + if (attribute === 'className' && typeof attributeValue === 'string') { + // JSX does not allow class as a valid name + element.setAttribute('class', attributeValue); + } else if (typeof attributeValue === 'boolean' && attributeValue) { + element.setAttribute(attribute, ''); + } else if (typeof attributeValue === 'string') { + element.setAttribute(attribute, attributeValue); + } else if (attribute.startsWith('on') && typeof attributeValue === 'function') { + element.addEventListener(attribute.substring(2).toLowerCase(), attributeValue); + } + }); + } + for (const child of children) { + appendChild(element, child); + } - return element; - } + return element; +} - function appendChild(parent: Node, child: any) { - if (typeof child === 'undefined' || child === null) { - return; - } +function appendChild(parent: Node, child: any): void { + if (typeof child === 'undefined' || child === null) { + return; + } - if (Array.isArray(child)) { - for (const value of child) { - appendChild(parent, value); - } - } else if (typeof child === 'string') { - parent.appendChild(document.createTextNode(child)); - } else if (child instanceof Node) { - parent.appendChild(child); - } else { - parent.appendChild(document.createTextNode(String(child))); - } + if (Array.isArray(child)) { + for (const value of child) { + appendChild(parent, value); } - -// export function createElement(tagName: keyof HTMLElementTagNameMap, attributes: Record, ...children: HTMLElement[]) { -// const el = document.createElement(tagName) -// Object.entries(attributes).forEach(([key, val]) => el[key] = val); -// children.forEach(child => el.appendChild(child)); -// return el; -// } - -// const form = createElement('form'); - - // c('form', {}, - // c('input', {}), - // ) + } else if (typeof child === 'string') { + parent.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + parent.appendChild(child); + } else { + parent.appendChild(document.createTextNode(String(child))); + } +} From 5f5479e2836528eec6ae71736667796a3aa35a23 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 10 Oct 2023 11:56:20 -0400 Subject: [PATCH 05/59] ref: move css into separate fns --- packages/feedback/src/index.ts | 220 +-------------------- packages/feedback/src/types/index.ts | 16 ++ packages/feedback/src/widget/Actor.css.ts | 72 +++++++ packages/feedback/src/widget/Dialog.css.ts | 160 +++++++++++++++ 4 files changed, 254 insertions(+), 214 deletions(-) create mode 100644 packages/feedback/src/widget/Actor.css.ts create mode 100644 packages/feedback/src/widget/Dialog.css.ts diff --git a/packages/feedback/src/index.ts b/packages/feedback/src/index.ts index c7cad754288b..7f257983af6e 100644 --- a/packages/feedback/src/index.ts +++ b/packages/feedback/src/index.ts @@ -4,7 +4,9 @@ import { isNodeEnv } from '@sentry/utils'; import type { FeedbackConfigurationWithDefaults } from './types'; import { sendFeedbackRequest } from './util/sendFeedbackRequest'; +import { createActorStyles } from './widget/Actor.css'; import { Dialog } from './widget/Dialog'; +import { createDialogStyles } from './widget/Dialog.css'; import { Icon } from './widget/Icon'; export { sendFeedbackRequest }; @@ -25,9 +27,11 @@ function isBrowser(): boolean { const THEME = { light: { + background: '#ffffff', foreground: '#2B2233', }, dark: { + background: '#29232f', foreground: '#EBE6EF', }, }; @@ -155,70 +159,7 @@ export class Feedback implements Integration { this.host.id = 'sentry-feedback'; this.shadow = this.host.attachShadow({ mode: 'open' }); - const style = document.createElement('style'); - style.textContent = ` - :host { - position: fixed; - right: 1rem; - bottom: 1rem; - font-family: 'Helvetica Neue', Arial, sans-serif; - --bg-color: #fff; - --bg-hover-color: #f6f6f7; - --fg-color: ${THEME.light.foreground}; - --border: 1.5px solid rgba(41, 35, 47, 0.13); - --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); - } - - .__sntry_fdbk_dark:host { - --bg-color: #29232f; - --bg-hover-color: #352f3b; - --fg-color: ${THEME.dark.foreground}; - --border: 1.5px solid rgba(235, 230, 239, 0.15); - --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); - } - - .widget-actor { - line-height: 25px; - - display: flex; - align-items: center; - gap: 8px; - - border-radius: 12px; - cursor: pointer; - font-size: 14px; - font-weight: 600; - padding: 12px 16px; - text-decoration: none; - z-index: 9000; - - color: var(--fg-color); - background-color: var(--bg-color); - border: var(--border); - box-shadow: var(--box-shadow); - opacity: 1; - transition: opacity 0.1s ease-in-out; - } - - .widget-actor:hover { - background-color: var(--bg-hover-color); - } - - .widget-actor svg { - width: 16px; - height: 16px; - } - - .widget-actor.hidden { - opacity: 0; - pointer-events: none; - visibility: hidden; - } - - .widget-actor-text { - } -`; - this.shadow.appendChild(style); + this.shadow.appendChild(createActorStyles(document, THEME)); const actorButton = document.createElement('button'); actorButton.type = 'button'; @@ -262,156 +203,7 @@ export class Feedback implements Integration { return; } - const style = document.createElement('style'); - style.textContent = ` -.dialog { - --bg-color: #fff; - --bg-hover-color: #f0f0f0; - --fg-color: #000; - --border: 1.5px solid rgba(41, 35, 47, 0.13); - --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); - - &.__sntry_fdbk_dark { - --bg-color: #29232f; - --bg-hover-color: #3a3540; - --fg-color: #ebe6ef; - --border: 1.5px solid rgba(235, 230, 239, 0.15); - --box-shadow: 0px 4px 24px 0px rgba(43, 34, 51, 0.12); - } - - line-height: 25px; - background-color: rgba(0, 0, 0, 0.05); - border: none; - position: fixed; - inset: 0; - z-index: 10000; - width: 100vw; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - opacity: 1; - transition: opacity 0.2s ease-in-out; -} -.dialog:not([open]) { - opacity: 0; - pointer-events: none; - visibility: hidden; -} - -.dialog__content { - position: fixed; - right: 1rem; - bottom: 1rem; - - border: var(--border); - padding: 24px; - border-radius: 20px; - background-color: var(--bg-color); - color: var(--fg-color); - - width: 320px; - max-width: 100%; - max-height: calc(100% - 2rem); - display: flex; - flex-direction: column; - box-shadow: - 0 0 0 1px rgba(0, 0, 0, 0.05), - 0 4px 16px rgba(0, 0, 0, 0.2); - transition: transform 0.2s ease-in-out; - transform: translate(0, 0) scale(1); - dialog:not([open]) & { - transform: translate(0, -16px) scale(0.98); - } -} - -.dialog__header { - font-size: 20px; - font-weight: 600; - padding: 0; - margin: 0; - margin-bottom: 16px; -} - -.error { - color: red; - margin-bottom: 16px; -} - -.form { - display: grid; - overflow: auto; - flex-direction: column; - gap: 16px; - padding: 0; -} - -.form__label { - display: flex; - flex-direction: column; - gap: 4px; - margin: 0px; -} - -.form__input { - font-family: inherit; - line-height: inherit; - box-sizing: border-box; - border: var(--border); - border-radius: 6px; - font-size: 14px; - font-weight: 500; - padding: 6px 12px; - &:focus { - border-color: rgba(108, 95, 199, 1); - } -} - -.form__input--textarea { - font-family: inherit; - resize: vertical; -} - -.btn-group { - display: grid; - gap: 8px; - margin-top: 8px; -} - -.btn { - line-height: inherit; - border: var(--border); - border-radius: 6px; - cursor: pointer; - font-size: 14px; - font-weight: 600; - padding: 6px 16px; - - &[disabled] { - opacity: 0.6; - pointer-events: none; - } -} - -.btn--primary { - background-color: rgba(108, 95, 199, 1); - border-color: rgba(108, 95, 199, 1); - color: #fff; - &:hover { - background-color: rgba(88, 74, 192, 1); - } -} - -.btn--default { - background-color: transparent; - color: var(--fg-color); - font-weight: 500; - &:hover { - background-color: var(--bg-accent-color); - } -} -`; - this.shadow?.appendChild(style); + this.shadow?.appendChild(createDialogStyles(document, THEME)); this.dialog = Dialog({ onCancel: this.closeDialog, options: this.options }); this.shadow?.appendChild(this.dialog.$el); } diff --git a/packages/feedback/src/types/index.ts b/packages/feedback/src/types/index.ts index c4a1001a5d54..9ce1eceb9299 100644 --- a/packages/feedback/src/types/index.ts +++ b/packages/feedback/src/types/index.ts @@ -111,3 +111,19 @@ export interface FeedbackConfigurationWithDefaults { namePlaceholder: string; // * End of text customization * // } + +interface BaseTheme { + /** + * Background color + */ + background: string; + /** + * Foreground color (i.e. text color) + */ + foreground: string; +} + +export interface FeedbackTheme { + light: BaseTheme; + dark: BaseTheme; +} diff --git a/packages/feedback/src/widget/Actor.css.ts b/packages/feedback/src/widget/Actor.css.ts new file mode 100644 index 000000000000..039350043820 --- /dev/null +++ b/packages/feedback/src/widget/Actor.css.ts @@ -0,0 +1,72 @@ +import type { FeedbackTheme } from '../types'; + +/** + * Creates