diff --git a/Gemfile.lock b/Gemfile.lock index 4d09ff90a..5a7dcd2a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,7 +168,7 @@ GEM google-protobuf (3.24.3-aarch64-linux) google-protobuf (3.24.3-arm64-darwin) google-protobuf (3.24.3-x86_64-linux) - googleapis-common-protos-types (1.8.0) + googleapis-common-protos-types (1.9.0) google-protobuf (~> 3.18) govuk_admin_template (6.10.0) bootstrap-sass (~> 3.4) @@ -190,7 +190,7 @@ GEM govuk_personalisation (0.15.0) plek (>= 1.9.0) rails (>= 6, < 8) - govuk_publishing_components (35.15.4) + govuk_publishing_components (35.16.0) govuk_app_config govuk_personalisation (>= 0.7.0) kramdown @@ -312,7 +312,7 @@ GEM notifications-ruby-client (5.3.0) jwt (>= 1.5, < 3) null_logger (0.0.1) - opentelemetry-api (1.2.2) + opentelemetry-api (1.2.3) opentelemetry-common (0.20.0) opentelemetry-api (~> 1.0) opentelemetry-exporter-otlp (0.26.1) diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index f2c77e945..e1d6e089d 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -3,5 +3,3 @@ //= link legacy_layout.css //= link application.js //= link legacy_layout.js - -//= link components/_option-select.css diff --git a/app/assets/images/option-select/input-icon.svg b/app/assets/images/option-select/input-icon.svg deleted file mode 100644 index ad3de76f1..000000000 --- a/app/assets/images/option-select/input-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/app/assets/javascripts/components/option-select.js b/app/assets/javascripts/components/option-select.js deleted file mode 100644 index 7fd99fbc0..000000000 --- a/app/assets/javascripts/components/option-select.js +++ /dev/null @@ -1,304 +0,0 @@ -window.GOVUK = window.GOVUK || {} -window.GOVUK.Modules = window.GOVUK.Modules || {}; - -(function (Modules) { - /* This JavaScript provides two functional enhancements to option-select components: - 1) A count that shows how many results have been checked in the option-container - 2) Open/closing of the list of checkboxes - */ - function OptionSelect ($module) { - this.$optionSelect = $module - this.$options = this.$optionSelect.querySelectorAll("input[type='checkbox']") - this.$optionsContainer = this.$optionSelect.querySelector('.js-options-container') - this.$optionList = this.$optionsContainer.querySelector('.js-auto-height-inner') - this.$allCheckboxes = this.$optionsContainer.querySelectorAll('.govuk-checkboxes__item') - this.hasFilter = this.$optionSelect.getAttribute('data-filter-element') || '' - - this.checkedCheckboxes = [] - } - - OptionSelect.prototype.init = function () { - if (this.hasFilter.length) { - var filterEl = document.createElement('div') - filterEl.innerHTML = this.hasFilter - - var optionSelectFilter = document.createElement('div') - optionSelectFilter.classList.add('app-c-option-select__filter') - optionSelectFilter.innerHTML = filterEl.childNodes[0].nodeValue - - this.$optionsContainer.parentNode.insertBefore(optionSelectFilter, this.$optionsContainer) - - this.$filter = this.$optionSelect.querySelector('input[name="option-select-filter"]') - this.$filterCount = document.getElementById(this.$filter.getAttribute('aria-describedby')) - this.filterTextSingle = ' ' + this.$filterCount.getAttribute('data-single') - this.filterTextMultiple = ' ' + this.$filterCount.getAttribute('data-multiple') - this.filterTextSelected = ' ' + this.$filterCount.getAttribute('data-selected') - this.checkboxLabels = [] - this.filterTimeout = 0 - - this.getAllCheckedCheckboxes() - for (var i = 0; i < this.$allCheckboxes.length; i++) { - this.checkboxLabels.push(this.cleanString(this.$allCheckboxes[i].textContent)) - } - - this.$filter.addEventListener('keyup', this.typeFilterText.bind(this)) - } - - // Attach listener to update checked count - this.$optionsContainer.querySelector('.gem-c-checkboxes__list').addEventListener('change', this.updateCheckedCount.bind(this)) - - // Replace div.container-head with a button - this.replaceHeadingSpanWithButton() - - // Add js-collapsible class to parent for CSS - this.$optionSelect.classList.add('js-collapsible') - - // Add open/close listeners - var button = this.$optionSelect.querySelector('.js-container-button') - button.addEventListener('click', this.toggleOptionSelect.bind(this)) - - var closedOnLoad = this.$optionSelect.getAttribute('data-closed-on-load') - var closedOnLoadMobile = this.$optionSelect.getAttribute('data-closed-on-load-mobile') - - // By default the .filter-content container is hidden on mobile - // By checking if .filter-content is hidden, we are in mobile view given the current implementation - var isFacetsContentHidden = this.isFacetsContainerHidden() - - // Check if the option select should be closed for mobile screen sizes - var closedForMobile = closedOnLoadMobile === 'true' && isFacetsContentHidden - - // Always set the contain height to 200px for mobile screen sizes - if (closedForMobile) { - this.setContainerHeight(200) - } - - if (closedOnLoad === 'true' || closedForMobile) { - this.close() - } else { - this.setupHeight() - } - - var checkedString = this.checkedString() - if (checkedString) { - this.attachCheckedCounter(checkedString) - } - } - - OptionSelect.prototype.typeFilterText = function (event) { - event.stopPropagation() - var ENTER_KEY = 13 - - if (event.keyCode !== ENTER_KEY) { - clearTimeout(this.filterTimeout) - this.filterTimeout = setTimeout( - function () { this.doFilter(this) }.bind(this), - 300 - ) - } else { - event.preventDefault() // prevents finder forms from being submitted when user presses ENTER - } - } - - OptionSelect.prototype.cleanString = function cleanString (text) { - text = text.replace(/&/g, 'and') - text = text.replace(/[’',:–-]/g, '') // remove punctuation characters - text = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape special characters - return text.trim().replace(/\s\s+/g, ' ').toLowerCase() // replace multiple spaces with one - } - - OptionSelect.prototype.getAllCheckedCheckboxes = function getAllCheckedCheckboxes () { - this.checkedCheckboxes = [] - - for (var i = 0; i < this.$options.length; i++) { - if (this.$options[i].checked) { - this.checkedCheckboxes.push(i) - } - } - } - - OptionSelect.prototype.doFilter = function doFilter (obj) { - var filterBy = obj.cleanString(obj.$filter.value) - var showCheckboxes = obj.checkedCheckboxes.slice() - var i = 0 - - for (i = 0; i < obj.$allCheckboxes.length; i++) { - if (showCheckboxes.indexOf(i) === -1 && obj.checkboxLabels[i].search(filterBy) !== -1) { - showCheckboxes.push(i) - } - } - - for (i = 0; i < obj.$allCheckboxes.length; i++) { - obj.$allCheckboxes[i].style.display = 'none' - } - - for (i = 0; i < showCheckboxes.length; i++) { - obj.$allCheckboxes[showCheckboxes[i]].style.display = 'block' - } - - var lenChecked = obj.$optionsContainer.querySelectorAll('.govuk-checkboxes__input:checked').length - var len = showCheckboxes.length + lenChecked - var html = len + (len === 1 ? obj.filterTextSingle : obj.filterTextMultiple) + ', ' + lenChecked + obj.filterTextSelected - obj.$filterCount.innerHTML = html - } - - OptionSelect.prototype.replaceHeadingSpanWithButton = function replaceHeadingSpanWithButton () { - /* Replace the span within the heading with a button element. This is based on feedback from Léonie Watson. - * The button has all of the accessibility hooks that are used by screen readers and etc. - * We do this in the JavaScript because if the JavaScript is not active then the button shouldn't - * be there as there is no JS to handle the click event. - */ - var containerHead = this.$optionSelect.querySelector('.js-container-button') - var jsContainerHeadHTML = containerHead.innerHTML - - // Create button and replace the preexisting html with the button. - var button = document.createElement('button') - button.setAttribute('class', 'js-container-button app-c-option-select__title app-c-option-select__button') - // Add type button to override default type submit when this component is used within a form - button.setAttribute('type', 'button') - button.setAttribute('aria-expanded', true) - button.setAttribute('id', containerHead.getAttribute('id')) - button.setAttribute('aria-controls', this.$optionsContainer.getAttribute('id')) - button.innerHTML = jsContainerHeadHTML - containerHead.parentNode.replaceChild(button, containerHead) - - // GA4 Accordion tracking. Relies on the ga4-finder-tracker setting the index first, so we wrap this in a custom event. - window.addEventListener('ga4-filter-indexes-added', function () { - if (window.GOVUK.analyticsGa4) { - if (window.GOVUK.analyticsGa4.Ga4FinderTracker) { - window.GOVUK.analyticsGa4.Ga4FinderTracker.addFilterButtonTracking(button, button.innerHTML) - } - } - }) - } - - OptionSelect.prototype.attachCheckedCounter = function attachCheckedCounter (checkedString) { - var element = document.createElement('div') - element.setAttribute('class', 'app-c-option-select__selected-counter js-selected-counter') - element.innerHTML = checkedString - this.$optionSelect.querySelector('.js-container-button').insertAdjacentElement('afterend', element) - } - - OptionSelect.prototype.updateCheckedCount = function updateCheckedCount () { - var checkedString = this.checkedString() - var checkedStringElement = this.$optionSelect.querySelector('.js-selected-counter') - - if (checkedString) { - if (checkedStringElement === null) { - this.attachCheckedCounter(checkedString) - } else { - checkedStringElement.textContent = checkedString - } - } else if (checkedStringElement) { - checkedStringElement.parentNode.removeChild(checkedStringElement) - } - } - - OptionSelect.prototype.checkedString = function checkedString () { - this.getAllCheckedCheckboxes() - var count = this.checkedCheckboxes.length - var checkedString = false - if (count > 0) { - checkedString = count + ' selected' - } - - return checkedString - } - - OptionSelect.prototype.toggleOptionSelect = function toggleOptionSelect (e) { - if (this.isClosed()) { - this.open() - } else { - this.close() - } - e.preventDefault() - } - - OptionSelect.prototype.open = function open () { - if (this.isClosed()) { - this.$optionSelect.querySelector('.js-container-button').setAttribute('aria-expanded', true) - this.$optionSelect.classList.remove('js-closed') - this.$optionSelect.classList.add('js-opened') - if (!this.$optionsContainer.style.height) { - this.setupHeight() - } - } - } - - OptionSelect.prototype.close = function close () { - this.$optionSelect.classList.remove('js-opened') - this.$optionSelect.classList.add('js-closed') - this.$optionSelect.querySelector('.js-container-button').setAttribute('aria-expanded', false) - } - - OptionSelect.prototype.isClosed = function isClosed () { - return this.$optionSelect.classList.contains('js-closed') - } - - OptionSelect.prototype.setContainerHeight = function setContainerHeight (height) { - this.$optionsContainer.style.height = height + 'px' - } - - OptionSelect.prototype.isCheckboxVisible = function isCheckboxVisible (option) { - var initialOptionContainerHeight = this.$optionsContainer.clientHeight - var optionListOffsetTop = this.$optionList.getBoundingClientRect().top - var distanceFromTopOfContainer = option.getBoundingClientRect().top - optionListOffsetTop - return distanceFromTopOfContainer < initialOptionContainerHeight - } - - OptionSelect.prototype.getVisibleCheckboxes = function getVisibleCheckboxes () { - var visibleCheckboxes = [] - for (var i = 0; i < this.$options.length; i++) { - if (this.isCheckboxVisible(this.$options[i])) { - visibleCheckboxes.push(this.$options[i]) - } - } - - // add an extra checkbox, if the label of the first is too long it collapses onto itself - if (this.$options[visibleCheckboxes.length]) { - visibleCheckboxes.push(this.$options[visibleCheckboxes.length]) - } - return visibleCheckboxes - } - - OptionSelect.prototype.isFacetsContainerHidden = function isFacetsContainerHidden () { - var facetsContent = this.$optionSelect.parentElement - var isFacetsContentHidden = false - // check whether this is hidden by progressive disclosure, - // because height calculations won't work - // would use offsetParent === null but for IE10+ - if (facetsContent) { - isFacetsContentHidden = !(facetsContent.offsetWidth || facetsContent.offsetHeight || facetsContent.getClientRects().length) - } - - return isFacetsContentHidden - } - - OptionSelect.prototype.setupHeight = function setupHeight () { - var initialOptionContainerHeight = this.$optionsContainer.clientHeight - var height = this.$optionList.offsetHeight - - var isFacetsContainerHidden = this.isFacetsContainerHidden() - - if (isFacetsContainerHidden) { - initialOptionContainerHeight = 200 - height = 200 - } - - // Resize if the list is only slightly bigger than its container - // If isFacetsContainerHidden is true, then 200 < 250 - // And the container height is always set to 201px - if (height < initialOptionContainerHeight + 50) { - this.setContainerHeight(height + 1) - return - } - - // Resize to cut last item cleanly in half - var visibleCheckboxes = this.getVisibleCheckboxes() - - var lastVisibleCheckbox = visibleCheckboxes[visibleCheckboxes.length - 1] - var position = lastVisibleCheckbox.parentNode.offsetTop // parent element is relative - this.setContainerHeight(position + (lastVisibleCheckbox.clientHeight / 1.5)) - } - - Modules.OptionSelect = OptionSelect -})(window.GOVUK.Modules) diff --git a/app/assets/stylesheets/components/_option-select.scss b/app/assets/stylesheets/components/_option-select.scss deleted file mode 100644 index 2470abbdb..000000000 --- a/app/assets/stylesheets/components/_option-select.scss +++ /dev/null @@ -1,168 +0,0 @@ -@import "govuk_publishing_components/individual_component_support"; - -.app-c-option-select { - position: relative; - padding: 0 0 govuk-spacing(2); - margin-bottom: govuk-spacing(2); - border-bottom: 1px solid $govuk-border-colour; - - @include govuk-media-query($from: desktop) { - // Redefine scrollbars on desktop where these lists are scrollable - // so they are always visible in option lists - ::-webkit-scrollbar { - -webkit-appearance: none; - width: 7px; - } - - ::-webkit-scrollbar-thumb { - border-radius: 4px; - - background-color: rgba(0, 0, 0, .5); - -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .87); - } - } - - .gem-c-checkboxes { - margin: 0; - } -} - -.app-c-option-select__title { - @include govuk-font(19); - margin: 0; -} - -.app-c-option-select__button { - z-index: 1; - background: none; - border: 0; - text-align: left; - padding: 0; - cursor: pointer; - color: $govuk-link-colour; - - &:hover { - text-decoration: underline; - text-underline-offset: .1em; - @include govuk-link-hover-decoration; - } - - &::-moz-focus-inner { - border: 0; - } - - &:focus { - outline: none; - text-decoration: none; - @include govuk-focused-text; - } - - &[disabled] { - background-image: none; - color: inherit; - } - - // Extend the touch area of the button to span the heading - &::after { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - } -} - -.app-c-option-select__icon { - display: none; - position: absolute; - top: 0; - left: 9px; - width: 30px; - height: 40px; - fill: govuk-colour("black"); -} - -.app-c-option-select__container { - position: relative; - max-height: 200px; - overflow-y: auto; - overflow-x: hidden; - background-color: govuk-colour("white"); - - &:focus { - outline: 0; - } -} - -.app-c-option-select__container--large { - max-height: 600px; -} - -.app-c-option-select__container-inner { - padding: govuk-spacing(1) 13px; -} - -.app-c-option-select__filter { - position: relative; - background: govuk-colour("white"); - padding: 13px 13px govuk-spacing(2) 13px; -} - -.app-c-option-select__filter-input { - @include govuk-font(19); - padding-left: 33px; - background: image-url("option-select/input-icon.svg") govuk-colour("white") no-repeat -5px -3px; - - @include govuk-media-query($from: tablet) { - @include govuk-font(16); - } -} - -.js-enabled { - .app-c-option-select__heading { - position: relative; - padding: 10px 8px 5px 43px; - } - - [aria-expanded="true"] ~ .app-c-option-select__icon--up { - display: block; - } - - [aria-expanded="false"] ~ .app-c-option-select__icon--down { - display: block; - } - - .app-c-option-select__container { - height: 200px; - } - - .app-c-option-select__container--large { - height: 600px; - } - - [data-closed-on-load="true"] .app-c-option-select__container { - display: none; - } -} - -.app-c-option-select__selected-counter { - @include govuk-font($size: 14); - color: $govuk-text-colour; - margin-top: 3px; -} - -.app-c-option-select.js-closed { - .app-c-option-select__filter, - .app-c-option-select__container { - display: none; - } -} - -.app-c-option-select.js-opened { - .app-c-option-select__filter, - .app-c-option-select__container { - display: block; - } -} diff --git a/app/views/components/_option_select.html.erb b/app/views/components/_option_select.html.erb deleted file mode 100644 index 144b3a9e3..000000000 --- a/app/views/components/_option_select.html.erb +++ /dev/null @@ -1,69 +0,0 @@ -<% add_app_component_stylesheet("option-select") %> -<% - title_id = "option-select-title-#{title.parameterize}" - checkboxes_id = "checkboxes-#{SecureRandom.hex(4)}" - checkboxes_count_id = checkboxes_id + "-count" - show_filter ||= false - large ||= false - - classes = %w[app-c-option-select__container js-options-container] - classes << "app-c-option-select__container--large" if large -%> - -<% if show_filter %> - <% - filter_id ||= "input-#{SecureRandom.hex(4)}" - %> - <% filter = capture do %> - <%= tag.label for: filter_id, class: "govuk-label govuk-visually-hidden" do %> - Filter <%= title %> - <% end %> - - <%= tag.input name: "option-select-filter", - id: filter_id, - class: "app-c-option-select__filter-input govuk-input", - type: "text", - aria: { - describedby: checkboxes_count_id, - controls: checkboxes_id - } - %> - <% end %> - <% filter_element = CGI::escapeHTML(filter) %> -<% end %> - -