commit
6bf01676da
9 changed files with 678 additions and 30 deletions
|
|
@ -12,6 +12,7 @@ module.exports = {
|
||||||
"!src/router/**/*.js",
|
"!src/router/**/*.js",
|
||||||
"!src/helpers/unit-test-helper.js",
|
"!src/helpers/unit-test-helper.js",
|
||||||
"!src/layouts/component-test/component-test.vue",
|
"!src/layouts/component-test/component-test.vue",
|
||||||
|
"!src/layouts/form-test/form-test.vue",
|
||||||
"!src/layouts/address-poc/address-poc.vue"
|
"!src/layouts/address-poc/address-poc.vue"
|
||||||
], //! means exclude from coverage.
|
], //! means exclude from coverage.
|
||||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"http-status-codes": "^2.1.4",
|
"http-status-codes": "^2.1.4",
|
||||||
"jest-junit": "^13.0.0",
|
"jest-junit": "^13.0.0",
|
||||||
|
"vee-validate": "^4.5.7",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-plugin-load-script": "^2.1.0",
|
"vue-plugin-load-script": "^2.1.0",
|
||||||
"vue-router": "^4.0.11",
|
"vue-router": "^4.0.11",
|
||||||
"vuex": "^4.0.2",
|
"vuex": "^4.0.2",
|
||||||
"vuex-persistedstate": "^4.1.0"
|
"vuex-persistedstate": "^4.1.0",
|
||||||
|
"yup": "^0.32.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
|
|
||||||
38
src/common-components/text-input/text-input.spec.js
Normal file
38
src/common-components/text-input/text-input.spec.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import textInput from "./text-input";
|
||||||
|
|
||||||
|
describe("text-input.vue", () => {
|
||||||
|
|
||||||
|
it("Should render a text input", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(textInput, {
|
||||||
|
propsData: {
|
||||||
|
name: "test",
|
||||||
|
label: "unit test label",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
expect(input.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should return aria-required state", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(textInput, {
|
||||||
|
propsData: {
|
||||||
|
name: "test",
|
||||||
|
label: "unit test label",
|
||||||
|
isRequired: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const input = wrapper.find("input");
|
||||||
|
|
||||||
|
expect(input.attributes()["aria-required"]).toEqual("true");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
71
src/common-components/text-input/text-input.vue
Normal file
71
src/common-components/text-input/text-input.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<!-- Simple implementation of an input field -->
|
||||||
|
<template>
|
||||||
|
<div class="d-flex w-50 mb-2" :class="{ 'has-error': !!errorMessage }">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:name="name"
|
||||||
|
:value="inputValue"
|
||||||
|
:id="name"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@input="handleChange"
|
||||||
|
@blur="handleBlur"
|
||||||
|
:data-focus-target="name"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="name"
|
||||||
|
:aria-labelledby="name"
|
||||||
|
class="d-flex justify-content-center py-3 px-4"
|
||||||
|
>
|
||||||
|
<span class="m-0">{{label}}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-show="errorMessage" class="row px-3 form-test-error">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useField } from 'vee-validate';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "textInput",
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isRequired: Boolean,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const {
|
||||||
|
value: inputValue,
|
||||||
|
errorMessage,
|
||||||
|
handleBlur,
|
||||||
|
handleChange,
|
||||||
|
meta,
|
||||||
|
} = useField(props.name, undefined, {
|
||||||
|
initialValue: props.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleChange,
|
||||||
|
handleBlur,
|
||||||
|
errorMessage,
|
||||||
|
inputValue,
|
||||||
|
meta,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
338
src/layouts/form-test/form-test.vue
Normal file
338
src/layouts/form-test/form-test.vue
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
<template>
|
||||||
|
<Form
|
||||||
|
:validation-schema="schema"
|
||||||
|
@submit="onSubmit"
|
||||||
|
@invalid-submit="onInvalidSubmit"
|
||||||
|
v-slot="{ values, meta }"
|
||||||
|
>
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<h2 class="mx-2 mt-4 mb-0">FORM VALIDATION TEST</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">How many chips are we repairing? (Required)</h4>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo-group"
|
||||||
|
class="d-flex flex-row p-0"
|
||||||
|
>
|
||||||
|
<h4 class="sr-only" id="demo-group">
|
||||||
|
How many chips are we repairing?
|
||||||
|
</h4>
|
||||||
|
<listButtonHorizontal
|
||||||
|
groupName="demo"
|
||||||
|
aria-required="true"
|
||||||
|
buttonID="1"
|
||||||
|
value="1"
|
||||||
|
buttonLabelSubCopy=""
|
||||||
|
textPosition="text-center"
|
||||||
|
sizeInRem="1"
|
||||||
|
:totalInGroup="3"
|
||||||
|
:positionInGroup="1"
|
||||||
|
screenReaderOnlyText=" opens new window"
|
||||||
|
/>
|
||||||
|
<listButtonHorizontal
|
||||||
|
groupName="demo"
|
||||||
|
aria-required="true"
|
||||||
|
buttonID="2"
|
||||||
|
value="2"
|
||||||
|
buttonLabelSubCopy=""
|
||||||
|
textPosition="text-center"
|
||||||
|
sizeInRem="1"
|
||||||
|
:totalInGroup="3"
|
||||||
|
:positionInGroup="2"
|
||||||
|
screenReaderOnlyText=" opens new window"
|
||||||
|
/>
|
||||||
|
<listButtonHorizontal
|
||||||
|
groupName="demo"
|
||||||
|
aria-required="true"
|
||||||
|
buttonID="3"
|
||||||
|
value="3"
|
||||||
|
buttonLabelSubCopy=""
|
||||||
|
textPosition="text-center"
|
||||||
|
sizeInRem="1"
|
||||||
|
:totalInGroup="3"
|
||||||
|
:positionInGroup="3"
|
||||||
|
screenReaderOnlyText=" opens new window"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 form-test-error">
|
||||||
|
<error-message name="demo"></error-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<div class="col">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">Which windshield part needs fixed? (Required)</h4>
|
||||||
|
<fieldset>
|
||||||
|
<legend class="sr-only">Which windshield part needs fixed?</legend>
|
||||||
|
<listButton
|
||||||
|
isMultiSelect
|
||||||
|
buttonID="Single Windshield"
|
||||||
|
value="Single Windshield"
|
||||||
|
groupName="demo2"
|
||||||
|
/>
|
||||||
|
<listButton
|
||||||
|
isMultiSelect
|
||||||
|
buttonID="Driver Side"
|
||||||
|
value="Driver Side"
|
||||||
|
groupName="demo2"
|
||||||
|
/>
|
||||||
|
<listButton
|
||||||
|
isMultiSelect
|
||||||
|
buttonID="Passenger Side"
|
||||||
|
value="Passenger Side"
|
||||||
|
groupName="demo2"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 form-test-error">
|
||||||
|
<error-message name="demo2"></error-message>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<div class="col">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">demo3: Enter your VIN (Required)</h4>
|
||||||
|
<textInput
|
||||||
|
name="demo3"
|
||||||
|
type="text"
|
||||||
|
label="VIN (Required)"
|
||||||
|
:isRequired="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<div class="col">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">demo4: Enter some numbers (optional)</h4>
|
||||||
|
<textInput
|
||||||
|
name="demo4"
|
||||||
|
type="text"
|
||||||
|
label="Numbers Only"
|
||||||
|
:isRequired="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">demo5: Where's your damage? (Required)</h4>
|
||||||
|
<div
|
||||||
|
aria-labelledby="demo5-group"
|
||||||
|
class="d-flex flex-row p-0"
|
||||||
|
>
|
||||||
|
<h4 class="sr-only" id="demo5-group">
|
||||||
|
Where's your damage?
|
||||||
|
</h4>
|
||||||
|
<listCard
|
||||||
|
isMultiSelect
|
||||||
|
buttonImage="windshield-damage.svg"
|
||||||
|
buttonLabel="Windshield"
|
||||||
|
value="Windshield"
|
||||||
|
altText=""
|
||||||
|
buttonID="List Card Checkbox a"
|
||||||
|
groupName="demo5"
|
||||||
|
/>
|
||||||
|
<listCard
|
||||||
|
isMultiSelect
|
||||||
|
buttonImage="side-window-damage-left-all.svg"
|
||||||
|
buttonLabel="Side Door"
|
||||||
|
value="Side Door"
|
||||||
|
altText=""
|
||||||
|
buttonID="List Card Checkbox b"
|
||||||
|
groupName="demo5"
|
||||||
|
/>
|
||||||
|
<listCard
|
||||||
|
isMultiSelect
|
||||||
|
buttonImage="back-glass-damage.svg"
|
||||||
|
buttonLabel="Rear Window"
|
||||||
|
value="Rear Window"
|
||||||
|
altText=""
|
||||||
|
buttonID="List Card Checkbox c"
|
||||||
|
groupName="demo5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-3 form-test-error">
|
||||||
|
<error-message name="demo5"></error-message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">demo6: What's your windshield damage? (Required if demo5 "Windshield" is selected)</h4>
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby="demo6-group"
|
||||||
|
class="d-flex flex-row p-0"
|
||||||
|
>
|
||||||
|
<h4 class="sr-only" id="demo6-group">
|
||||||
|
What's your windshield damage?
|
||||||
|
</h4>
|
||||||
|
<listCard
|
||||||
|
isRadio
|
||||||
|
buttonImage="windshield-damage.svg"
|
||||||
|
buttonLabel="Crack"
|
||||||
|
value="Crack"
|
||||||
|
buttonLabelSubCopy="My damage is larger than six inches."
|
||||||
|
altText=""
|
||||||
|
buttonID="Crack Checkbox a"
|
||||||
|
groupName="demo6"
|
||||||
|
/>
|
||||||
|
<listCard
|
||||||
|
isRadio
|
||||||
|
buttonImage="windshield-damage.svg"
|
||||||
|
buttonLabel="Chip(s)"
|
||||||
|
value="Chip"
|
||||||
|
buttonLabelSubCopy="I have three or fewer chips smaller than six inches."
|
||||||
|
altText=""
|
||||||
|
buttonID="Chip Checkbox b"
|
||||||
|
groupName="demo6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-3 form-test-error">
|
||||||
|
<error-message name="demo6"></error-message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<div class="col">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">Password (Required, enter any string)</h4>
|
||||||
|
<textInput
|
||||||
|
name="password"
|
||||||
|
type="text"
|
||||||
|
label="Password (Required)"
|
||||||
|
:isRequired="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2 mt-6 mx-4">
|
||||||
|
<div class="col">
|
||||||
|
<h4 class="mx-2 mt-4 mb-0">Confirm Password (Required, must match above Password field)</h4>
|
||||||
|
<textInput
|
||||||
|
name="passwordConfirmation"
|
||||||
|
type="text"
|
||||||
|
label="Confirm Password (Required)"
|
||||||
|
:isRequired="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row px-3 form-test-error" data-focus-target="demoX">
|
||||||
|
<error-message name="demoX"></error-message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="g-2 mt-6 mx-4">
|
||||||
|
<button class="mb-6" :class="!meta.valid && 'form-test-invalid'">Get your estimate</button>
|
||||||
|
<p>Current Form Values:</p>
|
||||||
|
<pre>{{ values }}</pre>
|
||||||
|
<p>Is Form Valid? (Meta.valid):</p>
|
||||||
|
<pre>{{ meta.valid }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { Form, ErrorMessage } from 'vee-validate';
|
||||||
|
import * as Yup from "yup";
|
||||||
|
|
||||||
|
import listCard from "@/ux-components/list-card/list-card";
|
||||||
|
import textInput from "@/common-components/text-input/text-input";
|
||||||
|
import listButton from "@/ux-components/list-button/list-button";
|
||||||
|
import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-button-horizontal";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "App",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
schema: Yup.object().shape({
|
||||||
|
demo: Yup.string().required('Please select chip(s)'),
|
||||||
|
demo2: Yup.array().required('Please select a windshield part'),
|
||||||
|
demo3: Yup.string().required('Invalid VIN. Please enter X, Y, Z...'),
|
||||||
|
demo4: Yup.string().matches('^[0-9]*$', 'Invalid Entry. Must enter only numbers, no letters.'),
|
||||||
|
demo5: Yup.array().required('Please select damage location'),
|
||||||
|
demo6: Yup.string()
|
||||||
|
.when('demo5', function (demo5, schema, value) {
|
||||||
|
if (demo5) {
|
||||||
|
const demo5String = demo5.toString();
|
||||||
|
if (demo5String.includes("Windshield")) {
|
||||||
|
// make demo6 required
|
||||||
|
return schema.required('You must pick an option for Windshield Damage.')
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
}),
|
||||||
|
demoX: Yup.string().notRequired(),
|
||||||
|
// // PASSWORD EXAMPLE, NOT BEING USED NOW
|
||||||
|
//password: Yup.string().min(6).required(),
|
||||||
|
//passwordConfirmation: Yup.string()
|
||||||
|
// .required()
|
||||||
|
// .oneOf([Yup.ref("password")], "Passwords do not match"),
|
||||||
|
}).test('repair-replace-conflict',
|
||||||
|
null, // need to pass null as error message so it won't get added to list of errors
|
||||||
|
function(value) {
|
||||||
|
// whole form test
|
||||||
|
console.log('ALL FORM VALUES: ', value);
|
||||||
|
|
||||||
|
// test problem scenario... return new error if conditions are true, allow submit if not
|
||||||
|
if (value.demo5 && value.demo6) {
|
||||||
|
const demo5String = value.demo5.toString();
|
||||||
|
console.log('demo5String: ', demo5String);
|
||||||
|
console.log('demo6: ', value.demo6);
|
||||||
|
|
||||||
|
if (demo5String.includes("Windshield") && value.demo5.length > 1 && value.demo6 == "Chip") {
|
||||||
|
return this.createError({ name: 'demoX', message: "Sorry, at this time we can't schedule a repair and replace at the same time. Please schedule these appointments separately as they are different technicians. Thanks.", path: 'demoX' });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
listCard,
|
||||||
|
listButton,
|
||||||
|
listButtonHorizontal,
|
||||||
|
textInput,
|
||||||
|
Form,
|
||||||
|
ErrorMessage,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit(values) {
|
||||||
|
window.alert('Success! Submitted values: ' + JSON.stringify(values, null ,2))
|
||||||
|
console.log('submitted: ', JSON.stringify(values, null ,2));
|
||||||
|
},
|
||||||
|
onInvalidSubmit({ values, errors, results }) {
|
||||||
|
// identify the first error field and put focus on it
|
||||||
|
console.log("values: ", values);
|
||||||
|
console.log("errors: ", errors);
|
||||||
|
console.log("results: ", results);
|
||||||
|
// get errorEls and order by alpha
|
||||||
|
const errorEls = Object.keys(errors).sort();
|
||||||
|
const firstErrorEl = errorEls[0];
|
||||||
|
console.log('firstErrorEl: ', firstErrorEl);
|
||||||
|
if (firstErrorEl) {
|
||||||
|
const qsString = "[data-focus-target='" + firstErrorEl + "']";
|
||||||
|
document.querySelector(qsString).focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.form-test-error {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.form-test-invalid {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.has-error {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,9 +3,8 @@ import { storeActions } from "@/constants/store-actions.js";
|
||||||
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
||||||
import { routingTable } from "@/router/router-constants/routing-table.js";
|
import { routingTable } from "@/router/router-constants/routing-table.js";
|
||||||
import ComponentTest from "@/layouts/component-test/component-test.vue";
|
import ComponentTest from "@/layouts/component-test/component-test.vue";
|
||||||
|
import FormTest from "@/layouts/form-test/form-test.vue";
|
||||||
import AddressPOC from "@/layouts/address-poc/address-poc.vue";
|
import AddressPOC from "@/layouts/address-poc/address-poc.vue";
|
||||||
|
|
||||||
import NotFound from "@/layouts/not-found/not-found.vue";
|
import NotFound from "@/layouts/not-found/not-found.vue";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
|
|
@ -20,6 +19,11 @@ const routes = [
|
||||||
name: "ComponentTest",
|
name: "ComponentTest",
|
||||||
component: ComponentTest,
|
component: ComponentTest,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/form-test", // This is a temporary route for testing.
|
||||||
|
name: "FormTest",
|
||||||
|
component: FormTest,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/address-poc", // This is a temporary route for testing.
|
path: "/address-poc", // This is a temporary route for testing.
|
||||||
name: "AddressPOC",
|
name: "AddressPOC",
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,50 @@
|
||||||
<!-- See the component-test.vue page for example implementation -->
|
<!-- See the component-test.vue page for example implementation -->
|
||||||
<template>
|
<template>
|
||||||
<!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. -->
|
<!-- IMPORTANT: Refrain from using more than 4 horizontal buttons on desktop, 3 on mobile. -->
|
||||||
<div v-if="isMultiSelect" class="list-group list-button-horizontal d-flex flex-column w-100 mb-2">
|
<div
|
||||||
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired">
|
v-if="isMultiSelect"
|
||||||
<label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton">
|
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleClick(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
:aria-labelledby="buttonID"
|
||||||
|
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||||
|
:class="isFirstOrLastButton"
|
||||||
|
>
|
||||||
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="col list-group list-button-horizontal d-flex flex-column mb-2">
|
<div
|
||||||
<input type="radio" :id="buttonID" :name="groupName" :value="buttonID" aria-required="true" @keyup.space="handleClick()" @click="handleClick()" />
|
v-else
|
||||||
<label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" :class="isFirstOrLastButton">
|
class="col list-group list-button-horizontal d-flex flex-column mb-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleClick(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
:aria-labelledby="buttonID"
|
||||||
|
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||||
|
:class="isFirstOrLastButton"
|
||||||
|
>
|
||||||
<span class="m-0" :class="textPosition">{{buttonID}}</span>
|
<span class="m-0" :class="textPosition">{{buttonID}}</span>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
<loader v-if="isLoaderDisplayed" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[loaderColor, loaderPosition]" />
|
<loader v-if="isLoaderDisplayed" :style="{width: `${sizeInRem}rem`, height: `${sizeInRem}rem`}" :class="[loaderColor, loaderPosition]" />
|
||||||
|
|
@ -19,6 +53,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
import { useField } from 'vee-validate';
|
||||||
import loader from "@/ux-components/loader/loader";
|
import loader from "@/ux-components/loader/loader";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -36,6 +72,10 @@ export default {
|
||||||
totalInGroup: Number, /* Required, total number of buttons in group. Used to tell first and last in group to apply border radius. */
|
totalInGroup: Number, /* Required, total number of buttons in group. Used to tell first and last in group to apply border radius. */
|
||||||
positionInGroup: Number, /* Required, position of button in group. Example, 1,2,3 */
|
positionInGroup: Number, /* Required, position of button in group. Example, 1,2,3 */
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
|
value: { // Field initial value
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -46,8 +86,9 @@ export default {
|
||||||
displayLoader() {
|
displayLoader() {
|
||||||
this.isLoaderDisplayed = true;
|
this.isLoaderDisplayed = true;
|
||||||
},
|
},
|
||||||
handleClick() {
|
handleClick(value) {
|
||||||
this.loaderEnabled && this.displayLoader();
|
this.loaderEnabled && this.displayLoader();
|
||||||
|
this.handleChange(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -64,6 +105,19 @@ export default {
|
||||||
return className;
|
return className;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { groupName, value } = toRefs(props);
|
||||||
|
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
const { checked, handleChange, errorMessage } = useField(groupName, undefined, {
|
||||||
|
type: inputType,
|
||||||
|
checkedValue: value
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
handleChange,
|
||||||
|
errorMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -72,8 +126,8 @@ export default {
|
||||||
input[type="radio"],
|
input[type="radio"],
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: fixed;
|
|
||||||
width: 0;
|
width: 0;
|
||||||
|
height: 0;
|
||||||
&:focus-visible + label {
|
&:focus-visible + label {
|
||||||
box-shadow: 0 0 0 2px $blue;
|
box-shadow: 0 0 0 2px $blue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- See the component-test.vue page for example implementation -->
|
<!-- See the component-test.vue page for example implementation -->
|
||||||
<div v-if="isMultiSelect" class="list-group list-button d-flex flex-column w-100 mb-2">
|
<div v-if="isMultiSelect" class="list-group list-button d-flex flex-column w-100 mb-2">
|
||||||
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired">
|
<input
|
||||||
<label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4">
|
type="checkbox"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleClick(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
:aria-labelledby="buttonID"
|
||||||
|
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||||
|
>
|
||||||
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
||||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
|
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="list-group list-button d-flex flex-column w-100 mb-2">
|
<div v-else class="list-group list-button d-flex flex-column w-100 mb-2">
|
||||||
<input type="radio" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" @keyup.space="displayLoader()">
|
<input
|
||||||
<label tabindex="-1" aria-checked="false" :for="buttonID" :aria-labelledby="buttonID" class="d-flex flex-column justify-content-center py-3 px-4" @click="displayLoader()">
|
type="radio"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleClick(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="-1"
|
||||||
|
:for="buttonID"
|
||||||
|
:aria-labelledby="buttonID"
|
||||||
|
class="d-flex flex-column justify-content-center py-3 px-4"
|
||||||
|
>
|
||||||
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
<span class="m-0" :class="[this.textPosition]">{{buttonID}}</span>
|
||||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
|
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="[this.textPosition]">{{buttonLabelSubCopy}}</span>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
<span v-if="screenReaderOnlyText" class="sr-only">{{screenReaderOnlyText}}</span>
|
||||||
|
|
@ -20,6 +46,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
import { useField } from 'vee-validate';
|
||||||
import loader from "@/ux-components/loader/loader";
|
import loader from "@/ux-components/loader/loader";
|
||||||
export default {
|
export default {
|
||||||
name: "listButton",
|
name: "listButton",
|
||||||
|
|
@ -35,6 +63,10 @@ export default {
|
||||||
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */
|
loaderColor: String, /* Specify color of loader/spinner. Options are blue, red, green, white, black. Default is blue */
|
||||||
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */
|
loaderPosition: String, /* Specify horizontal position of loader/spinner. Options are center, right, left */
|
||||||
sizeInRem: [Number,String], /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */
|
sizeInRem: [Number,String], /* Specify size of loader/spinner in rem. Example: 1.5 (equals 24px (16x1.5)) */
|
||||||
|
value: { // Field initial value
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -45,12 +77,38 @@ export default {
|
||||||
displayLoader() {
|
displayLoader() {
|
||||||
this.isLoaderDisplayed = true;
|
this.isLoaderDisplayed = true;
|
||||||
},
|
},
|
||||||
handleClick() {
|
handleClick(value) {
|
||||||
this.loaderEnabled && this.displayLoader();
|
this.loaderEnabled && this.displayLoader();
|
||||||
|
this.handleChange(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
loader,
|
loader,
|
||||||
}
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { groupName, value } = toRefs(props);
|
||||||
|
const inputType = props.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
const { checked, handleChange, errorMessage } = useField(groupName, undefined, {
|
||||||
|
type: inputType,
|
||||||
|
checkedValue: value
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
handleChange,
|
||||||
|
errorMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.list-group {
|
||||||
|
&.list-button {
|
||||||
|
input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
|
position: static !important; //override bootstrap
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,24 +1,61 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- Heavily documented below -->
|
<!-- Heavily documented below -->
|
||||||
<div v-if="isMultiSelect" class="list-card w-100 rounded-3 d-flex align-items-center h-100">
|
<div v-if="isMultiSelect" class="list-card w-100 rounded-3 d-flex align-items-center h-100">
|
||||||
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
|
<input
|
||||||
<label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2 px-2 h-100" tabindex="1">
|
type="checkbox"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonLabel"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleChange(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="buttonID"
|
||||||
|
class="d-flex flex-column w-100 align-items-center pt-4 pb-2 px-2 h-100"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
||||||
<p class="small m-0 order-3">{{buttonLabel}}</p>
|
<p class="small m-0 order-3">{{buttonLabel}}</p>
|
||||||
<p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p>
|
<p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-4">{{buttonLabelSubCopy}}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isRadio" class="list-card w-100 rounded-3 d-flex align-items-center h-100">
|
<div v-else-if="isRadio" class="list-card w-100 rounded-3 d-flex align-items-center h-100">
|
||||||
<input type="radio" :id="buttonID" :name="groupName" :value="buttonID" :aria-required="isRequired" />
|
<input
|
||||||
<label :for="buttonID" class="d-flex flex-column w-100 align-items-center pt-4 pb-2 px-2 h-100" tabindex="1">
|
type="radio"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonID"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleChange(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="buttonID"
|
||||||
|
class="d-flex flex-column w-100 align-items-center pt-4 pb-2 px-2 h-100"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
<img class="order-1" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
||||||
<p class="small mt-2 mb-0 order-2">{{buttonLabel}}</p>
|
<p class="small mt-2 mb-0 order-2">{{buttonLabel}}</p>
|
||||||
<p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-3">{{buttonLabelSubCopy}}</p>
|
<p v-if="buttonLabelSubCopy" class="fs-7 m-0 order-3">{{buttonLabelSubCopy}}</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isMultiSelectHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center h-100">
|
<div v-else-if="isMultiSelectHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center h-100">
|
||||||
<input type="checkbox" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
|
<input
|
||||||
<label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-r ps-3 pe-8 px-2 h-100" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}">
|
type="checkbox"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonLabel"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleChange(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="buttonID"
|
||||||
|
class="d-flex flex-row w-100 align-items-center py-r ps-3 pe-8 px-2 h-100"
|
||||||
|
tabindex="-1"
|
||||||
|
:class="{'checkboxTop': buttonLabelSubCopy}"
|
||||||
|
>
|
||||||
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
||||||
<div class="order-2">
|
<div class="order-2">
|
||||||
<p class="m-0 small">{{buttonLabel}}</p>
|
<p class="m-0 small">{{buttonLabel}}</p>
|
||||||
|
|
@ -27,8 +64,21 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isRadioHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center h-100">
|
<div v-else-if="isRadioHorizontal" class="list-card horizontal w-100 rounded-3 d-flex align-items-center h-100">
|
||||||
<input type="radio" :id="buttonID" :name="groupName" :value="buttonLabel" :aria-required="isRequired" />
|
<input
|
||||||
<label :for="buttonID" class="d-flex flex-row w-100 align-items-center py-r ps-3 pe-8 px-2 h-100" tabindex="1" :class="{'checkboxTop': buttonLabelSubCopy}">
|
type="radio"
|
||||||
|
:id="buttonID"
|
||||||
|
:name="groupName"
|
||||||
|
:value="buttonLabel"
|
||||||
|
:aria-required="isRequired"
|
||||||
|
@click="handleChange(value)"
|
||||||
|
:data-focus-target="groupName"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:for="buttonID"
|
||||||
|
class="d-flex flex-row w-100 align-items-center py-r ps-3 pe-8 px-2 h-100"
|
||||||
|
tabindex="-1"
|
||||||
|
:class="{'checkboxTop': buttonLabelSubCopy}"
|
||||||
|
>
|
||||||
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
<img class="ms-auto order-3" v-bind:src="require(`@/assets/img/icons/${buttonImage}`)" v-bind:alt="altText" />
|
||||||
<div class="order-2">
|
<div class="order-2">
|
||||||
<p class="m-0 small">{{buttonLabel}}</p>
|
<p class="m-0 small">{{buttonLabel}}</p>
|
||||||
|
|
@ -39,6 +89,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { toRefs } from 'vue';
|
||||||
|
import { useField } from 'vee-validate';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "listCard",
|
name: "listCard",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -53,9 +106,38 @@ export default {
|
||||||
isRequired: Boolean, //Required: is aria-required required or not?
|
isRequired: Boolean, //Required: is aria-required required or not?
|
||||||
altText: String,//Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
|
altText: String,//Leave empty. Screen readers read the buttonLabel text. If alt has content, it will repeat unnecessarily.
|
||||||
buttonID: String,//Required: Unique
|
buttonID: String,//Required: Unique
|
||||||
groupName: String,//Required: Unique
|
groupName: String,//Rquired: Unique
|
||||||
buttonLabelSubCopy: String,//Optional: sub tex
|
buttonLabelSubCopy: String,//Optional: sub text
|
||||||
}
|
value: { // Field initial value
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const { groupName, value } = toRefs(props);
|
||||||
|
const inputType = () => { // TODO: refactor props to streamline this logic for the whole component
|
||||||
|
if (props.isMultiSelect) {
|
||||||
|
return "checkbox";
|
||||||
|
} else if (props.isRadio) {
|
||||||
|
return "radio";
|
||||||
|
} else if (props.isMultiSelectHorizontal) {
|
||||||
|
return "checkbox";
|
||||||
|
} else if (props.isRadioHorizontal) {
|
||||||
|
return "radio";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.isMultiSelect ? "checkbox" : "radio";
|
||||||
|
const { checked, handleChange } = useField(groupName, undefined, {
|
||||||
|
type: inputType(),
|
||||||
|
checkedValue: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
checked,
|
||||||
|
handleChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -77,8 +159,8 @@ export default {
|
||||||
input[type='checkbox'],
|
input[type='checkbox'],
|
||||||
input[type="radio"] {
|
input[type="radio"] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: fixed;
|
|
||||||
width: 0;
|
width: 0;
|
||||||
|
height: 0;
|
||||||
+ label {
|
+ label {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue