=============== DEPRECATED ===============
Use Core Suggest →
Old readme:
Core Input
@nrk/core-input
enhances <input>
fields with keyboard accessible functionality for autocomplete suggestions, search results and smart select box abilities.
Installation
npm install @nrk/core-input --save-exact
import coreInput from '@nrk/core-input'
import CoreInput from '@nrk/core-input/jsx'
Demo
<input type="text" class="my-input" placeholder="Type "C"...">
<ul hidden>
<li><button>Chrome</button></li>
<li><button>Firefox</button></li>
<li><button>Opera</button></li>
<li><button>Safari</button></li>
<li><button>Microsoft Edge</button></li>
</ul>
<script>
coreInput('.my-input')
</script>
<div id="jsx-input"></div>
<script type="text/jsx">
ReactDOM.render(<CoreInput>
<input type='text' placeholder='Type "C"... (JSX)' />
<ul className='my-dropdown'>
<li><button>Chrome</button></li>
<li><button>Firefox</button></li>
<li><button>Opera</button></li>
<li><button>Safari</button></li>
<li><button>Microsoft Edge</button></li>
</ul>
</CoreInput>, document.getElementById('jsx-input'))
</script>
Usage
Typing toggles the hidden attribute on items of type <button>
and <a>
, based on matching textContent. Focusing the input unhides the following element. The default filtering behavior can easily be altered through the The default filtering behavior can easily be altered through the 'input.select'
, 'input.filter'
, 'input.ajax'
and 'input.ajax.beforeSend'
events.
Results will be rendered in the element directly after the <input>
.
Always use coreInput.escapeHTML(String)
to safely render data from API or user.
HTML / JavaScript
<input type="text" class="my-input">
<ul hidden>
<li><button>Item 1</button></li>
<li><button value="Suprise!">Item 2</button></li>
<li><a href="https://www.nrk.no/">NRK.no</a></li>
</ul>
import coreInput from '@nrk/core-input'
coreInput(
String|Element|Elements,
String|Object {
open: Boolean,
content: String,
limit: Number,
ajax: String
}
})
coreInput('.my-input', { limit: 5 })
coreInput('.my-input', '<li><a href="?q=' + coreInput.escapeHTML(input.value) + '">More results</a></li>')
coreInput('.my-input', '<li><button>' + coreInput.highlight(item.text, input.value) + '</button></li>')
React / Preact
import CoreInput from '@nrk/core-input/jsx'
<CoreInput open={Boolean}
limit={Number}
ajax={String|Object}
onFilter={Function}
onSelect={Function}
onAjax={Function}
onAjaxBeforeSend={Function}>
<input type="text" />
<ul> // Next element will be used for items. Accepts both elements and components
<li><button>Item 1</button></li> // Interactive items must be <button> or <a>
<li><button value="Suprise!">Item 2</button></li> // Alternative value can be defined
<li><a href="https://www.nrk.no/">NRK.no</a></li> // Actual links are recommended when applicable
</ul>
</CoreInput>
Events
input.filter
'input.filter'
is fired before a default filtering (both for VanillaJS and React/Preact components). The input.filter
event is cancelable, meaning you can use event.preventDefault()
to cancel default filtering and respond to users typing yourself. The event also bubbles, and can therefore be detected both from the input element itself, or any parent element (read event delegation):
document.addEventListener('input.filter', (event) => {
event.target
event.detail.relatedTarget
})
input.select
'input.select'
event is fired when the user clicks/selects a item (both for VanillaJS and React/Preact components). The input.select
event is cancelable, meaning you can use event.preventDefault()
to cancel replacing the input value and handle select-action yourself. The event also bubbles, and can therefore be detected both from the button element itself, or any parent element (read event delegation):
document.addEventListener('input.select', (event) => {
event.target
event.detail.relatedTarget
event.detail.currentTarget
event.detail.value
})
input.ajax.beforeSend
The 'input.ajax.beforeSend'
event is fired before sending debounced ajax requests. If you wish to alter the XMLHttpRequest, use event.preventDefault()
and then execute XHR methods on the event.detail
. If not prevented, requests are sent using the GET
method and the header 'X-Requested-With': 'XMLHttpRequest'
. The event bubbles, and can therefore be detected both from the input element itself, or any parent element (read event delegation):
document.addEventListener('input.ajax.beforeSend', (event) => {
event.target
event.detail
})
document.addEventListener('input.ajax.beforeSend', (event) => {
event.preventDefault()
event.detail.open('POST', 'https://example.com')
event.detail.setRequestHeader('Content-Type', 'application/json')
event.detail.setRequestHeader('my-custom-header', 'my-custom-value')
event.detail.send(JSON.stringify({query: event.target.value}))
})
input.ajax
'input.ajax'
event is fired when the input field receives data from ajax. The event also bubbles, and can therefore be detected both from the input element itself, or any parent element (read event delegation):
document.addEventListener('input.ajax', (event) => {
event.target
event.detail
event.detail.responseText
event.detail.responseJSON
})
Styling
All styling in documentation is example only. Both the <button>
and content element receive attributes reflecting the current toggle state:
.my-input {}
.my-input[aria-expanded="true"] {}
.my-input[aria-expanded="false"] {}
.my-input-content {}
.my-input-content:not([hidden]) {}
.my-input-content[hidden] {}
.my-input-content :focus {}
.my-input-content mark {}
Notes
Ajax
When using @nrk/core-input
with the ajax: https://search.com?q={{value}}
functionality, make sure to implement both a Searching...
status (while fetching data from the server), and a No hits
status (if server responds with no results). These status indicators are highly recommended, but not provided by default as the context of use will affect the optimal textual formulation. See example implementation →
If you need to alter default headers, request method or post data, use the input.ajax.beforeSend
event →
Demo: Ajax
Ajax requests can be stopped by calling event.preventDefault()
on 'input.filter'
. Remember to always escape html and debounce requests when fetching data from external sources. The http request sent by @nrk/core-input
will have header X-Requested-With: XMLHttpRequest
for easier server side detection and CSRF prevention.
<input class="my-input-ajax" placeholder="Country...">
<ul class="my-dropdown" hidden></ul>
<script>
coreInput('.my-input-ajax', {
ajax: 'https://restcountries.eu/rest/v2/name/{{value}}?fields=name'
})
document.addEventListener('input.filter', function (event) {
var input = event.target
var value = input.value.trim()
if (input.className.indexOf('my-input-ajax') === -1) return
coreInput(input, value ? '<li><button>Searching for ' + coreInput.highlight(value, value) + '...</button></li>' : '')
})
document.addEventListener('input.ajax', function (event) {
if (event.target.className.indexOf('my-input-ajax') === -1) return
var items = event.detail.responseJSON
coreInput(event.target, items.length ? items.slice(0, 10)
.map(function (item) { return coreInput.highlight(item.name, event.target.value) })
.map(function (html) { return '<li><button>' + html + '</button></li>' })
.join('') : '<li><button>No results</button></li>')
})
</script>
<div id="jsx-input-ajax"></div>
<script type="text/jsx">
class AjaxInput extends React.Component {
constructor (props) {
super(props)
this.onFilter = this.onFilter.bind(this)
this.onAjax = this.onAjax.bind(this)
this.state = { items: [], value: '' }
}
onFilter (event) {
const value = event.target.value
const items = value ? [{name: `Searching for ${value}...`}] : []
this.setState({value, items})
}
onAjax (event) {
const items = event.detail.responseJSON
this.setState({items: items.length ? items : [{name: 'No results'}]})
}
render () {
return <CoreInput
ajax="https://restcountries.eu/rest/v2/name/{{value}}?fields=name"
onFilter={this.onFilter}
onAjax={this.onAjax}>
<input type='text' placeholder='Country... (JSX)' />
<ul className='my-dropdown'>
{this.state.items.slice(0, 10).map((item, key) =>
<li key={key}>
<button>
<CoreInput.Highlight text={item.name} query={this.state.value} />
</button>
</li>
)}
</ul>
</CoreInput>
}
}
ReactDOM.render(<AjaxInput />, document.getElementById('jsx-input-ajax'))
</script>
Demo: Lazy
Hybrid solution; lazy load items, but let core-input
still handle filtering:
<input class="my-input-lazy" placeholder="Country...">
<ul class="my-dropdown" hidden></ul>
<script>
window.getCountries = function (callback) {
var xhr = new XMLHttpRequest()
var url = 'https://restcountries.eu/rest/v2/?fields=name'
xhr.onload = function () { callback(JSON.parse(xhr.responseText)) }
xhr.open('GET', url, true)
xhr.send()
}
document.addEventListener('focus', function (event) {
if (!event.target.className || event.target.className.indexOf('my-input-lazy') === -1) return
event.target.className = ''
window.getCountries(function (items) {
coreInput(event.target, items
.map(function (item) { return '<li><button>' + coreInput.escapeHTML(item.name) + '</button></li>'})
.join(''))
})
}, true)
</script>
<div id="jsx-input-lazy"></div>
<script type="text/jsx">
class LazyInput extends React.Component {
constructor (props) {
super(props)
this.onFocus = this.onFocus.bind(this)
this.state = {items: []}
}
onFocus (event) {
this.onFocus = null
window.getCountries((items) => this.setState({items}))
}
render () {
return <CoreInput onFocus={this.onFocus}>
<input type='text' placeholder='Country... (JSX)' />
<ul className='my-dropdown'>
{this.state.items.map((item, key) =>
<li key={key}><button>{item.name}</button></li>
)}
</ul>
</CoreInput>
}
}
ReactDOM.render(<LazyInput />, document.getElementById('jsx-input-lazy'))
</script>
Demo: Dynamic
Synchronous operation; dynamically populating items based input value:
<input class="my-input-dynamic" placeholder="Type your email...">
<ul class="my-dropdown" hidden></ul>
<script>
coreInput('.my-input-dynamic')
document.addEventListener('input.filter', (event) => {
if (event.target.className.indexOf('my-input-dynamic') === -1) return
event.preventDefault()
var mails = ['facebook.com', 'gmail.com', 'hotmail.com', 'mac.com', 'mail.com', 'msn.com', 'live.com']
var input = event.target
var value = input.value.trim()
coreInput(input, value ? mails.map(function (mail) {
return '<li><button>' + coreInput.highlight(value.replace(/(@.*|$)/, '@' + mail), value) + '</button><li>'
}).join('') : '')
})
</script>
<div id="jsx-input-dynamic"></div>
<script>
class DynamicInput extends React.Component {
constructor (props) {
super(props)
this.onFilter = this.onFilter.bind(this)
this.mails = ['facebook.com', 'gmail.com', 'hotmail.com', 'mac.com', 'mail.com', 'msn.com', 'live.com']
this.state = {items: []}
}
onFilter (event) {
const value = event.target.value.trim()
const items = value ? this.mails.map((mail) => value.replace(/(@.*|$)/, `@${mail}`)) : []
event.preventDefault()
this.setState({value, items})
}
render () {
return <CoreInput onFilter={this.onFilter}>
<input type='text' placeholder='Type your email... (JSX)' />
<ul className='my-dropdown'>
{this.state.items.map((text, key) =>
<li key={key}><button><CoreInput.Highlight text={text} query={this.state.value} /></button></li>
)}
</ul>
</CoreInput>
}
}
ReactDOM.render(<DynamicInput />, document.getElementById('jsx-input-dynamic'))
</script>
FAQ
Why not use <datalist>
instead?
Despite having a native <datalist>
element for autocomplete lists, there are several issues regarding browser support, varying accessibility support as well as no ability for custom styling or custom behavior.
Why is there only a subset of aria attributes in use?
Despite well documented examples in the aria 1.1 spesification, "best practice" simply becomes unusable in several screen reader due to implementation differences. core-input
aims to provide a equally good user experience regardless if a screen reader passes events to browser or not (events are often hijacked for quick-navigation). Several techniques and attributes have been thoroughly tested:
aria-activedescendant
/aria-selected
- ignored in Android, lacks indication of list length in JAWS
aria-owns
- full list is read for every keystroke in NVDA
role="listbox"
- VoiceOver needs aria-selected to falsely announce "0 selected"
role="option"
- falsely announces links and buttons as "text"
aria-live="assertive"
- fails to correctly inform user if current target is link or button
role="combobox"
- skipped in iOS as VoiceOver fails to inform current field is editable
How do I use core-input with multiple tags/output values?
Tagging and screen readers is a complex matter, requiring more than comma separated values. Currently, tagging is just a part of the wishlist for core-input. If tagging functionality is of interest for your project, please add a +1 to the tagging task, describe your needs in a comment, and you'll be updated about progress.