-
-
-
-
Vehicle Year Placeholder
-
-
{{returnVehicleYear}}
-
-
+
-
-
-
-
-
-
-
\ No newline at end of file
+ ];
+
+ let resultMap = await settleAllPromises(promiseResultMap);
+
+ // Call the "next" function to complete the transition to this page.
+ next((vm) => {
+ vm.setCmsContent(resultMap.cmsContent);
+ vm.$refs.yearQuestion.initializeComponent(
+ resultMap.yearQuestionInitialData
+ );
+ });
+ },
+
+ components: {
+ yearQuestion,
+ siteHeader,
+ siteSubHeader,
+ },
+ };
+
+
\ No newline at end of file
diff --git a/src/layouts/vehicle-year/year-question/year-question.vue b/src/layouts/vehicle-year/year-question/year-question.vue
new file mode 100644
index 00000000..0cd35425
--- /dev/null
+++ b/src/layouts/vehicle-year/year-question/year-question.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/store/index.js b/src/store/index.js
index 56668172..25e1c8b1 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -74,6 +74,15 @@ export const useMainStore = defineStore({
});
},
+ // Vehicle API Actions
+ getVehicleYears() {
+ return globalMethods.callHttpClient({
+ method: endpoints.GetVehicleYears.method,
+ endpoint: endpoints.GetVehicleYears.url,
+ payload: {},
+ });
+ },
+
// Vehicle API Actions
updateVehicleYear(year)
{
diff --git a/src/styles/common-error-styles.scss b/src/styles/common-error-styles.scss
new file mode 100644
index 00000000..def634dc
--- /dev/null
+++ b/src/styles/common-error-styles.scss
@@ -0,0 +1,151 @@
+html {
+ .has-error {
+ &.list-button,
+ &.list-card,
+ &.list-card.list-button {
+ border: none;
+ color: $red;
+ input[type=checkbox]:focus + label,
+ input[type=radio]:focus + label {
+ box-shadow: 0 0 0 2.5px $red;
+ }
+ input[type=checkbox]:checked + label {
+ box-shadow: 0 0 0 1px $red;
+ }
+ &:hover {
+ box-shadow: 0px 0px 0px 4px $red-200;
+ border-radius: .5rem;
+ }
+ label {
+ border: 1px solid $red;
+ border-radius: .5rem;
+ }
+ label:hover {
+ box-shadow: 0px 0px 0px 4px $red-200;
+ border-radius: 10px;
+ border: 1px solid $red;
+ }
+ }
+ &.list-button-horizontal {
+ color: $red;
+ label {
+ border: 1px solid $red;
+ &:hover {
+ box-shadow: 0px 0px 0px 4px $red-200;
+ }
+ }
+ input[type=checkbox]:focus + label,
+ input[type=radio]:focus + label {
+ box-shadow: 0 0 1px $red;
+ }
+ }
+ &.ui-radio,
+ &.ui-checkbox {
+ input[type=checkbox],
+ input[type=radio],
+ input[type=radio]+label:before,
+ input[type=checkbox]+label:before {
+ border: 1px solid $red;
+ background-color: initial;
+ }
+ input[type=checkbox]:checked + label:before {
+ border: 1px solid $blue;
+ }
+ }
+ &.textbox-question,
+ &.dropdown-question {
+ input:hover {
+ box-shadow: 0px 0px 0px 4px $red-200;
+ border-radius: .5rem;
+ border: 1px solid $red;
+ }
+ input:focus {
+ box-shadow: 0 0 0 2.5px $red;
+ }
+ p {
+ color: $red;
+ }
+ input,
+ select {
+ border: 1px solid transparent;
+ box-shadow: 0 0 0 1px $red;
+ &:focus {
+ border: 1px solid transparent;
+ }
+ }
+ select {
+ background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 8.89' xml:space='preserve'%3e%3cpath d='M8 8.89c-.24 0-.46-.09-.63-.26L.26 1.53a.901.901 0 0 1 0-1.27C.43.1.66 0 .9 0s.47.1.64.26L8 6.74 14.47.27c.17-.17.4-.27.64-.27s.47.1.63.27c.17.17.26.4.26.64s-.1.47-.27.63l-7.1 7.09a.86.86 0 0 1-.63.26z' fill='%23d4281c'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 16px 12px;
+ }
+ }
+ }
+ //Restore to default style if alert box is present
+ .alertError {
+ .has-error {
+ &.list-button,
+ &.list-card {
+ border: 1px solid $gray-500;
+ input:not(:focus) {
+ + label {
+ border: none;
+ box-shadow: 0 0 0 1px $gray-500;
+ border-radius: .5rem;
+ }
+ }
+ input:checked:focus {
+ + label {
+ border: none;
+ box-shadow: 0 0 0 2.5px $blue;
+ border-radius: .5rem;
+ }
+ }
+ input:checked:not(:focus) {
+ + label {
+ box-shadow: 0 0 0 1px $blue;
+ border-radius: .5rem;
+ }
+ }
+ input:focus {
+ border: 1px solid $gray-500;
+ + label {
+ box-shadow: 0 0 0 2.5px transparent;
+ }
+ }
+ &:hover {
+ box-shadow: 0 0 0 4px $blue-300;
+ + label {
+ box-shadow: 0 0 0 2.5px transparent;
+ border: 1px solid $blue;
+ }
+ }
+ }
+ }
+ }
+
+ .form-test-error {
+ color: $red;
+ font-size: .875rem;
+ font-weight: 500;
+ }
+
+ .form-test-invalid {
+ &.btn.btn-primary {
+ color: $gray-600;
+ background: $gray-200;
+ cursor: pointer;
+ pointer-events: all;
+ font-weight: $font-weight-normal;
+ }
+ &.btn.btn-primary:hover {
+ background: $gray-200;
+ box-shadow: none;
+ }
+ &.btn.btn-primary:focus,
+ &.btn.btn-primary:focus-visible {
+ box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
+ }
+ }
+ }
+
\ No newline at end of file
diff --git a/src/styles/common-styles.scss b/src/styles/common-styles.scss
new file mode 100644
index 00000000..a516165f
--- /dev/null
+++ b/src/styles/common-styles.scss
@@ -0,0 +1,60 @@
+// Common/Global Styles
+// Use this file for global styles that don't or won't have their own stylesheet
+body {
+ font-size: 16px;
+ background-color: #fff;
+ color: #4D5151;
+ .container-fluid {
+ max-width: 576px; //Remove once desktop app is complete
+ &.container-shadow {
+ box-shadow: 0px 0px 6px 0px rgba(0,0,0,0.15); //Use instead of Bootstrap's helper
+ }
+ &.make-tall {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ }
+ .prevent-squish{
+ overflow-x: unset;
+ }
+ }
+ .pointer {
+ cursor: pointer;
+ }
+ .container,
+ .container-fluid {
+ overflow-x: hidden;
+ }
+
+ .sub-container{
+ &.make-tall {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ }
+
+ .sr-only {
+ position: absolute;
+ left: -10000px;
+ top: auto;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ }
+
+ .page-container-grouped-styles {
+ @extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5;
+ }
+
+ //Footer modal backdrop adjustments for positioning
+ .modal-backdrop {
+ left: 50%;
+ transform: translateX(-50%);
+ max-width: 576px;
+ height: calc(100% - 72px);
+ }
+
+ }
+
\ No newline at end of file
diff --git a/src/styles/common-typography-styles.scss b/src/styles/common-typography-styles.scss
new file mode 100644
index 00000000..9ee9aa28
--- /dev/null
+++ b/src/styles/common-typography-styles.scss
@@ -0,0 +1,67 @@
+//Typography
+p,
+body {
+ font-size: 1rem;
+ line-height: 1.625;
+ font-weight: 400;
+}
+
+//Headings
+//add helper fw-bold to any element to get bold style (500)
+h1,.h1 {
+ line-height: 1.325;
+ font-weight: 300;
+}
+h2,.h2 {
+ line-height: 1.325;
+ font-weight: 300;
+}
+h3,.h3 {
+ line-height: 1.375;
+ font-weight: 300;
+}
+h4,.h4 {
+ line-height: 1.6;
+ font-weight: 300;
+}
+h5,.h5 {
+ line-height: 1.325;
+ font-weight: 400;
+}
+h6,.h6 {
+ line-height: 1.7;
+ letter-spacing: .75px;
+ text-transform: uppercase;
+}
+
+.small {
+ font-size: .875rem;
+ line-height: 1.7;
+ font-weight: 400;
+}
+
+label,
+.label {
+ font-size: 1rem;
+ line-height: 1.5;
+ font-weight: 400;
+}
+
+caption,
+.caption {
+ font-size: .75rem;
+ line-height: 1.7;
+ font-weight: 400;
+}
+
+
+// Font size
+.fs-5 {
+ line-height: 2;
+}
+
+.fs-6 {
+ font-size: 1rem !important;
+ line-height: 1.4;
+ font-weight: 500;
+}
\ No newline at end of file
diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss
new file mode 100644
index 00000000..52cf8916
--- /dev/null
+++ b/src/styles/ux-variables.scss
@@ -0,0 +1,186 @@
+//Colors
+
+// White/black
+$white: #FFFFFF;
+$black: #000000;
+
+// Blues
+$blue-100: #E4F1F7;// Used in theme
+$blue-200: #C1DFEE;
+$blue-300: #9FCEE6;
+$blue-400: #69ADCF;// Used in theme
+$blue-500: #3B8FB8;
+$blue: #1574A1;// Default Blue
+$blue-700: #06577C;
+$blue-800: #003D58;
+$blue-900: #002433;// Used in theme
+
+// Reds
+$red-100: #FFE6E4;
+$red-200: #FCBFBB;
+$red-300: #F89892;
+$red-400: #E65C53;
+$red: #D4281C;// Default Red
+$red-600: #AC160B;
+$red-700: #840900;
+$red-800: #5B0600;
+$red-900: #330300;
+
+// Greens
+$green-100: #E3F2EA;
+$green-200: #BCEDD4;
+$green-300: #94D7B6;
+$green-400: #5ABC8C;// Used in theme
+$green-500: #2CA168;
+$green: #0C7E47;// Default Green
+$green-700: #006A36;
+$green-800: #004F28;
+$green-900: #00331A;
+
+// Yellows
+$yellow-100: #FFF5EB;
+$yellow-200: #FFE1C6;
+$yellow-300: #FFCAA0;
+$yellow-400: #F5975D;
+$yellow: #E86421;// Default Yellow
+$yellow-600: #BB4B06;
+$yellow-700: #8E3C00;
+$yellow-800: #602D00;
+$yellow-900: #331A00;
+
+// Grays
+$gray-100: #F5F5F5;// Used in theme
+$gray-200: #E3E4E4;// Used in theme
+$gray-300: #D2D4D4;
+$gray: #B0B3B3;// Default Gray
+$gray-500: #8E9292;// Used in theme
+$gray-550: #727676;// Used in theme
+$gray-600: #4D5151;// Used in theme
+$gray-700: #303333;// Used in theme
+$gray-800: #222424;// Used in theme
+$gray-900: #181A1A;// Used in theme
+
+// Miscellaneous Colors
+$indigo: #6610f2;
+$purple: #6f42c1;
+$pink: #d63384;
+$orange: #fd7e14;
+$teal: #20c997;
+$cyan: #0dcaf0;
+
+// scss-docs-start colors-map
+$colors: (
+"blue": $blue,
+"indigo": $indigo,
+"purple": $purple,
+"pink": $pink,
+"red": $red,
+"orange": $orange,
+"yellow": $yellow,
+"green": $green,
+"teal": $teal,
+"cyan": $cyan,
+"white": $white,
+"gray": $gray,
+"gray-dark": $gray-500
+);
+
+// scss-docs-start theme-color-variables
+$primary: $blue;
+$secondary: $red;
+$success: $green;
+$info: $cyan;
+$warning: $yellow;
+$danger: $red;
+$light: $gray-100;
+$dark: $gray-500;
+
+// scss-docs-start theme-colors-map
+$theme-colors: (
+"primary": $primary,
+"secondary": $secondary,
+"success": $success,
+"info": $info,
+"warning": $warning,
+"danger": $danger,
+"light": $light,
+"dark": $dark
+);
+
+//Default font color
+$body-color: $gray-600;
+
+//Fonts
+$font-family-sans-serif: Roboto, Arial, Helvetica, sans-serif;
+$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+// stylelint-enable value-keyword-case
+$font-family-base: $font-family-sans-serif;
+$font-family-code: $font-family-monospace;
+$font-size-base: 1rem; // Assumes the browser default, typically `16px`
+
+//Custom Font size (extra small)
+$font-size-xsm: $font-size-base * .75;
+$font-sizes: (
+ 7: $font-size-xsm
+);
+
+//Font weight
+$font-weight-lighter: lighter;
+$font-weight-light: 300;
+$font-weight-normal: 400;
+$font-weight-bold: 500;
+$font-weight-bolder: bolder;
+
+//Headings
+$h1-font-size: $font-size-base * 3;
+$h2-font-size: $font-size-base * 2.625;
+$h3-font-size: $font-size-base * 2;
+$h4-font-size: $font-size-base * 1.625;
+$h5-font-size: $font-size-base * 1.25;
+$h6-font-size: $font-size-base * .875;
+
+//Border Radius
+// Helper classes are rounded, rounded-1, rounded-2, rounded-3
+$border-radius: .25rem;
+$border-radius-sm: .2rem;
+$border-radius-lg: .5rem;//Used for buttons. Can be used for other things, of course.
+$border-radius-pill: 50rem;
+
+//Spacing
+// 8 spacers available instead of the usual 5
+$spacer: 1rem;
+$spacers: (
+ 0: 0,
+ 1: $spacer * .25, /* 4px */
+ 2: $spacer * .5, /* 8px */
+ 3: $spacer * .75, /* 12px */
+ 4: $spacer * 1, /* 16px */
+ 5: $spacer * 1.5, /* 24px */
+ 6: $spacer * 2, /* 32px */
+ 7: $spacer * 2.5, /* 40px */
+ 8: $spacer * 3, /* 48px */
+);
+
+//Grid breakpoints
+$grid-breakpoints: (
+ xs: 0,
+ sm: 576px,
+ md: 838px,
+ lg: 1074px,
+ xl: 1416px
+);
+
+//Shadow
+$box-shadow: 0 .5rem 1rem rgba($black, .15);
+$box-shadow-sm: 0 .125rem .25rem rgba($black, .075);
+$box-shadow-lg: 0 1rem 3rem rgba($black, .25);//Safelite default
+$box-shadow-inset: inset 0 1px 2px rgba($black, .075);
+
+//Alerts
+$alert-bg-scale: -90%;
+$alert-border-scale: -100%;
+$alert-color-scale: 40%;
+
+//Modal animation
+$modal-fade-transform: translate(0, 0);
+$modal-backdrop-opacity: 0;
diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js b/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js
new file mode 100644
index 00000000..0d51bf57
--- /dev/null
+++ b/src/ux-components/list-button-horizontal/list-button-horizontal.spec.js
@@ -0,0 +1,264 @@
+import { shallowMount } from "@vue/test-utils";
+import listButtonHorizontal from "./list-button-horizontal";
+import { nextTick } from "vue";
+
+describe("list-button-horizontal.vue", () => {
+ it("Should return input type checkbox if isMultiSelect is true", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isMultiSelect: true,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes().type).toEqual("checkbox");
+ });
+
+ it("Should return input type radio if isMultiSelect is false or not specified", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isMultiSelect: false,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes().type).toEqual("radio");
+ });
+
+ it("Should return primary label text (buttonID)", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ buttonID: "List Card Checkbox",
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+
+ expect(label.attributes().for).toEqual("List Card Checkbox");
+ });
+
+ it("Should return screen reader text", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ screenReaderOnlyText: "Screen Reader Only Text",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("span.sr-only");
+
+ expect(paragraph.text()).toEqual("Screen Reader Only Text");
+ });
+
+ it("Should return text alignment class", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ textPosition: "text-center",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("span.m-0");
+
+ expect(paragraph.attributes("class")).toContain("text-center");
+ });
+
+ it("Should return aria-required state", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isRequired: true,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes()["aria-required"]).toEqual("true");
+ });
+
+ it("Should return loader enabled true", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+
+ const label = wrapper.find("label");
+
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.exists()).toBe(true);
+ });
+
+ it("Should return loader color", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ loaderColor: "blue",
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+
+ const label = wrapper.find("label");
+
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.attributes("class")).toContain("blue");
+ });
+
+ it("Should return loader position", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ loaderPosition: "right",
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+
+ const label = wrapper.find("label");
+
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.attributes("class")).toContain("right");
+ });
+
+ it("Should emit button value on click", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ value: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ modelValue: ["List Card Checkbox"],
+ },
+ });
+ wrapper.vm.handleCheckChange();
+ // Assert
+ expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean, buttonId: undefined, checkValue: false}]);
+ });
+
+ it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ modelValue: ["List Card Checkbox"],
+ isMultiSelect: false,
+ value: "Car-Front",
+ selectedValues: ["Car-Front"]
+ },
+ });
+ // Assert
+ expect(wrapper.vm.checkValue).toEqual(true);
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleInputChange();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+ it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ isMultiSelect: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButtonHorizontal, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ isMultiSelect: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+});
diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
new file mode 100644
index 00000000..534448b7
--- /dev/null
+++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ux-components/list-button/list-button.spec.js b/src/ux-components/list-button/list-button.spec.js
new file mode 100644
index 00000000..e54710ed
--- /dev/null
+++ b/src/ux-components/list-button/list-button.spec.js
@@ -0,0 +1,256 @@
+import { shallowMount } from "@vue/test-utils";
+import listButton from "./list-button";
+import { nextTick } from "vue";
+
+describe("list-button.vue", () => {
+ it("Should return input type checkbox if isMultiSelect is true", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isMultiSelect: true,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes().type).toEqual("checkbox");
+ });
+
+ it("Should return input type radio if isMultiSelect is false or not specified", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isMultiSelect: false,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes().type).toEqual("radio");
+ });
+
+ it("Should return primary label text (buttonID)", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ buttonID: "List Card Checkbox",
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+
+ expect(label.attributes().for).toEqual("List Card Checkbox");
+ });
+
+ it("Should return screen reader text", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ screenReaderOnlyText: "Screen Reader Only Text",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("span.sr-only");
+
+ expect(paragraph.text()).toEqual("Screen Reader Only Text");
+ });
+
+ it("Should return text alignment class", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ textPosition: "text-center",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("span.m-0");
+
+ expect(paragraph.attributes("class")).toContain("text-center");
+ });
+
+ it("Should return aria-required state", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isRequired: true,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ expect(input.attributes()["aria-required"]).toEqual("true");
+ });
+
+ it("Should return loader enabled true", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.exists()).toBe(true);
+ });
+
+ it("Should return loader color", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ loaderColor: "blue",
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.attributes("class")).toContain("blue");
+ });
+
+ it("Should return loader position", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ loaderPosition: "right",
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleCheckChange = jest.fn();
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ const loader = wrapper.find("loader-stub");
+
+ expect(loader.attributes("class")).toContain("right");
+ });
+
+ it("Should emit button value on click", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ value: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ modelValue: ["List Card Checkbox"],
+ buttonID: 'list-card-id'
+ },
+ });
+
+ wrapper.vm.handleCheckChange();
+
+ // Assert
+ expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: false, buttonId: 'list-card-id'}]);
+
+ });
+
+ it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ modelValue: ["List Card Checkbox"],
+ selectedValues: "Car-Front"
+ },
+ });
+ // Assert
+ expect(wrapper.componentVM.checkValue).toEqual(false);
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleInputChange();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+ it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ isMultiSelect: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listButton, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ isMultiSelect: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+});
diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue
new file mode 100644
index 00000000..c789060e
--- /dev/null
+++ b/src/ux-components/list-button/list-button.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ux-components/list-card/list-card.spec.js b/src/ux-components/list-card/list-card.spec.js
new file mode 100644
index 00000000..9c9e1986
--- /dev/null
+++ b/src/ux-components/list-card/list-card.spec.js
@@ -0,0 +1,329 @@
+import { shallowMount } from "@vue/test-utils";
+import listCard from "./list-card";
+import { nextTick } from "vue";
+
+describe("list-card.vue", () => {
+ it("Should return input type checkbox if isMultiSelect is true", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isMultiSelect: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "checkbox-demo-1",
+ groupName: "Checkbox 1",
+ buttonImage: "windshield-damage.svg",
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+ expect(input.attributes().type).toEqual("checkbox");
+ });
+
+ it("Should return primary label text", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("p");
+ expect(paragraph.text()).toEqual("Windshield");
+ });
+
+ it("Should return secondary (sub) label text", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ buttonLabelSubCopy: "Test",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("p:nth-of-type(2)");
+ expect(paragraph.text()).toEqual("Test");
+ });
+
+ it("Should return value used for various text settings including the label 'for' and input id", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ buttonLabelSubCopy: "Test",
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+ expect(label.attributes().for).toEqual("List Card Checkbox");
+ });
+
+ it("Should return input group name used for radio or checkbox", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ buttonLabelSubCopy: "Test",
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+ expect(input.attributes().name).toEqual("radio 1");
+ });
+
+ it("Should return aria-required state", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+ expect(input.attributes()["aria-required"]).toEqual("true");
+ });
+
+ it("Should return flex row classes if isWide is true", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: true,
+ buttonLabelSubCopy: "",
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+ expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4"]);
+ });
+
+ it("Should return flex row classes if isWide is true and checkboxTop if buttonLabelSubCopy is true", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: true,
+ buttonLabelSubCopy: "Button Subcopy",
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+ expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-row", "py-2", "ps-4", "pe-4", "checkboxTop"]);
+ });
+
+ it("Should return flex column classes if isWide is false", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ },
+ });
+
+ // Assert
+ const label = wrapper.find("label");
+ expect(label.classes()).toEqual(["d-flex", "w-100", "align-items-center", "px-2", "h-100", "flex-column", "pt-4", "pb-3"]);
+ });
+
+ it("Should emit button value on click", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ value: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ buttonID: 'list-card-id',
+ selectedValues: "List Card Checkbox"
+ },
+ });
+ wrapper.vm.handleCheckChange();
+ // Assert
+ expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: true, buttonId: 'list-card-id'}]);
+ });
+
+ it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isRadioHorizontal: true,
+ buttonLabel: "Windshield",
+ value: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ isWide: false,
+ selectedValues: "Car-Front"
+ },
+ });
+ // Assert
+ expect(wrapper.componentVM.checkValue).toEqual(false);
+ });
+
+ it("Should set an initial value for validation if selectedValues include the value", async () => {
+ // Arrange
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ value: "Windshield",
+ groupName: "radio 1",
+ selectedValues: ["Windshield"],
+ },
+ });
+
+ // Assert
+ expect(wrapper.vm.fieldOptions.initialValue).toEqual([ 'Windshield' ]);
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleInputChange();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+ it("Should do nothing if isMultiSelect is true and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ isMultiSelect: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleKeyupArrow).toHaveReturned;
+ });
+
+ it("Should run handleCheckChange if selectingInitiatesLoad is false and handleKeyupArrow is triggered", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ propsData: {
+ selectingInitiatesLoad: false,
+ isMultiSelect: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.handleKeyupArrow();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ });
+
+ it("Should run handleChange if triggerButton is triggered", async () => {
+
+ // Act
+ const wrapper = shallowMount(listCard, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ selectingInitiatesLoad: false,
+ },
+ });
+
+ // Assert
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleChange).toBeCalled;
+ expect(wrapper.vm.handleCheckChange).not.toBeCalled;
+ expect(wrapper.vm.displayLoader).not.toBeCalled;
+ });
+
+ it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => {
+ // Act
+ const wrapper = shallowMount(listCard, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ selectingInitiatesLoad: true,
+ },
+ });
+
+ // Assert
+ wrapper.vm.triggerButton();
+
+ await nextTick();
+
+ expect(wrapper.vm.handleCheckChange).toBeCalled;
+ expect(wrapper.vm.displayLoader).toBeCalled;
+ });
+
+});
diff --git a/src/ux-components/list-card/list-card.vue b/src/ux-components/list-card/list-card.vue
new file mode 100644
index 00000000..eb2897b2
--- /dev/null
+++ b/src/ux-components/list-card/list-card.vue
@@ -0,0 +1,399 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ux-components/loader/loader.vue b/src/ux-components/loader/loader.vue
new file mode 100644
index 00000000..25d7e02d
--- /dev/null
+++ b/src/ux-components/loader/loader.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ux-components/radio/radio.spec.js b/src/ux-components/radio/radio.spec.js
new file mode 100644
index 00000000..bf2594e0
--- /dev/null
+++ b/src/ux-components/radio/radio.spec.js
@@ -0,0 +1,110 @@
+import { shallowMount } from "@vue/test-utils";
+import radio from "./radio";
+
+describe("radio.vue", () => {
+ it("Should return group name", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ propsData: {
+ groupName: "radio-button-test",
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ // Expect
+ expect(input.attributes().name).toEqual("radio-button-test");
+ });
+
+ it("Should return checkbox id", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ propsData: {
+ buttonID: "Radio ID",
+ },
+ });
+
+ // Assert
+ const input = wrapper.find("input");
+
+ // Expect
+ expect(input.attributes().id).toEqual("Radio ID");
+ });
+
+ it("Should return label text", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ propsData: {
+ buttonLabel: "label text",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("p");
+
+ expect(paragraph.text()).toEqual("label text");
+ });
+
+ it("Should return label text", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ propsData: {
+ screenReaderOnlyText: "screenreader text",
+ },
+ });
+
+ // Assert
+ const paragraph = wrapper.find("span");
+
+ expect(paragraph.text()).toEqual("screenreader text");
+ });
+
+ it("Should emit button value on click", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ buttonLabel: "Windshield",
+ value: "List Card Checkbox",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ isRequired: true,
+ isWide: false,
+ modelValue: ["List Card Checkbox"],
+ },
+ });
+ wrapper.vm.handleCheckChange();
+ // Assert
+ expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{"buttonID": "List Card Checkbox", value: "List Card Checkbox", checkValue: false}]);;
+ });
+
+ it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
+ // Act
+ const wrapper = shallowMount(radio, {
+ global: {
+ mocks: {
+ '$route': { query: { issPage: 'page-name' } },
+ }
+ },
+ propsData: {
+ buttonLabel: "Windshield",
+ buttonID: "List Card Checkbox",
+ groupID: "radio-demo-1",
+ groupName: "radio 1",
+ buttonImage: "windshield-damage.svg",
+ isRequired: true,
+ modelValue: ["List Card Checkbox"],
+ value: "Car-Front",
+ selectedValues: "Car-Front"
+ },
+ });
+ // Assert
+ expect(wrapper.componentVM.checkValue).toEqual(true);
+ });
+});
diff --git a/src/ux-components/radio/radio.vue b/src/ux-components/radio/radio.vue
new file mode 100644
index 00000000..1a0d52d7
--- /dev/null
+++ b/src/ux-components/radio/radio.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vue.config.js b/vue.config.js
index ccad75f0..97730661 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -10,6 +10,7 @@ module.exports = {
// Note: only include functions, variables and mixins here
additionalData: `
@import "./node_modules/bootstrap/scss/functions";
+ @import "@/styles/ux-variables.scss";
@import "./node_modules/bootstrap/scss/variables";
@import "./node_modules/bootstrap/scss/mixins";
`,
diff --git a/vue.release.config.js b/vue.release.config.js
index f6b57ccf..49511e62 100644
--- a/vue.release.config.js
+++ b/vue.release.config.js
@@ -10,6 +10,7 @@ module.exports = {
// Note: only include functions, variables and mixins here
additionalData: `
@import "./node_modules/bootstrap/scss/functions";
+ @import "@/styles/ux-variables.scss";
@import "./node_modules/bootstrap/scss/variables";
@import "./node_modules/bootstrap/scss/mixins";
`,