DigitalConsumer.ISS/src/digital-components/button-question/button-question.vue
Alex Humphries 4b18f7e06a INSR-7758: part-questions parity
Mostly actually an update to questions-page-layout, question-chain,
button-question and list-button-horizontal, since those are the
actual bulk of what this page.
2026-01-27 16:57:37 -05:00

405 lines
13 KiB
Vue

<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template>
<div
:class="
isOverflowScrollable
? 'button-question button-question-overflow'
: 'button-question'
">
<div
v-if="questionText && answers && answers.length > 0"
class="question-text d-flex"
:class="{
'small-question-text': isSmallQuestionText,
'small-question-label-text': isSmallQuestionLabelText,
}">
<span
class="w-100"
:class="questionTextClasses">{{ questionText }}</span>
</div>
<div class="w-100 d-flex justify-content-center">
<fieldset
class="w-100"
:aria-required="isRequired"
:role="isMultiSelect ? 'group' : 'radiogroup'"
:aria-labelledby="formatString(groupName)">
<legend
:id="formatString(groupName)"
class="sr-only"
:data-focus-target="formatString(groupName)"
tabindex="-1">
{{ questionText }}
{{
isMultiSelect && answers && answers.length > 1
? 'Select one or more options below.'
: 'Select an option below.'
}}
</legend>
<div :class="getComponentLoopWrapperClasses">
<div
v-for="answer in buttonsInfo"
:key="answer.value ?? answer"
:class="getComponentWrapperClasses">
<component
:is="buttonTypeString"
v-model="selectedValues"
:buttonLabel="answer.buttonLabel"
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
:buttonBodyCopy="answer.buttonBodyCopy"
:buttonAuxiliaryCopy="answer.buttonAuxiliaryCopy"
:buttonFooterCopy="answer.buttonFooterCopy"
:buttonImage="answer.buttonImage"
:buttonImageId="answer.buttonImageId"
:groupName="answer.groupName"
:isMultiSelect="isMultiSelect"
:value="answer.value"
:selectingInitiatesLoad="selectingInitiatesLoad"
:isWide="isWide"
:validationRules="validationRules"
:textPosition="textPosition"
:additionalButtonData="additionalButtonData"
:additionalButtonStyling="additionalButtonStyling"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:suppressError="suppressError" />
<!-- For nested questions -->
<transition
name="fade"
mode="out-in">
<div
v-if="
typeof selectedValues == 'string' &&
selectedValues == answer.value
">
<slot></slot>
</div>
</transition>
</div>
</div>
</fieldset>
</div>
<div
v-if="!suppressError"
class="row form-test-error mt-1">
<error-message :name="formatString(groupName)"></error-message>
</div>
</div>
</template>
<script>
import listButton from '@/ux-components/list-button/list-button.vue';
import listButtonHorizontal from '@/ux-components/list-button-horizontal/list-button-horizontal.vue';
import listCard from '@/ux-components/list-card/list-card.vue';
import radio from '@/ux-components/radio/radio.vue';
import { useField, ErrorMessage } from 'vee-validate';
import providerPrefRadio from '@/layouts/provider-preference/provider-pref-radio/provider-pref-radio.vue';
export default {
name: 'button-question',
components: {
listButton,
listButtonHorizontal,
listCard,
ErrorMessage,
radio,
providerPrefRadio
},
props: {
buttonTypeString: {
type: String,
default: 'listButton'
},
buttonTypeObject: {
type: Object,
default: null
},
isMultiSelect: Boolean,
groupName: String,
questionText: String,
answers: Array,
questionTextClasses: Array,
textPosition: {
type: String,
default: 'text-center'
},
selectingInitiatesLoad: Boolean,
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
isCashOrInsurance: Boolean,
modelValue: [Array, Number, String],
value: [Number, String],
validationRules: String,
suppressError: Boolean,
useTextForValue: Boolean,
valueToLogType: String,
additionalButtonData: Object,
additionalButtonStyling: String,
isSmallQuestionText: Boolean,
isSmallQuestionLabelText: Boolean
},
emits: ['update:modelValue'],
setup(props) {
const propsClone = { ...props };
const { modelValue } = propsClone;
const fieldOptions = {
value: modelValue,
initialValue: null
};
const {
errorMessage,
handleBlur,
handleChange,
meta,
validate,
errors,
resetField
} = useField(props.groupName, props.validationRules, fieldOptions);
return {
errorMessage,
handleBlur,
handleChange,
validate,
meta,
errors,
resetField
};
},
data() {
return {
lastValuePushedToGa: null
};
},
computed: {
getFieldSetClasses() {
// eslint-disable-next-line no-nested-ternary
const baseClasses = this.isOverflowScrollable
? 'container-fluid overflow-scroll position-absolute px-5 pt-1 py-0'
: this.buttonTypeString === 'listCard'
? 'w-100'
: '';
const SmallQuestionTextClass = this.isSmallQuestionText
? `${baseClasses}small-question-text`
: baseClasses;
const SmallQuestionLabelText = this.isSmallQuestionLabelText
? `${SmallQuestionTextClass}small-question-label-text`
: SmallQuestionTextClass;
return SmallQuestionLabelText;
},
getComponentLoopWrapperClasses() {
let classes;
switch (this.buttonTypeString) {
case 'listButton':
classes = 'w-100';
break;
case 'listButtonHorizontal':
classes = 'd-flex flex-row p-0';
break;
case 'listCard':
classes =
'row g-2 g-md-5 justify-content-center mb-1 flex-nowrap';
if (this.isWide) {
classes += ' flex-column';
}
break;
case 'radio':
classes = 'ui-radio d-flex';
break;
case 'servicePackageRadio':
classes = 'package-main';
break;
case 'providerPrefRadio':
classes = 'option-main';
break;
default:
}
return classes;
},
getComponentWrapperClasses() {
let classes = '';
classes += this.isWide ? 'col-12' : 'col';
switch (this.buttonTypeString) {
case 'radio':
classes += ' radio-button-container';
break;
case 'servicePackageRadio':
classes = 'package-wrapper';
break;
case 'providerPrefRadio':
classes = 'option-wrapper';
break;
default:
}
// Determine number of buttons and add class accordingly.
// This is to accommodate unique spacing per design specs.
if (
this.buttonTypeString === 'listCard'
&& this.buttonsInfo.length > 2
) {
classes += ' two-list-card-width';
}
if (
this.buttonTypeString === 'listCard'
&& this.buttonsInfo.length === 1
) {
classes += ' one-list-card-width';
}
return classes;
},
buttonsInfo() {
return (Array.isArray(this.answers) ? this.answers : [])?.map((answer) => ({
buttonLabel: answer.buttonLabel ?? answer.Text ?? answer,
altText:
answer.altText ?? (answer.Name ? answer.Name : answer),
buttonLabelSubCopy:
answer.buttonLabelSubCopy ?? answer.SubText,
buttonBodyCopy:
answer.buttonBodyCopy ?? answer.buttonBodyCopy,
buttonAuxiliaryCopy:
answer.buttonAuxiliaryCopy
?? answer.buttonAuxiliaryCopy,
buttonFooterCopy:
answer.buttonFooterCopy ?? answer.buttonFooterCopy,
buttonImage: answer.buttonImage ?? answer.AnswerImageUrl,
buttonImageId: answer.buttonImageId ?? answer.ImageId,
groupName: this.formatString(this.groupName),
value:
answer.value
?? (this.useTextForValue && answer.Text
? answer.Text
: answer.Name)
?? (typeof answer !== 'object' ? answer : null)
}));
},
selectedValues: {
get() {
return this.modelValue;
},
set(selectedAnswers) {
this.$emit('update:modelValue', selectedAnswers);
}
}
},
watch: {
modelValue(newValue) {
this.resetField({
value: newValue
});
},
answers() {
this.resetField({
value: this.modelValue
});
}
},
beforeMount() {
if (this.buttonTypeObject) {
this.$options.components[this.buttonTypeString] =
this.buttonTypeObject;
}
},
beforeUnmount() {
this.resetField({
value: ''
});
},
methods: {
formatString(str) {
return String(str).replaceAll(' ', '-');
},
setLastValuePushedToGa(lastValuePushedToGa) {
this.lastValuePushedToGa = lastValuePushedToGa;
}
}
};
</script>
<style lang="scss" scoped>
.button-question {
color: $black;
.radio-button-container {
&:not(:last-child) {
padding-bottom: map-get($spacers, 2);
}
}
.question-text {
margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
span {
&.select-all-glass-parts {
color: #525656;
}
&.button-question-text-left {
text-align: left;
}
}
}
&.provider-preference {
.question-text {
span {
text-align: left;
}
}
}
&:has(.list-button-horizontal) {
.question-text {
margin-bottom: .25rem;
font-weight: 600;
}
fieldset {
border-radius: $border-radius-list-button;
box-shadow: $box-shadow-input;
}
}
}
.small-question-text {
&.question-text {
span {
font-size: 0.875rem;
text-align: left;
margin: 0 0 0.5rem 0;
}
}
&.question-text {
margin: 0;
}
&fieldset {
.ui-radio {
margin: 0;
}
}
}
.small-question-label-text {
&.question-text {
span {
text-align: left;
margin: 0 0 0.25rem 0;
}
}
&.question-text {
margin: 0;
}
&fieldset {
.ui-radio {
margin: 0;
}
}
}
</style>