Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ gem "jsbundling-rails"
gem "pg"
gem "propshaft"
gem "puma"
gem "stimulus-rails"
gem "thruster", require: false
gem "turbo-rails"

Expand Down
3 changes: 0 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,6 @@ GEM
solargraph (>= 0.48.0, < 0.57)
splunk-sdk-ruby (1.0.5)
stackprof (0.2.27)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.7)
swd (2.0.3)
activesupport (>= 3)
Expand Down Expand Up @@ -837,7 +835,6 @@ DEPENDENCIES
solargraph-rails
splunk-sdk-ruby
stackprof
stimulus-rails
syntax_tree
syntax_tree-haml
syntax_tree-rbs
Expand Down
28 changes: 28 additions & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "@hotwired/turbo-rails";
import {
createAll,
Button,
Checkboxes,
ErrorSummary,
Header,
NotificationBanner,
SkipLink,
} from "nhsuk-frontend";

import { Autocomplete } from "./components/autocomplete.js";
import { UpgradedRadios as Radios } from "./components/radios.js";

// Configure Turbo
Turbo.session.drive = false;

// Initiate NHS.UK frontend components on page load
document.addEventListener("DOMContentLoaded", () => {
createAll(Autocomplete);
createAll(Button, { preventDoubleClick: true });
createAll(Checkboxes);
createAll(ErrorSummary);
createAll(Header);
createAll(Radios);
createAll(NotificationBanner);
createAll(SkipLink);
});
102 changes: 102 additions & 0 deletions app/assets/javascripts/components/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import accessibleAutocomplete from "accessible-autocomplete";
import { Component } from "nhsuk-frontend";

/**
* Autocomplete component
*
* @augments Component<HTMLSelectElement>
*/
export class Autocomplete extends Component {
static elementType = HTMLSelectElement;

/**
* @param {Element | null} $root - HTML element to use for component
*/
constructor($root) {
super($root);

if ($root instanceof HTMLSelectElement) {
this.name = this.$root.name;
this.options = Array.from(this.$root.options);
this.value = this.$root.value;

this.enhanceSelectElement(this.$root);
}
}

/**
* Name for the component used when initialising using data-module attributes
*/
static moduleName = "app-autocomplete";

/**
* Enhance select element
*
* @param {HTMLSelectElement} $element - Select element to enhance
*/
enhanceSelectElement($element) {
accessibleAutocomplete.enhanceSelectElement({
selectElement: $element,
cssNamespace: "app-autocomplete",
defaultValue: this.value || "",
inputClasses: "nhsuk-input",
showNoOptionsFound: true,
templates: {
suggestion: (value) => this.suggestion(value, this.enhancedOptions),
},
onConfirm: (value) => {
const selectedOption = this.selectedOption(value, this.options);

if (selectedOption) {
selectedOption.selected = true;
}
},
});
}

/**
* Get enhanced information about each option
*
* @returns {object} Enhanced options
*/
get enhancedOptions() {
return this.options.map((option) => ({
name: option.label,
value: option.value,
append: option.getAttribute("data-append"),
hint: option.getAttribute("data-hint"),
}));
}

/**
* Selected option
*
* @param {*} value - Current value
* @param {Array} options - Available options
* @returns {HTMLOptionElement} Selected option
*/
selectedOption(value, options) {
return [].filter.call(
options,
(option) => (option.textContent || option.innerText) === value,
)[0];
}

/**
* HTML for suggestion
*
* @param {*} value - Current value
* @param {Array} options - Available options
* @returns {string} HTML for suggestion
*/
suggestion(value, options) {
const option = options.find(({ name }) => name === value);
if (option) {
const label = option.append ? `${value} – ${option.append}` : value;
return option.hint
? `${label}<br><span class="app-autocomplete__option-hint">${option.hint}</span>`
: label;
}
return "No results found";
}
}
81 changes: 81 additions & 0 deletions app/assets/javascripts/components/autocomplete.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Autocomplete } from "./autocomplete.js";

document.body.classList.add("nhsuk-frontend-supported");
document.body.innerHTML = `
<select id="fruit" name="fruit" data-module="app-autocomplete">
<option value=""></option>
<option data-hint="Red" data-append="Sliced">Apple</option>
<option data-hint="Yellow" data-append="Peeled">Banana</option>
<option data-hint="Orange" data-append="Squeezed">Orange</option>
</select>
`;

describe("Autocomplete", () => {
beforeAll(() => {
const $select = document.querySelector('[data-module="app-autocomplete"]');
return new Autocomplete($select);
});

test("should enhance select", () => {
const select = document.querySelector("select");
expect(window.getComputedStyle(select).display).toBe("none");

const autocomplete = document.querySelector(".app-autocomplete__wrapper");
expect(autocomplete).toBeTruthy();

const input = document.querySelector(".app-autocomplete__wrapper input");
expect(input.getAttribute("aria-controls")).toEqual("fruit__listbox");
});

test("should show matching options when a value is entered", async () => {
const input = document.querySelector('[aria-controls="fruit__listbox"]');
const listbox = document.querySelector("#fruit__listbox");

// Initially, the listbox should be hidden
expect(listbox.classList).toContain("app-autocomplete__menu--hidden");

// Simulate typing "a" in the input
input.focus();
input.value = "a";
input.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 100));

// The listbox should now be visible
expect(listbox.classList).toContain("app-autocomplete__menu--visible");

// Check that matching options are shown
const visibleOptions = listbox.querySelectorAll("li");
expect(visibleOptions.length).toEqual(3);

// Check that options display hint and appended text
expect(visibleOptions[0].innerHTML.trim()).toEqual(
`Apple – Sliced<br><span class="app-autocomplete__option-hint">Red</span>`,
);

// Simulate clicking first option
visibleOptions[0].click();
await new Promise((resolve) => setTimeout(resolve, 50));

// Check that selected options is saved
expect(input.value).toBe("Apple");
});

test("should shows a message if no values found", async () => {
const input = document.querySelector('[aria-controls="fruit__listbox"]');
const listbox = document.querySelector("#fruit__listbox");

// Simulate typing "z" in the input
input.focus();
input.value = "z";
input.dispatchEvent(new Event("input", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 100));

// Check that matching options are shown
const visibleOptions = listbox.querySelectorAll("li");
console.log(visibleOptions[0].innerHTML.trim());
expect(visibleOptions.length).toEqual(1);

// Option display hint text
expect(visibleOptions[0].innerHTML.trim()).toEqual("No results found");
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Controller } from "@hotwired/stimulus";
import { Radios } from "nhsuk-frontend";

class UpgradedRadios extends Radios {
export class UpgradedRadios extends Radios {
constructor($root) {
// Promote data-aria-controls attribute to a aria-controls attribute as per
// https://github.com/alphagov/govuk-frontend/blob/88fea750b5eb9c9d9f661405e68bfb59e59754b2/packages/govuk-frontend/src/govuk/components/radios/radios.mjs#L33-L34
Expand All @@ -25,10 +24,3 @@ class UpgradedRadios extends Radios {
super($root);
}
}

// Connects to data-module="nhsuk-radios"
export default class extends Controller {
connect() {
return new UpgradedRadios(this.element);
}
}
30 changes: 30 additions & 0 deletions app/assets/javascripts/components/radios.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { UpgradedRadios } from "./radios.js";

jest.mock("nhsuk-frontend/packages/components/radios/radios");

document.body.innerHTML = `
<div data-module="nhsuk-radios">
<input type="radio" data-aria-controls="target-id" />
<div id="target-id" class="nhsuk-radios__conditional"></div>
</div>
<div data-module="nhsuk-radios">
<input type="radio" />
</div>
`;

describe("UpgradedRadios", () => {
beforeEach(() => {
const $radios = document.querySelector('[data-module="nhsuk-radios"]');
return new UpgradedRadios($radios);
});

test("should call UpgradedRadios", () => {
expect(UpgradedRadios).toHaveBeenCalledTimes(1);
});

test("should promote 'data-aria-controls' to 'aria-controls'", () => {
const radioInput = document.querySelector('input[type="radio"]');
expect(radioInput.getAttribute("aria-controls")).toEqual("target-id");
expect(radioInput.getAttribute("data-aria-controls")).toBeNull();
});
});
20 changes: 20 additions & 0 deletions app/assets/javascripts/public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
createAll,
Button,
Checkboxes,
ErrorSummary,
SkipLink,
} from "nhsuk-frontend";

import { Autocomplete } from "./components/autocomplete.js";
import { UpgradedRadios as Radios } from "./components/radios.js";

// Initiate NHS.UK frontend components on page load
document.addEventListener("DOMContentLoaded", () => {
createAll(Autocomplete);
createAll(Button, { preventDoubleClick: true });
createAll(Checkboxes);
createAll(ErrorSummary);
createAll(Radios);
createAll(SkipLink);
});
56 changes: 0 additions & 56 deletions app/assets/stylesheets/components/_action-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,59 +33,3 @@
margin-right: 0;
padding-right: 0;
}

.nhsuk-action-link {
@include nhsuk-responsive-margin(6, "bottom");
}

.nhsuk-action-link__link {
display: inline-block; // [1]
padding-left: 38px; // [2]
position: relative; // [3]
text-decoration: none; // [4]

@include nhsuk-font(22, $weight: bold);

&:not(:focus):hover {
.nhsuk-action-link__text {
text-decoration: underline; // [6]
}
}

@include nhsuk-media-query($until: tablet) {
padding-left: 26px; // [2]
}

@include nhsuk-media-query($media-type: print) {
color: $nhsuk-print-text-color;

&:visited {
color: $nhsuk-print-text-color;
}
}

.nhsuk-icon__arrow-right-circle {
// stylelint-disable-next-line declaration-no-important
fill: $color_nhsuk-green !important;
height: 36px;
left: -3px;
position: absolute;
top: -3px;
width: 36px;

@include nhsuk-print-color($nhsuk-print-text-color);

@include nhsuk-media-query($until: tablet) {
height: 24px;
left: -2px;
margin-bottom: 0;
top: 1px;
width: 24px;
}
}

&:focus .nhsuk-icon__arrow-right-circle {
// stylelint-disable-next-line declaration-no-important
fill: $color_nhsuk-black !important;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@forward "pkg:nhsuk-frontend/dist/nhsuk/components/action-link";
@forward "pkg:nhsuk-frontend/dist/nhsuk/components/back-link";
@forward "pkg:nhsuk-frontend/dist/nhsuk/components/breadcrumb";
@forward "pkg:nhsuk-frontend/dist/nhsuk/components/button";
Expand Down Expand Up @@ -26,7 +27,6 @@
@forward "pkg:nhsuk-frontend/dist/nhsuk/components/warning-callout";

// Ignored components
// - action-link
// - character-count
// - contents-list
// - do-dont-list
Expand Down
2 changes: 1 addition & 1 deletion app/components/app_vaccinate_form_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<% if form.requires_supplied_by_user_id? %>
<%= f.govuk_select :supplied_by_user_id,
label: { text: "Which nurse identified and pre-screened the child and supplied the vaccine?" },
data: { module: "autocomplete" } do %>
data: { module: "app-autocomplete" } do %>
<%= tag.option "", value: "" %>
<% form.supplied_by_users.each do |user| %>
<%= tag.option user.full_name,
Expand Down
4 changes: 0 additions & 4 deletions app/javascript/application.js

This file was deleted.

Loading