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): Add open-ended choices for multiple and single choice surveys #901

Closed
Closed
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
99 changes: 99 additions & 0 deletions src/__tests__/extensions/surveys.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,103 @@ describe('survey display logic', () => {
expect(ratingQuestion2.querySelectorAll('.question-0-rating-0').length).toBe(0)
expect(ratingQuestion2.querySelectorAll('.question-0-rating-1').length).toBe(1)
})

test('open choice value on a multiple choice question is determined by a text input', () => {
mockSurveys = [
{
id: 'testSurvey2',
name: 'Test survey 2',
appearance: null,
questions: [
{
question: 'Which types of content would you like to see more of?',
description: 'This is a question description',
type: 'multiple_choice',
choices: ['Tutorials', 'Product Updates', 'Events', 'Other'],
hasOpenChoice: true,
},
],
},
]
const singleQuestionSurveyForm = createMultipleQuestionSurvey(mockPostHog, mockSurveys[0])

const checkboxInputs = singleQuestionSurveyForm
.querySelector('.tab.question-0')
.querySelectorAll('input[type=checkbox]')
let checkboxInputValues = [...checkboxInputs].map((input) => input.value)
expect(checkboxInputValues).toEqual(['Tutorials', 'Product Updates', 'Events', ''])
const openChoiceTextInput = singleQuestionSurveyForm
.querySelector('.tab.question-0')
.querySelector('input[type=text]')
openChoiceTextInput.value = 'NEW VALUE 1'
openChoiceTextInput.dispatchEvent(new Event('input'))
expect(singleQuestionSurveyForm.querySelector('.form-submit').disabled).toEqual(false)
checkboxInputValues = [...checkboxInputs].map((input) => input.value)
expect(checkboxInputValues).toEqual(['Tutorials', 'Product Updates', 'Events', 'NEW VALUE 1'])
checkboxInputs[0].click()
const checkboxInputsChecked = [...checkboxInputs].map((input) => input.checked)
expect(checkboxInputsChecked).toEqual([true, false, false, true])

singleQuestionSurveyForm.dispatchEvent(new Event('submit'))
expect(mockPostHog.capture).toBeCalledTimes(1)
expect(mockPostHog.capture).toBeCalledWith('survey sent', {
$survey_name: 'Test survey 2',
$survey_id: 'testSurvey2',
$survey_questions: ['Which types of content would you like to see more of?'],
$survey_response: ['Tutorials', 'NEW VALUE 1'],
sessionRecordingUrl: undefined,
$set: {
['$survey_responded/testSurvey2']: true,
},
})
})

test('open choice value on a single choice question is determined by a text input', () => {
mockSurveys = [
{
id: 'testSurvey2',
name: 'Test survey 2',
appearance: null,
questions: [
{
question: 'Which features do you use the most?',
description: 'This is a question description',
type: 'single_choice',
choices: ['Surveys', 'Feature flags', 'Analytics', 'Another Feature'],
hasOpenChoice: true,
},
],
},
]
const singleQuestionSurveyForm = createMultipleQuestionSurvey(mockPostHog, mockSurveys[0])

const radioInputs = singleQuestionSurveyForm
.querySelector('.tab.question-0')
.querySelectorAll('input[type=radio]')
let radioInputValues = [...radioInputs].map((input) => input.value)
expect(radioInputValues).toEqual(['Surveys', 'Feature flags', 'Analytics', ''])
const openChoiceTextInput = singleQuestionSurveyForm
.querySelector('.tab.question-0')
.querySelector('input[type=text]')
openChoiceTextInput.value = 'NEW VALUE 2'
openChoiceTextInput.dispatchEvent(new Event('input'))
expect(singleQuestionSurveyForm.querySelector('.form-submit').disabled).toEqual(false)
radioInputValues = [...radioInputs].map((input) => input.value)
expect(radioInputValues).toEqual(['Surveys', 'Feature flags', 'Analytics', 'NEW VALUE 2'])
const radioInputsChecked = [...radioInputs].map((input) => input.checked)
expect(radioInputsChecked).toEqual([false, false, false, true])

singleQuestionSurveyForm.dispatchEvent(new Event('submit'))
expect(mockPostHog.capture).toBeCalledTimes(1)
expect(mockPostHog.capture).toBeCalledWith('survey sent', {
$survey_name: 'Test survey 2',
$survey_id: 'testSurvey2',
$survey_questions: ['Which features do you use the most?'],
$survey_response: 'NEW VALUE 2',
sessionRecordingUrl: undefined,
$set: {
['$survey_responded/testSurvey2']: true,
},
})
})
})
95 changes: 76 additions & 19 deletions src/extensions/surveys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,13 @@ const style = (id: string, appearance: SurveyAppearance | null) => {
display: inline-block;
opacity: 100% !important;
}
.multiple-choice-options input[type=checkbox]:checked + label {
font-weight: bold;
}
.multiple-choice-options input:checked + label {
font-weight: bold;
border: 1.5px solid rgba(0,0,0);
}
.multiple-choice-options input:checked + label input {
font-weight: bold;
}
.multiple-choice-options label {
width: 100%;
cursor: pointer;
Expand All @@ -275,6 +276,26 @@ const style = (id: string, appearance: SurveyAppearance | null) => {
border-radius: 4px;
background: white;
}
.multiple-choice-options .choice-option-open label {
padding-right: 30px;
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 100%;
}
.multiple-choice-options .choice-option-open label span {
width: 100%;
}
.multiple-choice-options .choice-option-open input:disabled + label {
opacity: 0.6;
}
.multiple-choice-options .choice-option-open label input {
position: relative;
opacity: 1;
flex-grow: 1;
border: 0;
outline: 0;
}
.thank-you-message {
position: fixed;
bottom: 0px;
Expand Down Expand Up @@ -600,8 +621,9 @@ export const createMultipleChoicePopup = (
const surveyQuestion = question.question
const surveyDescription = question.description
const surveyQuestionChoices = question.choices
const singleOrMultiSelect = question.type
const isSingleChoice = question.type === 'single_choice'
const isOptional = !!question.optional
const hasOpenChoice = !!question.hasOpenChoice

const form = `
<div class="survey-${survey.id}-box">
Expand All @@ -613,9 +635,22 @@ export const createMultipleChoicePopup = (
<div class="multiple-choice-options">
${surveyQuestionChoices
.map((option, idx) => {
const inputType = singleOrMultiSelect === 'single_choice' ? 'radio' : 'checkbox'
const singleOrMultiSelectString = `<div class="choice-option"><input type=${inputType} id=surveyQuestion${questionIndex}Choice${idx} name="question${questionIndex}" value="${option}">
<label class="auto-text-color" for=surveyQuestion${questionIndex}Choice${idx}>${option}</label><span class="choice-check auto-text-color">${checkSVG}</span></div>`
let choiceClass = 'choice-option'
let val = option
if (hasOpenChoice && idx === surveyQuestionChoices.length - 1) {
option = `<span>${option}:</span><input type="text" value="">`
choiceClass += ' choice-option-open'
val = ''
}
const inputType = isSingleChoice ? 'radio' : 'checkbox'
const singleOrMultiSelectString = `<div class="${choiceClass}">
<input type="${inputType}" id=surveyQuestion${questionIndex}Choice${idx}
name="question${questionIndex}" value="${val}" ${val ? '' : 'disabled'}>
<label class="auto-text-color" for=surveyQuestion${questionIndex}Choice${idx}>
${option}
</label>
<span class="choice-check auto-text-color">${checkSVG}</span>
</div>`
return singleOrMultiSelectString
})
.join(' ')}
Expand All @@ -639,14 +674,13 @@ export const createMultipleChoicePopup = (
onsubmit: (e: Event) => {
e.preventDefault()
const targetElement = e.target as HTMLFormElement
const selectedChoices =
singleOrMultiSelect === 'single_choice'
? (targetElement.querySelector('input[type=radio]:checked') as HTMLInputElement)?.value
: [
...(targetElement.querySelectorAll(
'input[type=checkbox]:checked'
) as NodeListOf<HTMLInputElement>),
].map((choice) => choice.value)
const selectedChoices = isSingleChoice
? (targetElement.querySelector('input[type=radio]:checked') as HTMLInputElement)?.value
: [
...(targetElement.querySelectorAll(
'input[type=checkbox]:checked'
) as NodeListOf<HTMLInputElement>),
].map((choice) => choice.value)
posthog.capture('survey sent', {
$survey_name: survey.name,
$survey_id: survey.id,
Expand All @@ -670,17 +704,40 @@ export const createMultipleChoicePopup = (
}
if (!isOptional) {
formElement.addEventListener('change', () => {
const selectedChoices: NodeListOf<HTMLInputElement> =
singleOrMultiSelect === 'single_choice'
? formElement.querySelectorAll('input[type=radio]:checked')
: formElement.querySelectorAll('input[type=checkbox]:checked')
const selectedChoices: NodeListOf<HTMLInputElement> = isSingleChoice
? formElement.querySelectorAll('input[type=radio]:checked')
: formElement.querySelectorAll('input[type=checkbox]:checked')
if ((selectedChoices.length ?? 0) > 0) {
;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = false
} else {
;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = true
}
})
}
const openChoiceWrappers = formElement.querySelectorAll('.choice-option-open')
for (const openChoiceWrapper of openChoiceWrappers) {
const textInput = openChoiceWrapper.querySelector('input[type=text]') as HTMLInputElement
const inputType = isSingleChoice ? 'radio' : 'checkbox'
const checkInput = openChoiceWrapper.querySelector(`input[type=${inputType}]`) as HTMLInputElement
openChoiceWrapper.addEventListener('click', () => {
if (checkInput?.checked || checkInput?.disabled) textInput?.focus()
})
textInput.addEventListener('click', (e) => e.stopPropagation())
textInput.addEventListener('input', (e) => {
const textInput = e.target as HTMLInputElement
if (checkInput) {
checkInput.value = textInput.value
if (textInput.value) {
checkInput.disabled = false
checkInput.checked = true
} else {
checkInput.disabled = true
checkInput.checked = false
}
formElement.dispatchEvent(new Event('change'))
}
})
}

return formElement
}
Expand Down
1 change: 1 addition & 0 deletions src/posthog-surveys-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface RatingSurveyQuestion extends SurveyQuestionBase {
export interface MultipleSurveyQuestion extends SurveyQuestionBase {
type: SurveyQuestionType.SingleChoice | SurveyQuestionType.MultipleChoice
choices: string[]
hasOpenChoice?: boolean
}

export enum SurveyQuestionType {
Expand Down
Loading