diff --git a/package.json b/package.json
index 965b56c2..6073659b 100644
--- a/package.json
+++ b/package.json
@@ -7,14 +7,13 @@
"scripts": {
"build": "rollup --config",
"install": "node bin/index.js --install",
- "lint": "standard",
"publish:patch": "npm run build && node bin/index.js --publish=patch",
"publish:minor": "npm run build && node bin/index.js --publish=minor",
"publish:major": "npm run build && node bin/index.js --publish=major",
"start": "rollup --config --watch",
"static-publish": "npm run build && static-publish --directory=packages --account=nrk-core --latest --major",
- "test:js": "npm run build && jest",
- "test": "npm run lint && npm run test:js"
+ "test": "npm run build && jest && standard -v",
+ "test:watch": "jest --watch"
},
"devDependencies": {
"jest": "24.5.0",
diff --git a/packages/core-input/core-input.js b/packages/core-input/core-input.js
index 6950967a..8438f169 100644
--- a/packages/core-input/core-input.js
+++ b/packages/core-input/core-input.js
@@ -3,12 +3,12 @@ import { IS_IOS, addEvent, escapeHTML, dispatchEvent, requestAnimFrame, queryAll
const UUID = `data-${name}-${version}`.replace(/\W+/g, '-') // Strip invalid attribute characters
const KEYS = { ENTER: 13, ESC: 27, PAGEUP: 33, PAGEDOWN: 34, END: 35, HOME: 36, UP: 38, DOWN: 40 }
-const ITEM = '[tabindex="-1"]'
const AJAX_DEBOUNCE = 500
export default function input (elements, content) {
const options = typeof content === 'object' ? content : { content }
const repaint = typeof options.content === 'string'
+ const limit = Math.max(options.limit, 0) || 0
return queryAll(elements).map((input) => {
const list = input.nextElementSibling
@@ -16,12 +16,13 @@ export default function input (elements, content) {
const open = typeof options.open === 'undefined' ? input === document.activeElement : options.open
input.setAttribute(UUID, ajax || '')
+ input.setAttribute(`${UUID}-limit`, limit)
input.setAttribute(IS_IOS ? 'data-role' : 'role', 'combobox') // iOS does not inform user area is editable if combobox
input.setAttribute('aria-autocomplete', 'list')
input.setAttribute('autocomplete', 'off')
if (repaint) list.innerHTML = options.content
- queryAll('a,button', list).forEach(setupItem)
+ queryAll('a,button', list).forEach((...args) => setupItem(limit, ...args))
setupExpand(input, open)
return input
@@ -43,7 +44,7 @@ function onClickOrFocus (event) {
queryAll(`[${UUID}]`).forEach((input) => {
const list = input.nextElementSibling
const open = input === event.target || list.contains(event.target)
- const item = event.type === 'click' && open && queryAll(ITEM, list).filter((item) => item.contains(event.target))[0]
+ const item = event.type === 'click' && open && queryAll('[tabindex="-1"]', list).filter((item) => item.contains(event.target))[0]
if (item) onSelect(input, { relatedTarget: list, currentTarget: item, value: item.value || item.textContent.trim() })
else setupExpand(input, open)
@@ -64,7 +65,7 @@ addEvent(UUID, 'keydown', (event) => {
function onKey (input, event) {
const list = input.nextElementSibling
- const focusable = [input].concat(queryAll(`${ITEM}:not([hidden])`, list))
+ const focusable = [input].concat(queryAll(`[tabindex="-1"]:not([hidden])`, list))
const isClosing = event.keyCode === KEYS.ESC && input.getAttribute('aria-expanded') === 'true'
const index = focusable.indexOf(document.activeElement)
let item = false
@@ -92,15 +93,16 @@ function onSelect (input, detail) {
}
function onFilter (input, detail) {
+ const limit = Number(input.getAttribute(`${UUID}-limit`)) || Infinity
if (dispatchEvent(input, 'input.filter', detail) && ajax(input) === false) {
- queryAll(ITEM, input.nextElementSibling).reduce((acc, item) => {
+ queryAll('[tabindex="-1"]', input.nextElementSibling).reduce((acc, item, index) => {
const list = item.parentElement.nodeName === 'LI' && item.parentElement
const show = item.textContent.toLowerCase().indexOf(input.value.toLowerCase()) !== -1
const attr = show ? 'removeAttribute' : 'setAttribute'
if (list) list[attr]('hidden', '') // JAWS requires hiding of
too (if they exist)
item[attr]('hidden', '')
return show ? acc.concat(item) : acc
- }, []).forEach(setupItem)
+ }, []).forEach((...args) => setupItem(limit, ...args))
}
}
@@ -111,10 +113,11 @@ function setupExpand (input, open = input.getAttribute('aria-expanded') === 'tru
})
}
-function setupItem (item, index, items) {
+function setupItem (limit, item, index, items) {
item.setAttribute('aria-label', `${item.textContent.trim()}, ${index + 1} av ${items.length}`)
item.setAttribute('tabindex', '-1')
item.setAttribute('type', 'button')
+ if (index >= limit) item.parentElement.setAttribute('hidden', '')
}
function ajax (input) {
diff --git a/packages/core-input/core-input.jsx b/packages/core-input/core-input.jsx
index 17e073f3..a0e415da 100644
--- a/packages/core-input/core-input.jsx
+++ b/packages/core-input/core-input.jsx
@@ -4,7 +4,7 @@ import coreInput from './core-input'
import { exclude } from '../utils'
export default class Input extends React.Component {
- static get defaultProps () { return { open: null, ajax: null, onAjax: null, onAjaxBeforeSend: null, onFilter: null, onSelect: null } }
+ static get defaultProps () { return { open: null, limit: null, ajax: null, onAjax: null, onAjaxBeforeSend: null, onFilter: null, onSelect: null } }
constructor (props) {
super(props)
this.onFilter = (event) => this.props.onFilter && this.props.onFilter(event)
@@ -47,6 +47,7 @@ Input.propTypes = {
onSelect: PropTypes.func,
onAjax: PropTypes.func,
open: PropTypes.bool,
+ limit: PropTypes.number,
ajax: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object
diff --git a/packages/core-input/core-input.test.js b/packages/core-input/core-input.test.js
index f309efb7..192ecb2c 100644
--- a/packages/core-input/core-input.test.js
+++ b/packages/core-input/core-input.test.js
@@ -1,31 +1,15 @@
const coreInput = require('./core-input.min')
-function expectOpenedState (input, suggestions) {
- expect(input.getAttribute('role')).toEqual('combobox')
- expect(input.getAttribute('aria-autocomplete')).toEqual('list')
- expect(input.getAttribute('autocomplete')).toEqual('off')
- expect(input.getAttribute('aria-expanded')).toEqual('true')
- expect(suggestions.hasAttribute('hidden')).toBeFalsy()
-}
-
-function expectClosedState (input, suggestions) {
- expect(input.getAttribute('role')).toEqual('combobox')
- expect(input.getAttribute('aria-autocomplete')).toEqual('list')
- expect(input.getAttribute('autocomplete')).toEqual('off')
- expect(input.getAttribute('aria-expanded')).toEqual('false')
- expect(suggestions.hasAttribute('hidden')).toBeTruthy()
-}
-
const standardHTML = `
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
`
describe('core-input', () => {
@@ -41,11 +25,9 @@ describe('core-input', () => {
expect(coreInput).toBeInstanceOf(Function)
})
- it('should initialize input with props when core-input is called', () => {
+ it('should initialize input with props', () => {
document.body.innerHTML = standardHTML
-
const input = document.querySelector('.my-input')
-
coreInput(input)
expect(input.getAttribute('role')).toEqual('combobox')
expect(input.getAttribute('aria-autocomplete')).toEqual('list')
@@ -53,77 +35,80 @@ describe('core-input', () => {
expect(input.getAttribute('aria-expanded')).toEqual('false')
})
- it('should expand suggestions when input field is clicked', () => {
+ it('should expand suggestions when input is clicked', () => {
document.body.innerHTML = standardHTML
-
const input = document.querySelector('.my-input')
const suggestions = document.querySelector('.my-input + ul')
-
coreInput(input)
-
input.click()
- expectOpenedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('true')
+ expect(suggestions.hasAttribute('hidden')).toBeFalsy()
})
- it('should set input value to that of clicked suggestion', () => {
+ it('should set input value to clicked suggestion', () => {
document.body.innerHTML = standardHTML
-
const input = document.querySelector('.my-input')
const suggestions = document.querySelector('.my-input + ul')
const firefoxBtn = suggestions.querySelector('li:nth-child(2) button')
const callback = jest.fn()
-
coreInput(input)
-
input.addEventListener('input.select', callback)
input.click()
firefoxBtn.click()
-
expect(callback).toHaveBeenCalled()
expect(input.value).toEqual('Firefox')
- expectClosedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('false')
+ expect(suggestions.hasAttribute('hidden')).toBeTruthy()
})
- it('should close suggestions if focus is placed outside on elements outside list/input', () => {
+ it('should close suggestions on focusing outside', () => {
document.body.innerHTML = standardHTML
-
const input = document.querySelector('.my-input')
const suggestions = document.querySelector('.my-input + ul')
const someOtherBtn = document.querySelector('#something-else')
-
coreInput(input)
-
input.click()
someOtherBtn.click()
-
- expectClosedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('false')
+ expect(suggestions.hasAttribute('hidden')).toBeTruthy()
})
- it('should filter suggestion list according to value in input', () => {
+ it('should filter suggestion from input value', () => {
document.body.innerHTML = standardHTML
-
const input = document.querySelector('.my-input')
const event = new window.CustomEvent('input', { bubbles: true })
-
coreInput(input)
-
input.value = 'Chrome'
input.dispatchEvent(event)
-
expect(document.querySelectorAll('button[hidden]').length).toEqual(4)
})
it('should set type="button" on all buttons in list', () => {
document.body.innerHTML = standardHTML
-
coreInput(document.querySelector('.my-input'))
document.querySelectorAll('ul button').forEach((button) => {
expect(button.type).toEqual('button')
})
})
-})
-module.exports = {
- expectOpenedState,
- expectClosedState
-}
+ it('should limit length of suggestions from option', () => {
+ document.body.innerHTML = standardHTML
+ const input = document.querySelector('.my-input')
+ const suggestions = document.querySelector('.my-input + ul')
+ coreInput(input, { limit: 2 })
+ expect(suggestions.children[0].hasAttribute('hidden')).toBe(false)
+ expect(suggestions.children[1].hasAttribute('hidden')).toBe(false)
+ expect(suggestions.children[2].hasAttribute('hidden')).toBe(true)
+ expect(suggestions.children[3].hasAttribute('hidden')).toBe(true)
+ expect(suggestions.children[4].hasAttribute('hidden')).toBe(true)
+ })
+})
diff --git a/packages/core-input/core-input.test.jsx b/packages/core-input/core-input.test.jsx
index 27c9331e..e6fa9ad2 100644
--- a/packages/core-input/core-input.test.jsx
+++ b/packages/core-input/core-input.test.jsx
@@ -1,7 +1,6 @@
const React = require('react')
const ReactDOM = require('react-dom')
const CoreInput = require('./jsx')
-const { expectOpenedState, expectClosedState } = require('./core-input.test.js')
const mount = (props = {}, keepInstance) => {
if (!keepInstance) {
@@ -53,7 +52,11 @@ describe('core-input/jsx', () => {
const suggestions = document.querySelector('.my-input + ul')
input.click()
- expectOpenedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('true')
+ expect(suggestions.hasAttribute('hidden')).toBeFalsy()
})
it('should set input value to that of clicked suggestion', () => {
@@ -69,7 +72,11 @@ describe('core-input/jsx', () => {
expect(callback).toHaveBeenCalled()
expect(input.value).toEqual('Firefox')
- expectClosedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('false')
+ expect(suggestions.hasAttribute('hidden')).toBeTruthy()
})
it('should close suggestions if focus is placed outside on elements outside list/input', () => {
@@ -81,7 +88,11 @@ describe('core-input/jsx', () => {
input.click()
document.body.click()
- expectClosedState(input, suggestions)
+ expect(input.getAttribute('role')).toEqual('combobox')
+ expect(input.getAttribute('aria-autocomplete')).toEqual('list')
+ expect(input.getAttribute('autocomplete')).toEqual('off')
+ expect(input.getAttribute('aria-expanded')).toEqual('false')
+ expect(suggestions.hasAttribute('hidden')).toBeTruthy()
})
it('should filter suggestion list according to value in input', () => {
diff --git a/packages/core-input/readme.md b/packages/core-input/readme.md
index 67e7081f..27ce348d 100644
--- a/packages/core-input/readme.md
+++ b/packages/core-input/readme.md
@@ -58,7 +58,7 @@ demo-->
## Usage
-Typing toggles the [hidden attribute](https://developer.mozilla.org/en/docs/Web/HTML/Global_attributes/hidden) on items of type `