mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-05-24 10:44:00 -04:00
Merge pull request #2558 from GSA/2554-add-cancel-button-and-modal-to-preview-send-page
2554 add cancel button and modal to preview send page
This commit is contained in:
100
app/assets/javascripts/notifyModal.js
Normal file
100
app/assets/javascripts/notifyModal.js
Normal file
@@ -0,0 +1,100 @@
|
||||
let activeModal = null;
|
||||
let lastFocusedElement = null;
|
||||
|
||||
function openModal(modalId) {
|
||||
const wrapper = document.getElementById(modalId);
|
||||
if (!wrapper) return;
|
||||
|
||||
const modal = wrapper.querySelector('.usa-modal, dialog');
|
||||
if (!modal) return;
|
||||
|
||||
lastFocusedElement = document.activeElement;
|
||||
|
||||
wrapper.classList.remove('is-hidden');
|
||||
modal.removeAttribute('aria-hidden');
|
||||
modal.removeAttribute('inert');
|
||||
modal.removeAttribute('hidden');
|
||||
document.body.classList.add('modal-open');
|
||||
|
||||
|
||||
// Set focus to the first focusable element inside modal
|
||||
const focusTarget = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusTarget) focusTarget.focus();
|
||||
|
||||
modal.addEventListener('keydown', function(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
activeModal = wrapper;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (!activeModal) return;
|
||||
|
||||
const modal = activeModal.querySelector('.usa-modal, dialog');
|
||||
if (modal) {
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.setAttribute('inert', '');
|
||||
modal.setAttribute('hidden', '');
|
||||
}
|
||||
|
||||
activeModal.classList.add('is-hidden');
|
||||
document.body.classList.remove('modal-open');
|
||||
|
||||
if (lastFocusedElement) lastFocusedElement.focus();
|
||||
|
||||
activeModal = null;
|
||||
|
||||
}
|
||||
|
||||
function attachModalTriggers() {
|
||||
document.querySelectorAll('[data-open-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const modalId = btn.getAttribute('data-open-modal');
|
||||
openModal(modalId);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Escape key closes modal
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && activeModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Optional: click outside modal closes it
|
||||
document.addEventListener('click', (e) => {
|
||||
if (activeModal && e.target.classList.contains('usa-modal-overlay')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
attachModalTriggers();
|
||||
});
|
||||
|
||||
// ✅ Check if we're in a Node.js environment (for Jest) before using `module.exports`
|
||||
if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
|
||||
module.exports = { closeModal, openModal, attachModalTriggers };
|
||||
}
|
||||
@@ -1,37 +1,45 @@
|
||||
(function() {
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
let disableSubmitButtons = function(event) {
|
||||
|
||||
var $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') == 'true') {
|
||||
const disableSubmitButtons = function (event) {
|
||||
const $submitButton = $(this).find(':submit');
|
||||
|
||||
if ($submitButton.data('clicked') === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
$submitButton.data('clicked', 'true');
|
||||
|
||||
$submitButton.data('clicked', 'true');
|
||||
// Add dot animation for Send/Schedule/Cancel buttons
|
||||
const buttonName = $submitButton.attr('name')?.toLowerCase();
|
||||
if (["send", "schedule", "cancel"].includes(buttonName)) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
if ($submitButton.is('[name="Send"], [name="Schedule"]')) {
|
||||
$submitButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000);
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
// Inject dot animation span if not already present
|
||||
if ($submitButton.find('.dot-anim').length === 0) {
|
||||
$submitButton.append('<span class="dot-anim" aria-hidden="true"></span>');
|
||||
}
|
||||
|
||||
// Disable Cancel button too
|
||||
const $cancelButton = $('button[name]').filter(function () {
|
||||
return $(this).attr('name')?.toLowerCase() === 'cancel';
|
||||
});
|
||||
$cancelButton.prop('disabled', true);
|
||||
|
||||
setTimeout(() => {
|
||||
renableSubmitButton($submitButton);
|
||||
}, 10000); // fallback safety
|
||||
} else {
|
||||
setTimeout(renableSubmitButton($submitButton), 1500);
|
||||
}
|
||||
};
|
||||
|
||||
let renableSubmitButton = $submitButton => () => {
|
||||
|
||||
const renableSubmitButton = ($submitButton) => () => {
|
||||
$submitButton.data('clicked', '');
|
||||
$submitButton.prop('disabled', false);
|
||||
$submitButton.find('.dot-anim').remove(); // clean up if needed
|
||||
};
|
||||
|
||||
$('form').on('submit', disableSubmitButtons);
|
||||
|
||||
})();
|
||||
|
||||
8
app/assets/js/init.uswds.js
Normal file
8
app/assets/js/init.uswds.js
Normal file
@@ -0,0 +1,8 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
if (window.uswds && typeof window.uswds.init === 'function') {
|
||||
console.log("Calling USWDS init");
|
||||
window.uswds.init();
|
||||
} else {
|
||||
console.error("USWDS not found or init is not a function");
|
||||
}
|
||||
});
|
||||
@@ -352,6 +352,19 @@ h2.recipient-list {
|
||||
}
|
||||
|
||||
// Button ellipses loading
|
||||
.dot-anim {
|
||||
display: inline-block;
|
||||
margin-left: 0; /* remove left margin if it exists */
|
||||
padding-left: 0;
|
||||
font-size: 1em;
|
||||
animation: dots 1s steps(3, end) infinite;
|
||||
}
|
||||
|
||||
/* Optional: reduce spacing by removing whitespace node */
|
||||
button span.dot-anim {
|
||||
margin-left: 0; /* forces no space even if white-space exists */
|
||||
}
|
||||
|
||||
.dot-anim::after {
|
||||
content: '.';
|
||||
animation: dotPulse 1.5s steps(3, end) infinite;
|
||||
@@ -363,3 +376,7 @@ h2.recipient-list {
|
||||
66% { content: '..'; }
|
||||
100% { content: '...'; }
|
||||
}
|
||||
|
||||
.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="sessionTimerHeading">
|
||||
Your session will end soon.
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
@@ -176,7 +176,7 @@
|
||||
{% block extra_javascripts %}
|
||||
{% endblock %}
|
||||
<script type="text/javascript" src="{{ asset_url('javascripts/all.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
<script src="{{ asset_url('js/uswds.min.js') }}"></script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{# Determine type of element to use, if not explicitly set -#}
|
||||
|
||||
{% if params.element %}
|
||||
{% set element = params.element | lower %}
|
||||
{% else %}
|
||||
@@ -10,26 +9,35 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{#- Define common attributes that we can use across all element types #}
|
||||
|
||||
{# Define common attributes to use across all element types -#}
|
||||
{%- set commonAttributes %} class="usa-button{% if params.classes %} {{ params.classes }}{% endif %}{% if params.disabled %} usa-button--disabled{% endif %}"{% for attribute, value in params.attributes %} {{attribute}}="{{value}}"{% endfor %}{% endset %}
|
||||
|
||||
{#- Define common attributes we can use for both button and input types #}
|
||||
|
||||
{# Define attributes for button/input -#}
|
||||
{%- set buttonAttributes %}{% if params.name %} name="{{ params.name | trim }}"{% endif %} type="{{ params.type if params.type else 'submit' }}"{% if params.disabled %} disabled="disabled" aria-disabled="true"{% endif %}{% if params.preventDoubleClick %} data-prevent-double-click="true"{% endif %}{% endset %}
|
||||
|
||||
{#- Actually create a button... or a link! #}
|
||||
|
||||
{%- if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{- commonAttributes | safe }}>
|
||||
{# Auto-append .dot-anim span for Send/Schedule buttons -#}
|
||||
{%- set isSendOrSchedule = params.name and (params.name | lower == 'send' or params.name | lower == 'schedule') -%}
|
||||
{%- set textContent %}
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</a>
|
||||
{%- if isSendOrSchedule -%}<span class="dot-anim" aria-hidden="true"></span>{%- endif -%}
|
||||
{%- endset %}
|
||||
|
||||
{%- elseif element == 'button' %}
|
||||
<button {%- if params.value %} value="{{ params.value }}"{% endif %} {{- buttonAttributes | safe }} {{- commonAttributes | safe }} {% if params.disabled %}disabled{% endif %}>
|
||||
{{ params.html | safe if params.html else params.text }}
|
||||
</button>
|
||||
{# Render the appropriate element -#}
|
||||
{% if element == 'a' %}
|
||||
<a href="{{ params.href if params.href else '#' }}" role="button" draggable="false" {{ commonAttributes | safe }}>
|
||||
{{ textContent | safe }}
|
||||
</a>
|
||||
|
||||
{%- elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{- buttonAttributes | safe }} {{- commonAttributes | safe }}>
|
||||
{%- endif %}
|
||||
{% elseif element == 'button' %}
|
||||
<button
|
||||
{% if params.value %} value="{{ params.value }}"{% endif %}
|
||||
{{ buttonAttributes | safe }}
|
||||
{{ commonAttributes | safe }}
|
||||
{% if params.disabled %}disabled{% endif %}
|
||||
>
|
||||
{{ textContent | safe }}
|
||||
</button>
|
||||
|
||||
{% elseif element == 'input' %}
|
||||
<input value="{{ params.text }}" {{ buttonAttributes | safe }} {{ commonAttributes | safe }}>
|
||||
{% endif %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -39,7 +39,7 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-4" }) }}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
{% if not error %}
|
||||
{% if choose_time_form %}
|
||||
{{ choose_time_form.scheduled_for(param_extensions={
|
||||
'formGroup': {'classes': 'bottom-gutter-2-3'},
|
||||
'formGroup': {'classes': ''},
|
||||
'attributes': {
|
||||
'data-module': 'radio-select',
|
||||
'data-categories': choose_time_form.scheduled_for.categories|join(','),
|
||||
@@ -66,8 +66,8 @@
|
||||
{% set button_text %}
|
||||
Preview
|
||||
{% endset %}
|
||||
{{ usaButton({ "text": button_text }) }}
|
||||
{% endif %}
|
||||
{{ usaButton({ "text": button_text, "classes": "margin-top-2" }) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
help='3' if help else 0
|
||||
)}}" class='page-footer'>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<h3>Does everything look good?</h3>
|
||||
<h3 class="margin-bottom-">Does everything look good?</h3>
|
||||
{% if not error %}
|
||||
{% set button_text %}
|
||||
{{ "Schedule" if scheduled_for else 'Send'}}
|
||||
@@ -84,9 +84,54 @@
|
||||
{{ usaButton({
|
||||
"text": button_text,
|
||||
"name": button_text
|
||||
}) }}
|
||||
}) }}
|
||||
{{ usaButton({
|
||||
"text": "Cancel",
|
||||
"name": "Cancel",
|
||||
"classes": "usa-button--secondary",
|
||||
"type": "button",
|
||||
"attributes": {
|
||||
"data-open-modal": "cancelModal"
|
||||
}
|
||||
}) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
data-module="usa-modal"
|
||||
id="cancelModal"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="cancelModalHeading"
|
||||
aria-describedby="cancelModalDesc"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="cancelModalHeading">Are you sure you want to cancel this message?</h2>
|
||||
<p id="cancelModalDesc">Your template is saved, but your message and recipient details will not be. Canceling will bring you back to the template selection page.</p>
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{{ url_for('main.choose_template', service_id=current_service.id) }}" class="usa-button">Yes, cancel
|
||||
<span class="usa-sr-only">and return to the template selection page</span></a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="usa-button usa-button--unstyled padding-105 text-center" data-close-modal type="button">
|
||||
No, go back <span class="usa-sr-only">to message send</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button class="usa-button usa-modal__close" aria-label="Close this window" data-close-modal type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{{ asset_url('img/sprite.svg') }}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="grid-row bottom-gutter">
|
||||
{% for noti_type in global_stats %}
|
||||
<div class="grid-col-6">
|
||||
<span class="big-number-dark bottom-gutter-2-3">
|
||||
<span class="big-number-dark">
|
||||
<span class="big-number-number">
|
||||
{{ "{:,}".format(noti_type.black_box.number) }}
|
||||
</span>
|
||||
|
||||
12
gulpfile.js
12
gulpfile.js
@@ -50,7 +50,7 @@ const javascripts = () => {
|
||||
paths.npm + 'textarea-caret/index.js',
|
||||
paths.npm + 'cbor-js/cbor.js',
|
||||
paths.npm + 'd3/dist/d3.min.js',
|
||||
paths.npm + 'socket.io-client/dist/socket.io.min.js',
|
||||
paths.npm + 'socket.io-client/dist/socket.io.min.js'
|
||||
])
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ const javascripts = () => {
|
||||
paths.src + 'javascripts/radioSlider.js',
|
||||
paths.src + 'javascripts/updateStatus.js',
|
||||
paths.src + 'javascripts/errorBanner.js',
|
||||
paths.src + 'javascripts/notifyModal.js',
|
||||
paths.src + 'javascripts/timeoutPopup.js',
|
||||
paths.src + 'javascripts/date.js',
|
||||
paths.src + 'javascripts/loginAlert.js',
|
||||
@@ -119,6 +120,12 @@ const copyPDF = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const copyUSWDSJS = () => {
|
||||
return src('node_modules/@uswds/uswds/dist/js/uswds.min.js')
|
||||
.pipe(dest(paths.dist + 'js/'));
|
||||
};
|
||||
|
||||
|
||||
// Configure USWDS paths
|
||||
uswds.settings.version = 3;
|
||||
uswds.paths.dist.css = paths.dist + 'css';
|
||||
@@ -172,7 +179,8 @@ exports.default = series(
|
||||
copySetTimezone,
|
||||
copyImages,
|
||||
copyPDF,
|
||||
copyAssets
|
||||
copyAssets,
|
||||
copyUSWDSJS
|
||||
);
|
||||
exports.backstopTest = backstopTest;
|
||||
exports.backstopReference = backstopReference;
|
||||
|
||||
161
tests/javascripts/notifyModal.test.js
Normal file
161
tests/javascripts/notifyModal.test.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
const { openModal, closeModal, attachModalTriggers } = require("../../app/assets/javascripts/notifyModal.js"); // adjust path if needed
|
||||
|
||||
describe("Modal functionality", () => {
|
||||
let modalWrapper, modalElement, openBtn, closeBtn, anotherFocusable;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<button data-open-modal="myModal">Open Modal</button>
|
||||
|
||||
<div id="myModal" class="is-hidden">
|
||||
<div class="usa-modal">
|
||||
<div class="usa-modal-overlay">
|
||||
<div class="usa-modal-content">
|
||||
<button data-close-modal>Close</button>
|
||||
<a href="#">Focusable Link</a>
|
||||
<input type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modalWrapper = document.getElementById("myModal");
|
||||
modalElement = modalWrapper.querySelector(".usa-modal");
|
||||
openBtn = document.querySelector('[data-open-modal]');
|
||||
closeBtn = modalWrapper.querySelector('[data-close-modal]');
|
||||
anotherFocusable = modalWrapper.querySelector('a');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
test("Opens the modal and sets focus to the first focusable element", () => {
|
||||
document.activeElement.blur(); // ensure focus starts elsewhere
|
||||
openModal("myModal");
|
||||
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(false);
|
||||
expect(modalElement.hasAttribute("aria-hidden")).toBe(false);
|
||||
expect(modalElement.hasAttribute("inert")).toBe(false);
|
||||
expect(modalElement.hasAttribute("hidden")).toBe(false);
|
||||
expect(document.body.classList.contains("modal-open")).toBe(true);
|
||||
expect(document.activeElement).toBe(closeBtn);
|
||||
});
|
||||
|
||||
test("Closes the modal and restores focus", () => {
|
||||
openBtn.focus();
|
||||
openModal("myModal");
|
||||
closeModal();
|
||||
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(true);
|
||||
expect(modalElement.getAttribute("aria-hidden")).toBe("true");
|
||||
expect(modalElement.hasAttribute("inert")).toBe(true);
|
||||
expect(modalElement.hasAttribute("hidden")).toBe(true);
|
||||
expect(document.body.classList.contains("modal-open")).toBe(false);
|
||||
expect(document.activeElement).toBe(openBtn);
|
||||
});
|
||||
|
||||
test("Closes the modal when pressing Escape", () => {
|
||||
openModal("myModal");
|
||||
|
||||
const event = new KeyboardEvent("keydown", { key: "Escape" });
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(true);
|
||||
});
|
||||
|
||||
test("Traps focus within the modal when Tab is pressed", () => {
|
||||
openModal("myModal");
|
||||
|
||||
const focusableElements = modalElement.querySelectorAll(
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
focusableElements[focusableElements.length - 1].focus(); // Last element
|
||||
|
||||
const tabEvent = new KeyboardEvent("keydown", {
|
||||
key: "Tab",
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
modalElement.dispatchEvent(tabEvent);
|
||||
expect(document.activeElement).toBe(focusableElements[0]);
|
||||
});
|
||||
|
||||
test("Traps focus backwards when Shift+Tab is pressed from first element", () => {
|
||||
openModal("myModal");
|
||||
|
||||
const focusableElements = modalElement.querySelectorAll(
|
||||
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
focusableElements[0].focus(); // First element
|
||||
|
||||
const shiftTabEvent = new KeyboardEvent("keydown", {
|
||||
key: "Tab",
|
||||
shiftKey: true,
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
modalElement.dispatchEvent(shiftTabEvent);
|
||||
expect(document.activeElement).toBe(focusableElements[focusableElements.length - 1]);
|
||||
});
|
||||
|
||||
test("Closes modal when clicking on overlay", () => {
|
||||
openModal("myModal");
|
||||
const overlay = modalElement.querySelector(".usa-modal-overlay");
|
||||
|
||||
const clickEvent = new MouseEvent("click", {
|
||||
bubbles: true
|
||||
});
|
||||
|
||||
overlay.dispatchEvent(clickEvent);
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Modal trigger buttons", () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<button data-open-modal="myModal">Open Modal</button>
|
||||
<div id="myModal" class="is-hidden">
|
||||
<div class="usa-modal">
|
||||
<div class="usa-modal-content">
|
||||
<button data-close-modal>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
test("Clicking [data-open-modal] opens the modal", () => {
|
||||
attachModalTriggers();
|
||||
const openButton = document.querySelector('[data-open-modal]');
|
||||
openButton.click();
|
||||
|
||||
const modalWrapper = document.getElementById("myModal");
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(false);
|
||||
});
|
||||
|
||||
test("Clicking [data-close-modal] closes the modal", () => {
|
||||
const modalWrapper = document.getElementById("myModal");
|
||||
modalWrapper.classList.remove("is-hidden");
|
||||
|
||||
attachModalTriggers();
|
||||
const closeButton = document.querySelector('[data-close-modal]');
|
||||
closeModal(); // ensure modal is open to begin with
|
||||
openModal("myModal");
|
||||
|
||||
closeButton.click();
|
||||
expect(modalWrapper.classList.contains("is-hidden")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ beforeAll(() => {
|
||||
<dialog class="usa-modal" id="sessionTimer" aria-labelledby="sessionTimerHeading" aria-describedby="timeLeft">
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="sessionTimerHeading">
|
||||
<h2 class="usa-modal__heading font-body-lg" id="sessionTimerHeading">
|
||||
Your session will end soon.
|
||||
<span class="usa-sr-only">Please choose to extend your session or sign out. Your session will expire in 5 minutes or less.</span>
|
||||
</h2>
|
||||
|
||||
Reference in New Issue
Block a user