Skip to content

Commit

Permalink
feat(surveys): custom and tab widget (#933)
Browse files Browse the repository at this point in the history
* move reuseable methods into utils file

* update widget code

* tests

* fix bug for selector widgets
  • Loading branch information
liyiy authored Dec 12, 2023
1 parent abbcb20 commit e7a8527
Show file tree
Hide file tree
Showing 5 changed files with 1,139 additions and 819 deletions.
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

0 comments on commit e7a8527

Please sign in to comment.