Compare commits

...

13 Commits

Author SHA1 Message Date
Jonathan Bobel
2905fa9426 Update main.test.js
Adding test for main.js
2025-04-16 17:07:22 -04:00
Jonathan Bobel
f98884b858 Update main.js 2025-04-16 15:47:34 -04:00
Jonathan Bobel
19bc1b9aee Removing js for autofocus 2025-04-16 14:32:46 -04:00
Jonathan Bobel
53034b7e89 Merge branch 'main' of https://github.com/GSA/notifications-admin into 2489-clean-up-js-audit-and-cleanup 2025-04-16 11:24:22 -04:00
Jonathan Bobel
81d4fc0b8c Merge branch 'main' of https://github.com/GSA/notifications-admin into 2489-clean-up-js-audit-and-cleanup 2025-04-14 14:27:36 -04:00
Jonathan Bobel
0b441e36f1 More js cleanup 2025-04-14 14:27:22 -04:00
Jonathan Bobel
1bf7e58445 Merge branch 'main' of https://github.com/GSA/notifications-admin into 2489-clean-up-js-audit-and-cleanup 2025-04-14 11:41:20 -04:00
Jonathan Bobel
487866d81c Updating tests for the table 2025-04-09 14:52:56 -04:00
Jonathan Bobel
b71a941634 Update _legacy-styles.scss 2025-04-09 12:07:32 -04:00
Jonathan Bobel
c599407850 Merge branch 'main' of https://github.com/GSA/notifications-admin into 2489-clean-up-js-audit-and-cleanup 2025-04-09 11:38:44 -04:00
Jonathan Bobel
42ac6912b0 Simplifying table js 2025-04-09 11:38:36 -04:00
Jonathan Bobel
6f6fd01ea2 Merge branch 'main' of https://github.com/GSA/notifications-admin into 2489-clean-up-js-audit-and-cleanup 2025-04-08 10:15:50 -04:00
Jonathan Bobel
9921809f90 2489 - Clean up site js 2025-04-07 15:25:29 -04:00
24 changed files with 315 additions and 848 deletions

View File

@@ -1,27 +0,0 @@
(function(Modules) {
"use strict";
Modules.Autofocus = function() {
this.start = function(component) {
var $component = $(component),
forceFocus = $component.data('forceFocus');
// if the page loads with a scroll position, we can't assume the item to focus onload
// is still where users intend to start
if (($(window).scrollTop() > 0) && !forceFocus) { return; }
// See if the component itself is something we want to send focus to
var target = $component.filter('input, textarea, select');
// Otherwise look inside the component to see if there are any elements
// we want to send focus to
if (target.length === 0) {
target = $('input, textarea, select', $component);
}
target.eq(0).trigger('focus');
};
};
})(window.GOVUK.Modules);

View File

@@ -1,29 +0,0 @@
(function(Modules) {
"use strict";
let isSixDigitHex = value => value.match(/^#[0-9A-F]{6}$/i);
let colourOrWhite = value => isSixDigitHex(value) ? value : '#FFFFFF';
Modules.ColourPreview = function() {
this.start = component => {
this.$input = $(component);
this.$input.closest('.usa-form-group').append(
this.$preview = $('<span class="textbox-colour-preview"></span>')
);
this.$input
.on('input', this.update)
.trigger('input');
};
this.update = () => this.$preview.css(
'background', colourOrWhite(this.$input.val())
);
};
})(window.GOVUK.Modules);

View File

@@ -1,124 +1,41 @@
(function(Modules) {
"use strict";
(function () {
'use strict';
Modules.FullscreenTable = function() {
function initFullscreenTables() {
const tables = document.querySelectorAll('.fullscreen-table');
this.start = function(component) {
tables.forEach((component) => {
const table = component.querySelector('table');
const wrapper = component.querySelector('.table-wrapper');
const rightShadow = component.querySelector('.fullscreen-right-shadow');
this.$component = $(component);
this.$table = this.$component.find('table');
this.nativeHeight = this.$component.innerHeight() + 20; // 20px to allow room for scrollbar
this.topOffset = this.$component.offset().top;
if (!table || !wrapper) return;
this.insertShims();
this.maintainWidth();
this.maintainHeight();
this.toggleShadows();
const toggleShadows = () => {
const scrollLeft = wrapper.scrollLeft;
const maxScrollLeft = wrapper.scrollWidth - wrapper.clientWidth;
$(window)
.on('scroll resize', this.maintainHeight)
.on('resize', this.maintainWidth);
wrapper.classList.toggle('scrolled', scrollLeft > 0);
this.$scrollableTable
.on('scroll', this.toggleShadows)
.on('scroll', this.maintainHeight)
.on('focus blur', () => this.$component.toggleClass('js-focus-style'));
if (rightShadow) {
rightShadow.classList.toggle('visible', scrollLeft < maxScrollLeft);
}
};
if (
window.GOVUK.stickAtBottomWhenScrolling &&
window.GOVUK.stickAtBottomWhenScrolling.recalculate
) {
window.GOVUK.stickAtBottomWhenScrolling.recalculate();
}
wrapper.addEventListener('scroll', toggleShadows);
toggleShadows(); // init state
});
}
this.maintainWidth();
// Auto-init on page load in browser
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initFullscreenTables);
} else {
initFullscreenTables();
}
};
this.insertShims = () => {
const attributesForFocus = 'role aria-labelledby tabindex';
let captionId = this.$table.find('caption').text().toLowerCase().replace(/[^A-Za-z]+/g, '');
this.$table.find('caption').attr('id', captionId);
this.$table.wrap(`<div class="fullscreen-scrollable-table" role="region" aria-labelledby="${captionId}" tabindex="0"/>`);
this.$component
.append(
this.$component.find('.fullscreen-scrollable-table')
.clone()
.addClass('fullscreen-fixed-table')
.removeClass('fullscreen-scrollable-table')
.removeAttr(attributesForFocus)
.attr('aria-hidden', true)
.find('caption')
.removeAttr('id')
.closest('.fullscreen-fixed-table')
)
.append(
'<div class="fullscreen-right-shadow" />'
)
.after(
$("<div class='fullscreen-shim'/>").css({
'height': this.nativeHeight,
'top': this.topOffset
})
)
.css('position', 'absolute');
this.$scrollableTable = this.$component.find('.fullscreen-scrollable-table');
this.$fixedTable = this.$component.find('.fullscreen-fixed-table');
};
this.maintainHeight = () => {
let height = Math.min(
$(window).height() - this.topOffset + $(window).scrollTop(),
this.nativeHeight
);
this.$scrollableTable.outerHeight(height);
this.$fixedTable.outerHeight(height);
};
this.maintainWidth = () => {
let indexColumnWidth = this.$fixedTable.find('.table-field-index').outerWidth();
this.$scrollableTable
.css({
'width': this.$component.parent('main').width() - indexColumnWidth,
'margin-left': indexColumnWidth
});
this.$fixedTable
.width(indexColumnWidth + 4);
};
this.toggleShadows = () => {
this.$fixedTable
.toggleClass(
'fullscreen-scrolled-table',
this.$scrollableTable.scrollLeft() > 0
);
this.$component.find('.fullscreen-right-shadow')
.toggleClass(
'visible',
this.$scrollableTable.scrollLeft() < (this.$table.width() - this.$scrollableTable.width())
);
setTimeout(
() => this.$component.find('.fullscreen-right-shadow').addClass('with-transition'),
3000
);
};
};
})(window.GOVUK.Modules);
// Expose it globally in test environments
if (typeof window !== 'undefined') {
window.initFullscreenTables = initFullscreenTables;
}
})();

View File

@@ -1,24 +0,0 @@
(function(window) {
"use strict";
window.GOVUK.Modules.Homepage = function() {
this.start = function(component) {
let $component = $(component);
let iterations = 0;
let timeout = null;
$component.on('click', () => {
if (++iterations == 5) {
$component.toggleClass('product-page-intro-wrapper--alternative');
}
clearTimeout(timeout);
timeout = setTimeout(() => iterations = 0, 1500);
});
};
};
})(window);

View File

@@ -1,27 +1,38 @@
window.GOVUK.Frontend.initAll();
$(() => $("time.timeago").timeago());
// Initialize GOV.UK Frontend components (modern init)
window.GOVUK.Frontend?.initAll?.();
var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init();
$(() => GOVUK.modules.start());
$(() => $('.error-message, .usa-error-message').eq(0).parent('label').next('input').trigger('focus'));
$(() => $('.govuk-header__container').on('click', function() {
$(this).css('border-color', '#005ea5');
}));
// Applies our expanded focus style to the siblings of links when that link is wrapped in a heading.
//
// This will be possible in CSS in the future, using the :has pseudo-class. When :has is available
// in the browsers we support, this code can be replaced with a CSS-only solution.
$('.js-mark-focus-on-parent').on('focus blur', '*', e => {
$target = $(e.target);
if (e.type === 'focusin') {
$target.parent().addClass('js-child-has-focus');
} else {
$target.parent().removeClass('js-child-has-focus');
document.addEventListener('DOMContentLoaded', () => {
// Autofocus the first error input
const errorEl = document.querySelector('.error-message, .usa-error-message');
if (errorEl) {
const label = errorEl.closest('label');
if (label) {
const input = label.nextElementSibling;
if (input && input.focus) {
input.focus();
}
}
}
// Optional: highlight header on click
const header = document.querySelector('.govuk-header__container');
if (header) {
header.addEventListener('click', () => {
header.style.borderColor = '#005ea5';
});
}
// Focus styling for heading-wrapped links
const containers = document.querySelectorAll('.js-mark-focus-on-parent');
containers.forEach(container => {
container.addEventListener('focusin', e => {
e.target.parentElement.classList.add('js-child-has-focus');
});
container.addEventListener('focusout', e => {
e.target.parentElement.classList.remove('js-child-has-focus');
});
});
});

View File

@@ -174,8 +174,10 @@ h2.recipient-list {
}
.spreadsheet {
margin-bottom: units (1);
margin-bottom: units(1);
overflow-x: auto;
position: relative;
padding-left: 0;
.table {
margin-bottom: 0;
@@ -186,27 +188,56 @@ h2.recipient-list {
}
th,
.table-field-index {
background: color('gray-cool-10');
td {
padding-inline: 10px;
border: 1px solid color('gray-cool-10');
background: white;
z-index: 1;
}
td {
min-width: calc(194px - 11px);
&:first-child {
min-width: auto;
}
}
th.table-field-heading {
background: color('gray-cool-10');
font-weight: bold;
text-align: center;
}
th, td {
padding-left: 10px;
padding-right: 10px;
border: 1px solid color('gray-cool-10');
}
.table-field-index,
.table-field-heading-first {
position: sticky;
left: 0;
z-index: 10;
background: white;
text-align: center;
width: units(2);
td {
border-top: 0;
// 194 is the width of the table * 1/3.5, so the overflow cuts off
// at 3.5 columns wide.
// 11 accounts for the padding of the table cell
min-width: 194px - 11px;
&:first-child {
min-width: auto;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0px;
z-index: 11;
outline: 1px solid color('gray-cool-10');
}
&::after {
content: "";
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 1px;
background: color('gray-cool-10');
z-index: 11;
}
}
@@ -344,3 +375,9 @@ h2.recipient-list {
}
}
}
.usa-search__input, [type=search] {
border-right: 1px solid;
height: 40px;
margin-top: units(1);
}

View File

@@ -722,7 +722,6 @@ details form {
.placeholder,
.placeholder-conditional {
background-color: #face00;
border: 1px solid #face00;
border-radius: 7px;
}

View File

@@ -3,7 +3,7 @@
show=False,
form=None,
label=None,
autofocus=False
autofocus=True
) %}
{%- set search_label = label or form.search.label.text %}
@@ -13,11 +13,11 @@
} %}
{% if autofocus %}
{% set x=param_extensions.__setitem__("attributes", {"data-module": "autofocus"}) %}
{% set _ = param_extensions.__setitem__("attributes", {"autofocus": "autofocus"}) %}
{% endif %}
{% if show %}
<div class="live-search js-header margin-top-0" data-module="live-search" data-targets="{{ target_selector }}">
<div class="live-search js-header" data-targets="{{ target_selector }}">
{{ form.search(param_extensions=param_extensions) }}
<div aria-live="polite" class="live-search__status usa-sr-only"></div>
</div>

View File

@@ -57,7 +57,7 @@
{{notification.key_name}}
</span>
<span class="grid-col-6 api-notifications-item__meta-time">
<time class="timeago" datetime="{{ notification.created_at }}">
<time datetime="{{ notification.created_at }}">
{{ notification.created_at|format_delta }}
</time>
</span>

View File

@@ -1,7 +1,7 @@
{% extends "withnav_template.html" %}
{% from "components/banner.html" import banner_wrapper %}
{% from "components/radios.html" import radio_select %}
{% from "components/table.html" import list_table, field, text_field, index_field, hidden_field_heading %}
{% from "components/table.html" import list_table, field, text_field, index_field, index_field_heading %}
{% from "components/file-upload.html" import file_upload %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
@@ -161,18 +161,15 @@ recipients.column_headers %}
<h2 class="font-body-lg" id="file-preview">{{ original_file_name }}</h2>
<div class="fullscreen-content" data-module="fullscreen-table">
<div class="table-wrapper spreadsheet" data-module="fullscreen-table">
{% call(item, row_number) list_table(
recipients.displayed_rows,
caption=original_file_name,
caption_visible=False,
field_headings=[
'<span class="usa-sr-only">Row in file</span><span aria-hidden="true">1</span>'|safe
] + column_headers
) %}
field_headings=[''] + column_headers )%}
{% call index_field() %}
<span>
{% set displayed_index = item.index + 2 %}
{% set displayed_index = item.index + 1 %}
{{ displayed_index }}
</span>
{% endcall %}

View File

@@ -55,7 +55,7 @@
</div>
</div>
<div class="fullscreen-content" data-module="fullscreen-table">
<div class="table-wrapper spreadsheet" data-module="fullscreen-table">
{% call(item, row_number) mapping_table(
caption="Errors in " + original_file_name,
caption_visible=False,

View File

@@ -60,7 +60,7 @@
<p class="hint">This person has never logged in</p>
{% else %}
<p>Last logged in
<time class="timeago" datetime="{{ user.logged_in_at }}">
<time datetime="{{ user.logged_in_at }}">
{{ user.logged_in_at|format_delta }}
</time>
</p>

View File

@@ -30,13 +30,13 @@
</div>
{% if show_search_box %}
<div data-module="autofocus">
<div>
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
</div>
{% endif %}
<div class="user-list">
{% for user in users %}
<div class="user-list">
{% if user.status != 'cancelled' %}
<div class="user-list-item">
<h2 class="user-list-item-heading font-body-lg margin-top-0" title="{{ user.email_address }}">
@@ -101,7 +101,7 @@
{% endif %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -24,13 +24,13 @@
</h1>
{% if show_search_box %}
<div data-module="autofocus">
<div>
{{ live_search(target_selector='.user-list-item', show=True, form=form) }}
</div>
{% endif %}
<div class="user-list">
{% for user in users %}
<div class="user-list">
<div class="user-list-item">
<div class="grid-row">
<div class="grid-col-9">
@@ -60,8 +60,8 @@
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="js-stick-at-bottom-when-scrolling">
{{ usaButton({

View File

@@ -5,6 +5,7 @@
{% from "components/table.html" import list_table, text_field, index_field, index_field_heading %}
{% from "components/components/back-link/macro.njk" import usaBackLink %}
<script type="text/javascript" src="{{ asset_url('js/setTimezone.js') }}"></script>
import { FullscreenTable } from './fullscreenTable.js';
{% block service_page_title %}
Upload your bulk-sending spreadsheet
@@ -71,7 +72,7 @@
<h2 class="font-body-lg">A spreadsheet is available to use</h2>
<div class="spreadsheet" data-module="fullscreen-table">
<div class="table-wrapper spreadsheet" data-module="fullscreen-table">
{% call(item, row_number) list_table(
example,
caption="Example",

View File

@@ -47,7 +47,6 @@ const javascripts = () => {
paths.npm + 'hogan.js/dist/hogan-3.0.2.js',
paths.npm + 'jquery/dist/jquery.min.js',
paths.npm + 'query-command-supported/dist/queryCommandSupported.min.js',
paths.npm + 'timeago/jquery.timeago.js',
paths.npm + 'textarea-caret/index.js',
paths.npm + 'cbor-js/cbor.js',
paths.npm + 'd3/dist/d3.min.js',
@@ -58,7 +57,6 @@ const javascripts = () => {
paths.toolkit + 'javascripts/govuk/modules.js',
paths.toolkit + 'javascripts/govuk/show-hide-content.js',
paths.src + 'javascripts/copyToClipboard.js',
paths.src + 'javascripts/autofocus.js',
paths.src + 'javascripts/enhancedTextbox.js',
paths.src + 'javascripts/fileUpload.js',
paths.src + 'javascripts/radioSelect.js',
@@ -68,13 +66,11 @@ const javascripts = () => {
paths.src + 'javascripts/errorTracking.js',
paths.src + 'javascripts/preventDuplicateFormSubmissions.js',
paths.src + 'javascripts/fullscreenTable.js',
paths.src + 'javascripts/colourPreview.js',
paths.src + 'javascripts/templateFolderForm.js',
paths.src + 'javascripts/collapsibleCheckboxes.js',
paths.src + 'javascripts/radioSlider.js',
paths.src + 'javascripts/updateStatus.js',
paths.src + 'javascripts/errorBanner.js',
paths.src + 'javascripts/homepage.js',
paths.src + 'javascripts/timeoutPopup.js',
paths.src + 'javascripts/date.js',
paths.src + 'javascripts/loginAlert.js',

9
package-lock.json generated
View File

@@ -26,7 +26,6 @@
"query-command-supported": "1.0.0",
"sass-embedded": "^1.86.3",
"textarea-caret": "3.1.0",
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
},
@@ -12680,14 +12679,6 @@
"node": ">=0.10.0"
}
},
"node_modules/timeago": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/timeago/-/timeago-1.6.7.tgz",
"integrity": "sha512-FikcjN98+ij0siKH4VO4dZ358PR3oDDq4Vdl1+sN9gWz1/+JXGr3uZbUShYH/hL7bMhcTpPbplJU5Tej4b4jbQ==",
"dependencies": {
"jquery": ">=1.5.0 <4.0"
}
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",

View File

@@ -42,7 +42,6 @@
"query-command-supported": "1.0.0",
"sass-embedded": "^1.86.3",
"textarea-caret": "3.1.0",
"timeago": "1.6.7",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0"
},

2
poetry.lock generated
View File

@@ -18,7 +18,6 @@ version = "5.0.1"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@@ -1551,7 +1550,6 @@ version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},

View File

@@ -18,6 +18,12 @@ Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
});
// beforeAll hook to set up the DOM and load D3.js script
beforeAll(() => {
jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
resolvedOptions: () => ({ timeZone: 'UTC' })
}));
});
beforeAll(done => {
// Set up the DOM with the D3 script included
document.body.innerHTML = `

View File

@@ -1,92 +0,0 @@
const helpers = require('./support/helpers.js');
beforeAll(() => {
require('../../app/assets/javascripts/autofocus.js');
});
afterAll(() => {
require('./support/teardown.js');
});
describe('Autofocus', () => {
const labelText = 'Search by name';
let focusHandler;
let search;
beforeEach(() => {
document.title = 'Find services by name - GOV.UK Notify';
// set up DOM
document.body.innerHTML =
`<div id="wrapper">
<label class="form-label" for="search">
${labelText}
</label>
<input autocomplete="off" class="form-control form-control-1-1" id="search" name="search" type="search" value="" data-module="autofocus">
</div>`;
focusHandler = jest.fn();
search = document.getElementById('search');
search.addEventListener('focus', focusHandler, false);
});
afterEach(() => {
document.body.innerHTML = '';
search.removeEventListener('focus', focusHandler);
focusHandler = null;
});
test('is focused when modules start', () => {
// start module
window.GOVUK.modules.start();
expect(focusHandler).toHaveBeenCalled();
});
test('is focused when attribute is set on outer element', () => {
document.getElementById('search').removeAttribute('data-module');
document.getElementById('wrapper').setAttribute('data-module', 'autofocus');
// start module
window.GOVUK.modules.start();
expect(focusHandler).toHaveBeenCalled();
});
test('is not focused if the window has scrolled', () => {
// mock the window being scrolled 25px
$.prototype.scrollTop = jest.fn(() => 25);
// start module
window.GOVUK.modules.start();
expect(focusHandler).not.toHaveBeenCalled();
});
test('is focused if the window has scrolled but the force-focus flag is set', () => {
// mock the window being scrolled 25px
$.prototype.scrollTop = jest.fn(() => 25);
// set the force-focus flag
document.querySelector('#search').setAttribute('data-force-focus', true);
// start module
window.GOVUK.modules.start();
expect(focusHandler).toHaveBeenCalled();
});
});

View File

@@ -1,139 +0,0 @@
const helpers = require('./support/helpers.js');
beforeAll(() => {
require('../../app/assets/javascripts/colourPreview.js');
});
afterAll(() => {
require('./support/teardown.js');
});
describe('Colour preview', () => {
let field;
let textbox;
let swatchEl;
beforeEach(() => {
// set up DOM
document.body.innerHTML = `
<div class="usa-form-group">
<label class="govuk-form-label" for="colour">
Colour
</label>
<input class="govuk-input govuk-input--width-6" id="colour" name="colour" rows="8" type="text" value="" data-module="colour-preview">
</div>`;
field = document.querySelector('.usa-form-group');
textbox = document.querySelector('input[type=text]');
});
afterEach(() => {
document.body.innerHTML = '';
});
describe("When the page loads", () => {
test("It should add a swatch element for the preview", () => {
// start the module
window.GOVUK.modules.start();
swatchEl = document.querySelector('.textbox-colour-preview');
expect(swatchEl).not.toBeNull();
});
test("If the textbox is empty it should make the swatch white", () => {
// start the module
window.GOVUK.modules.start();
swatchEl = document.querySelector('.textbox-colour-preview');
// textbox defaults to empty
// colours are output in RGB
expect(swatchEl.style.background).toEqual('rgb(255, 255, 255)');
});
test("If the textbox has a value which is a hex code it should add that colour to the swatch", () => {
textbox.setAttribute('value', '#00FF00');
// start the module
window.GOVUK.modules.start();
swatchEl = document.querySelector('.textbox-colour-preview');
// colours are output in RGB
expect(swatchEl.style.background).toEqual('rgb(0, 255, 0)');
});
test("If the textbox has a value which isn't a hex code it should make the swatch white", () => {
textbox.setAttribute('value', 'green');
// start the module
window.GOVUK.modules.start();
swatchEl = document.querySelector('.textbox-colour-preview');
// colours are output in RGB
expect(swatchEl.style.background).toEqual('rgb(255, 255, 255)');
});
});
describe("When input is added to the textbox", () => {
beforeEach(() => {
// start the module
window.GOVUK.modules.start();
swatchEl = document.querySelector('.textbox-colour-preview');
});
test("If the textbox is empty it should make the swatch white", () => {
helpers.triggerEvent(document.querySelector('input[type=text]'), 'input');
// textbox defaults to empty
expect(swatchEl.style.background).toEqual('rgb(255, 255, 255)');
});
test("If the textbox has a value which is a hex code it should add that colour to the swatch", () => {
textbox.setAttribute('value', '#00FF00');
helpers.triggerEvent(document.querySelector('input[type=text]'), 'input');
// textbox defaults to empty
expect(swatchEl.style.background).toEqual('rgb(0, 255, 0)');
});
test("If the textbox has a value which isn't a hex code it should make the swatch white", () => {
textbox.setAttribute('value', 'green');
helpers.triggerEvent(document.querySelector('input[type=text]'), 'input');
// textbox defaults to empty
expect(swatchEl.style.background).toEqual('rgb(255, 255, 255)');
});
});
});

View File

@@ -1,370 +1,88 @@
const helpers = require('./support/helpers');
require('../../app/assets/javascripts/fullscreenTable.js');
beforeAll(() => {
// TODO: remove this when tests for sticky JS are written
require('../../app/assets/javascripts/stick-to-window-when-scrolling.js');
describe('fullscreenTable', () => {
let wrapper, rightShadow;
require('../../app/assets/javascripts/fullscreenTable.js');
});
afterAll(() => {
require('./support/teardown.js');
});
describe('FullscreenTable', () => {
let screenMock;
let container;
let tableFrame;
let table;
let numberColumnFrame;
beforeEach(() => {
const tableHeadings = () => {
let result = '';
const headings = ['1', 'name', 'email address', 'age', 'building number', 'address line 1', 'address line 2', 'postcode'];
headings.forEach((heading, idx) => {
if (idx === 0) {
result += `<th scope="col" class="table-field-heading-first">
<span class="visually-hidden">Row in file</span><span aria-hidden="true" class="table-field-invisible-error">${heading}</span>
</th>`;
} else {
result += `<th scope="col" class="table-field-heading">
${heading}
</th>`;
}
});
return result;
}
const rowCells = (cells) => {
let result = '';
Object.keys(cells).forEach((key, idx) => {
if (idx === 0) {
result += `<td class="table-field-index">
<span class="table-field-error">
${key}
</span>
</td>`;
} else {
result += `<td class="table-field-left-aligned ">
<div class="table-field-status-default">
${key}
</div>
</td>`;
}
});
return result;
};
const tableRows = () => {
let result = '';
const rows = [
['John Logie Baird', 'johnlbaird@gmail.com', '37', '22', 'Frith Street', 'Soho, London', 'W1D 4RF'],
['Guglielmo Marconi', 'gmarconi@hotmail.com', '21', 'Pontecchio Marconi', 'Via Celestini 1', 'Bologna', ''],
['Louis Braille', 'louisbraille@yahoo.co.uk', '', '56', 'Boulevard des Invalides', 'Paris', '75007'],
['Ray Tomlinson', 'hedy.lamarr@msn.com', '25', '870', '870 Winter Street', 'Waltham', 'MA 02451']
];
rows.forEach(row => {
result += `<tr class="table-row">${rowCells(row)}</tr>`;
});
return result;
}
screenMock = new helpers.ScreenMock(jest);
screenMock.setWindow({
width: 1990,
height: 940,
scrollTop: 0
});
// set up DOM
document.body.innerHTML =
`<main>
<div class="fullscreen-content" data-module="fullscreen-table">
<table class="table table-font-xsmall">
<caption class="heading-medium table-heading visuallyhidden">
people.csv
</caption>
<thead class="table-field-headings-visible">
<tr>
${tableHeadings()}
</tr>
</thead>
<tbody>
${tableRows()}
</tbody>
const setupDOM = (scrollLeft = 0, scrollWidth = 1000, clientWidth = 500) => {
document.body.innerHTML = `
<div class="fullscreen-table">
<div class="table-wrapper" style="overflow-x: scroll;">
<table>
<tr><td>Test</td></tr>
</table>
</div>
</main>`;
<div class="fullscreen-right-shadow"></div>
</div>
`;
container = document.querySelector('.fullscreen-content');
wrapper = document.querySelector('.table-wrapper');
rightShadow = document.querySelector('.fullscreen-right-shadow');
});
// Mock scroll values
Object.defineProperty(wrapper, 'scrollLeft', {
get: () => scrollLeft,
configurable: true
});
Object.defineProperty(wrapper, 'scrollWidth', {
get: () => scrollWidth,
configurable: true
});
Object.defineProperty(wrapper, 'clientWidth', {
get: () => clientWidth,
configurable: true
});
// 👇 Now that DOM is ready, initialize the logic
window.initFullscreenTables();
// Force trigger scroll event if needed
wrapper.dispatchEvent(new Event('scroll'));
};
afterEach(() => {
document.body.innerHTML = '';
});
describe("when it loads", () => {
test("it fixes the number column for each row without changing the semantics", () => {
// start module
window.GOVUK.modules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
expect(tableFrame).not.toBeNull();
expect(numberColumnFrame).not.toBeNull();
expect(numberColumnFrame.getAttribute('aria-hidden')).toEqual('true');
});
test("it calls the sticky JS to update any cached dimensions", () => {
const stickyJSSpy = jest.spyOn(window.GOVUK.stickAtBottomWhenScrolling, 'recalculate');
// start module
window.GOVUK.modules.start();
expect(stickyJSSpy.mock.calls.length).toBe(1);
stickyJSSpy.mockClear();
});
test("the scrolling section is focusable and has an accessible name matching the table caption", () => {
// start module
window.GOVUK.modules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
caption = tableFrame.querySelector('caption');
expect(tableFrame.hasAttribute('role')).toBe(true);
expect(tableFrame.getAttribute('role')).toEqual('region');
expect(tableFrame.hasAttribute('aria-labelledby')).toBe(true);
expect(tableFrame.getAttribute('aria-labelledby')).toEqual(caption.getAttribute('id'));
});
test("the section providing the fixed row headers is not focusable and is hidden from assistive tech'", () => {
// start module
window.GOVUK.modules.start();
fixedRowHeaders = document.querySelector('.fullscreen-fixed-table');
expect(fixedRowHeaders.hasAttribute('role')).toBe(false);
expect(fixedRowHeaders.hasAttribute('aria-labelledby')).toBe(false);
expect(fixedRowHeaders.hasAttribute('tabindex')).toBe(false);
expect(fixedRowHeaders.hasAttribute('aria-hidden')).toBe(true);
expect(fixedRowHeaders.getAttribute('aria-hidden')).toEqual('true');
});
test('adds right shadow on initial load if scrollable', () => {
setupDOM(0, 1000, 500);
expect(wrapper.classList.contains('scrolled')).toBe(false);
expect(rightShadow.classList.contains('visible')).toBe(true);
});
describe("the height of the table should fit the vertical space available to it", () => {
let containerBoundingClientRectSpy;
let containerClientRectsSpy;
beforeEach(() => {
// set the height and offset of the window and table container from the top of the document
// so just the top 268px of it appears on-screen
screenMock.window.setHeightTo(768);
screenMock.mockPositionAndDimension('container', container, {
'offsetHeight': 1000,
'offsetWidth': 641,
'offsetTop': 500
});
// start module
window.GOVUK.modules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
});
afterEach(() => {
screenMock.reset();
});
test("when the page has loaded", () => {
// the frames should crop to the top 268px of the table that is visible
expect(window.getComputedStyle(tableFrame)['height']).toEqual('268px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('268px');
});
test("when the page has scrolled", () => {
// scroll the window so the table fills the height of the window (768px)
screenMock.window.scrollTo(500);
// the frames should crop to the window height
expect(window.getComputedStyle(tableFrame)['height']).toEqual('768px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('768px');
});
test("when the page has resized", () => {
// resize the window by 232px (from 768px to 1000px)
screenMock.window.resizeTo({ height: 1000, width: 1024 });
// the frames should crop to the top 500px of the table now visible
expect(window.getComputedStyle(tableFrame)['height']).toEqual('500px');
expect(window.getComputedStyle(numberColumnFrame)['height']).toEqual('500px');
});
test('hides right shadow when scrolled to max', () => {
setupDOM(500, 1000, 500);
expect(wrapper.classList.contains('scrolled')).toBe(true);
expect(rightShadow.classList.contains('visible')).toBe(false);
});
describe("the width of the table should fit the horizontal space available to it", () => {
let rowNumberColumnCell;
beforeEach(() => {
rowNumberColumnCell = container.querySelector('.table-field-index');
// set main content column width (used as module as gauge for table width)
screenMock.window.setWidthTo(1024);
document.querySelector('main').setAttribute('style', 'width: 742px');
// set total width of column for row numbers in table to 40px
rowNumberColumnCell.setAttribute('style', 'width: 40px');
// start module
window.GOVUK.modules.start();
tableFrame = document.querySelector('.fullscreen-scrollable-table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
});
afterEach(() => {
screenMock.reset();
});
test("when the page has loaded", () => {
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
expect(window.getComputedStyle(tableFrame)['width']).toEqual('702px'); // width of content column - numbers column
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
// table for number column has 4px extra to allow space for drop shadow
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
});
test("when the page has resized", () => {
// resize window and content column
document.querySelector('main').setAttribute('style', 'width: 720px');
screenMock.window.resizeTo({ height: 768, width: 960 });
// table should set its width to be that of `<main>`, minus margin-left for the row numbers column
expect(window.getComputedStyle(tableFrame)['width']).toEqual('680px'); // width of content column - numbers column
expect(window.getComputedStyle(tableFrame)['margin-left']).toEqual('40px'); // width of numbers column
// table for number column has 4px extra to allow space for drop shadow
expect(window.getComputedStyle(numberColumnFrame)['width']).toEqual('44px');
});
test('handles mid-scroll: left + right shadows active', () => {
setupDOM(250, 1000, 500);
expect(wrapper.classList.contains('scrolled')).toBe(true);
expect(rightShadow.classList.contains('visible')).toBe(true);
});
describe("when the table scrolls horizontally", () => {
let rightEdgeShadow;
test('gracefully handles missing right shadow', () => {
document.body.innerHTML = `
<div class="fullscreen-table">
<div class="table-wrapper">
<table>
<tr><td>Only content</td></tr>
</table>
</div>
</div>
`;
beforeEach(() => {
wrapper = document.querySelector('.table-wrapper');
// start module
window.GOVUK.modules.start();
Object.defineProperty(wrapper, 'scrollLeft', { get: () => 100, configurable: true });
Object.defineProperty(wrapper, 'scrollWidth', { get: () => 500, configurable: true });
Object.defineProperty(wrapper, 'clientWidth', { get: () => 200, configurable: true });
tableFrame = document.querySelector('.fullscreen-scrollable-table');
table = tableFrame.querySelector('table');
numberColumnFrame = document.querySelector('.fullscreen-fixed-table');
rightEdgeShadow = container.querySelector('.fullscreen-right-shadow');
tableFrame.setAttribute('style', 'width: 742px');
table.setAttribute('style', 'width: 1000px');
});
test("the right edge of the table scroll area should have a drop-shadow if it isn't scrolled", () => {
tableFrame.scrollLeft = 0;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(false);
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
});
test("the left edge of the table scroll area should have a drop-shadow if the table is scrolled to 100%", () => {
// scroll to end of table
tableFrame.scrollLeft = 258;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
expect(rightEdgeShadow.classList.contains('visible')).toBe(false);
});
test("both edges of the table scroll area should have a drop-shadow if the table is scrolled between 0% and 100%", () => {
// scroll to middle of table
tableFrame.scrollLeft = 129;
helpers.triggerEvent(tableFrame, 'scroll');
expect(numberColumnFrame.classList.contains('fullscreen-scrolled-table')).toBe(true);
expect(rightEdgeShadow.classList.contains('visible')).toBe(true);
});
// 👇 Call after building DOM
window.initFullscreenTables();
wrapper.dispatchEvent(new Event('scroll'));
expect(wrapper.classList.contains('scrolled')).toBe(true);
// No error thrown for missing .fullscreen-right-shadow
});
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);
});
});
});

View File

@@ -0,0 +1,108 @@
/**
* @jest-environment jsdom
*/
describe('Main init script', () => {
beforeEach(() => {
// Clear DOM
document.body.innerHTML = '';
jest.resetModules();
jest.clearAllMocks();
});
it('calls GOVUK.Frontend.initAll if available', () => {
const initAllMock = jest.fn();
const showHideInitMock = jest.fn();
global.GOVUK = {
Frontend: { initAll: initAllMock },
ShowHideContent: jest.fn().mockImplementation(() => ({
init: showHideInitMock
}))
};
require('../../app/assets/javascripts/main.js');
expect(initAllMock).toHaveBeenCalled();
});
it('initializes GOVUK.ShowHideContent', () => {
const initMock = jest.fn();
const ShowHideContentMock = jest.fn().mockImplementation(() => ({
init: initMock
}));
global.GOVUK = {
ShowHideContent: ShowHideContentMock
};
require('../../app/assets/javascripts/main.js');
expect(ShowHideContentMock).toHaveBeenCalled();
expect(initMock).toHaveBeenCalled();
});
it('focuses input next to error message inside label', () => {
document.body.innerHTML = `
<form>
<label>
<span class="error-message">Required</span>
</label>
<input id="name" />
</form>
`;
const focusMock = jest.fn();
const input = document.querySelector('input');
input.focus = focusMock;
require('../../app/assets/javascripts/main.js');
// simulate DOMContentLoaded
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(focusMock).toHaveBeenCalled();
});
it('adds border color on header click', () => {
document.body.innerHTML = `
<div class="govuk-header__container"></div>
`;
global.GOVUK = {
ShowHideContent: jest.fn().mockImplementation(() => ({ init: jest.fn() })),
Frontend: { initAll: jest.fn() }
};
require('../../app/assets/javascripts/main.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
const header = document.querySelector('.govuk-header__container');
header.click();
expect(header.style.borderColor).toBe('#005ea5');
});
it('adds and removes class on parent during focusin and focusout', () => {
document.body.innerHTML = `
<div class="js-mark-focus-on-parent">
<input type="text" />
</div>
`;
global.GOVUK = {
ShowHideContent: jest.fn().mockImplementation(() => ({ init: jest.fn() })),
Frontend: { initAll: jest.fn() }
};
require('../../app/assets/javascripts/main.js');
document.dispatchEvent(new Event('DOMContentLoaded'));
const container = document.querySelector('.js-mark-focus-on-parent');
const input = container.querySelector('input');
input.dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
expect(container.classList.contains('js-child-has-focus')).toBe(true);
input.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
expect(container.classList.contains('js-child-has-focus')).toBe(false);
});
});