├── .nvmrc
├── Procfile
├── preact.js
├── react.js
├── .gitignore
├── .github
├── dependabot.yml
└── workflows
│ ├── integration_tests.yaml
│ └── test.yaml
├── app.json
├── src
├── dropdown-arrow-down.js
├── wrapper.js
├── status.js
├── autocomplete.css
└── autocomplete.js
├── scripts
└── check-staged.mjs
├── .babelrc.js
├── test
├── karma.config.js
├── functional
│ ├── dropdown-arrow-down.js
│ └── wrapper.js
├── wdio.config.js
└── integration
│ └── index.js
├── LICENSE.txt
├── accessibility-criteria.md
├── dist
├── accessible-autocomplete.min.css
└── lib
│ └── accessible-autocomplete.react.min.js
├── package.json
├── webpack.config.babel.js
├── examples
├── multiselect.html
├── preact
│ └── index.html
├── react
│ └── index.html
├── form-single.html
└── index.html
├── CONTRIBUTING.md
├── CHANGELOG.md
└── README.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14.18.2
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: python -m SimpleHTTPServer $PORT
2 |
--------------------------------------------------------------------------------
/preact.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/lib/accessible-autocomplete.preact.min')
2 |
--------------------------------------------------------------------------------
/react.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/lib/accessible-autocomplete.react.min')
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /npm-debug.log
3 | /build
4 | .DS_Store
5 | /coverage
6 | /.idea
7 | *.log
8 | /screenshots
9 | .env
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: /
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 0
8 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessible-autocomplete",
3 | "scripts": {
4 | },
5 | "env": {
6 | },
7 | "formation": {
8 | },
9 | "addons": [
10 |
11 | ],
12 | "buildpacks": [
13 |
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/dropdown-arrow-down.js:
--------------------------------------------------------------------------------
1 | import { createElement } from 'preact' /** @jsx createElement */
2 |
3 | const DropdownArrowDown = ({ className }) => (
4 |
5 |
6 |
7 |
8 |
9 | )
10 |
11 | export default DropdownArrowDown
12 |
--------------------------------------------------------------------------------
/scripts/check-staged.mjs:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk'
2 | import * as cp from 'child_process'
3 |
4 | cp.exec('git diff --name-only dist/', (err, stdout) => {
5 | if (err) {
6 | console.log(chalk.red('ERROR:'), err)
7 | return process.exit(1)
8 | }
9 | if (stdout.toString().length) {
10 | console.log(chalk.red('ERROR:'), 'There are unstaged changes in `dist/` after running `npm run build`. Please commit them.')
11 | return process.exit(1)
12 | }
13 | process.exit()
14 | })
15 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {
4 | shippedProposals: true,
5 | useBuiltIns: 'usage',
6 | loose: true
7 | }]
8 | ],
9 | plugins: [
10 | ['@babel/plugin-proposal-class-properties', { loose: true }],
11 | ['@babel/plugin-proposal-decorators', { legacy: true }],
12 | ['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
13 |
14 | // Improve legacy IE compatibility
15 | ['@babel/plugin-transform-modules-commonjs', { loose: true }],
16 | '@babel/plugin-transform-member-expression-literals',
17 | '@babel/plugin-transform-property-literals'
18 | ],
19 | env: {
20 | test: {
21 | plugins: [
22 | 'istanbul'
23 | ]
24 | }
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/test/karma.config.js:
--------------------------------------------------------------------------------
1 | require('@babel/register')({
2 | cwd: require('path').resolve(__dirname, '../')
3 | })
4 | var puppeteer = require('puppeteer')
5 | var webpack = require('../webpack.config.babel.js')[0]
6 |
7 | // Use Chrome headless
8 | process.env.CHROME_BIN = puppeteer.executablePath()
9 |
10 | module.exports = function (config) {
11 | config.set({
12 | basePath: '../',
13 | frameworks: ['mocha', 'chai-sinon'],
14 | reporters: ['mocha'],
15 |
16 | browsers: ['ChromeHeadless'],
17 |
18 | files: [
19 | 'test/functional/**/*.js'
20 | ],
21 |
22 | preprocessors: {
23 | 'test/**/*.js': ['webpack'],
24 | 'src/**/*.js': ['webpack'],
25 | '**/*.js': ['sourcemap']
26 | },
27 |
28 | webpack: webpack,
29 | webpackMiddleware: {
30 | logLevel: 'error',
31 | stats: 'errors-only'
32 | }
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/integration_tests.yaml:
--------------------------------------------------------------------------------
1 | name: Integration tests
2 |
3 | on: [push, pull_request]
4 |
5 |
6 | jobs:
7 | test-saucelabs:
8 | name: Build & test
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Read node version from .nvmrc
15 | id: nvm
16 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
17 |
18 | - name: "Setup Node v${{ steps.nvm.outputs.NVMRC }}"
19 | uses: actions/setup-node@v2
20 | with:
21 | node-version: "${{ steps.nvm.outputs.NVMRC }}"
22 | cache: 'npm'
23 |
24 | - name: Install dependencies
25 | run: npm ci
26 |
27 | - name: Build
28 | run: npm run build
29 |
30 | - name: "Run integration tests"
31 | run: npm run wdio:test
32 | env:
33 | SAUCE_ENABLED: "false" # ensure we don't use Sauce Labs
34 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | build:
7 | name: Build & basic tests
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2
12 |
13 | - name: Read node version from .nvmrc
14 | id: nvm
15 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
16 |
17 | - name: "Setup Node v${{ steps.nvm.outputs.NVMRC }}"
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: "${{ steps.nvm.outputs.NVMRC }}"
21 | cache: 'npm'
22 |
23 | - name: Install dependencies
24 | run: npm ci
25 |
26 | - name: Build
27 | run: npm run build
28 |
29 | - name: Functional tests (Chromium)
30 | run: npm run karma
31 |
32 | # Run linter last so other tests run even if there is a code formatting error
33 | - name: Lint
34 | run: npm run standard
35 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Crown Copyright (Government Digital Service)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/functional/dropdown-arrow-down.js:
--------------------------------------------------------------------------------
1 | /* global before, beforeEach, after, describe, expect, it */
2 | import { createElement, render } from 'preact' /** @jsx createElement */
3 | import DropdownArrowDown from '../../src/dropdown-arrow-down'
4 |
5 | describe('DropdownArrowDown', () => {
6 | describe('rendering', () => {
7 | let scratch
8 |
9 | before(() => {
10 | scratch = document.createElement('div');
11 | (document.body || document.documentElement).appendChild(scratch)
12 | })
13 |
14 | beforeEach(() => {
15 | scratch.innerHTML = ''
16 | })
17 |
18 | after(() => {
19 | scratch.parentNode.removeChild(scratch)
20 | scratch = null
21 | })
22 |
23 | describe('basic usage', () => {
24 | it('renders an svg', () => {
25 | render( , scratch)
26 |
27 | expect(scratch.innerHTML).to.contain('svg')
28 | })
29 |
30 | it('renders with a given custom class', () => {
31 | render( , scratch)
32 |
33 | expect(scratch.innerHTML).to.contain('class="foo"')
34 | })
35 |
36 | // IE issue so the dropdown svg is not focusable (tabindex won't work for this)
37 | it('renders an svg where focusable attribute is false', () => {
38 | render( , scratch)
39 |
40 | expect(scratch.innerHTML).to.contain('focusable="false"')
41 | })
42 | })
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/accessibility-criteria.md:
--------------------------------------------------------------------------------
1 | # Accessibility Acceptance Criteria
2 |
3 | Authors: [Theodor Vararu](https://github.com/tvararu), [Léonie Watson](https://github.com/LJWatson), [Ed Horsford](https://github.com/edwardhorsford).
4 |
5 | ## What's this?
6 |
7 | This describes the necessary behaviours that an autocomplete needs to meet to be usable by assistive technologies.
8 |
9 | They are useful if you are evaluating `accessible-autocomplete`, or a different autocomplete solution.
10 |
11 | ## User story
12 |
13 | > As an Assistive Technology (AT) user, I want to be able to search using autocomplete, so I can find and choose a matching result easily and accurately.
14 |
15 | ## Acceptance criteria
16 |
17 | The field with autocomplete must:
18 |
19 | 1. Be focusable with a keyboard
20 | 1. Indicate when it has keyboard focus
21 | 1. Inform the user that it is an editable field
22 | 1. Inform the user if there is a pre-filled value
23 | 1. Inform the user that autocomplete is available
24 | 1. Explain how to use autocomplete
25 | 1. Inform the user that content has been expanded
26 | 1. Inform the user when there are matches, or if there are no matches
27 | 1. (Optional) Inform the user how many matches are currently available
28 | 1. Inform the user as the number of matches changes
29 | 1. Enable the user to navigate the available matches using touch or keyboard
30 | 1. Inform the user when a match is selected
31 | 1. (Optional) Inform the user which number the currently selected match is (1 of 3 for example)
32 | 1. Inform the user if a match is pre-selected
33 | 1. Enable the user to confirm the selected match
34 | 1. Inform the user when a match is confirmed
35 | 1. Return focus to the editable field when a selected match is confirmed
36 |
37 | Helpful definitions:
38 |
39 | - `navigate`: When the user selects between matches
40 | - `selected`: When one of the matches is highlighted, and ready to be confirmed, for example by pressing `enter`
41 | - `pre-selected`: When an item is selected on the user's behalf without them navigating to it
42 | - `confirmed`: When one of the matches has been confirmed, and will be submitted as a value of the parent form
43 |
--------------------------------------------------------------------------------
/test/wdio.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 | const puppeteer = require('puppeteer')
3 | const staticServerPort = process.env.PORT || 4567
4 | const services = [
5 | ['static-server', {
6 | folders: [
7 | { mount: '/', path: './examples' },
8 | { mount: '/dist/', path: './dist' }
9 | ],
10 | port: staticServerPort
11 | }]
12 | ]
13 |
14 | const sauceEnabled = process.env.SAUCE_ENABLED === 'true'
15 | const sauceUser = process.env.SAUCE_USERNAME
16 | const sauceKey = process.env.SAUCE_ACCESS_KEY
17 | const buildNumber = process.env.SAUCE_BUILD_NUMBER
18 | const sauceConfig = {
19 | user: sauceUser,
20 | key: sauceKey,
21 | capabilities: [
22 | {
23 | browserName: 'chrome',
24 | platformName: 'Windows 10',
25 | 'sauce:options': {
26 | build: buildNumber
27 | }
28 | },
29 | {
30 | browserName: 'firefox',
31 | browserVersion: '55',
32 | platformName: 'Windows 10',
33 | 'sauce:options': {
34 | build: buildNumber
35 | }
36 | },
37 | {
38 | browserName: 'internet explorer',
39 | browserVersion: '11.285',
40 | platformName: 'Windows 10',
41 | 'sauce:options': {
42 | build: buildNumber
43 | }
44 | },
45 | {
46 | browserName: 'internet explorer',
47 | browserVersion: '10',
48 | platformName: 'Windows 7',
49 | 'sauce:options': {
50 | build: buildNumber
51 | }
52 | },
53 | /* IE9 on Sauce Labs needs to use legacy JSON Wire Protocol */
54 | {
55 | browserName: 'internet explorer',
56 | version: '9',
57 | platform: 'Windows 7',
58 | build: buildNumber
59 | }
60 | ],
61 | services: services.concat([['sauce', { sauceConnect: true }]])
62 | }
63 |
64 | const puppeteerConfig = {
65 | automationProtocol: 'devtools',
66 | capabilities: [
67 | // { browserName: 'firefox' },
68 | {
69 | browserName: 'chrome',
70 | 'goog:chromeOptions': {
71 | args: ['--headless'],
72 | binary: puppeteer.executablePath()
73 | }
74 | }
75 | ],
76 | services: services
77 | }
78 |
79 | exports.config = Object.assign({
80 | outputDir: './logs/',
81 | specs: ['./test/integration/**/*.js'],
82 | baseUrl: 'http://localhost:' + staticServerPort,
83 | screenshotPath: './screenshots/',
84 | reporters: ['spec'],
85 | framework: 'mocha',
86 | mochaOpts: { timeout: 30 * 1000 }
87 | }, sauceEnabled ? sauceConfig : puppeteerConfig)
88 |
--------------------------------------------------------------------------------
/dist/accessible-autocomplete.min.css:
--------------------------------------------------------------------------------
1 | .autocomplete__wrapper{position:relative}.autocomplete__hint,.autocomplete__input{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-weight:400;box-sizing:border-box;width:100%;height:40px;border:2px solid #0b0c0c;border-radius:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:0}.autocomplete__input{padding:5px;background-color:transparent;position:relative}.autocomplete__hint{color:#b1b4b6;position:absolute}.autocomplete__input--default{padding:5px}.autocomplete__input--focused,.autocomplete__input:focus{outline-offset:0;box-shadow:inset 0 0 0 2px}.autocomplete__input--show-all-values{padding:5px 34px 5px 5px;cursor:pointer}.autocomplete__dropdown-arrow-down{z-index:-1;display:inline-block;position:absolute;right:8px;width:24px;height:24px;top:10px}.autocomplete__menu{background-color:#fff;border:2px solid #0b0c0c;border-top:0;color:#0b0c0c;margin:0;max-height:342px;overflow-x:hidden;padding:0;width:100%;width:calc(100% - 4px)}.autocomplete__menu--visible{display:block}.autocomplete__menu--hidden{display:none}.autocomplete__menu--overlay{box-shadow:rgba(0,0,0,.256863) 0 2px 6px;left:0;position:absolute;top:100%;z-index:100}.autocomplete__menu--inline{position:relative}.autocomplete__option{border-bottom:solid #b1b4b6;border-width:1px 0;cursor:pointer;display:block;position:relative}.autocomplete__option>*{pointer-events:none}.autocomplete__option:first-of-type{border-top-width:0}.autocomplete__option:last-of-type{border-bottom-width:0}.autocomplete__option--odd{background-color:#fafafa}.autocomplete__option--focused,.autocomplete__option:hover{background-color:#1d70b8;border-color:#1d70b8;color:#fff;outline:0}@media (-ms-high-contrast:active),(forced-colors:active){.autocomplete__menu{border-color:FieldText}.autocomplete__option{background-color:Field;color:FieldText}.autocomplete__option--focused,.autocomplete__option:hover{forced-color-adjust:none;background-color:SelectedItem;border-color:SelectedItem;color:SelectedItemText;outline-color:SelectedItemText}}.autocomplete__option--no-results{background-color:#fafafa;color:#646b6f;cursor:not-allowed}.autocomplete__hint,.autocomplete__input,.autocomplete__option{font-size:16px;line-height:1.25}.autocomplete__hint,.autocomplete__option{padding:5px}@media (min-width:641px){.autocomplete__hint,.autocomplete__input,.autocomplete__option{font-size:19px;line-height:1.31579}}.autocomplete__list{list-style-type:none;padding:0;margin:20px 0 0}.autocomplete__list .autocomplete__selected-option,.autocomplete__list .autocomplete__selected-option:hover{cursor:inherit;color:#0b0c0c;margin:5px 0}.autocomplete__list .autocomplete__remove-option{cursor:pointer;color:#005ea5;text-decoration:underline;background:0 0;border:0;font-size:inherit;margin-left:5px}.autocomplete__list .autocomplete__remove-option:hover{color:#2b8cc4}.autocomplete__list .autocomplete__remove-option:focus{color:#0b0c0c;outline:3px solid #ffbf47;outline-offset:0;background-color:#ffbf47}.autocomplete__list .autocomplete__remove-option:before{content:'× ';color:inherit}.autocomplete__list-item-description{display:none}
--------------------------------------------------------------------------------
/src/wrapper.js:
--------------------------------------------------------------------------------
1 | import { createElement, render } from 'preact' /** @jsx createElement */
2 | import Autocomplete from './autocomplete'
3 |
4 | function accessibleAutocomplete (options) {
5 | if (!options.element) { throw new Error('element is not defined') }
6 | if (!options.id) { throw new Error('id is not defined') }
7 | if (!options.source) { throw new Error('source is not defined') }
8 | if (Array.isArray(options.source)) {
9 | options.source = createSimpleEngine(options.source)
10 | }
11 | render( , options.element)
12 | }
13 |
14 | const createSimpleEngine = (values) => (query, syncResults) => {
15 | var matches = values.filter(r => r.toLowerCase().indexOf(query.toLowerCase()) !== -1)
16 | syncResults(matches)
17 | }
18 |
19 | accessibleAutocomplete.enhanceSelectElement = (configurationOptions) => {
20 | if (!configurationOptions.selectElement) { throw new Error('selectElement is not defined') }
21 |
22 | const selectElement = configurationOptions.selectElement
23 | const selectableOptions = [].filter.call(selectElement.options, option => (option.value || configurationOptions.preserveNullOptions))
24 |
25 | // Set defaults.
26 | if (!configurationOptions.source) {
27 | configurationOptions.source = selectableOptions.map(option => option.textContent || option.innerText)
28 | }
29 |
30 | if (selectElement.multiple) {
31 | configurationOptions.multiple = true
32 | configurationOptions.confirmOnBlur = false
33 | configurationOptions.showNoOptionsFound = false
34 | configurationOptions.selectedOptions = selectableOptions.filter(option => option.selected).map(option => option.textContent)
35 | configurationOptions.onRemove = configurationOptions.onRemove || (value => {
36 | const optionToRemove = [].filter.call(configurationOptions.selectElement.options, option => (option.textContent || option.innerText) === value)[0]
37 | if (optionToRemove) { optionToRemove.selected = false }
38 | })
39 | }
40 |
41 | configurationOptions.onConfirm = configurationOptions.onConfirm || (query => {
42 | let options = configurationOptions.selectElement.options
43 | let matchingOption
44 | if (query) {
45 | matchingOption = [].filter.call(options, option => (option.textContent || option.innerText) === query)[0]
46 | } else {
47 | matchingOption = [].filter.call(options, option => option.value === '')[0]
48 | }
49 | if (matchingOption) { matchingOption.selected = true }
50 | })
51 |
52 | if (!configurationOptions.multiple && (selectElement.value || configurationOptions.defaultValue === undefined)) {
53 | const option = selectElement.options[selectElement.options.selectedIndex]
54 | if (option.textContent || option.innerText) {
55 | configurationOptions.defaultValue = option.textContent || option.innerText
56 | }
57 | }
58 |
59 | if (configurationOptions.name === undefined) configurationOptions.name = ''
60 | if (configurationOptions.id === undefined) {
61 | if (selectElement.id === undefined) {
62 | configurationOptions.id = ''
63 | } else {
64 | configurationOptions.id = selectElement.id
65 | }
66 | }
67 | if (configurationOptions.autoselect === undefined) configurationOptions.autoselect = true
68 |
69 | const element = document.createElement('div')
70 |
71 | selectElement.parentNode.insertBefore(element, selectElement)
72 |
73 | accessibleAutocomplete({
74 | ...configurationOptions,
75 | element: element
76 | })
77 |
78 | selectElement.style.display = 'none'
79 | selectElement.id = selectElement.id + '-select'
80 | }
81 |
82 | export default accessibleAutocomplete
83 |
--------------------------------------------------------------------------------
/src/status.js:
--------------------------------------------------------------------------------
1 | import { createElement, Component } from 'preact' /** @jsx createElement */
2 |
3 | const debounce = function (func, wait, immediate) {
4 | var timeout
5 | return function () {
6 | var context = this
7 | var args = arguments
8 | var later = function () {
9 | timeout = null
10 | if (!immediate) func.apply(context, args)
11 | }
12 | var callNow = immediate && !timeout
13 | clearTimeout(timeout)
14 | timeout = setTimeout(later, wait)
15 | if (callNow) func.apply(context, args)
16 | }
17 | }
18 | const statusDebounceMillis = 1400
19 |
20 | export default class Status extends Component {
21 | static defaultProps = {
22 | tQueryTooShort: (minQueryLength) => `Type in ${minQueryLength} or more characters for results`,
23 | tNoResults: () => 'No search results',
24 | tSelectedOption: (selectedOption, length, index) => `${selectedOption} ${index + 1} of ${length} is highlighted`,
25 | tResults: (length, contentSelectedOption) => {
26 | const words = {
27 | result: (length === 1) ? 'result' : 'results',
28 | is: (length === 1) ? 'is' : 'are'
29 | }
30 |
31 | return `${length} ${words.result} ${words.is} available. ${contentSelectedOption}`
32 | }
33 | };
34 |
35 | state = {
36 | bump: false,
37 | debounced: false
38 | }
39 |
40 | componentWillMount () {
41 | const that = this
42 | this.debounceStatusUpdate = debounce(function () {
43 | if (!that.state.debounced) {
44 | const shouldSilence = !that.props.isInFocus || that.props.validChoiceMade
45 | that.setState(({ bump }) => ({ bump: !bump, debounced: true, silenced: shouldSilence }))
46 | }
47 | }, statusDebounceMillis)
48 | }
49 |
50 | componentWillReceiveProps ({ queryLength }) {
51 | this.setState({ debounced: false })
52 | }
53 |
54 | render () {
55 | const {
56 | id,
57 | length,
58 | queryLength,
59 | minQueryLength,
60 | selectedOption,
61 | selectedOptionIndex,
62 | tQueryTooShort,
63 | tNoResults,
64 | tSelectedOption,
65 | tResults
66 | } = this.props
67 | const { bump, debounced, silenced } = this.state
68 |
69 | const queryTooShort = queryLength < minQueryLength
70 | const noResults = length === 0
71 |
72 | const contentSelectedOption = selectedOption
73 | ? tSelectedOption(selectedOption, length, selectedOptionIndex)
74 | : ''
75 |
76 | let content = null
77 | if (queryTooShort) {
78 | content = tQueryTooShort(minQueryLength)
79 | } else if (noResults) {
80 | content = tNoResults()
81 | } else {
82 | content = tResults(length, contentSelectedOption)
83 | }
84 |
85 | this.debounceStatusUpdate()
86 |
87 | return (
88 |
101 |
106 | {(!silenced && debounced && bump) ? content : ''}
107 |
108 |
113 | {(!silenced && debounced && !bump) ? content : ''}
114 |
115 |
116 | )
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessible-autocomplete",
3 | "version": "2.0.4",
4 | "main": "dist/accessible-autocomplete.min.js",
5 | "style": "dist/accessible-autocomplete.min.css",
6 | "description": "An autocomplete component, built to be accessible.",
7 | "repository": "alphagov/accessible-autocomplete",
8 | "author": "Government Digital Service (https://www.gov.uk/government/organisations/government-digital-service)",
9 | "license": "MIT",
10 | "keywords": [
11 | "a11y",
12 | "accessibility",
13 | "autocomplete",
14 | "component",
15 | "plugin",
16 | "typeahead",
17 | "widget"
18 | ],
19 | "scripts": {
20 | "build:css": "csso src/autocomplete.css -o dist/accessible-autocomplete.min.css",
21 | "build:js": "cross-env NODE_ENV=production webpack --progress --display-modules",
22 | "build": "run-s 'build:js' 'build:css'",
23 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress",
24 | "karma:dev": "cross-env NODE_ENV=test karma start test/karma.config.js",
25 | "karma": "npm run karma:dev -- --single-run",
26 | "preversion": "npm test",
27 | "standard": "standard",
28 | "test": "run-p standard karma wdio",
29 | "version": "npm run build && git add -A dist",
30 | "wdio:test": "cross-env NODE_ENV=test wdio test/wdio.config.js",
31 | "wdio": "npm run build wdio:test && git checkout dist/"
32 | },
33 | "husky": {
34 | "hooks": {
35 | "pre-push": "npm run build && node scripts/check-staged.mjs"
36 | }
37 | },
38 | "dependencies": {
39 | "preact": "^8.3.1"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.1.5",
43 | "@babel/plugin-proposal-class-properties": "^7.1.0",
44 | "@babel/plugin-proposal-decorators": "^7.1.2",
45 | "@babel/plugin-transform-member-expression-literals": "^7.0.0",
46 | "@babel/plugin-transform-modules-commonjs": "^7.1.0",
47 | "@babel/plugin-transform-property-literals": "^7.0.0",
48 | "@babel/plugin-transform-react-jsx": "^7.0.0",
49 | "@babel/preset-env": "^7.1.5",
50 | "@babel/register": "^7.0.0",
51 | "@wdio/cli": "^7.16.13",
52 | "@wdio/local-runner": "^7.16.13",
53 | "@wdio/mocha-framework": "^7.16.13",
54 | "@wdio/sauce-service": "^7.16.13",
55 | "@wdio/spec-reporter": "^7.16.13",
56 | "@wdio/static-server-service": "^7.16.13",
57 | "@wdio/sync": "^7.16.13",
58 | "babel-eslint": "^10.0.1",
59 | "babel-loader": "^8.2.3",
60 | "babel-plugin-istanbul": "^5.1.0",
61 | "chai": "^4.2.0",
62 | "chalk": "^2.4.1",
63 | "copy-webpack-plugin": "^6.4.1",
64 | "cross-env": "^5.2.0",
65 | "csso-cli": "^3.0.0",
66 | "dotenv": "^6.1.0",
67 | "husky": "^1.1.3",
68 | "karma": "^6.3.9",
69 | "karma-chai": "^0.1.0",
70 | "karma-chai-sinon": "^0.1.5",
71 | "karma-chrome-launcher": "^3.1.0",
72 | "karma-mocha": "^1.0.1",
73 | "karma-mocha-reporter": "^2.2.5",
74 | "karma-sourcemap-loader": "^0.3.7",
75 | "karma-webpack": "^4.0.2",
76 | "mocha": "^10.2.0",
77 | "npm-run-all": "^4.1.5",
78 | "puppeteer": "^13.3.1",
79 | "replace-bundle-webpack-plugin": "^1.0.0",
80 | "sinon": "^6.3.5",
81 | "sinon-chai": "^3.2.0",
82 | "source-map-loader": "^1.1.3",
83 | "standard": "^12.0.1",
84 | "uglifyjs-webpack-plugin": "^2.0.1",
85 | "webdriverio": "^7.16.13",
86 | "webpack": "^4.46.0",
87 | "webpack-cli": "^3.3.12",
88 | "webpack-dev-server": "^4.15.1"
89 | },
90 | "browserslist": [
91 | ">0.1%",
92 | "last 2 Chrome versions",
93 | "last 2 Firefox versions",
94 | "last 2 Edge versions",
95 | "last 2 Samsung versions",
96 | "Safari >= 9",
97 | "ie 8-11",
98 | "iOS >= 9"
99 | ],
100 | "standard": {
101 | "parser": "babel-eslint"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack'
2 | import path from 'path'
3 | import CopyWebpackPlugin from 'copy-webpack-plugin'
4 | import UglifyJsPlugin from 'uglifyjs-webpack-plugin'
5 | const ENV = process.env.NODE_ENV || 'development'
6 |
7 | const plugins = [
8 | new webpack.NoEmitOnErrorsPlugin(),
9 | new webpack.DefinePlugin({
10 | 'process.env.NODE_ENV': JSON.stringify(ENV)
11 | })
12 | ]
13 |
14 | const developmentPlugins = [
15 | new CopyWebpackPlugin({ patterns: [
16 | { from: './autocomplete.css', to: 'accessible-autocomplete.min.css' }
17 | ] })
18 | ]
19 |
20 | const config = {
21 | context: path.resolve(__dirname, 'src'),
22 |
23 | optimization: {
24 | minimize: ENV === 'production',
25 | minimizer: [new UglifyJsPlugin({
26 | cache: true,
27 | parallel: true,
28 | sourceMap: true,
29 | uglifyOptions: {
30 | compress: {
31 | negate_iife: false,
32 | properties: false,
33 | ie8: true
34 | },
35 | mangle: {
36 | ie8: true
37 | },
38 | output: {
39 | comments: false,
40 | ie8: true
41 | }
42 | }
43 | })]
44 | },
45 |
46 | resolve: {
47 | extensions: ['.js'],
48 | modules: [
49 | path.resolve(__dirname, 'node_modules'),
50 | 'node_modules'
51 | ]
52 | },
53 |
54 | module: {
55 | rules: [
56 | {
57 | test: /\.js$/,
58 | include: path.resolve(__dirname, 'src'),
59 | enforce: 'pre',
60 | loader: 'source-map-loader'
61 | },
62 | {
63 | test: /\.js$/,
64 | exclude: /node_modules/,
65 | loader: 'babel-loader'
66 | }
67 | ]
68 | },
69 |
70 | stats: { colors: true },
71 |
72 | node: {
73 | global: true,
74 | process: false,
75 | Buffer: false,
76 | __filename: false,
77 | __dirname: false,
78 | setImmediate: false
79 | },
80 |
81 | mode: ENV === 'production' ? 'production' : 'development',
82 | devtool: ENV === 'production' ? 'source-map' : 'cheap-module-eval-source-map',
83 |
84 | devServer: {
85 | setup (app) {
86 | // Grab potential subdirectory with :dir*?
87 | app.get('/dist/:dir*?/:filename', (request, response) => {
88 | if (!request.params.dir || request.params.dir === undefined) {
89 | response.redirect('/' + request.params.filename)
90 | } else {
91 | response.redirect('/' + request.params.dir + '/' + request.params.filename)
92 | }
93 | })
94 | },
95 | port: process.env.PORT || 8080,
96 | host: '0.0.0.0',
97 | publicPath: '/dist/',
98 | contentBase: ['./examples', './src'],
99 | historyApiFallback: true,
100 | open: true,
101 | watchContentBase: true,
102 | disableHostCheck: true
103 | }
104 | }
105 |
106 | const bundleStandalone = {
107 | ...config,
108 | entry: {
109 | 'accessible-autocomplete.min': './wrapper.js'
110 | },
111 | output: {
112 | path: path.resolve(__dirname, 'dist'),
113 | publicPath: '/',
114 | filename: '[name].js',
115 | library: 'accessibleAutocomplete',
116 | libraryExport: 'default',
117 | libraryTarget: 'umd'
118 | },
119 | plugins: plugins
120 | .concat([new webpack.DefinePlugin({
121 | 'process.env.COMPONENT_LIBRARY': '"PREACT"'
122 | })])
123 | .concat(ENV === 'development'
124 | ? developmentPlugins
125 | : []
126 | )
127 | }
128 |
129 | const bundlePreact = {
130 | ...config,
131 | entry: {
132 | 'lib/accessible-autocomplete.preact.min': './autocomplete.js'
133 | },
134 | output: {
135 | path: path.resolve(__dirname, 'dist'),
136 | publicPath: '/',
137 | filename: '[name].js',
138 | library: 'Autocomplete',
139 | libraryTarget: 'umd'
140 | },
141 | externals: {
142 | preact: {
143 | amd: 'preact',
144 | commonjs: 'preact',
145 | commonjs2: 'preact',
146 | root: 'preact'
147 | }
148 | },
149 | plugins: plugins
150 | .concat([new webpack.DefinePlugin({
151 | 'process.env.COMPONENT_LIBRARY': '"PREACT"'
152 | })])
153 | .concat(ENV === 'development'
154 | ? developmentPlugins
155 | : []
156 | )
157 | }
158 |
159 | const bundleReact = {
160 | ...config,
161 | entry: {
162 | 'lib/accessible-autocomplete.react.min': './autocomplete.js'
163 | },
164 | output: {
165 | path: path.resolve(__dirname, 'dist'),
166 | publicPath: '/',
167 | filename: '[name].js',
168 | library: 'Autocomplete',
169 | libraryTarget: 'umd',
170 | globalObject: 'this'
171 | },
172 | externals: {
173 | preact: {
174 | amd: 'react',
175 | commonjs: 'react',
176 | commonjs2: 'react',
177 | root: 'React'
178 | }
179 | },
180 | plugins: plugins
181 | .concat([new webpack.DefinePlugin({
182 | 'process.env.COMPONENT_LIBRARY': '"REACT"'
183 | })])
184 | .concat(ENV === 'development'
185 | ? developmentPlugins
186 | : []
187 | )
188 | }
189 |
190 | module.exports = [
191 | bundleStandalone,
192 | bundlePreact,
193 | bundleReact
194 | ]
195 |
--------------------------------------------------------------------------------
/src/autocomplete.css:
--------------------------------------------------------------------------------
1 | .autocomplete__wrapper {
2 | position: relative;
3 | }
4 |
5 | .autocomplete__hint,
6 | .autocomplete__input {
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | font-weight: 400;
10 | font-size: 16px;
11 | line-height: 1.25;
12 | box-sizing: border-box;
13 | width: 100%;
14 | height: 40px;
15 | padding: 5px;
16 | border: 2px solid #0b0c0c;
17 | border-radius: 0;
18 | -webkit-appearance: none;
19 | -moz-appearance: none;
20 | appearance: none;
21 | margin-bottom: 0;
22 | }
23 |
24 | .autocomplete__input {
25 | background-color: transparent;
26 | position: relative;
27 | }
28 |
29 | .autocomplete__hint {
30 | color: #b1b4b6;
31 | position: absolute;
32 | }
33 |
34 | .autocomplete__input--default {
35 | padding: 5px;
36 | }
37 |
38 | .autocomplete__input--focused,
39 | .autocomplete__input:focus {
40 | outline-offset: 0;
41 | box-shadow: inset 0 0 0 2px;
42 | }
43 |
44 | .autocomplete__input--show-all-values {
45 | padding: 5px 34px 5px 5px; /* Space for arrow. Other padding should match .autocomplete__input--default. */
46 | cursor: pointer;
47 | }
48 |
49 | .autocomplete__dropdown-arrow-down{
50 | z-index: -1;
51 | display: inline-block;
52 | position: absolute;
53 | right: 8px;
54 | width: 24px;
55 | height: 24px;
56 | top: 10px;
57 | }
58 |
59 | .autocomplete__menu {
60 | background-color: #fff;
61 | border: 2px solid #0B0C0C;
62 | border-top: 0;
63 | color: #0B0C0C;
64 | margin: 0;
65 | max-height: 342px;
66 | overflow-x: hidden;
67 | padding: 0;
68 | width: 100%;
69 | width: calc(100% - 4px);
70 | }
71 |
72 | .autocomplete__menu--visible {
73 | display: block;
74 | }
75 |
76 | .autocomplete__menu--hidden {
77 | display: none;
78 | }
79 |
80 | .autocomplete__menu--overlay {
81 | box-shadow: rgba(0, 0, 0, 0.256863) 0px 2px 6px;
82 | left: 0;
83 | position: absolute;
84 | top: 100%;
85 | z-index: 100;
86 | }
87 |
88 | .autocomplete__menu--inline {
89 | position: relative;
90 | }
91 |
92 | .autocomplete__option {
93 | border-bottom: solid #b1b4b6;
94 | border-width: 1px 0;
95 | cursor: pointer;
96 | display: block;
97 | position: relative;
98 | }
99 |
100 | .autocomplete__option > * {
101 | pointer-events: none;
102 | }
103 |
104 | .autocomplete__option:first-of-type {
105 | border-top-width: 0;
106 | }
107 |
108 | .autocomplete__option:last-of-type {
109 | border-bottom-width: 0;
110 | }
111 |
112 | .autocomplete__option--odd {
113 | background-color: #FAFAFA;
114 | }
115 |
116 | .autocomplete__option--focused,
117 | .autocomplete__option:hover {
118 | background-color: #1d70b8;
119 | border-color: #1d70b8;
120 | color: white;
121 | outline: none;
122 | }
123 |
124 | @media (-ms-high-contrast: active), (forced-colors: active) {
125 | .autocomplete__menu {
126 | border-color: FieldText;
127 | }
128 |
129 | .autocomplete__option {
130 | background-color: Field;
131 | color: FieldText;
132 | }
133 |
134 | .autocomplete__option--focused,
135 | .autocomplete__option:hover {
136 | forced-color-adjust: none; /* prevent backplate from obscuring text */
137 | background-color: Highlight;
138 | border-color: Highlight;
139 | color: HighlightText;
140 |
141 | /* Prefer SelectedItem / SelectedItemText in browsers that support it */
142 | background-color: SelectedItem;
143 | border-color: SelectedItem;
144 | color: SelectedItemText;
145 | outline-color: SelectedItemText;
146 | }
147 | }
148 |
149 | .autocomplete__option--no-results {
150 | background-color: #FAFAFA;
151 | color: #646b6f;
152 | cursor: not-allowed;
153 | }
154 |
155 | .autocomplete__hint,
156 | .autocomplete__input,
157 | .autocomplete__option {
158 | font-size: 16px;
159 | line-height: 1.25;
160 | }
161 |
162 | .autocomplete__hint,
163 | .autocomplete__option {
164 | padding: 5px;
165 | }
166 |
167 | @media (min-width: 641px) {
168 | .autocomplete__hint,
169 | .autocomplete__input,
170 | .autocomplete__option {
171 | font-size: 19px;
172 | line-height: 1.31579;
173 | }
174 | }
175 |
176 | .autocomplete__list {
177 | list-style-type: none;
178 | padding: 0;
179 | margin: 20px 0 0;
180 | }
181 |
182 | .autocomplete__list .autocomplete__selected-option,
183 | .autocomplete__list .autocomplete__selected-option:hover {
184 | cursor: inherit;
185 | color: #0b0c0c;
186 | margin: 5px 0;
187 | }
188 |
189 | .autocomplete__list .autocomplete__remove-option {
190 | cursor: pointer;
191 | color: #005ea5;
192 | text-decoration: underline;
193 | background: transparent;
194 | border: none;
195 | font-size: inherit;
196 | margin-left: 5px;
197 | }
198 |
199 | .autocomplete__list .autocomplete__remove-option:hover{
200 | color: #2b8cc4;
201 | }
202 |
203 | .autocomplete__list .autocomplete__remove-option:focus{
204 | color: #0b0c0c;
205 | outline: 3px solid #ffbf47;
206 | outline-offset: 0;
207 | background-color: #ffbf47;
208 | }
209 |
210 | .autocomplete__list .autocomplete__remove-option:before {
211 | content: '× ';
212 | color: inherit;
213 | }
214 |
215 | .autocomplete__list-item-description {
216 | display: none;
217 | }
218 |
--------------------------------------------------------------------------------
/examples/multiselect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Accessible Autocomplete examples
7 |
50 |
51 |
52 |
53 |
54 | Accessible Autocomplete examples
55 |
56 | Example in a real HTML form that you can submit
57 |
58 | Multiselect
59 |
60 | This example demonstrates how to use the autocomplete on a multiselect <select multiple> element
61 | Turn off JavaScript to see the <select multiple> element.
62 |
63 | Uses accessibleAutocomplete.enhanceSelectElement.
64 | Select your country
65 |
66 |
67 | France
68 | Germany
69 | United Kingdom
70 | United Arab Emirates
71 |
72 |
73 |
74 | Multiselect with pre-selected values
75 |
76 | This example demonstrates how to use the autocomplete on a multiselect <select multiple> element
77 | Turn off JavaScript to see the <select multiple> element.
78 |
79 | Uses accessibleAutocomplete.enhanceSelectElement.
80 | Select your country
81 |
82 |
83 | France
84 | Germany
85 | United Kingdom
86 | United Arab Emirates
87 |
88 |
89 |
90 | Multiselect with empty option
91 |
92 | This example demonstrates how to use the autocomplete on a multiselect <select multiple> element
93 | Turn off JavaScript to see the <select multiple> element.
94 |
95 | Uses accessibleAutocomplete.enhanceSelectElement.
96 | Select your country
97 |
98 |
99 |
100 | France
101 | Germany
102 | United Kingdom
103 | United Arab Emirates
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
117 |
118 |
124 |
125 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions welcome, please raise a pull request.
4 |
5 | If you want to help and want to get more familiar with the codebase, try starting with the ["good for beginners"](https://github.com/alphagov/accessible-autocomplete/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+for+beginners%22) issues. Feel free to request more guidance in the issue comments.
6 |
7 | ## Requirements
8 |
9 | You will need a recent version of Node and npm installed:
10 |
11 | ```bash
12 | $ node -v
13 | v7.10.0
14 | $ npm -v
15 | v5.0.0
16 | ```
17 |
18 | If you want to run the selenium tests, you will also need a local copy of the Java Development Kit:
19 |
20 | ```bash
21 | $ java -version
22 | java version "1.8.0_131"
23 | Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
24 | Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
25 | ```
26 |
27 | To install Node (with npm) and Java locally on macOS, you can use [brew](https://brew.sh/):
28 |
29 | ```bash
30 | brew install node
31 | brew cask install java
32 | ```
33 |
34 | ## Project structure
35 |
36 | ```
37 | $ ls
38 | dist/ # The compiled and ready to distribute build artefacts.
39 | screenshots/ # Gets outputted by the end to end tests when something goes wrong.
40 | examples/ # GitHub pages examples of using the autocomplete.
41 | scripts/ # Build scripts that don't fit in `package.json`.
42 | src/ # The source code for the library.
43 | test/ # The tests for the library.
44 | ```
45 |
46 | ## Build tasks
47 |
48 | To develop locally:
49 |
50 | ```bash
51 | npm install
52 | npm run dev
53 | ```
54 |
55 | Contributions will need to pass the linter and tests. To run everything once:
56 |
57 | ```bash
58 | npm test
59 | ```
60 |
61 | To run the linter on its own:
62 |
63 | ```bash
64 | npm run standard
65 | ```
66 |
67 | To run the functional tests in dev mode (automatically reruns when a file changes):
68 |
69 | ```bash
70 | npm run karma:dev
71 | ```
72 |
73 | To run the integration tests locally with Chrome (specified in [wdio.config.js](test/wdio.config.js)):
74 |
75 | ```bash
76 | npm run wdio
77 | ```
78 |
79 | To run the integration tests on [Sauce Labs](https://saucelabs.com/), create a `.env` file with the following:
80 |
81 | ```bash
82 | SAUCE_ENABLED="true"
83 | SAUCE_USERNAME="XXXXXXXX"
84 | SAUCE_ACCESS_KEY="YYYYYYYY"
85 | ```
86 |
87 | And run the same command:
88 |
89 | ```bash
90 | npm run wdio
91 | ```
92 |
93 | Failed integration tests should output screenshots to the `./screenshots/` folder.
94 |
95 | To build the project for distribution:
96 |
97 | ```bash
98 | npm run build
99 | ```
100 |
101 | You should do this and commit it before you attempt to `git push`, otherwise the prepush checks will prevent you from pushing.
102 |
103 | ## Prepush checks
104 |
105 | When you push to a branch, git will run a `npm run prepush` [script](scripts/check-staged.mjs) that will compile the build on your behalf to the `dist/` folder. If it then finds unstaged files in `dist/`, it will fail your push.
106 |
107 | The solution is to commit the files, preferably as part of a separate commit:
108 |
109 | ```bash
110 | npm run build
111 | git add dist/
112 | git commit -m "Rebuild dist"
113 | git push
114 | ```
115 |
116 | If you want to ignore the checks and push regardless:
117 |
118 | ```bash
119 | git push --no-verify
120 | ```
121 |
122 | ## PR nice to haves
123 |
124 | - Tests for your feature or fix
125 | - Updates to the README.md when necessary
126 | - A 1 line update in CHANGELOG.md describing your changes
127 |
128 | ## Cutting a new release
129 |
130 | `git pull --rebase` and then run:
131 |
132 | ```bash
133 | git checkout -b "v1.2.3"
134 | vim CHANGELOG.md # Update CHANGELOG, put all unreleased changes under new heading.
135 | git commit -am "Update CHANGELOG"
136 | npm version -m "## 1.2.3 - 2017-01-13
137 |
138 | - Change included in this release
139 | - Another change included in this release"
140 | ```
141 |
142 | Then run:
143 | ```bash
144 | git push --tags --set-upstream origin refs/heads/v1.2.3:refs/heads/v1.2.3
145 | ```
146 |
147 | Create a pull request for the release and merge once it has been approved, then run:
148 |
149 | ```bash
150 | git checkout main
151 | git pull --rebase
152 | ```
153 |
154 | ### Publish the release
155 |
156 | 1. Sign in to npm (`npm login`) as `govuk-patterns-and-tools` using the credentials from BitWarden.
157 | 2. Run `npm publish` to publish to npm.
158 | 3. Open the ['create a new release' dialog](https://github.com/alphagov/accessible-autocomplete/releases/new) on GitHub.
159 | 4. Select the latest tag version.
160 | 5. Set 'v[VERSION-NUMBER]' as the title.
161 | 6. Add the release notes from the changelog.
162 | 7. Add a summary of highlights.
163 | 8. Select **Publish release**.
164 |
165 | You do not need to manually attach source code files to the release on GitHub.
166 |
167 | Post a short summary of the release in the cross-government and GDS #govuk-design-system Slack channels. For example:
168 |
169 | 🚀 We’ve just released Accessible Autocomplete v2.0.1. You can now use the acccessible autocomplete multiple times on one page. Thanks to @ and @ for helping with this release. [https://github.com/alphagov/accessible-autocomplete/releases/tag/v2.0.1](https://github.com/alphagov/accessible-autocomplete/releases/tag/v2.0.1)
170 |
--------------------------------------------------------------------------------
/test/integration/index.js:
--------------------------------------------------------------------------------
1 | /* global $, afterEach, beforeEach, browser, describe, it */
2 | const expect = require('chai').expect
3 | const { browserName, browserVersion } = browser.capabilities
4 | const isChrome = browserName === 'chrome'
5 | // const isFireFox = browserName === 'firefox'
6 | const isIE = browserName === 'internet explorer'
7 | // const isIE9 = isIE && browserVersion === '9'
8 | // const isIE10 = isIE && browserVersion === '10'
9 | // const isIE11 = isIE && browserVersion === '11.103'
10 | const liveRegionWaitTimeMillis = 10000
11 |
12 | const basicExample = (runner = null) => {
13 | describe('basic example', function () {
14 | const input = 'input#autocomplete-default'
15 | const menu = `${input} + ul`
16 | const firstOption = `${menu} > li:first-child`
17 | const secondOption = `${menu} > li:nth-child(2)`
18 |
19 | it('should show the input', () => {
20 | $(input).waitForExist()
21 | expect($(input).isDisplayed()).to.equal(true)
22 | })
23 |
24 | it('should allow focusing the input', () => {
25 | $(input).click()
26 | expect($(input).isFocused()).to.equal(true)
27 | })
28 |
29 | it('should display suggestions', () => {
30 | $(input).click()
31 | $(input).setValue('ita')
32 | $(menu).waitForDisplayed()
33 | expect($(menu).isDisplayed()).to.equal(true)
34 | })
35 |
36 | // These tests are flakey when run through Saucelabs so we only run them
37 | // in Chrome
38 | if (isChrome) {
39 | it('should announce status changes using two alternately updated aria live regions', () => {
40 | const regionA = $('#autocomplete-default__status--A')
41 | const regionB = $('#autocomplete-default__status--B')
42 |
43 | expect(regionA.getText()).to.equal('')
44 | expect(regionB.getText()).to.equal('')
45 |
46 | $(input).click()
47 | $(input).setValue('a')
48 |
49 | // We can't tell which region will be used first, so we have to allow for
50 | // either region changing
51 | browser.waitUntil(() => { return regionA.getText() !== '' || regionB.getText() !== '' },
52 | liveRegionWaitTimeMillis,
53 | 'expected the first aria live region to be populated within ' + liveRegionWaitTimeMillis + ' milliseconds'
54 | )
55 |
56 | if (regionA.getText()) {
57 | $(input).addValue('s')
58 | browser.waitUntil(() => { return (regionA.getText() === '' && regionB.getText() !== '') },
59 | liveRegionWaitTimeMillis,
60 | 'expected the first aria live region to be cleared, and the second to be populated within ' +
61 | liveRegionWaitTimeMillis + ' milliseconds'
62 | )
63 |
64 | $(input).addValue('h')
65 | browser.waitUntil(() => { return (regionA.getText() !== '' && regionB.getText() === '') },
66 | liveRegionWaitTimeMillis,
67 | 'expected the first aria live region to be populated, and the second to be cleared within ' +
68 | liveRegionWaitTimeMillis + ' milliseconds'
69 | )
70 | } else {
71 | $(input).addValue('s')
72 | browser.waitUntil(() => { return (regionA.getText() !== '' && regionB.getText() === '') },
73 | liveRegionWaitTimeMillis,
74 | 'expected the first aria live region to be populated, and the second to be cleared within ' +
75 | liveRegionWaitTimeMillis + ' milliseconds'
76 | )
77 |
78 | $(input).addValue('h')
79 | browser.waitUntil(() => { return (regionA.getText() === '' && regionB.getText() !== '') },
80 | liveRegionWaitTimeMillis,
81 | 'expected the first aria live region to be cleared, and the second to be populated within ' +
82 | liveRegionWaitTimeMillis + ' milliseconds'
83 | )
84 | }
85 | })
86 | }
87 |
88 | it('should set aria-selected to true on selected option and unset aria-selected on other options', () => {
89 | $(input).click()
90 | $(input).setValue('ita')
91 | browser.keys(['ArrowDown'])
92 | expect($(firstOption).getAttribute('aria-selected')).to.equal('true')
93 | if (runner === 'react') {
94 | expect($(secondOption).getAttribute('aria-selected')).to.equal('false')
95 | } else {
96 | expect($(secondOption).getAttribute('aria-selected')).to.equal(null)
97 | }
98 | browser.keys(['ArrowDown'])
99 | if (runner === 'react') {
100 | expect($(firstOption).getAttribute('aria-selected')).to.equal('false')
101 | } else {
102 | expect($(firstOption).getAttribute('aria-selected')).to.equal(null)
103 | }
104 | expect($(secondOption).getAttribute('aria-selected')).to.equal('true')
105 | })
106 |
107 | describe('keyboard use', () => {
108 | it('should allow typing', () => {
109 | $(input).click()
110 | $(input).addValue('ita')
111 | expect($(input).getValue()).to.equal('ita')
112 | })
113 |
114 | it('should allow selecting an option', () => {
115 | $(input).click()
116 | $(input).setValue('ita')
117 | browser.keys(['ArrowDown'])
118 | expect($(input).isFocused()).to.equal(false)
119 | expect($(firstOption).isFocused()).to.equal(true)
120 | browser.keys(['ArrowDown'])
121 | expect($(menu).isDisplayed()).to.equal(true)
122 | expect($(input).getValue()).to.equal('ita')
123 | expect($(firstOption).isFocused()).to.equal(false)
124 | expect($(secondOption).isFocused()).to.equal(true)
125 | })
126 |
127 | it('should allow confirming an option', () => {
128 | $(input).click()
129 | $(input).setValue('ita')
130 | browser.keys(['ArrowDown', 'Enter'])
131 | browser.waitUntil(() => $(input).getValue() !== 'ita')
132 | expect($(input).isFocused()).to.equal(true)
133 | expect($(input).getValue()).to.equal('Italy')
134 | })
135 |
136 | it('should redirect keypresses on an option to input', () => {
137 | if (!isIE) {
138 | $(input).click()
139 | $(input).setValue('ita')
140 | browser.keys(['ArrowDown'])
141 | expect($(input).isFocused()).to.equal(false)
142 | expect($(firstOption).isFocused()).to.equal(true)
143 | $(firstOption).addValue(['l'])
144 | expect($(input).isFocused()).to.equal(true)
145 | expect($(input).getValue()).to.equal('ital')
146 | } else {
147 | // FIXME: This feature does not work correctly on IE 9 to 11.
148 | }
149 | })
150 | })
151 |
152 | describe('mouse use', () => {
153 | it('should allow confirming an option', () => {
154 | $(input).click()
155 | $(input).setValue('ita')
156 | $(firstOption).click()
157 | expect($(input).isFocused()).to.equal(true)
158 | expect($(input).getValue()).to.equal('Italy')
159 | })
160 | })
161 | })
162 | }
163 |
164 | const customTemplatesExample = () => {
165 | describe('custom templates example', function () {
166 | const input = 'input#autocomplete-customTemplates'
167 | const menu = `${input} + ul`
168 | const firstOption = `${menu} > li:first-child`
169 | const firstOptionInnerElement = `${firstOption} > strong`
170 |
171 | beforeEach(() => {
172 | $(input).setValue('') // Prevent autofilling, IE likes to do this.
173 | })
174 |
175 | describe('mouse use', () => {
176 | it('should allow confirming an option by clicking on child elements', () => {
177 | $(input).setValue('uni')
178 |
179 | if (isIE) {
180 | // FIXME: This feature works correctly on IE but testing it doesn't seem to work.
181 | return
182 | }
183 |
184 | try {
185 | $(firstOptionInnerElement).click()
186 | } catch (error) {
187 | // In some cases (mainly ChromeDriver) the automation protocol won't
188 | // allow clicking span elements. In this case we just skip the test.
189 | if (error.toString().match(/Other element would receive the click/)) {
190 | return
191 | } else {
192 | throw error
193 | }
194 | }
195 |
196 | expect($(input).isFocused()).to.equal(true)
197 | expect($(input).getValue()).to.equal('United Kingdom')
198 | })
199 | })
200 | })
201 | }
202 |
203 | const takeScreenshotsIfFail = () => {
204 | afterEach(function () {
205 | const testFailed = this.currentTest.state === 'failed'
206 | if (testFailed) {
207 | const timestamp = +new Date()
208 | const browserVariant = isIE ? `ie${browserVersion}` : browserName
209 | const testTitle = this.currentTest.title.replace(/\W/g, '-')
210 | const filename = `./screenshots/${timestamp}-${browserVariant}-${testTitle}.png`
211 | browser.saveScreenshot(filename)
212 | console.log(`Test failed, created: ${filename}`)
213 | }
214 | })
215 | }
216 |
217 | describe('Accessible Autocomplete', () => {
218 | beforeEach(() => {
219 | browser.url('/')
220 | })
221 |
222 | it('should have the right title', () => {
223 | expect(browser.getTitle()).to.equal('Accessible Autocomplete examples')
224 | })
225 |
226 | basicExample()
227 | customTemplatesExample()
228 |
229 | takeScreenshotsIfFail()
230 | })
231 |
232 | describe('Accessible Autocomplete Preact', () => {
233 | beforeEach(() => {
234 | browser.url('/preact')
235 | })
236 |
237 | it('should have the right title', () => {
238 | expect(browser.getTitle()).to.equal('Accessible Autocomplete Preact examples')
239 | })
240 |
241 | basicExample()
242 |
243 | takeScreenshotsIfFail()
244 | })
245 |
246 | describe('Accessible Autocomplete React', () => {
247 | beforeEach(() => {
248 | browser.url('/react')
249 | })
250 |
251 | it('should have the right title', () => {
252 | expect(browser.getTitle()).to.equal('Accessible Autocomplete React examples')
253 | })
254 |
255 | basicExample('react')
256 |
257 | takeScreenshotsIfFail()
258 | })
259 |
--------------------------------------------------------------------------------
/examples/preact/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Accessible Autocomplete Preact examples
7 |
50 |
51 |
52 |
53 |
54 | Accessible Autocomplete Preact examples
55 |
56 | This page demonstrates using the autocomplete directly in Preact.
57 |
58 | Select your country
59 |
60 |
61 |
62 |
63 |
64 |
65 |
333 |
334 |
345 |
346 |
347 |
--------------------------------------------------------------------------------
/examples/react/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Accessible Autocomplete React examples
7 |
50 |
51 |
52 |
53 |
54 | Accessible Autocomplete React examples
55 |
56 | This page demonstrates using the autocomplete directly in React.
57 |
58 | Select your country
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
334 |
335 |
346 |
347 |
348 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## Unreleased
4 |
5 | ## 2.0.5 - 2023-04-25
6 |
7 | - [Pull request #591: Add menuAttributes to fix #361](https://github.com/alphagov/accessible-autocomplete/pull/591)
8 |
9 | ## 2.0.4 - 2022-02-07
10 |
11 | ### Fixes
12 |
13 | - [Pull request #512: Make sure highlighted option is distinguishable in forced colors mode](https://github.com/alphagov/accessible-autocomplete/pull/512)
14 |
15 | ## 2.0.3 - 2020-07-01
16 |
17 | ### Fixes
18 |
19 | - [Pull request #415: Make React bundle work server-side in a NodeJS environment](https://github.com/alphagov/accessible-autocomplete/pull/415)
20 |
21 | ## 2.0.2 - 2020-01-30
22 |
23 | ### Fixes
24 |
25 | - [Pull request #388: Set aria-selected as a string instead of a boolean to avoid being dropped](https://github.com/alphagov/accessible-autocomplete/pull/388).
26 | - [Pull request #400: Remove pointer events check](https://github.com/alphagov/accessible-autocomplete/pull/400).
27 | - [Pull request #406: Make hint padding match input padding](https://github.com/alphagov/accessible-autocomplete/pull/406).
28 | - [Pull request #407: Use a div element to wrap enhanced component](https://github.com/alphagov/accessible-autocomplete/pull/407).
29 | - [Pull request #410: Fix long clicks not selecting options](https://github.com/alphagov/accessible-autocomplete/pull/410).
30 |
31 | ## 2.0.1 - 2019-10-07
32 |
33 | ### Fixes
34 |
35 | - [Pull request #379: Ensure multiple autocompletes on one page do not have conflicting id attributes](https://github.com/alphagov/accessible-autocomplete/pull/379)
36 |
37 | ## 2.0.0 - 2019-09-26
38 |
39 | We recommend you update to the latest release using npm:
40 |
41 | `npm install accessible-autocomplete@latest`
42 |
43 | ### Breaking changes
44 |
45 | You must make the following change when you migrate to this release, or your service may break.
46 |
47 | #### Migrate to the new accessible focus state
48 |
49 | The focus state [now meets the new WCAG 2.1 level AA requirements](https://designnotes.blog.gov.uk/2019/07/29/weve-made-the-gov-uk-design-system-more-accessible/).
50 |
51 | You do not need to do anything if you’re using Sass.
52 |
53 | If you’ve previously copied CSS from our code into your project, you must copy all the CSS from our [`accessible-autocomplete.min.css` file](https://github.com/alphagov/accessible-autocomplete/blob/v2.0.0/dist/accessible-autocomplete.min.css) into your CSS file.
54 |
55 | If you’ve created custom CSS, you should check that your component meets WCAG 2.1 level AA requirements. You can [read how we made the GOV.UK Design System focus states accessible](https://design-system.service.gov.uk/get-started/focus-states/).
56 |
57 | [Pull request #360: Update focus styles to meet WCAG 2.1 level AA non-text contrast requirements](https://github.com/alphagov/accessible-autocomplete/pull/360).
58 |
59 | ### Fixes
60 |
61 | #### Better compatibility with screen readers
62 |
63 | The input field is now visible to all screen readers, because the input field now meets the Accessible Rich Internet Applications (ARIA) 1.0 standard instead of ARIA 1.1. ARIA 1.0 is better supported by the current versions of most screen readers.
64 |
65 | Screen readers will now consistently tell users:
66 |
67 | - when users have entered too few characters in the input field
68 | - the correct number of search results, and what the results are
69 | - which result users have highlighted
70 | - how to use autocomplete in different screen readers - by reading hidden hint text
71 |
72 | Screen readers will also now avoid telling users information they do not need to know after they highlight an option.
73 |
74 | Thanks to [Mark Hunter](https://github.com/markhunter27), Chris Moore and everyone at HMRC who worked on these improvements.
75 |
76 | [Pull request #355: Refinements to address accessibility issues](https://github.com/alphagov/accessible-autocomplete/pull/355)
77 |
78 | ## 1.6.2 - 2018-11-13
79 |
80 | - Update all packages and add `@babel/preset-env` for browser polyfills. Updates Preact and avoids React 16 `onFocusOut` warnings - thanks [@colinrotherham](https://github.com/colinrotherham)
81 | ([#316](https://github.com/alphagov/accessible-autocomplete/pull/316))
82 |
83 | - Fix mouse event issues in IE9-11 including looping `mouseout` and click event being prevented on child elements (e.g. bold text) - thanks [@colinrotherham](https://github.com/colinrotherham)
84 | ([#310](https://github.com/alphagov/accessible-autocomplete/pull/310))
85 |
86 | - Fix position being incorrectly reported as '1 of n' regardless of actual
87 | position in list – thanks [@PRGfx](https://github.com/PRGfx)
88 | ([#291](https://github.com/alphagov/accessible-autocomplete/pull/291))
89 |
90 | - Fix spacebar input not being registered when seeing 'No results found'
91 | message – thanks [@AdenFraser](https://github.com/AdenFraser)
92 | ([#287](https://github.com/alphagov/accessible-autocomplete/pull/287))
93 |
94 | - Update following dependencies (from "Current" to "Wanted"). This fixes failing WebdriverIO tests and updates JS Standard to use eslint 4.
95 |
96 | | Package | Current | Wanted | Latest |
97 | |------------------------------------------|---------|--------|--------|
98 | | babel-eslint | 8.0.0 | 8.2.6 | 8.2.6 |
99 | | babel-loader | 7.1.0 | 7.1.5 | 7.1.5 |
100 | | babel-plugin-transform-decorators-legacy | 1.3.4 | 1.3.5 | 1.3.5 |
101 | | babel-register | 6.24.1 | 6.26.0 | 6.26.0 |
102 | | babel-runtime | 6.23.0 | 6.26.0 | 6.26.0 |
103 | | chai | 4.0.2 | 4.1.2 | 4.1.2 |
104 | | chalk | 2.0.1 | 2.4.1 | 2.4.1 |
105 | | copy-webpack-plugin | 4.0.1 | 4.5.2 | 4.5.2 |
106 | | coveralls | 2.13.1 | 2.13.3 | 3.0.2 |
107 | | cross-env | 5.0.1 | 5.2.0 | 5.2.0 |
108 | | csso-cli | 1.0.0 | 1.1.0 | 1.1.0 |
109 | | husky | 0.14.1 | 0.14.3 | 0.14.3 |
110 | | karma | 1.7.0 | 1.7.1 | 2.0.4 |
111 | | karma-coverage | 1.1.1 | 1.1.2 | 1.1.2 |
112 | | karma-mocha-reporter | 2.2.3 | 2.2.5 | 2.2.5 |
113 | | karma-webpack | 2.0.3 | 2.0.13 | 3.0.0 |
114 | | mocha | 3.4.2 | 3.5.3 | 5.2.0 |
115 | | npm-run-all | 4.0.2 | 4.1.3 | 4.1.3 |
116 | | phantomjs-prebuilt | 2.1.14 | 2.1.16 | 2.1.16 |
117 | | preact | 8.1.0 | 8.2.9 | 8.2.9 |
118 | | sinon-chai | 2.11.0 | 2.14.0 | 3.2.0 |
119 | | source-map-loader | 0.2.1 | 0.2.3 | 0.2.3 |
120 | | standard | 10.0.2 | 11.0.1 | 11.0.1 |
121 | | wdio-mocha-framework | 0.5.10 | 0.5.13 | 0.6.2 |
122 | | wdio-sauce-service | 0.4.0 | 0.4.10 | 0.4.10 |
123 | | wdio-selenium-standalone-service | 0.0.8 | 0.0.10 | 0.0.10 |
124 | | wdio-spec-reporter | 0.1.0 | 0.1.5 | 0.1.5 |
125 | | webdriverio | 4.8.0 | 4.13.1 | 4.13.1 |
126 | | webpack | 3.0.0 | 3.12.0 | 4.16.1 |
127 | | webpack-dev-server | 2.5.0 | 2.11.2 | 3.1.4 |
128 | | webpack-sources | 1.0.1 | 1.1.0 | 1.1.0 |
129 |
130 | ## 1.6.1 - 2017-09-25
131 |
132 | - Fix role attr by moving `role='combobox'` to wrapper and adding `role='textbox'` to the input. By [@tobias-g](https://github.com/tobias-g)
133 | - Fix examples page by removing unrequired npm package `v8-lazy-parse-webpack-plugin`
134 | - Fix scrolling on iOS by reverting #85, new issue raised to find better fix for clicking custom suggestions #177
135 | - Fix selection and timeout race condition. By [@tobias-g](https://github.com/tobias-g)
136 | - Fix dropdown on IE to ensure it isn't focusable. By [@tobias-g](https://github.com/tobias-g)
137 |
138 | ## 1.6.0 - 2017-07-20
139 |
140 | - [Feature] Allow customization of the dropdown arrow. By [@sventschui](https://github.com/sventschui).
141 |
142 | ## 1.5.0 - 2017-07-18
143 |
144 | - [Feature] Add ability to translate texts. Relates to #96. By [@sventschui](https://github.com/sventschui).
145 |
146 | ## 1.4.2 - 2017-07-18
147 |
148 | - Allow space to confirm an option, fixes #98.
149 | - Add support for navigating lists on IE9, 10 and 11, fixes #193.
150 |
151 | ## 1.4.1 - 2017-07-06
152 |
153 | - Fix use of HTML entities in enhanced select options. #151. By [@dracos](https://github.com/dracos).
154 |
155 | ## 1.4.0 - 2017-07-04
156 |
157 | - [Feature] Add option to show all values on dropdown. By [@joelanman](https://github.com/joelanman).
158 |
159 | ## 1.3.2 - 2017-07-03
160 |
161 | - Redirect keypresses on an option to input, fixes #179.
162 |
163 | ## 1.3.1 - 2017-06-08
164 |
165 | - Fix `autoselect: false` not working when using `enhanceSelectElement`.
166 |
167 | ## 1.3.0 - 2017-06-02
168 |
169 | - [Feature] Add support for passing an array of strings to `source`. By [@joelanman](https://github.com/joelanman).
170 |
171 | ## 1.2.1 - 2017-05-24
172 |
173 | - Fix progressive enhancement in FireFox < 48. By [@revilossor](https://github.com/revilossor).
174 |
175 | ## 1.2.0 - 2017-05-23
176 |
177 | - [Feature] Export Preact and React bundles.
178 |
179 | ## 1.1.0 - 2017-05-18
180 |
181 | - [Feature] Add `required` option. By [@samtsai](https://github.com/samtsai).
182 |
183 | ## 1.0.6 - 2017-05-17
184 |
185 | - Update preact dependency to v8.1.0.
186 |
187 | ## 1.0.5 - 2017-05-16
188 |
189 | - Add support for handling null/placeholder options when using `enhanceSelectElement`. Use `preserveNullOptions: true` to include options with `value=''` in the autocomplete results when enhancing a select element. By @lennym.
190 |
191 | ## 1.0.4 - 2017-05-15 (deprecated)
192 |
193 | - This release does not contain any changes compared to the previous one and is due to a mistake in our build scripts.
194 |
195 | ## 1.0.3 - 2017-05-15
196 |
197 | - Do not copy `name` attribute when using `enhanceSelectElement`. By [@lennym](https://github.com/lennym).
198 |
199 | ## 1.0.2 - 2017-05-12
200 |
201 | - Add support for an empty `defaultValue` when enhancing a select element. By [@lennym](https://github.com/lennym).
202 |
203 | ## 1.0.1 - 2017-05-12
204 |
205 | - Update `style` property in package.json to reflect updated filename. By [@lennym](https://github.com/lennym).
206 |
207 | ## 1.0.0 - 2017-05-10
208 |
209 | - [Breaking] Default `autoselect` to `true` when using `enhanceSelectElement`.
210 | - [Breaking] Make `id` a required attribute.
211 | - [Breaking] Rename `onSelect` to `onConfirm`.
212 | - [Breaking] Rename `selectOnBlur` to `confirmOnBlur`.
213 | - Fix an issue where users couldn't click on custom suggestions on Chrome.
214 |
215 | ## 0.6.0 - 2017-05-10
216 |
217 | - [Breaking] Rename component from `accessible-typeahead` to `accessible-autocomplete`.
218 | - Default `defaultValue` when progressively enhancing.
219 | - Throw an error when `enhanceSelectElement` is called without a `selectElement`.
220 | - Throw errors when `accessibleAutocomplete` is called without `element` or `source`.
221 |
222 | ## 0.5.0 - 2017-05-09
223 |
224 | - Test the typeahead with end to end tests.
225 | - Don't display hints on browsers that don't support pointer-events.
226 | - [Breaking] Rename `dist/styled.min.css` to `dist/accessible-typeahead.min.css`.
227 | - [Breaking] Rename library main export from `AccessibleTypeahead` to `accessibleTypeahead`.
228 | - Fix aria status region to more reliably trigger when the number of results stay the same.
229 | - Fix hint rendering and being picked up by assistive technologies.
230 | - More aria status region above input so it's more easily picked while navigating.
231 |
232 | ## 0.4.2 - 2017-05-03
233 |
234 | - Add touchEnd handler for iOS and touch devices, fixes custom suggestions.
235 | - Add `style` declaration in package.json
236 | - Add support for UMD/commonjs module definition.
237 |
238 | ## 0.4.1 - 2017-04-26
239 |
240 | - Minify `styled.css` for production.
241 |
242 | ## 0.4.0 - 2017-04-11
243 |
244 | - [Breaking] Don't focus suggestions when hovering them, add `:hover` CSS class.
245 | - Add `showNoOptionsFound` property to allow users to disable this behaviour.
246 | - Pass through unrecognised key events to input, allowing users to continue typing when they are focusing an option.
247 |
248 | ## 0.3.5 - 2017-04-06
249 |
250 | - Don't prepopulate `defaultValue` in `enhanceSelectElement`.
251 |
252 | ## 0.3.4 - 2017-04-06
253 |
254 | - Pass actual selected object into `onSelect`.
255 | - Add `selectOnBlur` property to allow users to disable this behaviour.
256 | - Add `placeholder` property.
257 |
258 | ## 0.3.3 - 2017-04-04
259 |
260 | - Add `templates.inputValue` and `templates.suggestion` properties. These allow users to override how the suggestions are displayed.
261 |
262 | ## 0.3.2 - 2017-04-03
263 |
264 | - Add `AccessibleTypeahead.enhanceSelectElement` function.
265 | - Add `onSelect` property.
266 |
267 | ## 0.3.1 - 2017-03-09
268 |
269 | - Add ability to specify a `defaultValue` to prefill the input.
270 | - When user has selected an option with the keyboard, blurring will select.
271 | - When user has no selected but autoselect is on, blurring will select.
272 | - Hovering no longer selects, just focuses.
273 | - When hovering out of component, focus returns to selected.
274 | - Allow enter to submit forms when menu isn't opened.
275 | - Hide results when going under minLength.
276 |
277 | ## 0.3.0 - 2017-03-09
278 |
279 | - [Breaking] Add `displayMenu` property. The default is `inline` which was not the previous default.
280 | - CSS colour changes, and more properties moved away from inline styles.
281 | - Turn off native browser autocomplete so it doesn't interfere with typeahead overlay.
282 | - Change the content and styling of the 'No results found' feature.
283 |
284 | ## 0.2.4 - 2017-03-02
285 |
286 | - Display "No options found" when there are no results.
287 | - Add `autoselect` property. This refactors the `:focused` CSS class to `--focused`, but because previous styling should still work as before, is not a breaking change.
288 | - Poll the input element periodically to pick up value changes. This makes it more resilient to direct modifications from applications like Dragon, or from interventions from other JavaScript snippets.
289 |
290 | ## 0.2.3 - 2017-02-21
291 |
292 | - Add `minLength` property, which:
293 | - Tells the aria region to display text that the user should type in more characters;
294 | - Doesn't call the `source` until that lower limit is reached.
295 | - Select text only when component is unfocused.
296 |
297 | ## 0.2.2 - 2017-02-16
298 |
299 | - Fix focus/blur events on IE11.
300 | - Fix value of `aria-expanded` attribute to be based on `menuOpen`.
301 | - Remove `aria-activedescendant` attribute when no option selected.
302 | - Set `aria-selected` on options when they are focused.
303 | - Fix clicking on options on Safari.
304 | - Use a darker blue in the styled example for better contrast ratios.
305 | - Don't close menu when blurring options or input on iOS, to allow VoiceOver users the ability to select from the available options.
306 | - Autoselect entire text region when focusing into the input.
307 |
308 | ## 0.2.1 - 2017-02-03
309 |
310 | - Don't close menu when closing the keyboard on iOS, to allow VoiceOver users the ability to select from the available options.
311 | - Add ability to set `name` attribute on input.
312 |
313 | ## 0.2.0 - 2017-01-31
314 |
315 | - [Breaking] Change the CSS classes to our own instead of the jQuery typeahead ones.
316 | - Allow importing styling from file in `examples/styled.css`.
317 | - Tweak the styled example to fix two Safari bugs:
318 | - fix scroll bar appearing in menu where none is necessary;
319 | - fix weird margin separating the input from the menu.
320 |
321 | ## 0.1.3 - 2017-01-31
322 |
323 | - Don't apply focused CSS on hover, change handler to MouseOver instead of MouseMove.
324 | - (WIP) Make enter select first option if `autoselect` is enabled. This feature is not finished yet.
325 | - Close results when focusing out of component after hovering an element.
326 | - Update styled example:
327 | - unbold results;
328 | - make height of options consistent with input;
329 | - remove top border from results menu;
330 | - remove default focus outline on options.
331 | - Don't display the menu when there are no options.
332 | - Prevent accidental form submission by `preventDefault`ing on enter key.
333 | - Add form around basic example.
334 |
335 | ## 0.1.2 - 2017-01-20
336 |
337 | - Don't specify typeahead menu width inline. Allows custom CSS to override it.
338 |
339 | ## 0.1.0 - 2017-01-19
340 |
341 | - Initial release.
342 | - Basic functionality, minimal styling, only two examples, incomplete tests.
343 |
--------------------------------------------------------------------------------
/test/functional/wrapper.js:
--------------------------------------------------------------------------------
1 | /* global before, beforeEach, after, describe, expect, it */
2 | import accessibleAutocomplete from '../../src/wrapper'
3 |
4 | const DEFAULT_OPTIONS = {
5 | '': 'Select',
6 | fr: 'France',
7 | de: 'Germany',
8 | gb: 'United Kingdom of Great Britain & Northern Ireland'
9 | }
10 |
11 | const injectSelectToEnhanceIntoDOM = (element, settings) => {
12 | settings = settings || {}
13 | settings.options = settings.options || DEFAULT_OPTIONS
14 | settings.id = settings.id !== undefined ? settings.id : 'location-picker-id'
15 | settings.name = settings.name !== undefined ? settings.name : 'location-picker-name'
16 | var $select = document.createElement('select')
17 | if (settings.id) {
18 | $select.id = settings.id
19 | }
20 | if (settings.name) {
21 | $select.name = settings.name
22 | }
23 | if (settings.multiple) {
24 | $select.multiple = settings.multiple
25 | }
26 |
27 | if (settings.multiple) {
28 | const selected = settings.selected || []
29 | Object.keys(settings.options)
30 | .map(optionKey => {
31 | const option = document.createElement('option')
32 | option.value = optionKey
33 | option.text = settings.options[optionKey]
34 | option.selected = selected.indexOf(optionKey) !== -1
35 | return option
36 | })
37 | .forEach(option => $select.appendChild(option))
38 | } else {
39 | Object.keys(settings.options)
40 | .map(optionKey => {
41 | const option = document.createElement('option')
42 | option.value = optionKey
43 | option.text = settings.options[optionKey]
44 | option.selected = (settings.selected === optionKey)
45 | return option
46 | })
47 | .forEach(option => $select.appendChild(option))
48 | }
49 |
50 | element.appendChild($select)
51 |
52 | return $select
53 | }
54 |
55 | describe('Wrapper', () => {
56 | let scratch
57 | before(() => {
58 | scratch = document.createElement('div');
59 | (document.body || document.documentElement).appendChild(scratch)
60 | })
61 |
62 | beforeEach(() => {
63 | scratch.innerHTML = ''
64 | })
65 |
66 | after(() => {
67 | scratch.parentNode.removeChild(scratch)
68 | scratch = null
69 | })
70 |
71 | it('throws an error when called on nonexistent element', () => {
72 | expect(
73 | accessibleAutocomplete.bind(null, {
74 | element: document.querySelector('#nothing-container'),
75 | id: 'scratch',
76 | source: () => {}
77 | })
78 | ).to.throw('element is not defined')
79 | })
80 |
81 | it('throws an error when called without an id ', () => {
82 | expect(
83 | accessibleAutocomplete.bind(null, {
84 | element: scratch,
85 | source: () => {}
86 | })
87 | ).to.throw('id is not defined')
88 | })
89 |
90 | it('throws an error when called without a source', () => {
91 | expect(
92 | accessibleAutocomplete.bind(null, {
93 | element: scratch,
94 | id: 'scratch'
95 | })
96 | ).to.throw('source is not defined')
97 | })
98 |
99 | it('throws an error when called on nonexistent selectElement', () => {
100 | expect(
101 | accessibleAutocomplete.enhanceSelectElement.bind(null, {
102 | selectElement: document.querySelector('#nothing')
103 | })
104 | ).to.throw('selectElement is not defined')
105 | })
106 |
107 | it('can enhance a select element', () => {
108 | const select = injectSelectToEnhanceIntoDOM(scratch)
109 | const id = select.id
110 |
111 | accessibleAutocomplete.enhanceSelectElement({
112 | selectElement: select
113 | })
114 |
115 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
116 | expect(autocompleteInstances.length).to.equal(1)
117 |
118 | const autocompleteInstance = autocompleteInstances[0]
119 |
120 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
121 | expect(autocompleteInput.tagName.toLowerCase()).to.equal('input')
122 | expect(autocompleteInput.id).to.equal(id)
123 | })
124 |
125 | it('can enhance a select multiple element', () => {
126 | const select = injectSelectToEnhanceIntoDOM(scratch, { multiple: true })
127 | const id = select.id
128 |
129 | accessibleAutocomplete.enhanceSelectElement({
130 | selectElement: select
131 | })
132 |
133 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
134 | expect(autocompleteInstances.length).to.equal(1)
135 |
136 | const autocompleteInstance = autocompleteInstances[0]
137 |
138 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
139 | const autocompleteList = autocompleteInstance.querySelector('.autocomplete__list')
140 | expect(autocompleteInput.tagName.toLowerCase()).to.equal('input')
141 | expect(autocompleteInput.id).to.equal(id)
142 | expect(autocompleteList).to.equal(null)
143 | })
144 |
145 | it('uses the defaultValue setting to populate the input field if no option is selected', () => {
146 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: '' })
147 | accessibleAutocomplete.enhanceSelectElement({
148 | defaultValue: '',
149 | selectElement: select
150 | })
151 |
152 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
153 | const autocompleteInstance = autocompleteInstances[0]
154 |
155 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
156 | expect(autocompleteInput.value).to.equal('')
157 | })
158 |
159 | it('uses the option label as the default input element value if an option is selected', () => {
160 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: 'de' })
161 | accessibleAutocomplete.enhanceSelectElement({
162 | defaultValue: '',
163 | selectElement: select
164 | })
165 |
166 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
167 | const autocompleteInstance = autocompleteInstances[0]
168 |
169 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
170 | expect(autocompleteInput.value).to.equal('Germany')
171 | })
172 |
173 | it('plays back the selected options when enhancing a select multiple', () => {
174 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: ['de', 'fr'], multiple: true })
175 |
176 | accessibleAutocomplete.enhanceSelectElement({
177 | selectElement: select
178 | })
179 |
180 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
181 | const autocompleteInstance = autocompleteInstances[0]
182 |
183 | const autocompleteSelectedItems = autocompleteInstance.querySelectorAll('ul.autocomplete__list li.autocomplete__selected-option')
184 | expect(autocompleteSelectedItems.length).to.equal(2)
185 | expect(autocompleteSelectedItems[0].textContent).to.contain('France')
186 | expect(autocompleteSelectedItems[1].textContent).to.contain('Germany')
187 | })
188 |
189 | it('can make selections when enhancing a select multiple', (done) => {
190 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: ['fr'], multiple: true })
191 |
192 | accessibleAutocomplete.enhanceSelectElement({
193 | selectElement: select
194 | })
195 |
196 | const autocompleteInstance = document.querySelector('.autocomplete__wrapper')
197 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
198 |
199 | autocompleteInput.value = 'Germany'
200 | setTimeout(() => {
201 | const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
202 | expect(autocompleteOption.textContent).to.equal('Germany')
203 | autocompleteOption.click()
204 |
205 | const selectedOptions = select.querySelectorAll('option:checked')
206 | expect(selectedOptions.length).to.equal(2)
207 | expect(selectedOptions[0].textContent).to.equal('France')
208 | expect(selectedOptions[1].textContent).to.equal('Germany')
209 | done()
210 | }, 250)
211 | })
212 |
213 | it('can remove selections when enhancing a select multiple', (done) => {
214 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: ['de', 'fr'], multiple: true })
215 |
216 | accessibleAutocomplete.enhanceSelectElement({
217 | selectElement: select
218 | })
219 |
220 | const autocompleteInstance = document.querySelectorAll('.autocomplete__wrapper')[0]
221 | const removeFrance = autocompleteInstance.querySelector('.autocomplete__remove-option')
222 | removeFrance.click()
223 |
224 | setTimeout(() => {
225 | const autocompleteSelectedItems = autocompleteInstance.querySelectorAll('.autocomplete__list li')
226 | expect(autocompleteSelectedItems.length).to.equal(1)
227 | expect(autocompleteSelectedItems[0].textContent).to.contain('Germany')
228 |
229 | const selectedOptions = select.querySelectorAll('option:checked')
230 | expect(selectedOptions.length).to.equal(1)
231 | expect(selectedOptions[0].textContent).to.equal('Germany')
232 |
233 | done()
234 | }, 250)
235 | })
236 |
237 | it('gives the autocomplete element a blank name attribute by default', () => {
238 | const select = injectSelectToEnhanceIntoDOM(scratch)
239 |
240 | accessibleAutocomplete.enhanceSelectElement({
241 | selectElement: select
242 | })
243 |
244 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
245 |
246 | const autocompleteInstance = autocompleteInstances[0]
247 |
248 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
249 | expect(autocompleteInput.name).to.equal('')
250 | })
251 |
252 | it('can define a name for the autocomplete element', () => {
253 | const select = injectSelectToEnhanceIntoDOM(scratch)
254 |
255 | accessibleAutocomplete.enhanceSelectElement({
256 | name: 'location-picker-autocomplete',
257 | selectElement: select
258 | })
259 |
260 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
261 |
262 | const autocompleteInstance = autocompleteInstances[0]
263 |
264 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
265 | expect(autocompleteInput.name).to.equal('location-picker-autocomplete')
266 | })
267 |
268 | it('does not include "null" options in autocomplete', (done) => {
269 | const select = injectSelectToEnhanceIntoDOM(scratch)
270 |
271 | accessibleAutocomplete.enhanceSelectElement({
272 | selectElement: select
273 | })
274 |
275 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
276 | const autocompleteInstance = autocompleteInstances[0]
277 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
278 |
279 | // Using setTimeouts here since changes in values take a while to reflect in lists
280 | autocompleteInput.value = 'e'
281 | setTimeout(() => {
282 | const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
283 | expect(autocompleteOptions.length).to.equal(3)
284 | expect([].map.call(autocompleteOptions, o => o.textContent)).not.to.contain('Select')
285 | done()
286 | }, 250)
287 | })
288 |
289 | it('includes "null" options in autocomplete if `preserveNullOptions` flag is true', (done) => {
290 | const select = injectSelectToEnhanceIntoDOM(scratch)
291 |
292 | accessibleAutocomplete.enhanceSelectElement({
293 | preserveNullOptions: true,
294 | selectElement: select
295 | })
296 |
297 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
298 | const autocompleteInstance = autocompleteInstances[0]
299 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
300 |
301 | // Using setTimeouts here since changes in values take a while to reflect in lists
302 | autocompleteInput.value = 'e'
303 | setTimeout(() => {
304 | const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
305 | expect(autocompleteOptions.length).to.equal(4)
306 | expect([].map.call(autocompleteOptions, o => o.textContent)).to.contain('Select')
307 | done()
308 | }, 250)
309 | })
310 |
311 | it('has all options when typing', (done) => {
312 | const select = injectSelectToEnhanceIntoDOM(scratch)
313 |
314 | accessibleAutocomplete.enhanceSelectElement({
315 | selectElement: select
316 | })
317 |
318 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
319 | const autocompleteInstance = autocompleteInstances[0]
320 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
321 | const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
322 |
323 | // Using setTimeouts here since changes in values take a while to reflect in lists
324 | autocompleteInput.value = 'Fran'
325 | setTimeout(() => {
326 | expect(autocompleteOption.textContent).to.equal('France')
327 | autocompleteInput.value = 'Ger'
328 | setTimeout(() => {
329 | expect(autocompleteOption.textContent).to.equal('Germany')
330 | autocompleteInput.value = 'United'
331 | setTimeout(() => {
332 | const autocompleteHint = autocompleteInstance.querySelector('.autocomplete__hint')
333 | expect(autocompleteOption.textContent).to.equal('United Kingdom of Great Britain & Northern Ireland')
334 | expect(autocompleteHint.value).to.equal('United Kingdom of Great Britain & Northern Ireland')
335 | done()
336 | }, 250)
337 | }, 250)
338 | }, 250)
339 | })
340 |
341 | it('includes aria attributes on each option, to indicate position within the full set of list item elements', (done) => {
342 | const select = injectSelectToEnhanceIntoDOM(scratch)
343 |
344 | accessibleAutocomplete.enhanceSelectElement({
345 | selectElement: select
346 | })
347 |
348 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
349 | const autocompleteInstance = autocompleteInstances[0]
350 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
351 | autocompleteInput.value = 'e'
352 | setTimeout(() => {
353 | const autocompleteOptions = autocompleteInstance.querySelectorAll('.autocomplete__option')
354 | expect(autocompleteOptions.length).to.equal(3)
355 | expect(autocompleteOptions[0].getAttribute('aria-posinset')).to.equal('1')
356 | expect(autocompleteOptions[0].getAttribute('aria-setsize')).to.equal('3')
357 | expect(autocompleteOptions[1].getAttribute('aria-posinset')).to.equal('2')
358 | expect(autocompleteOptions[1].getAttribute('aria-setsize')).to.equal('3')
359 | expect(autocompleteOptions[2].getAttribute('aria-posinset')).to.equal('3')
360 | expect(autocompleteOptions[2].getAttribute('aria-setsize')).to.equal('3')
361 | done()
362 | }, 250)
363 | })
364 |
365 | it('includes an explicit position suffix on each list item option when iOS is detected', (done) => {
366 | Object.defineProperty(global.navigator, 'userAgent', { value: 'iPhone AppleWebKit', configurable: true })
367 |
368 | const select = injectSelectToEnhanceIntoDOM(scratch)
369 |
370 | accessibleAutocomplete.enhanceSelectElement({
371 | selectElement: select
372 | })
373 |
374 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
375 | const autocompleteInstance = autocompleteInstances[0]
376 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
377 | const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
378 |
379 | autocompleteInput.value = 'Fran'
380 | setTimeout(() => {
381 | expect(autocompleteOption.textContent).to.equal('France 1 of 1')
382 | const iosSuffixSpan = autocompleteOption.querySelector('#location-picker-id__option-suffix--0')
383 | expect(iosSuffixSpan.textContent).to.equal(' 1 of 1')
384 | done()
385 | }, 250)
386 | })
387 |
388 | it('does not include a position suffix on each list item option, when iOS is not detected', (done) => {
389 | Object.defineProperty(global.navigator, 'userAgent', { value: 'definitely not an iDevice', configurable: true })
390 |
391 | const select = injectSelectToEnhanceIntoDOM(scratch)
392 |
393 | accessibleAutocomplete.enhanceSelectElement({
394 | selectElement: select
395 | })
396 |
397 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
398 | const autocompleteInstance = autocompleteInstances[0]
399 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
400 | const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
401 |
402 | autocompleteInput.value = 'Fran'
403 | setTimeout(() => {
404 | expect(autocompleteOption.textContent).to.equal('France')
405 | const iosSuffixSpan = autocompleteOption.querySelector('#location-picker-id__option-suffix--0')
406 | expect(iosSuffixSpan).to.equal(null)
407 | done()
408 | }, 250)
409 | })
410 |
411 | it('onConfirm updates original select', (done) => {
412 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: 'de' })
413 |
414 | accessibleAutocomplete.enhanceSelectElement({
415 | selectElement: select
416 | })
417 |
418 | const autocompleteInstances = document.querySelectorAll('.autocomplete__wrapper')
419 | const autocompleteInstance = autocompleteInstances[0]
420 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
421 | const autocompleteOption = autocompleteInstance.querySelector('.autocomplete__option')
422 |
423 | // Check the initial value of the original selectElement
424 | expect(select.value).to.equal('de')
425 | // Using setTimeouts here since changes in values take a while to reflect in lists
426 | autocompleteInput.value = 'United'
427 | setTimeout(() => {
428 | expect(autocompleteOption.textContent).to.equal('United Kingdom of Great Britain & Northern Ireland')
429 | autocompleteOption.click()
430 | expect(select.value).to.equal('gb')
431 | setTimeout(() => {
432 | expect(autocompleteInput.value).to.equal('United Kingdom of Great Britain & Northern Ireland')
433 | done()
434 | }, 250)
435 | }, 250)
436 | })
437 |
438 | it('onConfirm selects blank option when available', (done) => {
439 | const select = injectSelectToEnhanceIntoDOM(scratch, { selected: 'de' })
440 |
441 | accessibleAutocomplete.enhanceSelectElement({
442 | selectElement: select
443 | })
444 |
445 | const autocompleteInstance = document.querySelectorAll('.autocomplete__wrapper')[0]
446 | const autocompleteInput = autocompleteInstance.querySelector('.autocomplete__input')
447 |
448 | autocompleteInput.value = ''
449 | autocompleteInput.dispatchEvent(new window.Event('blur'))
450 | setTimeout(() => {
451 | expect(autocompleteInput.textContent).to.equal('')
452 | expect(select.value).to.equal('')
453 | done()
454 | }, 500)
455 | })
456 | })
457 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Accessible autocomplete multi select (RETIRED)
2 |
3 | > **NOTE**: This project is retired as the component is not accessible.
4 | >
5 | > Similar functionality is now provided by the [Multiple](https://components.publishing.service.gov.uk/component-guide/select_with_search#with_multiple_select_enabled) variant of the [Select With Search](https://components.publishing.service.gov.uk/component-guide/select_with_search) component, which is available in the [govuk_publishing_components](https://github.com/alphagov/govuk_publishing_components) gem.
6 |
7 | ---
8 |
9 | The accessible autocomplete is a component that helps users choose answers from a list you provide. You can also use it to make the answers you get from users more consistent.
10 |
11 | If you're asking users to provide their country or territory, the [govuk-country-and-territory-autocomplete](https://github.com/alphagov/govuk-country-and-territory-autocomplete/blob/main/README.md) might be more appropriate.
12 |
13 | [](http://npm.im/accessible-autocomplete)
14 | [](https://standardjs.com)
15 | [](https://unpkg.com/accessible-autocomplete/dist/accessible-autocomplete.min.js)
16 |
17 | [](https://saucelabs.com/u/tvararu-alphagov)
18 |
19 | This is a fork of the [accessible-autocomplete](https://github.com/alphagov/accessible-autocomplete) component. It adds additional functionality such as support for [select multiple](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attr-multiple).
20 |
21 | `accessible-autocomplete` is a JavaScript autocomplete built from the ground up to be accessible. The design goals are:
22 |
23 | - **Accessibility**: Following WAI-ARIA best practices and testing with assistive technologies.
24 | - **User experience**: Supporting a wide variety of user needs.
25 | - **Compatibility**: Working with [recommended browsers](https://www.gov.uk/service-manual/technology/designing-for-different-browsers-and-devices#browsers-to-test-in) and [assistive technologies](https://www.gov.uk/service-manual/technology/testing-with-assistive-technologies#which-assistive-technologies-to-test-with).
26 |
27 | [Try out the examples!](https://alphagov.github.io/accessible-autocomplete/examples/)
28 |
29 | ---
30 |
31 | ## Support
32 |
33 | The GOV.UK Design System team maintains the accessible autocomplete as a standalone component. However, we’re only able to put in minimal work to support it.
34 |
35 | [Read about our plans to maintain this component](https://github.com/alphagov/accessible-autocomplete/issues/532).
36 |
37 | [Read more about the types of support we can provide](https://github.com/alphagov/accessible-autocomplete/issues/430).
38 |
39 | ---
40 |
41 | ## Installation / usage
42 |
43 | ### Using npm and a module system
44 |
45 | Install it by running:
46 |
47 | ```bash
48 | npm install --save accessible-autocomplete
49 | ```
50 |
51 | The `accessibleAutocomplete` function will render an autocomplete ` ` and its accompanying suggestions and `aria-live` region. Your page should provide a `` and a container element:
52 |
53 | ```html
54 | Select your country
55 |
56 | ```
57 |
58 | Then import it using a module system like Browserify / Webpack / Rollup, and call the `accessibleAutocomplete` function, providing an array of values:
59 |
60 | ```js
61 | import accessibleAutocomplete from 'accessible-autocomplete'
62 |
63 | const countries = [
64 | 'France',
65 | 'Germany',
66 | 'United Kingdom'
67 | ]
68 |
69 | accessibleAutocomplete({
70 | element: document.querySelector('#my-autocomplete-container'),
71 | id: 'my-autocomplete', // To match it to the existing .
72 | source: countries
73 | })
74 | ```
75 |
76 | If you want to use it as a replacement for a `` element, read the [Progressive enhancement](#progressive-enhancement) section.
77 |
78 | ### As a plain JavaScript module
79 |
80 | You can copy the [dist/accessible-autocomplete.min.js](dist/accessible-autocomplete.min.js) file to your JavaScript folder and import it into the browser:
81 |
82 | ```html
83 |
84 | ```
85 |
86 | ### Styling the autocomplete
87 |
88 | A stylesheet is included with the package at [dist/accessible-autocomplete.min.css](dist/accessible-autocomplete.min.css).
89 |
90 | You can copy it to your stylesheets folder and import it into the browser:
91 |
92 | ```html
93 |
94 | ```
95 |
96 | You can also import it using Sass:
97 |
98 | ```css
99 | @import "accessible-autocomplete";
100 | ```
101 |
102 | ### Using with Preact
103 |
104 | If you already use Preact in your application, you can import a bundle that will use that:
105 |
106 | ```js
107 | import preact from 'preact'
108 | import Autocomplete from 'accessible-autocomplete/preact'
109 |
110 | preact.render(
111 | ,
112 | document.querySelector('#container')
113 | )
114 | ```
115 |
116 | [Try out the Preact example!](https://alphagov.github.io/accessible-autocomplete/examples/preact/)
117 |
118 | ### Using with React
119 |
120 | If you already use React in your application, you can import a bundle that will use that:
121 |
122 | ```js
123 | import React from 'react'
124 | import ReactDOM from 'react-dom'
125 | import Autocomplete from 'accessible-autocomplete/react'
126 |
127 | ReactDOM.render(
128 | ,
129 | document.querySelector('#container')
130 | )
131 | ```
132 |
133 | [Try out the React example!](https://alphagov.github.io/accessible-autocomplete/examples/react/)
134 |
135 | #### React versions
136 |
137 | React v15.5.4 has been tested to work with the Accessible Autocomplete - although make sure to check
138 | out [documented issues](https://github.com/alphagov/accessible-autocomplete/issues).
139 |
140 | React v15.6.2 and 16.0 have been incompletely tested with the Accessible Autocomplete: while no undocumented issues were found, we recommend you carry out thorough testing if you wish to use these or later versions of React.
141 |
142 | ## API documentation
143 |
144 | ### Required options
145 |
146 | #### `element`
147 |
148 | Type: `HTMLElement`
149 |
150 | The container element in which the autocomplete will be rendered in.
151 |
152 | #### `id`
153 |
154 | Type: `string`
155 |
156 | The `id` to assign to the autocomplete input field, to use with a ``. Not required if using `enhanceSelectElement`.
157 |
158 | #### `source`
159 |
160 | Type: `Array | Function`
161 |
162 | An array of values to search when the user types in the input field, or a function to take what the user types and call a callback function with the results to be displayed.
163 |
164 | An example of an array of values:
165 |
166 | ```js
167 | const countries = [
168 | 'France',
169 | 'Germany',
170 | 'United Kingdom'
171 | ]
172 | ```
173 |
174 | If `source` is a function, the arguments are: `query: string, populateResults: Function`
175 |
176 | Similar to the [`source` argument for typeahead.js](https://github.com/corejavascript/typeahead.js/blob/47d46b40cb834d8285ac9328c4b436e5eccf7197/doc/jquery_typeahead.md#datasets), a backing data source for suggestions. `query` is what gets typed into the input field, which will callback to `populateResults` synchronously with the array of string results to display in the menu.
177 |
178 | An example of a simple suggestion engine:
179 |
180 | ```js
181 | function suggest (query, populateResults) {
182 | const results = [
183 | 'France',
184 | 'Germany',
185 | 'United Kingdom'
186 | ]
187 | const filteredResults = results.filter(result => result.indexOf(query) !== -1)
188 | populateResults(filteredResults)
189 | }
190 | ```
191 |
192 | ### Other options
193 |
194 | #### `menuAttributes` (default: `{}`)
195 |
196 | Type: `Object`
197 |
198 | Sets html attributes and their values on the `menu`. Useful for adding `aria-labelledby` and setting to the value of the `id` attribute on your existing label, to provide context to an assistive technology user.
199 |
200 | #### `autoselect` (default: `false`)
201 |
202 | Type: `Boolean`
203 |
204 | Set to true to highlight the first option when the user types in something and receives results. Pressing enter will select it.
205 |
206 | #### `confirmOnBlur` (default: `true`)
207 |
208 | Type: `Boolean`
209 |
210 | The autocomplete will confirm the currently selected option when the user clicks outside of the component. Set to `false` to disable.
211 |
212 | #### `cssNamespace` (default: `'autocomplete'`)
213 |
214 | Type: `string`
215 |
216 | Use this property to override the [BEM](http://getbem.com/) block name that the JavaScript component will use. You will need to rewrite the CSS class names to use your specified block name.
217 |
218 | #### `defaultValue` (default: `''`)
219 |
220 | Type: `string`
221 |
222 | Specify a string to prefill the autocomplete with.
223 |
224 | #### `displayMenu` (default: `'inline'`)
225 |
226 | Type: `'inline' | 'overlay'`
227 |
228 | You can set this property to specify the way the menu should appear, whether inline or as an overlay.
229 |
230 | #### `minLength` (default: `0`)
231 |
232 | Type: `number`
233 |
234 | The minimum number of characters that should be entered before the autocomplete will attempt to suggest options. When the query length is under this, the aria status region will also provide helpful text to the user informing them they should type in more.
235 |
236 | #### `name` (default: `'input-autocomplete'`)
237 |
238 | Type: `string`
239 |
240 | The `name` for the autocomplete input field, to use with a parent `