-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental
search_with_autocomplete
component
This adds a new experimental `search_with_autocomplete` component that enhances the existing `search` component with the GOV.UK Accessible Autocomplete Javascript library, and configures it to dynamically fetch a list of suggested searches from a remote source as the user types. - Add `accessible-autocomplete` package - Set up `search_with_autocomplete` component that wraps existing `search` component and sets it up to use `accessible-autocomplete` with a remote source - Inline basic `accessible-autocomplete` styling and adapt it to match new designs - Add extra input wrapper to `search` component markup to help manage the replacement of the original search input field Co-authored-by: Andy Sellick <andy.sellick@digital.cabinet-office.gov.uk> Co-authored-by: Alex Bowen <alex.bowen@digital.cabinet-office.gov.uk>
- Loading branch information
1 parent
ced7508
commit 92272b2
Showing
12 changed files
with
709 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
...sets/images/govuk_publishing_components/icon-autocomplete-search-suggestion.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 123 additions & 0 deletions
123
app/assets/javascripts/govuk_publishing_components/components/search-with-autocomplete.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* global accessibleAutocomplete, fetch */ | ||
//= require accessible-autocomplete/dist/accessible-autocomplete.min.js | ||
|
||
window.GOVUK = window.GOVUK || {} | ||
window.GOVUK.Modules = window.GOVUK.Modules || {}; | ||
|
||
(function (Modules) { | ||
class GemSearchWithAutocomplete { | ||
constructor ($module) { | ||
this.$module = $module | ||
|
||
this.$originalInput = this.$module.querySelector('input') | ||
this.$inputWrapper = this.$module.querySelector('.js-search-input-wrapper') | ||
this.$form = this.$module.closest('form') | ||
|
||
this.sourceUrl = this.$module.getAttribute('data-source-url') | ||
this.sourceKey = this.$module.getAttribute('data-source-key') | ||
} | ||
|
||
init () { | ||
const configOptions = { | ||
element: this.$inputWrapper, | ||
id: this.$originalInput.id, | ||
name: this.$originalInput.name, | ||
inputClasses: this.$originalInput.classList, | ||
defaultValue: this.$originalInput.value, | ||
cssNamespace: 'gem-c-search-with-autocomplete', | ||
confirmOnBlur: false, | ||
showNoOptionsFound: false, | ||
source: this.getResults.bind(this), | ||
onConfirm: this.submitContainingForm.bind(this), | ||
templates: { | ||
suggestion: this.constructSuggestionHTMLString.bind(this) | ||
}, | ||
tStatusNoResults: () => 'No search suggestions found', | ||
tStatusQueryTooShort: (minQueryLength) => `Type in ${minQueryLength} or more characters for search suggestions`, | ||
tStatusResults: (length, contentSelectedOption) => { | ||
const words = { | ||
result: (length === 1) ? 'search suggestion' : 'search suggestions', | ||
is: (length === 1) ? 'is' : 'are' | ||
} | ||
|
||
return `${length} ${words.result} ${words.is} available. ${contentSelectedOption}` | ||
}, | ||
tAssistiveHint: () => 'When search suggestions are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.' | ||
} | ||
accessibleAutocomplete(configOptions) | ||
|
||
// The accessible-autocomplete component is meant to generate a new input element rather than enhancing an existing one, so we need to do some cleanup here. | ||
this.$autocompleteInput = this.$inputWrapper.querySelector( | ||
'.gem-c-search-with-autocomplete__input' | ||
) | ||
// Ensure the new input element generated by accessible-autocomplete has the correct type | ||
this.$autocompleteInput.setAttribute('type', 'search') | ||
// Remove the original input from the DOM | ||
this.$originalInput.parentNode.removeChild(this.$originalInput) | ||
} | ||
|
||
// Callback used by accessible-autocomplete to generate the HTML for each suggestion based on | ||
// the values returned from the source | ||
constructSuggestionHTMLString (result) { | ||
const sanitizedResult = this.sanitizeResult(result) | ||
const inputValue = this.$inputWrapper.querySelector('input').value.toLowerCase() | ||
|
||
const index = sanitizedResult.toLowerCase().indexOf(inputValue) | ||
|
||
let html = sanitizedResult | ||
if (index !== -1) { | ||
const before = sanitizedResult.slice(0, index) | ||
const match = sanitizedResult.slice(index, index + inputValue.length) | ||
const after = sanitizedResult.slice(index + inputValue.length) | ||
|
||
html = `${before}<mark class="gem-c-search-with-autocomplete__suggestion-highlight">${match}</mark>${after}` | ||
} | ||
|
||
return ` | ||
<div class="gem-c-search-with-autocomplete__option-wrapper"> | ||
<span class="gem-c-search-with-autocomplete__suggestion-icon"></span> | ||
<span class="gem-c-search-with-autocomplete__suggestion-text">${html}</span> | ||
</div> | ||
` | ||
} | ||
|
||
// Callback used by accessible-autocomplete to fetch results from the source | ||
getResults (query, populateResults) { | ||
const url = new URL(this.sourceUrl) | ||
url.searchParams.set('q', query) | ||
fetch(url, { headers: { Accept: 'application/json' } }) | ||
.then(response => response.json()) | ||
.then((data) => { populateResults(data[this.sourceKey]) }) | ||
.catch(() => { populateResults([]) }) | ||
} | ||
|
||
// Callback used by accessible-autocomplete to submit the containing form when a suggestion is | ||
// confirmed by the user (e.g. by pressing Enter or clicking on it) | ||
submitContainingForm (value) { | ||
if (this.$form) { | ||
// The accessible-autocomplete component calls this callback _before_ it updates its | ||
// internal state, so the value of the input field is not yet updated when this callback is | ||
// called. We need to force the value to be updated before submitting the form, but the rest | ||
// of the state can catch up later. | ||
this.$autocompleteInput.value = value | ||
|
||
if (this.$form.requestSubmit) { | ||
this.$form.requestSubmit() | ||
} else { | ||
// Fallback for certain Grade C browsers that don't support `requestSubmit` | ||
this.$form.submit() | ||
} | ||
} | ||
} | ||
|
||
// Sanitises a result coming back from the source to prevent XSS issues if the result happens to | ||
// contain HTML. | ||
sanitizeResult (value) { | ||
const scratch = document.createElement('div') | ||
scratch.textContent = value | ||
return scratch.innerHTML | ||
} | ||
} | ||
|
||
Modules.GemSearchWithAutocomplete = GemSearchWithAutocomplete | ||
})(window.GOVUK.Modules) |
200 changes: 200 additions & 0 deletions
200
app/assets/stylesheets/govuk_publishing_components/components/_search-with-autocomplete.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
@import "govuk_publishing_components/individual_component_support"; | ||
|
||
// These styles are adapted from the original Accessible Autocomplete component stylesheet, mostly | ||
// to remove superfluous styles that are already provided by the GOV.UK Design System, and to adapt | ||
// the styling to match the new GOV.UK search box designs (e.g. to remove the zebra striping on | ||
// rows, adjust whitespace, and manage the tweaked markup we use in the suggestion template). | ||
// | ||
// Note that most selectors targetted within this file are those constructed by the Accessible | ||
// Autocomplete component, so they may not 100% match our own component conventions. | ||
// | ||
// see https://github.com/alphagov/accessible-autocomplete/blob/main/src/autocomplete.css | ||
|
||
// Helps to make the autocomplete menu as wide as the entire search box _including_ the submit | ||
// button, not just the width of the input field. | ||
@mixin enhance-autocomplete-menu-width($button-size) { | ||
margin-right: -$button-size; | ||
} | ||
|
||
$input-size: 40px; | ||
$large-input-size: 50px; | ||
|
||
.gem-c-search-with-autocomplete__wrapper { | ||
position: relative; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__menu { | ||
margin: 0; | ||
padding: 0; | ||
overflow-x: hidden; | ||
background-color: govuk-colour("white"); | ||
border: 1px solid $govuk-border-colour; | ||
border-top: 0; | ||
|
||
@include enhance-autocomplete-menu-width($input-size); | ||
} | ||
|
||
.gem-c-search-with-autocomplete__menu--visible { | ||
display: block; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__menu--hidden { | ||
display: none; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__menu--inline { | ||
position: relative; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option { | ||
display: block; | ||
cursor: pointer; | ||
|
||
@include govuk-font(19); | ||
|
||
// Ensure only the option itself receives pointer events | ||
& > * { | ||
pointer-events: none; | ||
} | ||
|
||
// Accessible Autocomplete's iOS screenreader inset has broken CSS which hasn't been fixed | ||
// upstream, and means that its text is not just visible to screenreader users, but displayed | ||
// for everyone. This span is added dynamically only on iOS and not given a class, so we need to | ||
// target it in a roundabout way and make it invisible to non-screenreader users. | ||
& > span { | ||
clip: rect(0 0 0 0); | ||
clip-path: inset(50%); | ||
height: 1px; | ||
overflow: hidden; | ||
position: absolute; | ||
white-space: nowrap; | ||
width: 1px; | ||
} | ||
} | ||
|
||
// Common styling for _all_ focus states, including keyboard focus, mouse hover, and keyboard focus | ||
// but mouse on another option. | ||
.gem-c-search-with-autocomplete__option--focused, | ||
.gem-c-search-with-autocomplete__option:hover, | ||
.gem-c-search-with-autocomplete__option:focus-visible { | ||
background-color: govuk-colour("light-grey"); | ||
outline: none; | ||
|
||
@include govuk-link-decoration; | ||
@include govuk-link-hover-decoration; | ||
|
||
.gem-c-search-with-autocomplete__suggestion-icon { | ||
background-color: $govuk-text-colour; | ||
} | ||
} | ||
|
||
// Styling specifically _only_ for keyboard focus | ||
.gem-c-search-with-autocomplete__option:focus-visible { | ||
.gem-c-search-with-autocomplete__suggestion-text { | ||
background-color: $govuk-focus-colour; | ||
} | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option-wrapper { | ||
display: flex; | ||
align-items: center; | ||
margin: 0 govuk-spacing(3); | ||
padding: govuk-spacing(1) 0; | ||
border-bottom: 1px solid govuk-colour("mid-grey"); | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option:last-child .gem-c-search-with-autocomplete__option-wrapper { | ||
border-bottom: 0; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-icon { | ||
width: calc($input-size / 2); | ||
height: $input-size; | ||
margin-right: govuk-spacing(2); | ||
flex: none; | ||
mask-image: url("govuk_publishing_components/icon-autocomplete-search-suggestion.svg"); | ||
-webkit-mask-image: url("govuk_publishing_components/icon-autocomplete-search-suggestion.svg"); | ||
background-color: $govuk-secondary-text-colour; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-text { | ||
font-weight: bold; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-highlight { | ||
font-weight: normal; | ||
background: none; | ||
} | ||
|
||
// Tweak the look and feel for the autocomplete in large mode | ||
.gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--large { | ||
.gem-c-search-with-autocomplete__menu { | ||
@include enhance-autocomplete-menu-width($large-input-size); | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option { | ||
min-height: $large-input-size; | ||
} | ||
} | ||
|
||
// Fix top border styling on "borderless" search input when rendered on a GOV.UK blue background | ||
.gem-c-search-with-autocomplete.gem-c-search-with-autocomplete--on-govuk-blue { | ||
.gem-c-search-with-autocomplete__menu { | ||
border-top: 1px solid $govuk-border-colour; | ||
} | ||
} | ||
|
||
// High contrast mode adjustments | ||
@media (forced-colors: active) { | ||
.gem-c-search-with-autocomplete__menu { | ||
border-color: FieldText; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option { | ||
forced-color-adjust: none; // opt out of all default forced-colors adjustments | ||
background-color: Field; | ||
color: FieldText; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__option--focused, | ||
.gem-c-search-with-autocomplete__option:hover, | ||
.gem-c-search-with-autocomplete__option:focus-visible { | ||
background-color: Highlight; | ||
color: HighlightText; | ||
border-color: FieldText; | ||
|
||
.gem-c-search-with-autocomplete__suggestion-text { | ||
background: none; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-highlight { | ||
color: HighlightText; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-icon { | ||
background-color: HighlightText; | ||
} | ||
} | ||
|
||
// Allow mouse hover styling to take precedence over keyboard focus styling | ||
.gem-c-search-with-autocomplete__option:focus-visible:not(:hover) { | ||
background-color: SelectedItem; | ||
color: SelectedItemText; | ||
|
||
.gem-c-search-with-autocomplete__suggestion-highlight { | ||
color: SelectedItemText; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-icon { | ||
background-color: SelectedItemText; | ||
} | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-highlight { | ||
color: FieldText; | ||
} | ||
|
||
.gem-c-search-with-autocomplete__suggestion-icon { | ||
background-color: FieldText; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.