DigitalConsumer.ISS/src/digital-components/modal/modal.vue
Alex Humphries be86421a7b INSR-8123: Update some more modals
- Remove modalPosition related code now that all modals are centered
- Allow parent components to set text of the close link
- Updates styling for specific modals so that they match their
  Heritage counterparts
2026-03-03 14:04:50 -05:00

313 lines
8.6 KiB
Vue

<template>
<!-- Modal -->
<div
:id="modalId"
class="modal fade modal-component"
tabindex="-1"
aria-labelledby="ModalComponentLabel"
aria-hidden="true"
@keydown.enter="onKeyDown"
v-on="{
'hidden.bs.modal': onModalClosed,
'shown.bs.modal': onModalOpened
}">
<div class="modal-dialog">
<div
class="modal-content"
role="dialog"
aria-modal="true">
<div class="modal-header">
<span
v-if="headerText"
class="modal-title d-flex pb-0 w-100"
:innerHTML="headerText">
</span>
<button
v-if="displayCloseButton"
ref="closeButton"
type="button"
class="btn-close"
aria-label="Close"
@mousedown="closeModal"
@keypress.space="closeModal"></button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<buttonMain
v-if="displayFooterButton"
id="modalbtn"
ref="modalButtonMain"
class="w-100"
:buttonText="footerButtonText"
pushToGA
:variant="buttonVariant"
:class="[
(isButtonDisabled || isFooterButtonDisabled) &&
'form-test-invalid'
]"
@clickEvent="validateAndEmit" />
<textLink
v-if="displayCloseLink"
class="mt-4 mb-0"
linkType="text"
:text="closeLinkText"
href="#!"
@click="closeModal" />
</div>
</div>
</div>
</div>
</template>
<script>
import ButtonMain from '@/ux-components/button-main/button-main.vue';
import { Modal } from 'bootstrap';
import { useForm } from 'vee-validate';
import textLink from '@/ux-components/text-link/text-link.vue';
export default {
name: 'modal',
components: {
ButtonMain,
textLink
},
props: {
modalId: String,
headerText: String,
footerButtonText: String,
onModalOpenedCallback: {
type: Function
},
onModalClosedCallback: {
type: Function
},
isButtonDisabled: Boolean,
buttonVariant: {
type: String,
default: 'success'
},
allowInvalidSubmit: {
type: Boolean,
default: false
},
displayFooterButton: {
type: Boolean,
default: true
},
displayCloseButton: {
type: Boolean,
default: true
},
displayCloseLink: {
type: Boolean,
default: false
},
closeLinkText: {
type: String,
default: 'Close window'
}
},
emits: ['footer-button-event', 'isModalOpened'],
setup(props) {
const modalId = props.modalId
? props.modalId
: `modal-${crypto.randomUUID()}`;
const { meta, validate, resetForm } = useForm();
// TODO: Correct Duplicate key 'modalId' issue.
// Probably just rename the prop. -br
return {
modalId,
meta,
validate,
resetForm
};
},
computed: {
isFooterButtonDisabled() {
if (!this.meta.touched) {
return !this.meta.valid;
}
return !this.meta.dirty || !this.meta.valid;
}
},
watch: {
$route() {
this.closeModal();
}
},
methods: {
async validateAndEmit() {
const validationResult = await this.validate();
if (validationResult.valid || this.allowInvalidSubmit) {
this.$emit('footer-button-event');
}
},
async onKeyDown(e) {
e.preventDefault();
document.getElementById('modalbtn')?.focus();
this.$refs.modalButtonMain.clicked();
document.getElementById(this.modalId)?.focus();
},
onModalOpened() {
this.onModalOpenedCallback?.();
},
onModalClosed() {
this.onModalClosedCallback?.();
},
openModal() {
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
modal?.show();
this.$emit('isModalOpened', true);
},
closeModal() {
const modal = Modal.getInstance(document.getElementById(this.modalId));
modal?.hide();
this.$emit('isModalOpened', false);
}
}
};
</script>
<style lang="scss">
@import "@/styles/ux-variables-svg-strings.scss";
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding-right: 1rem;
h5,
strong,
.subheader-text {
color: $black;
}
.modal-header {
padding: 1.5rem 0 0.5rem 0;
border-bottom: none;
.btn-close {
position: absolute;
right: 1rem;
top: 1rem;
background-image: url($svg-modal-header-button-close);
opacity: 1;
}
.modal-title {
font-weight: 400;
font-size: 1.25rem;
line-height: 2rem;
color: $black;
padding-left: 1.5rem;
}
}
.modal-footer {
position: sticky;
width: 100%;
bottom: 0;
z-index: 5;
border-radius: 0;
justify-content: center;
> .navigation-link {
color: $black;
font-weight: $font-weight-bold;
line-height: 1.625rem;
white-space: nowrap;
align-items: center;
text-decoration: underline;
text-underline-offset: .3125rem;
}
}
&.modal-component {
.modal-dialog {
display: flex;
width: 37.5rem;
max-width: unset;
min-height: auto;
.modal-content {
box-shadow: 0px 1rem 3rem -1rem rgba(0, 0, 0, 0.25);
border: 1px solid;
border-radius: 5px;
overflow: auto;
}
.modal-content {
margin: 0 auto;
.modal-body {
padding: 0 1.5rem 0 1.5rem;
.col {
max-width: 100%;
}
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
padding-left: 20px;
}
li {
margin-bottom: 0.5rem;
line-height: 22px;
}
p {
&:last-child {
margin-bottom: 0;
}
}
@include media-breakpoint-up(md) {
overflow: auto;
flex: none;
}
h5 {
text-align: left;
line-height: 2rem;
}
}
}
}
.modal-footer {
border-top: none;
padding: .625rem 1.5rem 1.5rem 1.5rem;
button {
margin: 0;
background-color: $white;
color: $blue;
border-color: $blue;
font-weight: 400;
&:hover {
background-color: #167cac;
color: $white;
}
}
}
}
@include media-breakpoint-up(md) {
&.fade {
transition: opacity 0.25s linear;
}
}
@include media-breakpoint-down(md) {
&.modal-component {
.modal-dialog {
width: auto;
max-width: unset;
margin: .625rem;
}
}
}
}
body {
.modal-backdrop {
height: 100%;
&.show {
opacity: 0.4;
}
}
}
</style>