Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(surveys): preact surveys components #964

Merged
merged 38 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
61f982f
setup wip
liyiy Jan 17, 2024
7947b7f
support new components in tests and add event listeners
liyiy Jan 19, 2024
29fdbc6
multiple choice and multiple question surveys
liyiy Jan 22, 2024
3fe72a5
connect multiple questions surveys and type fixes
liyiy Jan 23, 2024
f93a43e
convert feedback widget
liyiy Jan 23, 2024
b6f32ec
Merge branch 'master' into preact-surveys-components
liyiy Jan 23, 2024
8ec9328
working open choice option
liyiy Jan 25, 2024
31f4c97
clean up unit tests
liyiy Jan 28, 2024
cecfc52
fix confirmation box close
liyiy Jan 30, 2024
2bfc349
Merge branch 'master' into preact-surveys-components
liyiy Jan 30, 2024
1b4c7cd
missing lock file?
liyiy Jan 30, 2024
77008cf
fix test
liyiy Jan 30, 2024
f1d1a48
test
liyiy Jan 30, 2024
1928497
test flakey test
liyiy Jan 30, 2024
733cf0f
change order of capture wait
liyiy Jan 30, 2024
eefa9c6
test
liyiy Jan 30, 2024
35d4847
cypress leaving too quickly
liyiy Jan 30, 2024
d74a801
flakey test
liyiy Jan 30, 2024
3a33658
final fix??
liyiy Jan 30, 2024
185dc3d
Merge branch 'main' into preact-surveys-components
liyiy Feb 5, 2024
bb4b0fd
clean up useRef code
liyiy Feb 5, 2024
c86e25f
add todos and address rating question type comments
liyiy Feb 5, 2024
8725d5d
remove unused auto-text-color class
liyiy Feb 5, 2024
6b5e5d8
collapse else ifs
liyiy Feb 5, 2024
3115dbf
singular display state for surveys
liyiy Feb 5, 2024
cbdccb2
fix display status state
liyiy Feb 5, 2024
40955cd
fix test
liyiy Feb 5, 2024
87a6ea8
test
liyiy Feb 5, 2024
93964d5
this one test....
liyiy Feb 5, 2024
839e9cb
fix: TSconfig to allow file structure for surveys (#1001)
benjackwhite Feb 8, 2024
7c1a413
fix check mark color on white backgrounds
liyiy Feb 8, 2024
c0e07d9
test
liyiy Feb 9, 2024
82a6206
fix weird cached state test
liyiy Feb 9, 2024
01ef0b7
fix open ended answer submit bug
liyiy Feb 12, 2024
49b977a
split up the survey shown and dismissed event test
liyiy Feb 12, 2024
7877762
Merge branch 'main' into preact-surveys-components
liyiy Feb 12, 2024
f49ee3c
Merge branch 'main' into preact-surveys-components
liyiy Feb 13, 2024
c68be8f
feat(surveys): render surveys preview method (#1020)
liyiy Feb 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
[
"@babel/transform-react-jsx",
{
"pragma": "Preact.h",
"pragmaFrag": "Preact.Fragment"
"runtime": "automatic",
"importSource": "preact"
}
]
]
Expand Down
685 changes: 558 additions & 127 deletions cypress/e2e/surveys.cy.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"babel-eslint": "10.1.0",
"babel-jest": "^26.6.3",
"cypress": "13.6.3",
"cypress-localstorage-commands": "^2.2.5",
"eslint": "8.56.0",
"eslint-config-posthog-js": "link:eslint-rules",
"eslint-config-prettier": "^8.5.0",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

501 changes: 0 additions & 501 deletions src/__tests__/extensions/surveys.js

This file was deleted.

58 changes: 58 additions & 0 deletions src/__tests__/extensions/surveys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'regenerator-runtime/runtime'
import { generateSurveys } from '../../extensions/surveys'
import { createShadow } from '../../extensions/surveys/surveys-utils'
import { SurveyType } from '../../posthog-surveys-types'

describe('survey display logic', () => {
beforeEach(() => {
// we have to manually reset the DOM before each test
document.getElementsByTagName('html')[0].innerHTML = ''
localStorage.clear()
jest.clearAllMocks()
})

test('createShadow', () => {
const surveyId = 'randomSurveyId'
const mockShadow = createShadow(`.survey-${surveyId}-form {}`, surveyId)
expect(mockShadow.mode).toBe('open')
expect(mockShadow.host.className).toBe(`PostHogSurvey${surveyId}`)
})

const mockSurveys: any[] = [
{
id: 'testSurvey1',
name: 'Test survey 1',
type: SurveyType.Popover,
appearance: null,
start_date: '2021-01-01T00:00:00.000Z',
questions: [
{
question: 'How satisfied are you with our newest product?',
description: 'This is a question description',
type: 'rating',
display: 'number',
scale: 10,
lower_bound_label: 'Not Satisfied',
upper_bound_label: 'Very Satisfied',
},
],
},
]
const mockPostHog = {
getActiveMatchingSurveys: jest.fn().mockImplementation((callback) => callback(mockSurveys)),
get_session_replay_url: jest.fn(),
capture: jest.fn().mockImplementation((eventName) => eventName),
}

test('callSurveys runs on interval irrespective of url change', () => {
jest.useFakeTimers()
jest.spyOn(global, 'setInterval')
generateSurveys(mockPostHog)
expect(mockPostHog.getActiveMatchingSurveys).toBeCalledTimes(1)
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 3000)

jest.advanceTimersByTime(3000)
expect(mockPostHog.getActiveMatchingSurveys).toBeCalledTimes(2)
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 3000)
})
})
230 changes: 29 additions & 201 deletions src/extensions/surveys-widget.ts
Original file line number Diff line number Diff line change
@@ -1,210 +1,38 @@
import { PostHog } from '../posthog-core'
import { Survey } from '../posthog-surveys-types'
import {
createMultipleQuestionSurvey,
createSingleQuestionSurvey,
setTextColors,
showQuestion,
style,
} from './surveys/surveys-utils'
import { document as _document, window as _window } from '../utils/globals'
import { addCancelListeners, createThankYouMessage } from './surveys'
import { document as _document } from '../utils/globals'

// We cast the types here which is dangerous but protected by the top level generateSurveys call
const document = _document as Document
const window = _window as Window & typeof globalThis

export class SurveysWidget {
instance: PostHog
survey: Survey
shadow: any
widget?: any

constructor(instance: PostHog, survey: Survey, widget?: any) {
this.instance = instance
this.survey = survey
this.shadow = this.createWidgetShadow()
this.widget = widget
}

createWidget(): void {
const surveyPopup = this.createSurveyForWidget()
let widget
if (this.survey.appearance?.widgetType === 'selector') {
// user supplied button
widget = document.querySelector(this.survey.appearance.widgetSelector || '')
} else if (this.survey.appearance?.widgetType === 'tab') {
widget = this.createTabWidget()
} else if (this.survey.appearance?.widgetType === 'button') {
widget = this.createButtonWidget()
}
this.widget = widget
if (this.survey.appearance?.widgetType !== 'selector') {
this.shadow.appendChild(this.widget)
export function createWidgetShadow(survey: Survey) {
const div = document.createElement('div')
div.className = `PostHogWidget${survey.id}`
const shadow = div.attachShadow({ mode: 'open' })
const widgetStyleSheet = `
.ph-survey-widget-tab {
position: fixed;
top: 50%;
right: 0;
background: ${survey.appearance?.widgetColor || '#e0a045'};
color: white;
transform: rotate(-90deg) translate(0, -100%);
transform-origin: right top;
min-width: 40px;
padding: 8px 12px;
font-weight: 500;
border-radius: 3px 3px 0 0;
text-align: center;
cursor: pointer;
z-index: 9999999;
}
setTextColors(this.shadow)
// reposition survey next to widget when opened
if (surveyPopup && this.survey.appearance?.widgetType === 'tab' && this.widget) {
surveyPopup.style.bottom = 'auto'
surveyPopup.style.borderBottom = `1.5px solid ${this.survey.appearance?.borderColor || '#c9c6c6'}`
surveyPopup.style.borderRadius = '10px'
const widgetPos = this.widget.getBoundingClientRect()
surveyPopup.style.top = '50%'
surveyPopup.style.left = `${widgetPos.right - 360}px`
.ph-survey-widget-tab:hover {
padding-bottom: 13px;
}
if (this.widget) {
this.widget.addEventListener('click', () => {
if (surveyPopup) {
surveyPopup.style.display = surveyPopup.style.display === 'none' ? 'block' : 'none'
}
})
this.widget.setAttribute('PHWidgetSurveyClickListener', 'true')
if (surveyPopup) {
window.addEventListener('PHSurveySent', () => {
if (surveyPopup) {
surveyPopup.style.display = 'none'
}
const tabs = document
?.getElementsByClassName(`PostHogWidget${this.survey.id}`)[0]
?.shadowRoot?.querySelectorAll('.tab') as NodeListOf<HTMLElement>
tabs.forEach((tab) => (tab.style.display = 'none'))
showQuestion(0, this.survey.id, this.survey.type)
})
}
.ph-survey-widget-button {
position: fixed;
}
}

createTabWidget(): HTMLDivElement {
// make a permanent tab widget
const tab = document.createElement('div')
const html = `
<div class="ph-survey-widget-tab auto-text-color">
<div class="ph-survey-widget-tab-icon">
</div>
${this.survey.appearance?.widgetLabel || ''}
</div>
`

tab.innerHTML = html
return tab
}

createButtonWidget(): HTMLButtonElement {
// make a permanent button widget
const label = 'Feedback :)'
const button = document.createElement('button')
const html = `
<div class="ph-survey-widget-button auto-text-color">
<div class="ph-survey-widget-button-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
</div>
${label}
</div>
`
button.innerHTML = html
return button
}

private createSurveyForWidget(): HTMLFormElement | null {
const surveyStyleSheet = style(this.survey.id, this.survey.appearance)
this.shadow.appendChild(Object.assign(document.createElement('style'), { innerText: surveyStyleSheet }))
const widgetSurvey =
this.survey.questions.length > 1
? createMultipleQuestionSurvey(this.instance, this.survey)
: createSingleQuestionSurvey(this.instance, this.survey, this.survey.questions[0])
if (widgetSurvey) {
widgetSurvey.style.display = 'none'
addCancelListeners(this.instance, widgetSurvey as HTMLFormElement, this.survey.id, this.survey.name)
if (this.survey.appearance?.whiteLabel) {
const allBrandingElements = widgetSurvey.getElementsByClassName('footer-branding')
for (const brandingElement of allBrandingElements) {
;(brandingElement as HTMLAnchorElement).style.display = 'none'
}
}
this.shadow.appendChild(widgetSurvey)
if (this.survey.questions.length > 1) {
const currentQuestion = 0
showQuestion(currentQuestion, this.survey.id, this.survey.type)
}
setTextColors(this.shadow)
window.dispatchEvent(new Event('PHSurveyShown'))
this.instance.capture('survey shown', {
$survey_name: this.survey.name,
$survey_id: this.survey.id,
sessionRecordingUrl: this.instance.get_session_replay_url?.(),
})
if (this.survey.appearance?.displayThankYouMessage) {
window.addEventListener('PHSurveySent', () => {
const thankYouElement = createThankYouMessage(this.survey)
if (thankYouElement && this.survey.appearance?.widgetType === 'tab') {
thankYouElement.style.bottom = 'auto'
thankYouElement.style.borderBottom = `1.5px solid ${
this.survey.appearance?.borderColor || '#c9c6c6'
}`
thankYouElement.style.borderRadius = '10px'
const widgetPos = this.widget.getBoundingClientRect()
thankYouElement.style.top = '50%'
thankYouElement.style.left = `${widgetPos.right - 400}px`
}
this.shadow.appendChild(thankYouElement)
// reposition thank you box next to widget when opened
const cancelButtons = thankYouElement.querySelectorAll('.form-cancel, .form-submit')
for (const button of cancelButtons) {
button.addEventListener('click', () => {
thankYouElement.remove()
})
}
const countdownEl = thankYouElement.querySelector('.thank-you-message-countdown')
if (this.survey.appearance?.autoDisappear && countdownEl) {
let count = 3
countdownEl.textContent = `(${count})`
const countdown = setInterval(() => {
count -= 1
if (count <= 0) {
clearInterval(countdown)
thankYouElement.remove()
return
}
countdownEl.textContent = `(${count})`
}, 1000)
}
setTextColors(this.shadow)
})
}
}
return widgetSurvey as HTMLFormElement
}

private createWidgetShadow() {
const div = document.createElement('div')
div.className = `PostHogWidget${this.survey.id}`
const shadow = div.attachShadow({ mode: 'open' })
const widgetStyleSheet = `
.ph-survey-widget-tab {
position: fixed;
top: 50%;
right: 0;
background: ${this.survey.appearance?.widgetColor || '#e0a045'};
color: white;
transform: rotate(-90deg) translate(0, -100%);
transform-origin: right top;
min-width: 40px;
padding: 8px 12px;
font-weight: 500;
border-radius: 3px 3px 0 0;
text-align: center;
cursor: pointer;
z-index: 9999999;
}
.ph-survey-widget-tab:hover {
padding-bottom: 13px;
}
.ph-survey-widget-button {
position: fixed;
}
`
shadow.append(Object.assign(document.createElement('style'), { innerText: widgetStyleSheet }))
document.body.appendChild(div)
return shadow
}
`
shadow.append(Object.assign(document.createElement('style'), { innerText: widgetStyleSheet }))
document.body.appendChild(div)
return shadow
}
Loading
Loading