Skip to content

Commit

Permalink
feat(surveys): Add open-ended choices for multiple and single choice …
Browse files Browse the repository at this point in the history
…surveys (#910)

* feat(surveys) Add open-ended choices for multiple and single choice surveys

* Change multiple and single choice questions to have has_open_choice field

* Change has_open_choice to hasOpenChoice

* Remove border around text input

---------

Co-authored-by: Soon-Mi Sugihara <[email protected]>
  • Loading branch information
liyiy and ssoonmi authored Nov 22, 2023
1 parent bde00d8 commit a0b1c8a
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 19 deletions.
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

0 comments on commit a0b1c8a

Please sign in to comment.