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): custom and tab widget #933

Merged
merged 5 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
134 changes: 127 additions & 7 deletions src/__tests__/extensions/surveys.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import {
createShadow,
callSurveys,
generateSurveys,
createMultipleQuestionSurvey,
createRatingsPopup,
} from '../../extensions/surveys'
import { createShadow, callSurveys, generateSurveys } from '../../extensions/surveys'
import { SurveyType } from '../../posthog-surveys-types'
import { createMultipleQuestionSurvey, createRatingsPopup } from '../../extensions/surveys/surveys-utils'

describe('survey display logic', () => {
beforeEach(() => {
Expand All @@ -25,6 +21,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey1',
name: 'Test survey 1',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand Down Expand Up @@ -124,6 +121,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
conditions: { seenSurveyWaitPeriodInDays: 10 },
questions: [
Expand Down Expand Up @@ -174,6 +172,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
conditions: { seenSurveyWaitPeriodInDays: 10 },
questions: [
Expand Down Expand Up @@ -206,6 +205,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand Down Expand Up @@ -241,6 +241,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand All @@ -257,6 +258,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey3',
name: 'Test survey 3',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand Down Expand Up @@ -285,6 +287,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand Down Expand Up @@ -335,6 +338,7 @@ describe('survey display logic', () => {
{
id: 'testSurvey2',
name: 'Test survey 2',
type: SurveyType.Popover,
appearance: null,
questions: [
{
Expand Down Expand Up @@ -379,3 +383,119 @@ describe('survey display logic', () => {
})
})
})

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

let mockSurveys = [
{
id: 'testWidget1',
name: 'Test widget 1',
type: SurveyType.Widget,
appearance: { widgetType: 'tab' },
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',
},
],
},
{
id: 'testWidget2',
name: 'Test widget 2',
type: SurveyType.Widget,
appearance: { widgetType: 'tab' },
questions: [
{
question: 'How satisfied are you with our newest product?',
description: 'This is a question description',
type: 'rating',
display: 'emoji',
scale: 3,
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('there can be multiple widgets on the same page as long as they are unique', () => {
callSurveys(mockPostHog, false)
const widget = document
.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`)[0]
.shadowRoot.querySelectorAll('.ph-survey-widget-tab')
expect(widget.length).toEqual(1)
expect(document.querySelectorAll("div[class^='PostHogWidget']").length).toEqual(2)
callSurveys(mockPostHog, false)
expect(document.querySelectorAll("div[class^='PostHogWidget']").length).toEqual(2)
})

test('tab type widgets show and close the survey when clicked', () => {
mockSurveys.pop()
callSurveys(mockPostHog, false)
const shadow = document.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`)[0].shadowRoot
const widget = shadow.querySelectorAll('.ph-survey-widget-tab')
widget[0].click()
const survey = shadow.querySelectorAll(`.survey-${mockSurveys[0].id}-form`)[0]
expect(survey.style.display).toEqual('block')
widget[0].click()
expect(survey.style.display).toEqual('none')
})

test('selector type widget can only display the survey when the selector is present on the page', () => {
mockSurveys = [
{
id: 'testWidget3',
name: 'Test widget 3',
type: SurveyType.Widget,
appearance: { widgetType: 'selector', widgetSelector: '.user-widget-button' },
questions: [
{
question: 'How satisfied are you with our newest product?',
description: 'This is a question description',
type: 'rating',
display: 'emoji',
scale: 3,
lower_bound_label: 'Not Satisfied',
upper_bound_label: 'Very Satisfied',
},
],
},
]
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`).length).toEqual(0)
const button = document.createElement('button')
button.className = 'user-widget-button'
document.body.appendChild(button)
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`).length).toEqual(1)
// widget should only be created once
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`).length).toEqual(1)
// expect survey style display to be none initially
const shadow = document.getElementsByClassName(`PostHogWidget${mockSurveys[0].id}`)[0].shadowRoot
const survey = shadow.querySelectorAll(`.survey-testWidget3-form`)[0]
expect(survey.style.display).toEqual('none')

// click on the button to show the survey test
button.click()
expect(survey.style.display).toEqual('block')
survey.querySelectorAll('.form-cancel')[0].click()
expect(survey.style.display).toEqual('none')
})
})
134 changes: 134 additions & 0 deletions src/extensions/surveys-widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { PostHog } from '../posthog-core'
import { Survey } from '../posthog-surveys-types'
import { createMultipleQuestionSurvey, createSingleQuestionSurvey, setTextColors, style } from './surveys/surveys-utils'
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

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

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

createWidget(): void {
const survey = 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()
}
if (this.survey.appearance?.widgetType !== 'selector') {
this.shadow.appendChild(widget)
}
setTextColors(this.shadow)
// reposition survey next to widget when opened
if (survey && this.survey.appearance?.widgetType === 'tab' && widget) {
survey.style.bottom = 'auto'
survey.style.borderBottom = `1.5px solid ${this.survey.appearance?.borderColor || '#c9c6c6'}`
survey.style.borderRadius = '10px'
const widgetPos = widget.getBoundingClientRect()
survey.style.top = '50%'
survey.style.left = `${widgetPos.right - 360}px`
}
if (widget) {
widget.addEventListener('click', () => {
if (survey) {
survey.style.display = survey.style.display === 'none' ? 'block' : 'none'
}
})
widget.setAttribute('PHWidgetSurveyClickListener', 'true')
survey?.addEventListener('PHSurveyClosed', () => (survey.style.display = 'none'))
}
}

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'
}
this.shadow.appendChild(widgetSurvey)
// add survey cancel listener
widgetSurvey?.addEventListener('PHSurveyClosed', () => (widgetSurvey.style.display = 'none'))
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
}
}
Loading