commit
This commit is contained in:
parent
02e703f395
commit
c67c9c5c87
6 changed files with 861 additions and 1036 deletions
|
|
@ -1,253 +1,271 @@
|
|||
<!-- 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
|
||||
:class="
|
||||
isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'
|
||||
">
|
||||
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
|
||||
<span class="fw-bold w-100">{{ questionText }}</span>
|
||||
<span class="fw-bold w-100">{{ questionText }}</span>
|
||||
</div>
|
||||
|
||||
<div class="w-100 d-flex justify-content-center">
|
||||
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formatString(groupName)">
|
||||
<legend class="sr-only" :data-focus-target="formatString(groupName)" :id="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 :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
|
||||
<component
|
||||
:is="buttonType"
|
||||
@isCheckedChanged="handleCheckedChanged"
|
||||
:buttonID="answer.Name ? formatString(groupName) + '-' + answer.Name : formatString(groupName) + '-' + getAnswerString(answer, 'Text')"
|
||||
:value="getValue(answer)"
|
||||
:buttonLabel="answer.Text ? answer.Text : getAnswerString(answer, 'Name')"
|
||||
:buttonLabelSubCopy="answer.SubText"
|
||||
:textPosition="textPosition"
|
||||
:isMultiSelect="isMultiSelect"
|
||||
:groupName="formatString(groupName)"
|
||||
:selectingInitiatesLoad="selectingInitiatesLoad"
|
||||
:loaderColor="loaderColor"
|
||||
:loaderPosition="loaderPosition"
|
||||
:isWide=isWide
|
||||
:isCashOrInsurance=isCashOrInsurance
|
||||
:isRequired=isRequired
|
||||
:buttonImage="answer.AnswerImageUrl"
|
||||
:buttonImageId="answer.ImageId"
|
||||
:altText="answer.Name ? answer.Name : answer"
|
||||
screenReaderOnlyText="(opens new window)"
|
||||
:colLength="getColLength"
|
||||
:selectedValues="selectedValues"
|
||||
data-test="button"
|
||||
:validationRules="validationRules"
|
||||
:class="[suppressError ? 'alertError' : '' , isCashOrInsurance ? 'radio-fancy' : '']"
|
||||
:valueToLogType="valueToLogType"
|
||||
/>
|
||||
<!-- For nested questions -->
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset
|
||||
class="w-100"
|
||||
:aria-required="isRequired"
|
||||
:class="getFieldSetClasses"
|
||||
:role="isMultiSelect ? 'group' : 'radiogroup'"
|
||||
:aria-labelledby="formatString(groupName)">
|
||||
<legend
|
||||
class="sr-only"
|
||||
:data-focus-target="formatString(groupName)"
|
||||
tabindex="-1"
|
||||
:id="formatString(groupName)">
|
||||
{{ questionText }}
|
||||
{{
|
||||
isMultiSelect && answers && answers.length > 1
|
||||
? "Select one or more options below."
|
||||
: "Select an option below."
|
||||
}}
|
||||
</legend>
|
||||
<div :class="getComponentLoopWrapperClasses">
|
||||
<div
|
||||
:class="getComponentWrapperClasses"
|
||||
v-for="answer in buttonsInfo"
|
||||
:key="answer.value ? answer.value : answer">
|
||||
<component
|
||||
:is="buttonTypeString"
|
||||
:buttonLabel="answer.buttonLabel"
|
||||
:buttonLabelSubCopy="answer.buttonLabelSubCopy"
|
||||
:buttonBodyCopy="answer.buttonBodyCopy"
|
||||
:buttonAuxillaryCopy="answer.buttonAuxillaryCopy"
|
||||
: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"
|
||||
:additionalButtonStyling="additionalButtonStyling"
|
||||
:lastValuePushedToGa="lastValuePushedToGa"
|
||||
:setLastValuePushedToGa="setLastValuePushedToGa"
|
||||
v-model="selectedValues" />
|
||||
<!-- 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 class="row form-test-error mt-1">
|
||||
<error-message :name="formatString(groupName)" v-if="!suppressError"></error-message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import listButton from "@/ux-components/list-button/list-button";
|
||||
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||
import listCard from "@/ux-components/list-card/list-card";
|
||||
import { ErrorMessage } from 'vee-validate';
|
||||
import radio from "@/ux-components/radio/radio";
|
||||
|
||||
export default {
|
||||
name: "buttonQuestion",
|
||||
props: {
|
||||
buttonType: {
|
||||
type: String,
|
||||
default: "listButton",
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import listButton from "@/ux-components/list-button/list-button";
|
||||
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||
import listCard from "@/ux-components/list-card/list-card";
|
||||
import radio from "@/ux-components/radio/radio";
|
||||
import { ErrorMessage } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "buttonQuestion",
|
||||
props: {
|
||||
buttonTypeString: {
|
||||
type: String,
|
||||
default: "listButton",
|
||||
},
|
||||
buttonTypeObject: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isMultiSelect: Boolean,
|
||||
groupName: String,
|
||||
questionText: String,
|
||||
answers: Array,
|
||||
textPosition: {
|
||||
type: String,
|
||||
default: "text-center",
|
||||
type: String,
|
||||
default: "text-center",
|
||||
},
|
||||
selectingInitiatesLoad: Boolean,
|
||||
loaderColor: {
|
||||
type: String,
|
||||
default: "blue",
|
||||
type: String,
|
||||
default: "blue",
|
||||
},
|
||||
loaderPosition: {
|
||||
type: String,
|
||||
default: "right",
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
isRequired: Boolean,
|
||||
isOverflowScrollable: Boolean,
|
||||
isWide: Boolean,
|
||||
isCashOrInsurance: Boolean,
|
||||
modelValue: [Array, String],
|
||||
modelValue: [Array, Number, String],
|
||||
value: [Number, String],
|
||||
validationRules: String,
|
||||
suppressError: Boolean,
|
||||
useTextForValue: Boolean,
|
||||
valueToLogType: String,
|
||||
},
|
||||
computed: {
|
||||
additionalButtonStyling: String,
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.buttonTypeObject) {
|
||||
this.$options.components[this.buttonTypeString] = this.buttonTypeObject;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastValuePushedToGa: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
getFieldSetClasses() {
|
||||
if (this.isOverflowScrollable) {
|
||||
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
|
||||
}
|
||||
else if (this.buttonType == "listCard") {
|
||||
return "w-100";
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
if (this.isOverflowScrollable) {
|
||||
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
|
||||
} else if (this.buttonTypeString == "listCard") {
|
||||
return "w-100";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
getComponentLoopWrapperClasses() {
|
||||
let classes;
|
||||
switch (this.buttonType) {
|
||||
case "listButton":
|
||||
classes = "w-100";
|
||||
break;
|
||||
case "listButtonHorizontal":
|
||||
classes = "d-flex flex-row p-0";
|
||||
break;
|
||||
case 'listCard':
|
||||
classes = "row g-2 justify-content-center";
|
||||
break;
|
||||
case 'radio':
|
||||
classes = 'ui-radio d-flex'
|
||||
break;
|
||||
}
|
||||
return classes;
|
||||
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 justify-content-center";
|
||||
if (this.isWide) {
|
||||
classes += " flex-column";
|
||||
}
|
||||
break;
|
||||
case "radio":
|
||||
classes = "ui-radio d-flex";
|
||||
break;
|
||||
case "servicePackageRadio":
|
||||
classes = "package-main";
|
||||
break;
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
getComponentWrapperClasses() {
|
||||
let classes = "";
|
||||
|
||||
classes += this.isWide ? "col-12" : "col";
|
||||
|
||||
if (this.buttonType == "radio") {
|
||||
classes += " radio-button-container";
|
||||
}
|
||||
|
||||
return classes;
|
||||
let classes = "";
|
||||
|
||||
classes += this.isWide ? "col-12" : "col";
|
||||
|
||||
if (this.buttonTypeString == "radio") {
|
||||
classes += " radio-button-container";
|
||||
} else if (this.buttonTypeString == "servicePackageRadio") {
|
||||
classes = "package-wrapper";
|
||||
}
|
||||
|
||||
return classes;
|
||||
},
|
||||
getColLength(){
|
||||
if(this.isWide) {
|
||||
return "12"
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
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,
|
||||
buttonAuxillaryCopy: answer.buttonAuxillaryCopy ?? answer.buttonAuxillaryCopy,
|
||||
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: function() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
get() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set(selectedAnswers) {
|
||||
this.$emit("update:modelValue", selectedAnswers);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
methods: {
|
||||
formatString(str) {
|
||||
return str.replace(" ", "-");
|
||||
return str?.replaceAll(" ", "-");
|
||||
},
|
||||
getValue(answer){
|
||||
if (this.useTextForValue) { return answer.Text }
|
||||
return answer.Name ? answer.Name : answer;
|
||||
setLastValuePushedToGa(lastValuePushedToGa) {
|
||||
this.lastValuePushedToGa = lastValuePushedToGa;
|
||||
},
|
||||
getAnswerString(answer, prop = "Name") {
|
||||
switch (typeof answer) {
|
||||
case "string":
|
||||
case "number":
|
||||
case "boolean":
|
||||
return this.formatString(answer.toString());
|
||||
default:
|
||||
return answer[prop] ? this.formatString(answer[prop]) : this.formatString(answer.toString());
|
||||
}
|
||||
},
|
||||
handleCheckedChanged(val) {
|
||||
if(this.selectingInitiatesLoad) {
|
||||
this.selectedValues = val.value;
|
||||
} else {
|
||||
if(this.isMultiSelect) {
|
||||
const newSelectedValues = this.selectedValues;
|
||||
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
|
||||
this.selectedValues = newSelectedValues;
|
||||
}
|
||||
else if (Array.isArray(this.selectedValues)) {
|
||||
this.selectedValues[0] = val.value;
|
||||
const temp = this.selectedValues;
|
||||
this.selectedValues = temp;
|
||||
}
|
||||
else {
|
||||
this.selectedValues = val.value;
|
||||
}
|
||||
}
|
||||
this.$emit("isCheckedChanged", val);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
},
|
||||
components: {
|
||||
listButton,
|
||||
listButtonHorizontal,
|
||||
listCard,
|
||||
ErrorMessage,
|
||||
radio,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.button-question-overflow {
|
||||
height: calc(100vh - 274px);
|
||||
|
||||
.overflow-scroll {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.button-question-overflow {
|
||||
height: calc(100vh - 274px);
|
||||
|
||||
.overflow-scroll {
|
||||
// Height will be determined by overall height of content above list
|
||||
height: calc(100% - 314px);
|
||||
overflow-x: hidden !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
}
|
||||
.button-question {
|
||||
color: $black;
|
||||
|
||||
.radio-button-container {
|
||||
}
|
||||
.button-question {
|
||||
color: $black;
|
||||
|
||||
.radio-button-container {
|
||||
&:not(:last-child) {
|
||||
padding-bottom: map-get($spacers, 2);
|
||||
padding-bottom: map-get($spacers, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.625rem;
|
||||
|
||||
& > span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-parts {
|
||||
.question-text {
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
|
||||
& > span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-parts {
|
||||
.question-text {
|
||||
span {
|
||||
font-size: .875rem;
|
||||
text-align: left;
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
}
|
||||
.question-text {
|
||||
margin: 0;
|
||||
}
|
||||
fieldset {
|
||||
.ui-radio {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
fieldset {
|
||||
.ui-radio {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,140 @@
|
|||
<template>
|
||||
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
|
||||
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
aria-checked="false"
|
||||
:name="checkboxName"
|
||||
:id="buttonID"
|
||||
:tabindex="tabIndex"
|
||||
:aria-required="isRequired" />
|
||||
<label class="d-flex align-items-start" :for="buttonID">
|
||||
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
:aria-disabled="isDisabled"
|
||||
class="btn d-flex align-items-center py-3 px-4 delay"
|
||||
:class="[
|
||||
isPrimary ? 'btn-primary' : 'btn-secondary',
|
||||
isFloat ? 'float-end' : '',
|
||||
isLoaderDisplayed ? 'has-loader' : '',
|
||||
]"
|
||||
@click="clicked">
|
||||
<span class="m-0">{{ this.buttonText }}</span>
|
||||
<loader
|
||||
class="ms-2"
|
||||
v-if="isLoaderDisplayed"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
|
||||
export default {
|
||||
name: "checkbox",
|
||||
name: "buttonMain",
|
||||
props: {
|
||||
checkboxName: String,
|
||||
buttonID: String,
|
||||
tabIndex: Number,
|
||||
checkboxLabel: String,
|
||||
screenReaderOnlyText: String,
|
||||
isRequired: Boolean,
|
||||
hasError: Boolean,
|
||||
isPrimary: Boolean,
|
||||
buttonText: String,
|
||||
isDisabled: Boolean,
|
||||
loaderColor: String,
|
||||
loaderPosition: String,
|
||||
isFloat: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
removeLoader() {
|
||||
this.isLoaderDisplayed = false;
|
||||
},
|
||||
clicked() {
|
||||
if (!this.isDisabled) {
|
||||
this.isLoaderDisplayed = true;
|
||||
this.$emit("click-event");
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-check {
|
||||
.form-check-input {
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: 2px;
|
||||
&:checked {
|
||||
background-size: 125%;
|
||||
border: 1px solid $blue;
|
||||
+ label {
|
||||
p {
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
<style lang="scss">
|
||||
.btn {
|
||||
&.btn-primary {
|
||||
position: relative;
|
||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
color: $white;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
@media (hover: hover) {
|
||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
}
|
||||
&:focus, // Mouse, touch, stylus focus
|
||||
&:focus-visible {
|
||||
// Keyboard focus for accessibility
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
color: $white;
|
||||
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
|
||||
}
|
||||
&:disabled {
|
||||
background: $gray-200 !important;
|
||||
background: linear-gradient(270deg, $gray-200 0%, $gray-200 100%) !important;
|
||||
color: $gray-600 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
&.has-loader {
|
||||
color: $white;
|
||||
background: $blue-700;
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
}
|
||||
&.delay {
|
||||
// fixes flicker while transitioning between states
|
||||
transition: background 0s 0s ease-in-out;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.form-check-input {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
&.btn-secondary {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 1px solid $blue;
|
||||
border-radius: $border-radius-lg;
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
&:hover {
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
}
|
||||
&:focus, // Mouse, touch, stylus focus
|
||||
&:focus-visible {
|
||||
// Keyboard focus for accessibility
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
}
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
color: $gray-550 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: 1px solid $gray-550;
|
||||
border-radius: $border-radius-lg;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
&.has-loader {
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
}
|
||||
&.delay {
|
||||
// fixes flicker while transitioning between states
|
||||
transition: background 0s 0s ease-in-out;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,206 +1,84 @@
|
|||
<template>
|
||||
<div
|
||||
class="list-group list-button-horizontal d-flex flex-column w-100"
|
||||
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
||||
@keyup.space="triggerButton()"
|
||||
@keyup.up="handleKeyupArrow()"
|
||||
@keyup.down="handleKeyupArrow()"
|
||||
@keyup.left="handleKeyupArrow()"
|
||||
@keyup.right="handleKeyupArrow()"
|
||||
>
|
||||
<input
|
||||
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
||||
:id="buttonID"
|
||||
:name="groupName"
|
||||
:aria-required="isRequired"
|
||||
v-model="checkValue"
|
||||
:checked="checkValue"
|
||||
@change="handleInputChange()"
|
||||
/>
|
||||
<label
|
||||
tabindex="-1"
|
||||
:for="buttonID"
|
||||
:aria-label="buttonLabel"
|
||||
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||
@mouseup="triggerButton()"
|
||||
>
|
||||
<span
|
||||
class="m-0"
|
||||
:class="textPosition"
|
||||
>
|
||||
{{buttonLabel}}
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</span>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
<loader v-if="isLoaderDisplayed && selectingInitiatesLoad" :class="[loaderColor, loaderPosition]" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useField } from "vee-validate";
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
import { toRef } from "vue";
|
||||
|
||||
export default {
|
||||
name: "listButtonHorizontal",
|
||||
props: {
|
||||
isMultiSelect: Boolean,
|
||||
groupName: String,
|
||||
buttonID: String,
|
||||
buttonLabel: String,
|
||||
buttonLabelSubCopy: String,
|
||||
screenReaderOnlyText: String,
|
||||
textPosition: String,
|
||||
selectingInitiatesLoad: Boolean,
|
||||
loaderColor: String,
|
||||
loaderPosition: String,
|
||||
isRequired: Boolean,
|
||||
isCashOrInsurance: Boolean,
|
||||
value: {
|
||||
// Field initial value
|
||||
type: String,
|
||||
default: "",
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
:buttonWrapperClasses="[
|
||||
'list-group list-button-horizontal d-flex flex-column w-100 base-input-button',
|
||||
{ strong: isStrongStyling },
|
||||
]"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
class="button-content list-button-horizontal-content d-flex flex-column justify-content-center p-3">
|
||||
<span class="m-0" :class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</span>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "listButtonHorizontal",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
computed: {
|
||||
isStrongStyling() {
|
||||
return this.additionalButtonStyling === "listButtonHorizontalStrong";
|
||||
},
|
||||
validationRules: String,
|
||||
selectedValues: [Array, String],
|
||||
hasError: Boolean,
|
||||
valueToLogType: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
checkValue: Boolean,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (Array.isArray(this.selectedValues)) {
|
||||
this.checkValue = this.isMultiSelect
|
||||
? this.selectedValues.includes(this.value)
|
||||
: this.selectedValues[0] == this.value;
|
||||
}
|
||||
else {
|
||||
this.checkValue = this.selectedValues === this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isValueSelectedByArray(arr) {
|
||||
return this.isMultiSelect
|
||||
? arr.includes(this.value)
|
||||
: arr[0];
|
||||
},
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
},
|
||||
handleInputChange() {
|
||||
if (!this.selectingInitiatesLoad) {
|
||||
this.handleCheckChange();
|
||||
}
|
||||
},
|
||||
handleKeyupArrow() {
|
||||
if (this.isMultiSelect) {
|
||||
return; // Prevent arrow keys from doing anything if element is a checkbox
|
||||
}
|
||||
|
||||
if (!this.selectingInitiatesLoad) {
|
||||
this.handleCheckChange();
|
||||
}
|
||||
},
|
||||
triggerButton() {
|
||||
if (this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
this.handleCheckChange();
|
||||
}
|
||||
|
||||
},
|
||||
handleCheckChange() {
|
||||
const emitEvent = {
|
||||
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
|
||||
value: this.value,
|
||||
buttonId: this.buttonID && this.buttonID.toString(),
|
||||
};
|
||||
|
||||
this.handleChange(this.value);
|
||||
this.$emit("isCheckedChanged", emitEvent);
|
||||
this.$emit("update:modelValue", emitEvent);
|
||||
}
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
},
|
||||
setup(props) {
|
||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||
|
||||
const fieldOptions = {
|
||||
type: inputType,
|
||||
checkedValue: props.value,
|
||||
potentialInitialValue: props.selectedValues,
|
||||
};
|
||||
|
||||
// Set initialValue for validation setup if pre-selected
|
||||
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
|
||||
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
|
||||
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
|
||||
}
|
||||
|
||||
const {
|
||||
handleChange,
|
||||
errors,
|
||||
value
|
||||
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
|
||||
|
||||
const validateValue = value;
|
||||
return {
|
||||
handleChange,
|
||||
errors,
|
||||
validateValue,
|
||||
fieldOptions, // only need to expose this for unit test purposes
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-button-horizontal {
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
},
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-button-horizontal {
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
|
||||
&:focus-visible+label {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
z-index: 2;
|
||||
|
||||
.list-button-horizontal-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:focus+label {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
z-index: 3;
|
||||
|
||||
&:focus-visible + .list-button-horizontal-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:checked+label {
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
outline: none;
|
||||
z-index: 2;
|
||||
|
||||
&:focus + .list-button-horizontal-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:checked:focus+label {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
|
||||
&:checked + .list-button-horizontal-content {
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
outline: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:checked+label p:first-child {
|
||||
font-weight: 500;
|
||||
|
||||
&:checked:focus + .list-button-horizontal-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
|
||||
&:checked + .list-button-horizontal-content p:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.list-button-horizontal-content {
|
||||
outline: none;
|
||||
position: relative;
|
||||
background: $white;
|
||||
|
|
@ -209,136 +87,132 @@
|
|||
border-radius: 0;
|
||||
width: 100%;
|
||||
color: $gray-600;
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
cursor: pointer;
|
||||
z-index: 4 !important;
|
||||
}
|
||||
}
|
||||
|
||||
+p {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
font-size: .875rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Cash/Insurance option radio button styling
|
||||
&.radio-fancy {
|
||||
label {
|
||||
border: 1px solid $blue-700;
|
||||
z-index: 2;
|
||||
color: $blue;
|
||||
|
||||
span {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// Cash/Insurance option radio button styling
|
||||
&.strong {
|
||||
.list-button-horizontal-content {
|
||||
border: 1px solid $blue-700;
|
||||
z-index: 2;
|
||||
color: $blue;
|
||||
|
||||
span {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
label:hover {
|
||||
background-color: $blue-700;
|
||||
color: $white;
|
||||
box-shadow: none;
|
||||
|
||||
.list-button-horizontal-content:hover {
|
||||
background-color: $blue-700;
|
||||
color: $white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
|
||||
&:focus-visible+label {
|
||||
border-radius: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:focus+label {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:checked+label {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
background: linear-gradient(84.45deg, #125B7E 0%, #3B8FB8 100%);
|
||||
border-radius: 0.5rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&:checked:focus+label {
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
}
|
||||
|
||||
&:checked+label p:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
position: absolute;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
|
||||
&:focus-visible + .list-button-horizontal-content {
|
||||
border-radius: 0.5rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&:focus + .list-button-horizontal-content {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:checked + .list-button-horizontal-content {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
background: linear-gradient(84.45deg, #125b7e 0%, #3b8fb8 100%);
|
||||
border-radius: 0.5rem;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&:checked:focus + .list-button-horizontal-content {
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
}
|
||||
|
||||
&:checked + .list-button-horizontal-content p:first-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.list-button-horizontal {
|
||||
}
|
||||
&.list-button-horizontal {
|
||||
height: 100%;
|
||||
label {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
&:first-of-type {
|
||||
}
|
||||
|
||||
.col,
|
||||
.list-group {
|
||||
border-radius: 0;
|
||||
|
||||
&:first-of-type {
|
||||
.list-button-horizontal {
|
||||
label {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
.list-button-horizontal {
|
||||
label {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.list-button-horizontal-content {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Cash/insurance styling
|
||||
&:first-of-type {
|
||||
.list-button-horizontal.radio-fancy {
|
||||
input[type="radio"] {
|
||||
&:checked+label {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
&:checked:focus+label {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
.list-button-horizontal.radio-fancy {
|
||||
input[type="radio"] {
|
||||
&:checked+label {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
&:checked:focus+label {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
//Cash/insurance styling
|
||||
&:first-of-type {
|
||||
.list-button-horizontal.strong {
|
||||
input[type="radio"] {
|
||||
&:checked + .list-button-horizontal-content {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
&:checked:focus + .list-button-horizontal-content {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
.list-button-horizontal.strong {
|
||||
input[type="radio"] {
|
||||
&:checked + .list-button-horizontal-content {
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
||||
&:checked:focus + .list-button-horizontal-content {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
border-top-left-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,241 +1,114 @@
|
|||
<template>
|
||||
<div
|
||||
class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
||||
@keyup.space="triggerButton"
|
||||
@keyup.enter="triggerButton"
|
||||
@keyup.up="handleKeyupArrow"
|
||||
@keyup.down="handleKeyupArrow"
|
||||
@keyup.left="handleKeyupArrow"
|
||||
@keyup.right="handleKeyupArrow">
|
||||
<input
|
||||
:type="isMultiSelect ? 'checkbox' : 'radio'"
|
||||
:id="buttonID"
|
||||
:name="groupName"
|
||||
:value="value"
|
||||
:aria-required="isRequired"
|
||||
v-model="checkValue"
|
||||
:checked="checkValue"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<label
|
||||
tabindex="-1"
|
||||
:for="buttonID"
|
||||
:aria-label="buttonLabel"
|
||||
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||
@mouseup="triggerButton"
|
||||
>
|
||||
<span
|
||||
class="m-0"
|
||||
:class="textPosition"
|
||||
>
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<span
|
||||
v-if="buttonLabelSubCopy"
|
||||
class="m-0 small"
|
||||
:class="textPosition"
|
||||
>
|
||||
{{ buttonLabelSubCopy }}
|
||||
</span>
|
||||
<span
|
||||
v-if="screenReaderOnlyText"
|
||||
class="sr-only"
|
||||
>
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||
:class="[this.loaderColor, this.loaderPosition]"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useField } from "vee-validate";
|
||||
import { toRef } from "vue";
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
|
||||
export default {
|
||||
name: "listButton",
|
||||
props: {
|
||||
isMultiSelect: Boolean,
|
||||
groupName: String,
|
||||
buttonLabel: [Number, String],
|
||||
buttonID: [Number, String],
|
||||
isRequired: Boolean,
|
||||
textPosition: String,
|
||||
buttonLabelSubCopy: String,
|
||||
screenReaderOnlyText: String,
|
||||
selectingInitiatesLoad: Boolean,
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
|
||||
<span class="m-0" :class="textPosition">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</span>
|
||||
<span v-if="screenReaderOnlyText" class="sr-only">
|
||||
{{ screenReaderOnlyText }}
|
||||
</span>
|
||||
<loader
|
||||
v-if="isLoaderDisplayed && selectingInitiatesLoad"
|
||||
:class="[this.loaderColor, this.loaderPosition]" />
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
import baseInputButton from "@/common-components/base-input-button/base-input-button";
|
||||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "listButton",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
props: {
|
||||
loaderColor: String,
|
||||
loaderPosition: String,
|
||||
value: {
|
||||
// Field initial value
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
|
||||
validationRules: String,
|
||||
selectedValues: [Array, String],
|
||||
hasError: Boolean,
|
||||
valueToLogType: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
checkValue: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (Array.isArray(this.validateValue)) {
|
||||
this.checkValue = this.isValueSelectedByArray(this.selectedValues);
|
||||
const isSelectedByValidator = this.isValueSelectedByArray(this.validateValue);
|
||||
|
||||
if (this.checkValue != isSelectedByValidator) {
|
||||
this.handleChange(this.value);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.checkValue = this.selectedValues == this.value;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isValueSelectedByArray(arr) {
|
||||
return this.isMultiSelect
|
||||
? arr.includes(this.value)
|
||||
: arr[0];
|
||||
loaderPosition: {
|
||||
type: String,
|
||||
default: "right",
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
displayLoader() {
|
||||
this.isLoaderDisplayed = true;
|
||||
this.isLoaderDisplayed = true;
|
||||
},
|
||||
handleInputChange() {
|
||||
if(!this.selectingInitiatesLoad) {
|
||||
this.handleCheckChange();
|
||||
}
|
||||
preHandleAnswerChange() {
|
||||
if (this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
}
|
||||
},
|
||||
handleKeyupArrow() {
|
||||
if (this.isMultiSelect) {
|
||||
return; // Prevent arrow keys from doing anything if element is a checkbox
|
||||
}
|
||||
},
|
||||
triggerButton() {
|
||||
if(this.selectingInitiatesLoad) {
|
||||
this.displayLoader();
|
||||
this.handleCheckChange();
|
||||
}
|
||||
},
|
||||
handleCheckChange() {
|
||||
const emitEvent = {
|
||||
checkValue: this.checkValue, // only read on checkboxes, on handleCheckedChanged on button-question
|
||||
value: this.value.toString(),
|
||||
buttonId: this.buttonID && this.buttonID.toString(),
|
||||
};
|
||||
|
||||
this.handleChange(this.value);
|
||||
this.$emit("isCheckedChanged", emitEvent);
|
||||
this.$emit("update:modelValue", emitEvent);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
},
|
||||
setup(props) {
|
||||
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||
|
||||
const fieldOptions = {
|
||||
type: inputType,
|
||||
checkedValue: props.value,
|
||||
potentialInitialValue: props.selectedValues,
|
||||
};
|
||||
|
||||
// Set initialValue for validation setup if pre-selected
|
||||
// NOTE: props.selectedValues could be an array of strings, or an array of integers...
|
||||
if (props.selectedValues && (props.selectedValues.includes(props.value) || props.selectedValues.includes(parseInt(props.value)))) {
|
||||
fieldOptions['initialValue'] = fieldOptions.potentialInitialValue;
|
||||
baseInputButton,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loader {
|
||||
position: absolute;
|
||||
}
|
||||
.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
|
||||
&:focus-visible + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
|
||||
const {
|
||||
handleChange,
|
||||
errors,
|
||||
value
|
||||
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
|
||||
|
||||
const validateValue = value;
|
||||
|
||||
return {
|
||||
handleChange,
|
||||
errors,
|
||||
validateValue,
|
||||
fieldOptions, // only need to expose this for unit test purposes
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-group {
|
||||
&.list-button {
|
||||
outline: none;
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
position: static; //override bootstrap
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&:focus-visible + label {
|
||||
&:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:focus + label {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + label {
|
||||
}
|
||||
&:checked + .list-button-content {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
}
|
||||
&:checked:focus + label {
|
||||
}
|
||||
&:checked:focus + .list-button-content {
|
||||
box-shadow: 0 0 0 2.5px $blue;
|
||||
}
|
||||
&:checked + label p,
|
||||
&:checked + label span {
|
||||
}
|
||||
&:checked + .list-button-content p,
|
||||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + label span:nth-child(2) {
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
label {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span {
|
||||
&.small {
|
||||
font-size: .75rem;
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0 0 0 4px $blue-300;
|
||||
}
|
||||
cursor: pointer;
|
||||
}
|
||||
+ p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
}
|
||||
.list-button-content {
|
||||
color: $gray-600;
|
||||
position: relative;
|
||||
background: $white;
|
||||
transition: all 150ms linear;
|
||||
border-radius: $border-radius-lg;
|
||||
border: 1px solid $gray-500;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
span {
|
||||
&.small {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,33 @@
|
|||
<template>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
:buttonWrapperClasses="[
|
||||
'list-card w-100 rounded-3 d-flex align-items-center h-100',
|
||||
{ horizontal: isWide },
|
||||
]"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
class="d-flex w-100 align-items-center px-2 h-100 list-card-content button-content rounded-3"
|
||||
:class="labelClasses">
|
||||
<img
|
||||
:id="buttonImageId"
|
||||
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
|
||||
:src="buttonImage"
|
||||
:alt="altText" />
|
||||
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'">
|
||||
{{ buttonLabel }}
|
||||
</p>
|
||||
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</p>
|
||||
<div v-if="isWide" class="order-2">
|
||||
<p class="m-0 small">{{ buttonLabel }}</p>
|
||||
<p v-if="buttonLabelSubCopy" class="m-0 fs-7 sub-copy">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
<baseInputButton
|
||||
v-bind="$props"
|
||||
:buttonWrapperClasses="[
|
||||
'list-card w-100 rounded-3 d-flex align-items-center h-100 base-input-button',
|
||||
{ horizontal: isWide },
|
||||
]"
|
||||
v-model="selectedValue">
|
||||
<div
|
||||
class="d-flex w-100 align-items-center px-2 h-100 list-card-content button-content rounded-3"
|
||||
:class="labelClasses">
|
||||
<img
|
||||
:id="buttonImageId"
|
||||
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
|
||||
:src="buttonImage"
|
||||
:alt="altText" />
|
||||
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'">
|
||||
{{ buttonLabel }}
|
||||
</p>
|
||||
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</p>
|
||||
<div v-if="isWide" class="order-2">
|
||||
<p class="m-0 small">{{ buttonLabel }}</p>
|
||||
<p v-if="buttonLabelSubCopy" class="m-0 fs-7 sub-copy">
|
||||
{{ buttonLabelSubCopy }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</baseInputButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -35,220 +35,209 @@ import baseInputButton from "@/common-components/base-input-button/base-input-bu
|
|||
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
|
||||
|
||||
export default {
|
||||
name: "listCard",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
computed: {
|
||||
labelClasses() {
|
||||
if (this.isWide) {
|
||||
let classes = "flex-row py-2 ps-4 pe-4";
|
||||
if (this.buttonLabelSubCopy) {
|
||||
classes += " checkboxTop";
|
||||
}
|
||||
return classes;
|
||||
} else {
|
||||
return "flex-column pt-4 pb-3";
|
||||
}
|
||||
},
|
||||
},
|
||||
name: "listCard",
|
||||
mixins: [inputButtonWrapperMixin],
|
||||
components: {
|
||||
baseInputButton,
|
||||
},
|
||||
computed: {
|
||||
labelClasses() {
|
||||
if (this.isWide) {
|
||||
let classes = "flex-row py-2 ps-4 pe-4";
|
||||
if (this.buttonLabelSubCopy) {
|
||||
classes += " checkboxTop";
|
||||
}
|
||||
return classes;
|
||||
} else {
|
||||
return "flex-column pt-4 pb-3";
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@mixin list-card-focus($box-shadow-color) {
|
||||
&:focus-visible + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:focus + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:checked:focus + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
}
|
||||
&:focus-visible + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:focus + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:checked:focus + .list-card-content {
|
||||
box-shadow: 0 0 0 2.5px $box-shadow-color;
|
||||
}
|
||||
}
|
||||
.list-card {
|
||||
border: 1px solid $gray-500;
|
||||
border: 1px solid $gray-500;
|
||||
|
||||
&.has-error {
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
@include list-card-focus($red);
|
||||
}
|
||||
}
|
||||
&.has-error {
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
@include list-card-focus($red);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
|
||||
height: auto;
|
||||
width: 6.5rem;
|
||||
margin-bottom: 2.2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
img {
|
||||
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
|
||||
height: auto;
|
||||
width: 6.5rem;
|
||||
margin-bottom: 2.2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include media-breakpoint-up(sm) {
|
||||
box-shadow: 0px 0px 0px 4px $blue-300;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
position: absolute;
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
position: absolute;
|
||||
+ .list-card-content {
|
||||
outline: none;
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
+ .list-card-content {
|
||||
outline: none;
|
||||
display: block;
|
||||
position: relative;
|
||||
p {
|
||||
color: $gray-600;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.sub-copy {
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-600;
|
||||
text-align: center;
|
||||
&:checked + .list-card-content {
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
&.sub-copy {
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include list-card-focus($blue);
|
||||
|
||||
&:checked + .list-card-content {
|
||||
background: $blue-100;
|
||||
box-shadow: 0 0 0 1px $blue;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
&:checked + .list-card-content {
|
||||
p {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@include list-card-focus($blue);
|
||||
.sub-copy {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + .list-card-content {
|
||||
p {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 3rem 0 0 0;
|
||||
background: white;
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: 2px;
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
color: $gray-600;
|
||||
}
|
||||
|
||||
.sub-copy {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
+ .list-card-content.checkboxTop::before {
|
||||
margin: -1.25rem 0.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 3rem 0 0 0;
|
||||
background: white;
|
||||
border: 1px solid $gray-500;
|
||||
border-radius: 2px;
|
||||
order: 2;
|
||||
flex-shrink: 0;
|
||||
color: $gray-600;
|
||||
}
|
||||
+ .list-card-content.checkboxTop::after {
|
||||
margin: -1.5rem 0.5rem 0 0 !important;
|
||||
}
|
||||
|
||||
+ .list-card-content.checkboxTop::before {
|
||||
margin: -1.25rem 0.5rem 0 0 !important;
|
||||
}
|
||||
&:checked + .list-card-content::before {
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
+ .list-card-content.checkboxTop::after {
|
||||
margin: -1.5rem 0.5rem 0 0 !important;
|
||||
}
|
||||
&:checked + .list-card-content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: 3.2rem 0 0 0;
|
||||
border-left: 2px solid $white;
|
||||
border-bottom: 2px solid $white;
|
||||
height: 6px;
|
||||
width: 11px;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + .list-card-content::before {
|
||||
background: $blue;
|
||||
}
|
||||
input[type="radio"] {
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:checked + .list-card-content::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: 3.2rem 0 0 0;
|
||||
border-left: 2px solid $white;
|
||||
border-bottom: 2px solid $white;
|
||||
height: 6px;
|
||||
width: 11px;
|
||||
transform: rotate(-45deg);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
+ .list-card-content::after {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
+ .list-card-content {
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ .list-card-content::after {
|
||||
content: "";
|
||||
display: none;
|
||||
}
|
||||
&.horizontal {
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
width: 5.5rem;
|
||||
}
|
||||
|
||||
+ .list-card-content {
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
position: relative;
|
||||
margin: 0 0.5rem 0 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
width: 5.5rem;
|
||||
}
|
||||
&:checked + .list-card-content::after {
|
||||
content: "";
|
||||
margin: -0.15rem 0 0 0;
|
||||
left: 1.175rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
+ .list-card-content::before {
|
||||
content: "";
|
||||
position: relative;
|
||||
margin: 0 0.5rem 0 0;
|
||||
order: 1;
|
||||
}
|
||||
&:checked + .list-card-content {
|
||||
p {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:checked + .list-card-content::after {
|
||||
content: "";
|
||||
margin: -0.15rem 0 0 0;
|
||||
left: 1.175rem;
|
||||
}
|
||||
.sub-copy {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&:checked + .list-card-content {
|
||||
p {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
+ .list-card-content {
|
||||
outline: none;
|
||||
min-height: 48px;
|
||||
color: $gray-600;
|
||||
|
||||
.sub-copy {
|
||||
color: $gray-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
+ .list-card-content {
|
||||
outline: none;
|
||||
min-height: 48px;
|
||||
color: $gray-600;
|
||||
p {
|
||||
color: $gray-600;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-600;
|
||||
text-align: left;
|
||||
|
||||
&.sub-copy {
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.sub-copy {
|
||||
color: $gray-550;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
<template>
|
||||
<div
|
||||
<div
|
||||
class="loader"
|
||||
role="alert"
|
||||
aria-label="Loading new page"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loader",
|
||||
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||
props: {
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "loader",
|
||||
/* Specify size in number value which translates to rem value. For example, 1.5 = 1.5rem = 24px */
|
||||
props: {
|
||||
/* Color options: red, green, blue, white, black */
|
||||
loaderColor: {
|
||||
type: String,
|
||||
type: String,
|
||||
},
|
||||
/* Position options: center, right, left (OPTIONAL, do NOT use on btn-* classes) */
|
||||
loaderPosition: {
|
||||
type: String,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loader {
|
||||
display: flex;
|
||||
//Open an overlay to prevent page interaction
|
||||
&:before {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loader {
|
||||
display: flex;
|
||||
|
||||
//Open an overlay to prevent page interaction
|
||||
&:before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -38,9 +38,9 @@
|
|||
background-color: transparent;
|
||||
z-index: 9999;
|
||||
cursor: default;
|
||||
}
|
||||
//Spinner basics
|
||||
&:after {
|
||||
}
|
||||
//Spinner basics
|
||||
&:after {
|
||||
content: "";
|
||||
mask: url(../../assets/icons/spinner.svg);
|
||||
mask-size: cover;
|
||||
|
|
@ -49,42 +49,38 @@
|
|||
height: 1rem;
|
||||
animation: rotation 1s infinite linear;
|
||||
@keyframes rotation {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
//Spinner position
|
||||
&.center {
|
||||
position: absolute;
|
||||
}
|
||||
//Spinner position
|
||||
&.center {
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
&.right {
|
||||
position: absolute;
|
||||
}
|
||||
&.right {
|
||||
right: 1rem;
|
||||
}
|
||||
&.left {
|
||||
position: absolute;
|
||||
}
|
||||
&.left {
|
||||
left: 1rem;
|
||||
}
|
||||
//Spinner color
|
||||
&:after {
|
||||
}
|
||||
//Spinner color
|
||||
&:after {
|
||||
//Default spinner color (blue) if no other color is specified from the options below
|
||||
background-color: $blue;
|
||||
}
|
||||
&.red:after {
|
||||
background-color: $red;
|
||||
}
|
||||
&.green:after {
|
||||
background-color: $green;
|
||||
}
|
||||
&.white:after {
|
||||
background-color: $white;
|
||||
}
|
||||
&.black:after {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
&.red:after {
|
||||
background-color: $red;
|
||||
}
|
||||
&.green:after {
|
||||
background-color: $green;
|
||||
}
|
||||
&.white:after {
|
||||
background-color: $white;
|
||||
}
|
||||
&.black:after {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue