Removed jquery from a few simple javascript files

This commit is contained in:
Alex Janousek
2025-10-20 18:44:33 -04:00
parent 43c076e504
commit c53c9f5c91
10 changed files with 160 additions and 360 deletions

View File

@@ -1 +1 @@
{"esversion": 6, "esnext": false}
{"esversion": 8, "esnext": false}

View File

@@ -1,94 +1,70 @@
(function(window) {
"use strict";
if (!document.queryCommandSupported('copy')) return;
// Only initialize if modern Clipboard API is available
if (!navigator.clipboard) return;
window.NotifyModules['copy-to-clipboard'] = function() {
const states = {
'valueVisible': (options) => `
<span class="copy-to-clipboard__value margin-bottom-1">${options.valueLabel ? '<span class="usa-sr-only">' + options.thing + ': </span>' : ''}${options.value}</span>
<span class="copy-to-clipboard__notice" aria-live="assertive">
${options.onload ? '' : options.thing + ' returned to page, press button to copy to clipboard'}
</span>
<button class="usa-button usa-button--outline copy-to-clipboard__button--copy">
Copy ${options.thing} to clipboard${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
</button>
`,
'valueCopied': (options) => `
<span class="copy-to-clipboard__notice" aria-live="assertive">
<span class="usa-sr-only">${options.thing} </span>Copied to clipboard<span class="usa-sr-only">, press button to show in page</span>
</span>
<button class="usa-button copy-to-clipboard__button--show">
Show ${options.thing}${options.name ? '<span class="usa-sr-only"> for ' + options.name + '</span>' : ''}
</button>
`
};
this.getRangeFromElement = function (copyableElement) {
const range = document.createRange();
const childNodes = Array.prototype.slice.call(copyableElement.childNodes);
let prefixIndex = -1;
childNodes.forEach((el, idx) => {
if ((el.nodeType === 1) && el.classList.contains('usa-sr-only')) {
prefixIndex = idx;
}
});
range.selectNodeContents(copyableElement);
if (prefixIndex !== -1) { range.setStart(copyableElement, prefixIndex + 1); }
return range;
};
this.copyValueToClipboard = function(copyableElement, callback) {
var selection = window.getSelection ? window.getSelection() : document.selection,
range = this.getRangeFromElement(copyableElement);
selection.removeAllRanges();
selection.addRange(range);
document.execCommand('copy');
selection.removeAllRanges();
callback();
/**
* Copy text to clipboard using modern Clipboard API
* @param {string} text - The text to copy
* @param {Function} callback - Called after successful copy
*/
this.copyValueToClipboard = async function(text, callback) {
try {
await navigator.clipboard.writeText(text);
callback();
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
this.start = function(component) {
const $component = $(component),
stateOptions = {
value: $component.data('value'),
thing: $component.data('thing')
},
name = $component.data('name');
const value = component.dataset.value;
const thing = component.dataset.thing;
const name = component.dataset.name;
// if the name is distinct from the thing:
// - it will be used in the rendering
// - the value won't be identified by a heading so needs its own label
if (name !== stateOptions.thing) {
stateOptions.name = name;
stateOptions.valueLabel = true;
}
// Determine button label
const isMultiple = name !== thing;
const buttonLabel = isMultiple
? `Copy ${thing}`
: `Copy ${thing} to clipboard`;
const srSuffix = isMultiple ? ` for ${name}` : '';
$component
.addClass('copy-to-clipboard')
.css('min-height', $component.height())
.html(states.valueVisible($.extend({ 'onload': true }, stateOptions)))
.on(
'click', '.copy-to-clipboard__button--copy', () =>
this.copyValueToClipboard(
$('.copy-to-clipboard__value', component)[0], () =>
$component
.html(states.valueCopied(stateOptions))
.find('.usa-button').focus()
)
)
.on(
'click', '.copy-to-clipboard__button--show', () =>
$component
.html(states.valueVisible(stateOptions))
.find('.usa-button').focus()
);
// Create simple HTML structure
component.classList.add('copy-to-clipboard');
component.innerHTML = `
<div class="copy-to-clipboard__value">${value}</div>
<button class="usa-button usa-button--outline copy-to-clipboard__button" type="button">
${buttonLabel}<span class="usa-sr-only">${srSuffix}</span>
</button>
<span class="usa-sr-only" aria-live="polite" aria-atomic="true"></span>
`;
const button = component.querySelector('.copy-to-clipboard__button');
const srAnnouncement = component.querySelector('[aria-live]');
// Handle copy button click
button.addEventListener('click', () => {
this.copyValueToClipboard(value, () => {
// Change button text to "Copied!"
const originalText = button.innerHTML;
button.innerHTML = `Copied!<span class="usa-sr-only">${srSuffix}</span>`;
button.disabled = true;
// Announce to screen readers
srAnnouncement.textContent = `${thing} copied to clipboard`;
// Reset button after 2 seconds
setTimeout(() => {
button.innerHTML = originalText;
button.disabled = false;
srAnnouncement.textContent = '';
}, 2000);
});
});
if ('stickAtBottomWhenScrolling' in window.NotifyModules) {
window.NotifyModules.stickAtBottomWhenScrolling.recalculate();

View File

@@ -4,15 +4,13 @@
window.NotifyModules['track-error'] = function() {
this.start = function(element) {
var component = $(element);
// Track error to analytics if available
if (window.NotifyModules && window.NotifyModules.analytics && window.NotifyModules.analytics.trackEvent) {
window.NotifyModules.analytics.trackEvent(
'Error',
component.data('error-type'),
element.dataset.errorType,
{
'label': component.data('error-label')
'label': element.dataset.errorLabel
}
);
}

View File

@@ -74,7 +74,6 @@ import './fileUpload.js';
import './errorTracking.js';
import './templateFolderForm.js';
import './collapsibleCheckboxes.js';
import './radioSlider.js';
import './updateStatus.js';
import './main.js';
import './listEntry.js';

View File

@@ -1,19 +1,23 @@
(function (window) {
'use strict';
var $ = window.jQuery;
window.NotifyModules.moduleSystem = {
find: function (container) {
container = container || $('body');
container = container || document.body;
var modules;
var moduleSelector = '[data-module]';
modules = container.find(moduleSelector);
// If container is already an element, use it directly
if (container instanceof Element) {
modules = Array.from(container.querySelectorAll(moduleSelector));
if (container.is(moduleSelector)) {
modules = modules.add(container);
// If the container itself is a module, include it
if (container.matches && container.matches(moduleSelector)) {
modules.unshift(container);
}
} else {
modules = Array.from(document.querySelectorAll(moduleSelector));
}
return modules;
@@ -25,16 +29,16 @@
for (var i = 0, l = modules.length; i < l; i++) {
try {
var module;
var element = $(modules[i]);
var type = this.camelCaseAndCapitalise(element.data('module'));
var started = element.data('module-started');
var element = modules[i];
var type = this.camelCaseAndCapitalise(element.dataset.module);
var started = element.dataset.moduleStarted;
if (typeof window.NotifyModules[type] === 'function' && !started) {
module = new window.NotifyModules[type]();
if (module.start) {
module.start(element);
}
element.data('module-started', true);
element.dataset.moduleStarted = 'true';
}
} catch (error) {
console.error('Failed to initialize module:', type || 'unknown', error);

View File

@@ -1,28 +0,0 @@
(function(window) {
"use strict";
window.NotifyModules['radio-slider'] = function() {
this.start = function(component) {
$(component)
.on('click', function() {
valuesInLabel = $(this).find(':checked').next('label').text().split('/');
if (valuesInLabel.length === 2) {
leftValue = valuesInLabel[0];
rightValue = valuesInLabel[1];
$(this).find('.radio-slider-left-value').text(leftValue);
$(this).find('.radio-slider-right-value').text(rightValue);
}
})
.trigger('click');
};
};
})(window);

View File

@@ -58,7 +58,6 @@ const javascripts = () => {
const notifyModules2 = src([
paths.src + 'javascripts/collapsibleCheckboxes.js',
paths.src + 'javascripts/radioSlider.js',
paths.src + 'javascripts/updateStatus.js',
paths.src + 'javascripts/timeoutPopup.js',
paths.src + 'javascripts/main.js',

View File

@@ -153,7 +153,9 @@ def _cleanup_test_service(page: Page, service_name: str):
print(f"Successfully cleaned up test service: {service_name}") # noqa: T201
else:
print(f"Service '{service_name}' not found, may have been deleted already") # noqa: T201
print( # noqa: T201
f"Service '{service_name}' not found, may have been deleted already"
)
except Exception as e:
raise Exception(f"Failed to cleanup service '{service_name}': {str(e)}")

View File

@@ -5,45 +5,30 @@ afterAll(() => {
require('./support/teardown.js');
// clear up methods in the global space
document.queryCommandSupported = undefined;
navigator.clipboard = undefined;
});
describe('copy to clipboard', () => {
let apiKey = '00000000-0000-0000-0000-000000000000';
let thing;
let component;
let selectionMock;
let rangeMock;
let screenMock;
let clipboardWriteTextMock;
const setUpDOM = function (options) {
// set up DOM
document.body.innerHTML =`
<h2 class="copy-to-clipboard__name">
${options.thing}
</h2>
<div data-module="copy-to-clipboard" data-value="${apiKey}" data-thing="${options.thing}" data-name="${options.name}">
<span class="copy-to-clipboard__value" aria-live="assertive">${(options.name === options.thing) ? '<span class="usa-sr-only">' + options.thing + ': </span>' : ''}${apiKey}</span>
<span class="copy-to-clipboard__notice" aria-live="assertive"></span>
</div>`;
<div data-module="copy-to-clipboard" data-value="${apiKey}" data-thing="${options.thing}" data-name="${options.name}"></div>`;
};
beforeEach(() => {
// mock objects used to manipulate the page selection
selectionMock = new helpers.SelectionMock(jest);
rangeMock = new helpers.RangeMock(jest);
// plug gaps in JSDOM's API for manipulation of selections
window.getSelection = jest.fn(() => selectionMock);
document.createRange = jest.fn(() => rangeMock);
// plug JSDOM not having execCommand
document.execCommand = jest.fn(() => {});
// Reset the clipboard mock for each test
if (clipboardWriteTextMock) {
clipboardWriteTextMock.mockClear();
}
// mock sticky JS
window.NotifyModules.stickAtBottomWhenScrolling = {
@@ -52,10 +37,10 @@ describe('copy to clipboard', () => {
});
test("If copy command isn't available, nothing should happen", () => {
test("If Clipboard API isn't available, nothing should happen", () => {
// fake support for the copy command not being available
document.queryCommandSupported = jest.fn(command => false);
// Remove clipboard API
navigator.clipboard = undefined;
require('../../app/assets/javascripts/copyToClipboard.js');
@@ -70,14 +55,15 @@ describe('copy to clipboard', () => {
});
describe("If copy command is available", () => {
let componentHeightOnLoad;
describe("If Clipboard API is available", () => {
beforeAll(() => {
// assume copy command is available
document.queryCommandSupported = jest.fn(command => command === 'copy');
// Mock modern Clipboard API BEFORE loading the module
clipboardWriteTextMock = jest.fn(() => Promise.resolve());
navigator.clipboard = {
writeText: clipboardWriteTextMock
};
// force module require to not come from cache
jest.resetModules();
@@ -86,10 +72,6 @@ describe('copy to clipboard', () => {
});
afterEach(() => {
screenMock.reset();
});
describe("On page load", () => {
describe("For all variations of the initial HTML", () => {
@@ -100,26 +82,6 @@ describe('copy to clipboard', () => {
component = document.querySelector('[data-module=copy-to-clipboard]');
// set default style for component height (queried by jQuery before checking DOM APIs)
const stylesheet = document.createElement('style');
stylesheet.innerHTML = '[data-module=copy-to-clipboard] { height: auto; }'; // set to browser default
document.getElementsByTagName('head')[0].appendChild(stylesheet);
componentHeightOnLoad = 50;
// mock the DOM APIs called for the position & dimension of the component
screenMock = new helpers.ScreenMock(jest);
screenMock.setWindow({
width: 1990,
height: 940,
scrollTop: 0
});
screenMock.mockPositionAndDimension('component', component, {
offsetHeight: componentHeightOnLoad,
offsetWidth: 641,
offsetTop: 0
});
// start the module
window.NotifyModules.start();
@@ -128,6 +90,7 @@ describe('copy to clipboard', () => {
test("It should add a button for copying the thing to the clipboard", () => {
expect(component.querySelector('button')).not.toBeNull();
expect(component.querySelector('button').textContent.trim()).toContain('Copy Some Thing');
});
@@ -139,21 +102,23 @@ describe('copy to clipboard', () => {
test("It should tell any sticky JS present the page has changed", () => {
// recalculate forces the sticky JS to recalculate any stored DOM position/dimensions
expect(window.NotifyModules.stickAtBottomWhenScrolling.recalculate).toHaveBeenCalled();
});
test("It should set the component's minimum height based on its height when the page loads", () => {
test("It should display the value", () => {
// to prevent the position of the button moving when the state changes
expect(window.getComputedStyle(component)['min-height']).toEqual(`${componentHeightOnLoad}px`);
expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe(apiKey);
});
test("It should render the 'thing' without extra whitespace", () => {
test("It should have an aria-live region for screen reader announcements", () => {
expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe('00000000-0000-0000-0000-000000000000');
const liveRegion = component.querySelector('[aria-live]');
expect(liveRegion).not.toBeNull();
expect(liveRegion.getAttribute('aria-live')).toBe('polite');
expect(liveRegion.getAttribute('aria-atomic')).toBe('true');
});
@@ -163,8 +128,6 @@ describe('copy to clipboard', () => {
beforeEach(() => {
// If 'thing' (what the id is) and 'name' (its specific idenifier on the page) are
// different, it will be one of others called the same 'thing'.
setUpDOM({ 'thing': 'ID', 'name': 'Default' });
component = document.querySelector('[data-module=copy-to-clipboard]');
@@ -174,19 +137,12 @@ describe('copy to clipboard', () => {
});
// Because it is not the only 'thing' on the page, the id will not have a heading
// and so needs some prefix text to label it
test("The id should have a hidden prefix to label what it is", () => {
test("the button should have shorter label and hidden suffix", () => {
const value = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
expect(value).not.toBeNull();
expect(value.textContent).toEqual('ID: ');
const button = component.querySelector('button');
expect(button.textContent).toContain('Copy ID');
});
test("the button should have a hidden suffix naming the id it is for", () => {
const buttonSuffix = component.querySelector('button .usa-sr-only');
const buttonSuffix = button.querySelector('.usa-sr-only');
expect(buttonSuffix).not.toBeNull();
expect(buttonSuffix.textContent).toEqual(' for Default');
@@ -198,9 +154,6 @@ describe('copy to clipboard', () => {
beforeEach(() => {
// The heading is added if 'thing' (what the id is) has the same value as 'name'
// (its specific identifier on the page) because this means it can assume it is
// the only one of its type there
setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
component = document.querySelector('[data-module=copy-to-clipboard]');
@@ -210,12 +163,11 @@ describe('copy to clipboard', () => {
});
test("Its button and id shouldn't have extra hidden text to identify them", () => {
test("the button should have full label without extra suffix", () => {
const value = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
const buttonSuffix = component.querySelector('button .usa-sr-only');
expect(value).toBeNull();
expect(buttonSuffix).toBeNull();
const button = component.querySelector('button');
expect(button.textContent).toContain('Copy Some Thing to clipboard');
expect(button.textContent).not.toContain(' for ');
})
@@ -223,187 +175,84 @@ describe('copy to clipboard', () => {
});
describe("If you click the 'Copy Some Thing to clipboard' button", () => {
describe("If you click the 'Copy' button", () => {
describe("For all variations of the initial HTML", () => {
beforeEach(async () => {
let keyEl;
jest.useFakeTimers();
beforeEach(() => {
setUpDOM({ 'thing': 'API key', 'name': 'API key' });
setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
component = document.querySelector('[data-module=copy-to-clipboard]');
// start the module
window.NotifyModules.start();
// start the module
window.NotifyModules.start();
component = document.querySelector('[data-module=copy-to-clipboard]');
keyEl = component.querySelector('.copy-to-clipboard__value');
const button = component.querySelector('button');
helpers.triggerEvent(button, 'click');
helpers.triggerEvent(component.querySelector('button'), 'click');
});
test("The live-region should be shown and its text should confirm the copy action", () => {
const liveRegion = component.querySelector('.copy-to-clipboard__notice');
expect(liveRegion.classList.contains('usa-sr-only')).toBe(false);
expect(liveRegion.textContent.trim()).toEqual(
expect.stringContaining('Copied to clipboard')
);
});
// The button also says this but its text after being changed is not announced due to being
// lower priority than the live-region
test("The live-region should contain some hidden text giving context to the statement shown", () => {
const liveRegionHiddenText = component.querySelectorAll('.copy-to-clipboard__notice .usa-sr-only');
expect(liveRegionHiddenText.length).toEqual(2);
expect(liveRegionHiddenText[0].textContent).toEqual('Some Thing ');
expect(liveRegionHiddenText[1].textContent).toEqual(', press button to show in page');
});
test("It should swap the button for one to show the Some Thing", () => {
expect(component.querySelector('button').textContent.trim()).toEqual(
expect.stringContaining('Show Some Thing')
);
});
test("It should remove the id from the page", () => {
expect(component.querySelector('.copy-to-clipboard__value')).toBeNull();
});
test("It should copy the thing to the clipboard", () => {
// it should make a selection (a range) from the contents of the element containing the Some Thing
expect(rangeMock.selectNodeContents.mock.calls[0]).toEqual([keyEl]);
// that selection (a range) should be added to that for the page (a selection)
expect(selectionMock.addRange.mock.calls[0]).toEqual([rangeMock]);
expect(document.execCommand).toHaveBeenCalled();
expect(document.execCommand.mock.calls[0]).toEqual(['copy']);
// reset any methods in the global space
window.queryCommandSupported = undefined;
window.getSelection = undefined;
document.createRange = undefined;
});
describe("If you then click the 'Show Some Thing'", () => {
beforeEach(() => {
helpers.triggerEvent(component.querySelector('button'), 'click');
});
test("It should change the text to show the Some Thing", () => {
expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
});
test("It should swap the button for one to copy the thing to the clipboard", () => {
expect(component.querySelector('button').textContent.trim()).toEqual(
expect.stringContaining('Copy Some Thing to clipboard')
);
})
});
// Wait for async clipboard operation
await Promise.resolve();
});
describe("If it's one of many in the page", () => {
afterEach(() => {
jest.useRealTimers();
});
beforeEach(() => {
test("The value should still be visible", () => {
// If 'thing' (what the id is) and 'name' (its specific idenifier on the page) are
// different, it will be one of others called the same 'thing'.
setUpDOM({ 'thing': 'ID', 'name': 'Default' });
// start the module
window.NotifyModules.start();
component = document.querySelector('[data-module=copy-to-clipboard]');
helpers.triggerEvent(component.querySelector('button'), 'click');
});
test("the button should have a hidden suffix naming the id it is for", () => {
const buttonSuffix = component.querySelector('button .usa-sr-only');
expect(buttonSuffix).not.toBeNull();
expect(buttonSuffix.textContent).toEqual(' for Default');
});
test("the copied selection (range) should start after visually hidden prefix", () => {
// that selection (a range) should have a startOffset of 1:
// index 0: the visually hidden prefix node, for example "Template ID: " or "API key: "
// index 1: the value node
expect(rangeMock.setStart).toHaveBeenCalled();
expect(rangeMock.setStart.mock.calls[0][1]).toEqual(1);
// reset any methods in the global space
window.queryCommandSupported = undefined;
window.getSelection = undefined;
document.createRange = undefined;
});
expect(component.querySelector('.copy-to-clipboard__value')).not.toBeNull();
expect(component.querySelector('.copy-to-clipboard__value').textContent).toBe(apiKey);
});
describe("If it's the only one on the page", () => {
test("The button text should change to 'Copied!'", () => {
beforeEach(() => {
const button = component.querySelector('button');
expect(button.textContent).toContain('Copied!');
// The heading is added if 'thing' (what the id is) has the same value as 'name'
// (its specific identifier on the page) because this means it can assume it is
// the only one of its type there
setUpDOM({ 'thing': 'Some Thing', 'name': 'Some Thing' });
});
// start the module
window.NotifyModules.start();
test("The button should be disabled", () => {
component = document.querySelector('[data-module=copy-to-clipboard]');
const button = component.querySelector('button');
expect(button.disabled).toBe(true);
helpers.triggerEvent(component.querySelector('button'), 'click');
});
});
test("Screen reader announcement should confirm the copy", () => {
test("Its button and id shouldn't have extra hidden text to identify them", () => {
const liveRegion = component.querySelector('[aria-live]');
expect(liveRegion.textContent).toBe('API key copied to clipboard');
const prefix = component.querySelector('.copy-to-clipboard__value .usa-sr-only');
const buttonSuffix = component.querySelector('button .usa-sr-only');
expect(prefix).toBeNull();
expect(buttonSuffix).toBeNull();
});
})
test("It should copy the value to clipboard using Clipboard API", () => {
test("the copied selection (range) should start at the default position", () => {
expect(clipboardWriteTextMock).toHaveBeenCalledWith(apiKey);
// that selection (a range) shouldn't call setStart to avoid the prefix:
expect(rangeMock.setStart).not.toHaveBeenCalled();
});
// reset any methods in the global space
window.queryCommandSupported = undefined;
window.getSelection = undefined;
document.createRange = undefined;
test("After 2 seconds, button should reset to original text", () => {
});
const button = component.querySelector('button');
jest.advanceTimersByTime(2000);
expect(button.textContent).toContain('Copy API key');
expect(button.textContent).not.toContain('Copied!');
expect(button.disabled).toBe(false);
});
test("After 2 seconds, screen reader announcement should clear", () => {
const liveRegion = component.querySelector('[aria-live]');
jest.advanceTimersByTime(2000);
expect(liveRegion.textContent).toBe('');
});

View File

@@ -2,7 +2,8 @@ module.exports = {
collectCoverage: true,
coverageDirectory: './coverage',
coveragePathIgnorePatterns: [
'support/polyfills.js'
'support/polyfills.js',
'support/helpers/'
],
coverageThreshold: {
global: {