mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-05 07:48:25 -04:00
Merge pull request #2440 from GSA/2138-cancel-button-CSV
2138 - Change cancel button to uploading button for CSV
This commit is contained in:
@@ -1,34 +1,69 @@
|
|||||||
|
function announceUploadStatusFromElement() {
|
||||||
|
const srRegion = document.getElementById('upload-status-live');
|
||||||
|
const success = document.getElementById('upload-success');
|
||||||
|
const error = document.getElementById('upload-error');
|
||||||
|
|
||||||
|
if (!srRegion) return;
|
||||||
|
|
||||||
|
const message = error?.textContent || success?.textContent;
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
srRegion.textContent = '';
|
||||||
|
setTimeout(() => {
|
||||||
|
srRegion.textContent = message + '\u00A0'; // add a non-breaking space
|
||||||
|
srRegion.focus(); // Optional
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Exported for use in tests
|
||||||
|
function initUploadStatusAnnouncer() {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
announceUploadStatusFromElement();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
(function(Modules) {
|
(function(Modules) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
Modules.FileUpload = function() {
|
Modules.FileUpload = function() {
|
||||||
|
|
||||||
this.submit = () => this.$form.trigger('submit');
|
this.submit = () => this.$form.trigger('submit');
|
||||||
|
|
||||||
this.showCancelButton = () => $('.file-upload-button', this.$form).replaceWith(`
|
this.showCancelButton = () => {
|
||||||
<a href="" class='usa-button usa-button--secondary'>Cancel upload</a>
|
$('.file-upload-button', this.$form).replaceWith(`
|
||||||
`);
|
<button class='usa-button uploading-button' aria-disabled="true" tabindex="0">
|
||||||
|
Uploading<span class="dot-anim" aria-hidden="true"></span>
|
||||||
this.start = function(component) {
|
</button>
|
||||||
|
`);
|
||||||
this.$form = $(component);
|
|
||||||
|
|
||||||
// The label gets styled like a button and is used to hide the native file upload control. This is so that
|
|
||||||
// users see a button that looks like the others on the site.
|
|
||||||
|
|
||||||
this.$form.find('label.file-upload-button').addClass('usa-button margin-bottom-1').attr( {role: 'button', tabindex: '0'} );
|
|
||||||
|
|
||||||
// Clear the form if the user navigates back to the page
|
|
||||||
$(window).on("pageshow", () => this.$form[0].reset());
|
|
||||||
|
|
||||||
// Need to put the event on the container, not the input for it to work properly
|
|
||||||
this.$form.on(
|
|
||||||
'change', '.file-upload-field',
|
|
||||||
() => this.submit() && this.showCancelButton()
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
this.start = function(component) {
|
||||||
|
this.$form = $(component);
|
||||||
|
|
||||||
|
this.$form.on('click', '[data-module="upload-trigger"]', function () {
|
||||||
|
const inputId = $(this).data('file-input-id');
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
if (fileInput) fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).on("pageshow", () => this.$form[0].reset());
|
||||||
|
|
||||||
|
this.$form.on('change', '.file-upload-field', () => {
|
||||||
|
this.submit();
|
||||||
|
this.showCancelButton();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
})(window.GOVUK.Modules);
|
})(window.GOVUK.Modules);
|
||||||
|
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = {
|
||||||
|
announceUploadStatusFromElement,
|
||||||
|
initUploadStatusAnnouncer
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
initUploadStatusAnnouncer();
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
this.$scrollableTable
|
this.$scrollableTable
|
||||||
.on('scroll', this.toggleShadows)
|
.on('scroll', this.toggleShadows)
|
||||||
.on('scroll', this.maintainHeight)
|
.on('scroll', this.maintainHeight)
|
||||||
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
window.GOVUK.stickAtBottomWhenScrolling &&
|
window.GOVUK.stickAtBottomWhenScrolling &&
|
||||||
@@ -37,11 +36,11 @@
|
|||||||
|
|
||||||
this.insertShims = () => {
|
this.insertShims = () => {
|
||||||
|
|
||||||
const attributesForFocus = 'role aria-labelledby tabindex';
|
const attributesForFocus = 'role aria-labelledby';
|
||||||
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
|
||||||
|
|
||||||
this.$table.find('caption').attr('id', captionId);
|
this.$table.find('caption').attr('id', captionId);
|
||||||
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
|
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}"/>`);
|
||||||
|
|
||||||
this.$component
|
this.$component
|
||||||
.append(
|
.append(
|
||||||
|
|||||||
@@ -344,3 +344,17 @@ h2.recipient-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button ellipses loading
|
||||||
|
|
||||||
|
.dot-anim::after {
|
||||||
|
content: '.';
|
||||||
|
animation: dotPulse 1.5s steps(3, end) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dotPulse {
|
||||||
|
0% { content: ''; }
|
||||||
|
33% { content: '.'; }
|
||||||
|
66% { content: '..'; }
|
||||||
|
100% { content: '...'; }
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@forward "uswds-theme";
|
@forward "uswds-theme";
|
||||||
@forward "uswds";
|
@forward "uswds";
|
||||||
@forward "uswds-theme-custom-styles";
|
@forward "uswds-theme-custom-styles";
|
||||||
@forward "legacy-styles";
|
@forward "main";
|
||||||
@forward "data-visualization";
|
@forward "data-visualization";
|
||||||
|
|||||||
@@ -11,6 +11,12 @@
|
|||||||
{% include "components/head.html" %}
|
{% include "components/head.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body class="usa-template__body {{ bodyClasses }}">
|
<body class="usa-template__body {{ bodyClasses }}">
|
||||||
|
<div id="upload-status-live"
|
||||||
|
class="usa-sr-only"
|
||||||
|
aria-live="assertive"
|
||||||
|
tabindex="-1"
|
||||||
|
role="status">
|
||||||
|
</div>
|
||||||
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
<script nonce="{{ csp_nonce() }}">document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
|
||||||
{% block bodyStart %}
|
{% block bodyStart %}
|
||||||
{% block extra_javascripts_before_body %}
|
{% block extra_javascripts_before_body %}
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
|
{% macro banner(body, type=None, with_tick=False, delete_button=None, subhead=None, context=None, action=None, id=None, thing=None) %}
|
||||||
<div
|
<div
|
||||||
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
|
class='banner{% if type %}-{{ type }}{% endif %}{% if with_tick %}-with-tick{% endif %}'
|
||||||
{% if type == 'dangerous' %}
|
|
||||||
role='group'
|
|
||||||
tabindex='-1'
|
|
||||||
{% endif %}
|
|
||||||
{% if id %}
|
{% if id %}
|
||||||
id={{ id }}
|
id={{ id }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -27,17 +27,21 @@
|
|||||||
</label>
|
</label>
|
||||||
{{ field(**{
|
{{ field(**{
|
||||||
'class': 'file-upload-field',
|
'class': 'file-upload-field',
|
||||||
|
'id': field.name,
|
||||||
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
|
'accept': allowed_file_extensions|format_list_items('.{item}')|join(',')|e
|
||||||
}) }}
|
}) }}
|
||||||
<label class="file-upload-button usa-button" for="{{ field.name }}">
|
<button type="button"
|
||||||
{{ button_text }}
|
class="usa-button file-upload-button"
|
||||||
</label>
|
data-module="upload-trigger"
|
||||||
|
data-file-input-id="{{ field.name }}">
|
||||||
|
{{ button_text }}
|
||||||
|
</button>
|
||||||
{% if alternate_link and alternate_link_text %}
|
{% if alternate_link and alternate_link_text %}
|
||||||
<span class="file-upload-alternate-link">
|
<span class="file-upload-alternate-link">
|
||||||
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
|
or <a class="usa-link" href="{{ alternate_link }}">{{ alternate_link_text }}</a>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<label class="file-upload-filename" for="{{ field.name }}"></label>
|
<span class="file-upload-filename" aria-live="polite"></span>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
|
{{ usaButton({ "text": "Submit", "classes": "file-upload-submit" }) }}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ Error
|
|||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
|
{# Alert for users of AT #}
|
||||||
|
<span id="upload-error" class="usa-sr-only">File upload failed</span>
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
{% call banner_wrapper(type='dangerous') %}
|
||||||
|
|
||||||
{% if recipients.too_many_rows %}
|
{% if recipients.too_many_rows %}
|
||||||
@@ -150,7 +154,7 @@ Error
|
|||||||
) }}
|
) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<a href="#content" class="usa-link back-to-top-link">Back to top</a>
|
<a href="#content" class="usa-link back-to-top-link display-block margin-top-2">Back to top</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
{{ template|string }}
|
{{ template|string }}
|
||||||
<div class="bottom-gutter-3-2">
|
<div class="bottom-gutter-3-2">
|
||||||
|
{# Alert for users of AT #}
|
||||||
|
<span id="upload-success" class="usa-sr-only">File uploaded successfully</span>
|
||||||
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
|
<form method="post" enctype="multipart/form-data" action="{{url_for('main.preview_job', service_id=current_service.id, template_id=template_id, upload_id=upload_id)}}" class='page-footer'>
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||||
{% if choose_time_form %}
|
{% if choose_time_form %}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
|
||||||
<div class="">
|
<div class="">
|
||||||
|
{# Alert for users of AT #}
|
||||||
|
<span id="upload-failed" class="usa-sr-only">File upload failed</span>
|
||||||
{% call banner_wrapper(type='dangerous') %}
|
{% call banner_wrapper(type='dangerous') %}
|
||||||
{% if row_errors|length == 1 %}
|
{% if row_errors|length == 1 %}
|
||||||
<div class="usa-alert usa-alert--error" role="alert">
|
<div class="usa-alert usa-alert--error" role="alert">
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
|
|
||||||
|
|
||||||
{% block backLink %}
|
{% block backLink %}
|
||||||
{{ usaBackLink(params) }}
|
{{ usaBackLink({
|
||||||
|
"href": url_for('main.send_one_off_step', service_id=current_service.id, template_id=template.id, step_index=0)
|
||||||
|
}) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% set phone_numbers = [
|
{% set phone_numbers = [
|
||||||
@@ -68,13 +70,14 @@
|
|||||||
show_errors=False
|
show_errors=False
|
||||||
)}}
|
)}}
|
||||||
</div>
|
</div>
|
||||||
|
<span id="upload-status-live" class="usa-sr-only" role="status" aria-live="polite" aria-atomic="true"></span>
|
||||||
|
|
||||||
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
|
||||||
|
|
||||||
<div class="spreadsheet" data-module="fullscreen-table">
|
<div class="spreadsheet" data-module="fullscreen-table">
|
||||||
{% call(item, row_number) list_table(
|
{% call(item, row_number) list_table(
|
||||||
example,
|
example,
|
||||||
caption="Example",
|
caption="Example spreadsheet",
|
||||||
caption_visible=False,
|
caption_visible=False,
|
||||||
field_headings=[''] + column_headings
|
field_headings=[''] + column_headings
|
||||||
) %}
|
) %}
|
||||||
|
|||||||
26
tests/javascripts/fileUpload.domLoad.test.js
Normal file
26
tests/javascripts/fileUpload.domLoad.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// This will use the real function and listener
|
||||||
|
const {
|
||||||
|
initUploadStatusAnnouncer
|
||||||
|
} = require('../../app/assets/javascripts/fileUpload.js');
|
||||||
|
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
test('writes upload message to the live region on DOMContentLoaded', () => {
|
||||||
|
// Setup the DOM
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="upload-status-live" aria-live="assertive" role="status" class="usa-sr-only">Old message</div>
|
||||||
|
<span id="upload-success" class="usa-sr-only">File upload successful</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Register the listener (same as page load does)
|
||||||
|
initUploadStatusAnnouncer();
|
||||||
|
|
||||||
|
// Simulate the page load event
|
||||||
|
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||||||
|
|
||||||
|
// Live region will be cleared first, then updated
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
const srRegion = document.getElementById('upload-status-live');
|
||||||
|
expect(srRegion.textContent).toBe('File upload successful\u00A0');
|
||||||
|
});
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
const helpers = require('./support/helpers.js');
|
const helpers = require('./support/helpers.js');
|
||||||
|
|
||||||
beforeAll(() => {
|
const { announceUploadStatusFromElement } = require('../../app/assets/javascripts/fileUpload.js');
|
||||||
require('../../app/assets/javascripts/fileUpload.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
require('./support/teardown.js');
|
require('./support/teardown.js');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('File upload', () => {
|
describe('File upload', () => {
|
||||||
|
|
||||||
let form;
|
let form;
|
||||||
@@ -72,12 +71,116 @@ describe('File upload', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("It should add a link to cancel the upload by reloading the page", () => {
|
test("It should display a disabled Uploading button", () => {
|
||||||
|
const uploadingButton = form.querySelector("button.uploading-button");
|
||||||
expect(form.querySelector("a[href='']")).not.toBeNull();
|
expect(uploadingButton).not.toBeNull();
|
||||||
|
expect(uploadingButton.textContent).toMatch(/Uploading/);
|
||||||
|
expect(uploadingButton.getAttribute('aria-disabled')).toBe("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('File upload "upload-trigger" click handler', () => {
|
||||||
|
let form;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<form method="post" enctype="multipart/form-data" data-module="file-upload">
|
||||||
|
<button type="button" data-module="upload-trigger" data-file-input-id="test-file-input">Upload your file</button>
|
||||||
|
<input type="file" id="test-file-input" style="display:none;">
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
form = document.querySelector('form');
|
||||||
|
|
||||||
|
// Register the module
|
||||||
|
window.GOVUK.modules.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking upload trigger simulates file input click', () => {
|
||||||
|
const uploadButton = form.querySelector('[data-module="upload-trigger"]');
|
||||||
|
const fileInput = document.getElementById('test-file-input');
|
||||||
|
|
||||||
|
// Spy on fileInput.click
|
||||||
|
const clickSpy = jest.spyOn(fileInput, 'click').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Trigger the click
|
||||||
|
helpers.triggerEvent(uploadButton, 'click');
|
||||||
|
|
||||||
|
expect(clickSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
clickSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('announceUploadStatusFromElement', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="upload-status-live" aria-live="assertive" role="status" class="usa-sr-only"></div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
test('announces error message from #upload-error', () => {
|
||||||
|
document.body.innerHTML += `
|
||||||
|
<span id="upload-error" class="usa-sr-only">File upload failed</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const srRegion = document.getElementById('upload-status-live');
|
||||||
|
|
||||||
|
// Call the function
|
||||||
|
announceUploadStatusFromElement();
|
||||||
|
|
||||||
|
// Confirm it clears first
|
||||||
|
expect(srRegion.textContent).toBe('');
|
||||||
|
|
||||||
|
// Fast-forward the timer
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
// Confirm it updates after delay
|
||||||
|
expect(srRegion.textContent).toBe('File upload failed\u00A0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('announces success message from #upload-success if no error is present', () => {
|
||||||
|
document.body.innerHTML += `
|
||||||
|
<span id="upload-success" class="usa-sr-only">File upload successful</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const srRegion = document.getElementById('upload-status-live');
|
||||||
|
|
||||||
|
announceUploadStatusFromElement();
|
||||||
|
|
||||||
|
expect(srRegion.textContent).toBe('');
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(301);
|
||||||
|
|
||||||
|
expect(srRegion.textContent).toBe('File upload successful\u00A0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does nothing if neither success nor error is present', () => {
|
||||||
|
const srRegion = document.getElementById('upload-status-live');
|
||||||
|
|
||||||
|
srRegion.textContent = 'Old message';
|
||||||
|
|
||||||
|
announceUploadStatusFromElement();
|
||||||
|
|
||||||
|
// Should not clear or update if no message element is found
|
||||||
|
expect(srRegion.textContent).toBe('Old message');
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
// Still unchanged
|
||||||
|
expect(srRegion.textContent).toBe('Old message');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -347,24 +347,4 @@ describe('FullscreenTable', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when the table is focused", () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
|
|
||||||
// start module
|
|
||||||
window.GOVUK.modules.start();
|
|
||||||
|
|
||||||
tableFrame = document.querySelector('.fullscreen-scrollable-table');
|
|
||||||
tableFrame.focus();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it should make the parent frame a focus style", () => {
|
|
||||||
|
|
||||||
expect(container.classList.contains('js-focus-style')).toBe(true);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user