From 723fd7ee7c9de4a5e82066a59bc41b7c188ceea4 Mon Sep 17 00:00:00 2001 From: mynamesleon Date: Sat, 27 Mar 2021 08:22:04 +0000 Subject: [PATCH] v1.4.0 --- CHANGELOG.md | 16 ++++++-- README.md | 20 +++++---- dist/aria-autocomplete.css | 13 ++++-- dist/aria-autocomplete.min.js | 2 +- dist/index.d.ts | 4 +- index.html | 22 +++++++--- package-lock.json | 2 +- package.json | 7 +++- src/aria-autocomplete-types.d.ts | 4 +- src/aria-autocomplete.less | 14 +++++-- src/autocomplete-constants.ts | 2 - src/autocomplete-options.ts | 4 +- src/autocomplete.ts | 70 ++++++++++++++++++++++++++------ 13 files changed, 134 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf29bb..b6faaba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,23 @@ All notable changes to this project will be documented in this file. -## [1.3.2] - 2021-03-24 +## [1.4.0] - 2021-03-27 + +### Added + +- the ability to provide a function for the `confirmOnBlur` option that can return a string to compare against the search result labels. +- page up and page down key handling within the options list to move the current focus position up or down by 10 options. +- handling for disabled options, including ones based on disabled checkboxes and disabled select options. ### Changed -- adjusted the `confirmOnBlur` string matching behaviour to used the "cleaned" version of the search term and option label when blurring off of the field without a currently focused menu option. +- adjusted the `confirmOnBlur` string matching behaviour to use the "cleaned" version of the search term and option label when blurring off of the field without a currently focused menu option. - defensive adjustment to setting the input value and moving focus to it after the component area is blurred. +### Fixed + +- the `create` option when used as a function not being called with the API as context. + ## [1.3.0] - 2021-03-07 ### Added @@ -104,7 +114,7 @@ All notable changes to this project will be documented in this file. - Issue when clicking on a single-select autocomplete with minLength of 0 with a current selection, which was correctly searching with an empty string, but the polling method was then triggering a search with the value afterwards. - Screen reader announcements for results ignoring the number of results rendered -[1.3.2]: https://github.com/mynamesleon/aria-autocomplete/compare/v1.3.0...v1.3.2 +[1.4.0]: https://github.com/mynamesleon/aria-autocomplete/compare/v1.3.0...v1.4.0 [1.3.0]: https://github.com/mynamesleon/aria-autocomplete/compare/v1.2.3...v1.3.0 [1.2.3]: https://github.com/mynamesleon/aria-autocomplete/compare/v1.2.0...v1.2.3 [1.2.0]: https://github.com/mynamesleon/aria-autocomplete/compare/v1.1.4...v1.2.0 diff --git a/README.md b/README.md index a596836..5dc7f68 100644 --- a/README.md +++ b/README.md @@ -3,18 +3,19 @@ [![npm version](https://img.shields.io/npm/v/aria-autocomplete.svg)](http://npm.im/aria-autocomplete) [![gzip size](http://img.badgesize.io/https://unpkg.com/aria-autocomplete/dist/aria-autocomplete.min.js?compression=gzip)](https://unpkg.com/aria-autocomplete/dist/aria-autocomplete.min.js) -Accessible, extensible, plain JavaScript autocomplete with multi-select. +Fast, accessible, extensible, plain JavaScript autocomplete with multi-select. [Try out the examples](https://mynamesleon.github.io/aria-autocomplete/). Key design goals and features: -- **multiple selection** +- **support multiple selection** - **extensible source options**: Array of Strings, Array of Objects, a Function, or an endpoint String - **progressive enhancement**: Automatic source building through specifying a `"),e.showAllControl&&s.push('');var l=e.srListLabelText,a=e.listClassName?" "+e.listClassName:"",h=l?' aria-label="'+l+'"':"";s.push('"),s.push('"),s.push(""),this.element.insertAdjacentHTML("afterend",s.join(""))},e.prototype.destroy=function(){var e=this;this.label&&this.label._ariaAutocompleteLabelOriginallyFor&&(this.label.setAttribute("for",this.label._ariaAutocompleteLabelOriginallyFor),delete this.label._ariaAutocompleteLabelOriginallyFor),this.documentClickBound&&document.removeEventListener("click",this.documentClick),this.autoGrow&&this.inputAutoWidth&&this.inputAutoWidth.destroy(),this.wrapper.parentNode.removeChild(this.wrapper),delete this.element.ariaAutocomplete,this.show(this.element),clearTimeout(this.filterTimer),clearTimeout(this.pollingTimer),clearTimeout(this.showAllPrepTimer),clearTimeout(this.announcementTimer),clearTimeout(this.componentBlurTimer),clearTimeout(this.clearAnnouncementTimer),clearTimeout(this.elementChangeEventTimer),["list","input","label","element","wrapper","showAll","deleteAll","srAssistance","srAnnouncements"].forEach((function(t){return e[t]=null}))},e.prototype.init=function(e,t){this.selected=[],this.element=e,this.label=document.querySelector('[for="'+this.element.id+'"]'),this.ids=new T(this.element.id,this.label?this.label.id:null,t.id),this.elementIsInput="INPUT"===e.nodeName,this.options=new h(t),this.label&&!this.label.id&&(this.label.id=this.ids.LABEL),this.source=this.options.source,this.multiple=this.options.multiple,this.autoGrow=this.options.autoGrow,this.cssNameSpace=this.options.cssNameSpace,this.documentClick=this.handleComponentBlur.bind(this),this.setHtml(),this.list=document.getElementById(this.ids.LIST),this.input=document.getElementById(this.ids.INPUT),this.wrapper=document.getElementById(this.ids.WRAPPER),this.showAll=document.getElementById(this.ids.BUTTON),this.srAssistance=document.getElementById(this.ids.SR_ASSISTANCE),this.srAnnouncements=document.getElementById(this.ids.SR_ANNOUNCEMENTS),this.prepListSource();var i=[];this.options.showAllControl&&i.push(this.cssNameSpace+"__wrapper--show-all"),this.autoGrow&&i.push(this.cssNameSpace+"__wrapper--autogrow"),this.multiple&&i.push(this.cssNameSpace+"__wrapper--multiple"),i.length&&a(this.wrapper,i.join(" ")),this.hide(this.list),this.hide(this.element),this.setInputStartingStates(),this.bindEvents(),this.api=new E(this),this.triggerOptionCallback("onReady",[this.wrapper])},e}();function V(e,t){return e&&e.ariaAutocomplete&&e.ariaAutocomplete.open?e.ariaAutocomplete:new K(e,t).api}t.default=V}])})); \ No newline at end of file +!function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(window,(function(){return function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=2)}([function(e,t,i){window,e.exports=function(e){var t={};function i(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,i),n.l=!0,n.exports}return i.m=e,i.c=t,i.d=function(e,t,s){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(i.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(s,n,function(t){return e[t]}.bind(null,n));return s},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t,i){"use strict";var s;function n(e,t){if(e)for(var i in t){var s="number"==typeof t[i]?t[i]+"px":t[i];e.style[i]=s+""}}i.r(t),i.d(t,"InputAutoWidth",(function(){return r}));var r=function(){function e(e,t){this.cache={},this.options=t,this.input=e,this.trigger(),this.eventHandler=this.trigger.bind(this),this.input.addEventListener("blur",this.eventHandler),this.input.addEventListener("input",this.eventHandler),this.input.addEventListener("keyup",this.eventHandler),this.input.addEventListener("keydown",this.eventHandler)}return e.prototype.measureString=function(e){return e?this.cache&&"number"==typeof this.cache[e]?this.cache[e]:(s||(n(s=document.createElement("span"),{position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}),document.body.appendChild(s)),s.textContent=e,function(e,t,i){if(e&&t){var s=getComputedStyle(e),r={};if(i&&i.length)for(var l=0,o=i.length;l=48&&t<=57||t>=65&&t<=90||t>=96&&t<=111||t>=186&&t<=222||32===t||8===t||46===t){var a=String.fromCharCode(n);s+=a=e.shiftKey?a.toUpperCase():a.toLowerCase()}}!s&&(i=this.input.getAttribute("placeholder"))&&(s=i);var h=this.measureString(s)+4;this.options&&this.options.cache&&this.cache&&(this.cache[s]=h);var u=this.options&&this.options.minWidth;"number"==typeof u&&hc&&(h=c),h!==this.currentWidth&&(this.currentWidth=h,this.input.style.width=h+"px")}},e.prototype.destroy=function(){this.input.removeEventListener("blur",this.eventHandler),this.input.removeEventListener("input",this.eventHandler),this.input.removeEventListener("keyup",this.eventHandler),this.input.removeEventListener("keydown",this.eventHandler),this.input=this.cache=null},e}();t.default=r}])},function(e,t){var i=Element.prototype;i.matches||(i.matches=i.msMatchesSelector||i.webkitMatchesSelector),i.closest||(i.closest=function(e){var t=this;do{if(t.matches(e))return t;t=t.parentElement||t.parentNode}while(null!==t&&1===t.nodeType);return null})},function(e,t,i){"use strict";i.r(t),i.d(t,"AriaAutocomplete",(function(){return H}));var s=i(0),n=i.n(s),r=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function l(e,t){if(e&&1===e.nodeType&&"string"==typeof t)!function(e,t){for(var i=e.getAttribute&&e.getAttribute("class")||"",s=" "+i+" ",n=0,l=t.split(" "),o=l.length;n"']/g,x={"&":"&","<":"<",">":">",'"':""","'":"'"},T=function(){function e(e){this.list=e.list,this.input=e.input,this.wrapper=e.wrapper,this.options=e.options,this.selected=e.selected,this.open=this.open.bind(e),this.close=this.close.bind(e),this.enable=this.enable.bind(e),this.disable=this.disable.bind(e),this.filter=this.filter.bind(e),this.destroy=this.destroy.bind(e),e.element.ariaAutocomplete=this}return e.prototype.open=function(){this.show.call(this)},e.prototype.close=function(){this.hide.call(this)},e.prototype.enable=function(){this.enable.call(this)},e.prototype.disable=function(e){this.disable.call(this,e)},e.prototype.filter=function(e){this.filter.call(this,e)},e.prototype.destroy=function(){this.destroy.call(this)},e}(),O=0,C=function(e,t,i){O+=1,this.ELEMENT=e;var s=i||e||"";this.PREFIX=s+"aria-autocomplete-"+O,this.LIST=this.PREFIX+"-list",this.BUTTON=this.PREFIX+"-button",this.OPTION=this.PREFIX+"-option",this.WRAPPER=this.PREFIX+"-wrapper",this.LABEL=t||this.PREFIX+"-label",this.INPUT=i||this.PREFIX+"-input",this.SR_ASSISTANCE=this.PREFIX+"-sr-assistance",this.OPTION_SELECTED=this.PREFIX+"-option-selected",this.SR_ANNOUNCEMENTS=this.PREFIX+"-sr-announcements"};i(1);function _(e){return null==e?"":(e+"").trim()}function I(e){return"string"==typeof e&&e?e.replace(E,(function(e){return x[e]})):""}var L=/&/g,N=/\s\s+/g,P=/[\u2018\u2019',:\u2013-]/g,k=/[\-\[\]{}()*+?.,\\\^$|#\s]/g;function F(e,t){return void 0===t&&(t=!1),e=_(e).toLowerCase().replace(P,"").replace(L,"and").replace(N," "),t&&(e=e.replace(k,"\\$&")),e}function D(){for(var e=[],t=0;t-1)&&i.clearAnnouncement("number"==typeof t?t:1e4)};0!==t?(clearTimeout(this.announcementTimer),this.announcementTimer=setTimeout((function(){return s()}),t)):s()}},e.prototype.setInputDescription=function(){var e=this.input.getAttribute("aria-describedby"),t=_(e),i=t.replace(this.ids.SR_ASSISTANCE,"");this.input.value.length||(i=i+" "+this.ids.SR_ASSISTANCE),(i=_(i))?i!==t&&this.input.setAttribute("aria-describedby",i):e&&this.input.removeAttribute("aria-describedby")},e.prototype.isSelectedElem=function(e){var t=e&&e[c];return!(!this.multiple||"object"!=typeof t)},e.prototype.getSelectedElems=function(){for(var e=[],t=0,i=this.wrapper.childNodes.length;t-1&&this.selected[i]){var r=D(this.selected[i]),l=r.label;M(r.element,!1,this),this.selected.splice(i,1),this.triggerOptionCallback("onDelete",[r]),this.setSourceElementValues(),this.buildMultiSelected(t?i:null),this.triggerAutoGrow(),this.announce(l+" "+this.options.srDeletedText,0)}}},e.prototype.createSelectedElemFrom=function(e,t){var i=e.label,s=this.cssNameSpace,n=s+"__selected",r=document.createElement("span"),l=t?s+"__delete-all "+n+" "+n+"--delete-all":n;return r.setAttribute("aria-describedby",this.ids.LABEL),r.setAttribute("class",l),r.setAttribute("role","button"),r.setAttribute("tabindex","0"),r.textContent=i,t||(r.setAttribute("aria-label",this.options.srDeleteText+" "+i),r[c]=e),r},e.prototype.buildMultiSelected=function(e){var t=this;if(this.multiple){this.multiple&&this.selected.length>=this.options.maxItems?this.disable():this.enable();var i=this.getSelectedElems();if(this.selected.length||i.length){var s=[];i.forEach((function(e){for(var i=e[c],n=0,r=t.selected.length;n=s.length)return this.currentSelectedIndex=s.length-1,void this.setOptionFocus(e,this.currentSelectedIndex);var n=s[t];if(n&&"string"==typeof n.getAttribute("tabindex"))return this.currentSelectedIndex=t,a(n,this.cssNameSpace+"__option--focused focused focus"),"true"!==n.getAttribute("aria-disabled")&&n.setAttribute("aria-selected","true"),void(i&&n.focus());this.currentSelectedIndex=-1},e.prototype.setSourceElementValues=function(){for(var e=[],t=0,i=this.selected.length;t=this.options.maxItems)&&this.filteredSource.length&&this.filteredSource[t]){var s=D(this.filteredSource[t]);if(!(s.disabled||s.element&&s.element.disabled)){for(var n=!1,r=0,l=this.selected.length;r-1||this.indexOfValueIn(this.source,n,"label")>-1)){var l;if(i){var o=this.element.querySelector("option"),a=o.cloneNode(!0);a.textContent=n,a.value=r,l=a,o.parentNode.insertBefore(a,o)}else if(s){var h=this.element.querySelector('input[type="checkbox"]'),u=h.cloneNode(!0),c=h.closest("label"),p=document.createElement("label");p.textContent=n,u.value=r,l=u,p.appendChild(u);var d=c||h;d.parentNode.insertBefore(p,d)}l&&(e.element=l,l.removeAttribute("id")),this.source.unshift(e)}}},e.prototype.prependEntryInCreateMode=function(e,t){var i=this.options.create;if((!0===i||"function"==typeof i)&&F(e)){var s,n=_(e),r=this.options.sourceMapping;if(!0===i&&(s=j(n,r)),"function"==typeof i){var l=this.triggerOptionCallback("create",[n]),o=typeof l;l&&("string"===o||"object"===o&&!Array.isArray(l))&&(s=j(l,r))}!s||!s.label||!s.value||this.indexOfValueIn(t,s[u],u)>-1||this.indexOfValueIn(t,s.value,"value")>-1||t.unshift(s)}},e.prototype.setListOptions=function(e){var t=this.options.sourceMapping;this.prependEntryInCreateMode(this.term,e);var i=this.removeSelectedFromResults(e),s=this.triggerOptionCallback("onResponse",[i.slice()]);this.filteredSource=Array.isArray(s)?K(s,t):i;for(var n=this.ids.OPTION,r=this.cssNameSpace,o=r+"__option",h=this.filteredSource.length,u="function"==typeof this.options.onItemRender,c=this.forceShowAll?9999:this.options.maxResults,p=c'+I(b)+"")}var A,S=!d.length;S?(l(this.list,r+"__list--has-results"),a(this.list,r+"__list--no-results")):(a(this.list,r+"__list--has-results"),l(this.list,r+"__list--no-results"));var w=this.options.noResultsText;S&&"string"==typeof w&&w.length&&(A=w,d.push('
  • '+I(w)+"
  • ")),this.cancelFilterPrep(),A||(A=this.triggerOptionCallback("srResultsText",[p])),A&&this.announce(A);var E=d.join("");if(this.currentListHtml!==E?(this.currentListHtml=E,this.list.innerHTML=E):this.resetOptionAttributes(),!d.length)return this.hide(),void(this.forceShowAll=!1);this.show(),this.forceShowAll=!1},e.prototype.handleAsync=function(e,t){var i=this;void 0===t&&(t=!1),this.xhr&&"function"==typeof this.xhr.abort&&this.xhr.abort();var s=new XMLHttpRequest,n=this.forceShowAll,r=t?null:this.api,l=this.multiple?this.selected.length:0,o=n||t||9999===this.options.maxResults,a=this.source+(/\?/.test(this.source)?"&":"?")+encodeURIComponent(this.options.asyncQueryParam)+"="+encodeURIComponent(e)+"&"+encodeURIComponent(this.options.asyncMaxResultsParam)+"="+(o?9999:l+this.options.maxResults),h=this.triggerOptionCallback("onAsyncPrep",[a,s,t],r);h&&"string"==typeof h&&(a=h),s.open("GET",a),s.onload=function(){if(s.readyState===s.DONE&&s.status>=200&&s.status<300){i.forceShowAll=n;var l=K(i.triggerOptionCallback("onAsyncSuccess",[e,s,t],r)||s.responseText,i.options.sourceMapping);t?(i.prepSelectedFromArray(l),i.setInputStartingStates(!1)):i.setListOptions(l),i.triggerOptionCallback("onAsyncComplete",[e,s,t],r)}},s.onerror=function(){i.triggerOptionCallback("onAsyncError",[e,s,t],r)},t||(this.xhr=s),this.triggerOptionCallback("onAsyncBeforeSend",[e,s,t],r),s.send()},e.prototype.filter=function(e){var t=this;if("string"==typeof e){var i=this.forceShowAll;if(!i){var s=this.triggerOptionCallback("onSearch",[e]);"string"==typeof s&&(e=s)}if(this.term=e,"string"==typeof this.source&&this.source.length)return this.handleAsync(e),void(this.forceShowAll=!1);if("function"!=typeof this.source){e||(i=!0);var n=[],r=this.source;if(r&&r.length){var l=[u];if(!i){e=F(e,!0);var o=this.options.alsoSearchIn;Array.isArray(o)&&o.length&&(l=function(e){var t=[];return e.forEach((function(e){if("string"==typeof e){for(var i=_(e),s="label"!==i,n=0,r=t.length;s&&n-1&&V(n,t,s))return!0}return!1}(t,e,l))&&n.push(t)}))}this.setListOptions(n)}else{var a=function(e){var i=K(e,t.options.sourceMapping);t.setListOptions(i)},h=this.source.call(this.api,this.term,a,!1);h&&"function"==typeof h.then&&h.then((function(e){return a(e)}))}}else this.cancelFilterPrep()},e.prototype.cancelFilterPrep=function(){clearTimeout(this.filterTimer),l(this.wrapper,this.cssNameSpace+"__wrapper--loading loading"),l(this.input,this.cssNameSpace+"__input--loading loading"),this.filtering=!1},e.prototype.filterPrep=function(e,t,i){var s=this;void 0===t&&(t=!1),void 0===i&&(i=!1);var n=this.forceShowAll,r=n||i?0:this.options.delay;this.cancelFilterPrep(),this.filtering=!0,this.filterTimer=setTimeout((function(){var i=s.input.value;if(s.inputPollingValue=i,(n||""===i||t&&!s.multiple&&s.selected.length&&_(s.selected[0].label)===_(i))&&(i=""),e&&e.type&&s.options.srAssistiveTextAutoClear&&s.setInputDescription(),!n&&i.length=t)&&this.filterPrep(e)}if(this.menuOpen&&!this.filtering){var i=this.currentSelectedIndex;"number"!=typeof i||i<0?this.setOptionFocus(e,0):this.setOptionFocus(e,i+1)}},e.prototype.handleEndKey=function(e){if(!this.disabled&&this.menuOpen&&e.target!==this.input){var t=B(this.list);t.length&&(e.preventDefault(),this.setOptionFocus(e,t.length-1))}},e.prototype.handleHomeKey=function(e){!this.disabled&&this.menuOpen&&e.target!==this.input&&(e.preventDefault(),this.setOptionFocus(e,0))},e.prototype.handlePageUpKey=function(e){if(!this.disabled&&this.menuOpen&&e.target!==this.input){e.preventDefault();var t=this.currentSelectedIndex,i=t>0&&t-10<0?0:0===t?-1:t-10;this.setOptionFocus(e,i)}},e.prototype.handlePageDownKey=function(e){if(!this.disabled&&this.menuOpen&&e.target!==this.input){e.preventDefault();var t=this.currentSelectedIndex;this.setOptionFocus(e,t<0?0:t+10)}},e.prototype.handleEnterKey=function(e){var t=e.target;this.isSelectedElem(t)?this.removeEntryFromSelected(t[c],!0):this.deleteAll&&t===this.deleteAll?this.deleteAllSelected():this.disabled||(this.showAll&&t===this.showAll?this.filterPrepShowAll(e):(this.menuOpen&&(e.preventDefault(),this.currentSelectedIndex>-1&&this.handleOptionSelect(e,this.currentSelectedIndex)),t===this.input&&this.filterPrep(e,!1,!0)))},e.prototype.handleKeyDownDefault=function(e){var t=e.keyCode,i=e.target===this.input;if(t===m&&!i||this.isSelectedElem(e.target)&&t===w)return e.preventDefault(),void this.handleEnterKey(e);if(!this.disabled){var s=this.selected&&this.selected.length;this.options.deleteOnBackspace&&t===p&&""===this.input.value&&s&&i&&this.multiple&&this.removeEntryFromSelected(this.selected[s-1]);var n=function(e){return e>=48&&e<=57||e>=65&&e<=90||e>=96&&e<=111||e>=186&&e<=222||32===e||8===e||46===e}(t),r=!i&&n;r&&this.input.focus(),(r||i&&n)&&this.filterPrep(e)}},e.prototype.prepKeyDown=function(e){switch(e.keyCode){case A:this.handleUpKey(e);break;case S:this.handleDownKey(e);break;case y:this.handleEndKey(e);break;case g:this.handleHomeKey(e);break;case d:this.handleEnterKey(e);break;case v:this.handlePageUpKey(e);break;case b:this.handlePageDownKey(e);break;case f:this.handleComponentBlur(e,!0);break;default:this.handleKeyDownDefault(e)}},e.prototype.cancelPolling=function(){clearTimeout(this.pollingTimer)},e.prototype.startPolling=function(){var e=this;this.filtering||this.input.value===this.inputPollingValue||this.filterPrep({}),this.pollingTimer=setTimeout((function(){e.startPolling()}),200)},e.prototype.bindEvents=function(){var e=this;this.wrapper.addEventListener("focusout",(function(t){e.handleComponentBlur(t,!1)})),this.wrapper.addEventListener("focusin",(function(t){e.list.contains(t.target)||(e.currentSelectedIndex=-1),e.isFocused||e.triggerOptionCallback("onFocus",[e.wrapper]),e.isFocused=!0})),this.wrapper.addEventListener("keydown",(function(t){e.prepKeyDown(t)})),this.wrapper.addEventListener("click",(function(t){t.target!==e.wrapper?(e.isSelectedElem(t.target)&&e.removeEntryFromSelected(t.target[c],!0),e.deleteAll&&t.target===e.deleteAll&&e.deleteAllSelected()):e.input.focus()}));var t=this.cssNameSpace+"__wrapper--focused focused focus",i=this.cssNameSpace+"__input--focused focused focus";this.input.addEventListener("blur",(function(){l(e.wrapper,t),l(e.input,i),e.cancelPolling()})),this.input.addEventListener("input",(function(t){document.activeElement===e.input&&e.filterPrep(t)})),this.input.addEventListener("click",(function(t){!e.menuOpen&&e.input.value.length>=e.options.minLength&&e.filterPrep(t,!0)})),this.input.addEventListener("focusin",(function(s){a(e.wrapper,t),a(e.input,i),e.startPolling(),e.disabled||e.menuOpen||e.filterPrep(s,!0)})),this.showAll&&this.showAll.addEventListener("click",(function(t){e.filterPrepShowAll(t)})),this.list.addEventListener("mouseenter",(function(t){e.resetOptionAttributes()})),this.list.addEventListener("click",(function(t){if(t.target!==e.list){var i=B(e.list);if(i.length){var s=i.indexOf(t.target);e.handleOptionSelect(t,s)}}})),this.autoGrow&&(this.inputAutoWidth=new n.a(this.input))},e.prototype.prepListSourceCheckboxes=function(){this.multiple=!0,this.source=[];for(var e=this.element.querySelectorAll('input[type="checkbox"]'),t=0,i=e.length;t1&&(this.options.maxItems=1),this.source=[];for(var t=this.element.querySelectorAll("option"),i=0,s=t.length;i-1&&t.selected.push(o[i])}}))}},e.prototype.prepListSourceArray=function(){this.source=K(this.source,this.options.sourceMapping),this.prepSelectedFromArray(this.source)},e.prototype.prepListSourceAsync=function(){var e=this.element;this.elementIsInput&&e.value&&this.handleAsync(e.value,!0)},e.prototype.prepListSourceFunction=function(){var e=this,t=this.element;if(this.elementIsInput&&t.value){var i=function(t){var i=K(t,e.options.sourceMapping);e.prepSelectedFromArray(i),e.setInputStartingStates(!1)},s=this.source.call(void 0,t.value,i,!0);s&&"function"==typeof s.then&&s.then((function(e){return i(e)}))}},e.prototype.prepListSource=function(){return"function"==typeof this.source?this.prepListSourceFunction():"string"==typeof this.source&&this.source.length?this.prepListSourceAsync():Array.isArray(this.source)&&this.source.length?this.prepListSourceArray():(this.sourceFromSelect="SELECT"===this.element.nodeName,this.sourceFromSelect?this.prepListSourceDdl():(this.sourceFromCheckboxList=!!this.element.querySelector('input[type="checkbox"]'),this.sourceFromCheckboxList?this.prepListSourceCheckboxes():void(this.source=[])))},e.prototype.setInputStartingStates=function(e){if(void 0===e&&(e=!0),e){this.label&&(this.label._ariaAutocompleteLabelOriginallyFor=this.ids.ELEMENT,this.label.setAttribute("for",this.ids.INPUT));var t=this.element.getAttribute("aria-describedby");t&&this.input.setAttribute("aria-describedby",t);var i=this.element.getAttribute("aria-labelledby");i&&this.input.setAttribute("aria-labelledby",i)}this.selected.length&&(this.multiple?(this.buildMultiSelected(),this.triggerAutoGrow()):this.setInputValue(this.selected[0].label||"",!0)),this.element.disabled&&this.disable(!0)},e.prototype.setHtml=function(){var e=this.options,t=this.cssNameSpace,i=e.wrapperClassName?" "+e.wrapperClassName:"",s=['
    '];s.push('

    ');var n=e.name?' name="'+e.name+'"':"",r=e.inputClassName?" "+e.inputClassName:"",l=e.placeholder?' placeholder="'+e.placeholder+'" aria-placeholder="'+e.placeholder+'"':"";s.push('"),e.showAllControl&&s.push('');var o=e.srListLabelText,a=e.listClassName?" "+e.listClassName:"",h=o?' aria-label="'+o+'"':"";s.push('"),s.push('"),s.push("
    "),this.element.insertAdjacentHTML("afterend",s.join(""))},e.prototype.destroy=function(){var e=this;this.label&&this.label._ariaAutocompleteLabelOriginallyFor&&(this.label.setAttribute("for",this.label._ariaAutocompleteLabelOriginallyFor),delete this.label._ariaAutocompleteLabelOriginallyFor),this.documentClickBound&&document.removeEventListener("click",this.documentClick),this.autoGrow&&this.inputAutoWidth&&this.inputAutoWidth.destroy(),this.wrapper.parentNode.removeChild(this.wrapper),delete this.element.ariaAutocomplete,this.show(this.element),clearTimeout(this.filterTimer),clearTimeout(this.pollingTimer),clearTimeout(this.showAllPrepTimer),clearTimeout(this.announcementTimer),clearTimeout(this.componentBlurTimer),clearTimeout(this.clearAnnouncementTimer),clearTimeout(this.elementChangeEventTimer),["list","input","label","element","wrapper","showAll","deleteAll","srAssistance","srAnnouncements"].forEach((function(t){return e[t]=null}))},e.prototype.init=function(e,t){this.selected=[],this.element=e,this.label=document.querySelector('[for="'+this.element.id+'"]'),this.ids=new C(this.element.id,this.label?this.label.id:null,t.id),this.elementIsInput="INPUT"===e.nodeName,this.options=new h(t),this.label&&!this.label.id&&(this.label.id=this.ids.LABEL),this.source=this.options.source,this.multiple=this.options.multiple,this.autoGrow=this.options.autoGrow,this.cssNameSpace=this.options.cssNameSpace,this.documentClick=this.handleComponentBlur.bind(this),this.setHtml(),this.list=document.getElementById(this.ids.LIST),this.input=document.getElementById(this.ids.INPUT),this.wrapper=document.getElementById(this.ids.WRAPPER),this.showAll=document.getElementById(this.ids.BUTTON),this.srAssistance=document.getElementById(this.ids.SR_ASSISTANCE),this.srAnnouncements=document.getElementById(this.ids.SR_ANNOUNCEMENTS),this.prepListSource();var i=[];this.options.showAllControl&&i.push(this.cssNameSpace+"__wrapper--show-all"),this.autoGrow&&i.push(this.cssNameSpace+"__wrapper--autogrow"),this.multiple&&i.push(this.cssNameSpace+"__wrapper--multiple"),i.length&&a(this.wrapper,i.join(" ")),this.hide(this.list),this.hide(this.element),this.setInputStartingStates(),this.bindEvents(),this.api=new T(this),this.triggerOptionCallback("onReady",[this.wrapper])},e}();function H(e,t){return e&&e.ariaAutocomplete&&e.ariaAutocomplete.open?e.ariaAutocomplete:new U(e,t).api}t.default=H}])})); \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index ba1739d..23e5bb4 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -4,12 +4,12 @@ export interface IAriaAutocompleteOptions { source?: string | string[] | any[] | Function | Promise; sourceMapping?: any; alsoSearchIn?: string[]; - create?: boolean | ((value: string) => string | any); + create?: boolean | ((value: string) => string | any | void); delay?: number; minLength?: number; maxResults?: number; showAllControl?: boolean; - confirmOnBlur?: boolean; + confirmOnBlur?: boolean | ((value: string, results: any[]) => string | void); multiple?: boolean; autoGrow?: boolean; maxItems?: number; diff --git a/index.html b/index.html index a90e7f7..4f4412f 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ @@ -449,7 +449,11 @@

    Core options

    create: boolean | Function
    -
    If no exact match is found, create an entry in the results list for the current search text
    Default: false
    +
    + If no exact match is found, create an entry in the results list for the current search text. + Can use a function that receives the search term, and returns a string or an object (like a normal static source entry). +
    Default: false +
    delay: number
    input delay before running a search
    Default: 100
    minLength: number
    @@ -458,8 +462,12 @@

    Core options

    Maximum number of results to render
    Default: 9999
    showAllControl: boolean
    Render a button that triggers showing all options
    Default: false
    -
    confirmOnBlur: boolean
    -
    Confirm current selection (from using arrow keys) when blurring off of the control. Will also check for an exact text match
    Default: true
    +
    confirmOnBlur: boolean | Function
    +
    + Confirm current selection (from using arrow keys) when blurring off of the control. Will also check for a label match if there is no current selection.
    + Can use a function which receives the search term and results, and returns a string to be used to compare against the result labels.
    + Default: true +
    multiple: boolean
    @@ -513,7 +521,8 @@

    Screen reader enhancements

    Set the delay in milliseconds before screen reader announcements are made.
    Note: if this is too short, some default announcements may interrupt it.
    - Default: 5000
    + Default: 5000 +
    srAutoClear: boolean | number
    Automatically clear the screen reader announcement element after the specified delay. Number is in milliseconds.
    Default: 10000
    srDeleteText: string
    @@ -532,7 +541,8 @@

    Screen reader enhancements

    Automatically remove the srAssistiveText once user input is detected, to reduce screen reader verbosity.
    The text is re-associated with the generated input if its value is emptied.
    - Default: 5000
    + Default: 5000 +
    srResultsText: Function
    Screen reader announcement after results are rendered
    Default: length => `${length} ${length === 1 ? 'result' : 'results'} available.`
    diff --git a/package-lock.json b/package-lock.json index 7c8c963..b4d2bb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "aria-autocomplete", - "version": "1.3.2", + "version": "1.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4b98c43..ad614da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aria-autocomplete", - "version": "1.3.2", + "version": "1.4.0", "description": "Accessible, extensible, JavaScript autocomplete with multi-select", "main": "dist/aria-autocomplete.min.js", "style": "dist/aria-autocomplete.css", @@ -27,7 +27,10 @@ "select", "combobox", "multiselect", - "multiple selection" + "multiple selection", + "component", + "plugin", + "widget" ], "browserslist": [ ">0.1%", diff --git a/src/aria-autocomplete-types.d.ts b/src/aria-autocomplete-types.d.ts index ba1739d..23e5bb4 100644 --- a/src/aria-autocomplete-types.d.ts +++ b/src/aria-autocomplete-types.d.ts @@ -4,12 +4,12 @@ export interface IAriaAutocompleteOptions { source?: string | string[] | any[] | Function | Promise; sourceMapping?: any; alsoSearchIn?: string[]; - create?: boolean | ((value: string) => string | any); + create?: boolean | ((value: string) => string | any | void); delay?: number; minLength?: number; maxResults?: number; showAllControl?: boolean; - confirmOnBlur?: boolean; + confirmOnBlur?: boolean | ((value: string, results: any[]) => string | void); multiple?: boolean; autoGrow?: boolean; maxItems?: number; diff --git a/src/aria-autocomplete.less b/src/aria-autocomplete.less index 672bd92..adb6088 100644 --- a/src/aria-autocomplete.less +++ b/src/aria-autocomplete.less @@ -135,15 +135,23 @@ font-weight: normal; text-decoration: underline; cursor: pointer; + + &[aria-disabled='true'] { + background: #f5f5f5; + } } - &--no-results { - margin-top: 0 !important; + &--no-results, + &[aria-disabled='true'] { opacity: 0.65 !important; + color: @near-black !important; cursor: not-allowed !important; text-decoration: none !important; + } + + &--no-results { + margin-top: 0 !important; background: transparent !important; - color: @near-black !important; } } diff --git a/src/autocomplete-constants.ts b/src/autocomplete-constants.ts index 7cbdb19..6b7bf52 100644 --- a/src/autocomplete-constants.ts +++ b/src/autocomplete-constants.ts @@ -12,9 +12,7 @@ export const KEYCODES = { PAGEDOWN: 34, END: 35, HOME: 36, - LEFT: 37, UP: 38, - RIGHT: 39, DOWN: 40, DELETE: 46, }; diff --git a/src/autocomplete-options.ts b/src/autocomplete-options.ts index f88e510..0a213fb 100644 --- a/src/autocomplete-options.ts +++ b/src/autocomplete-options.ts @@ -34,7 +34,7 @@ export default class AutocompleteOptions { * If no exact match is found, * create an entry in the options list for the current search text */ - create: boolean | ((value: string) => string | object) = false; + create: boolean | ((value: string) => string | any | void) = false; /** * Input delay after typing before running a search @@ -61,7 +61,7 @@ export default class AutocompleteOptions { * Confirm currently active selection when blurring off of the control. If * no active selection, will compare current input value against available labels */ - confirmOnBlur: boolean = true; + confirmOnBlur: boolean | ((value: string, options: any[]) => string | void) = true; /** * Allow multiple items to be selected diff --git a/src/autocomplete.ts b/src/autocomplete.ts index 20d53db..b3270eb 100644 --- a/src/autocomplete.ts +++ b/src/autocomplete.ts @@ -564,7 +564,10 @@ export default class Autocomplete { resetOptionAttributes(options: HTMLElement[] = getChildrenOf(this.list)) { const classToRemove = `${this.cssNameSpace}__option--focused focused focus`; options.forEach((option: HTMLElement) => { - option.setAttribute('aria-selected', 'false'); + // omit the aria-selected attribute from non-selectable options + if (option.getAttribute('aria-disabled') !== 'true') { + option.setAttribute('aria-selected', 'false'); + } removeClass(option, classToRemove); }); } @@ -599,7 +602,10 @@ export default class Autocomplete { if (toFocus && typeof toFocus.getAttribute('tabindex') === 'string') { this.currentSelectedIndex = index; addClass(toFocus, `${this.cssNameSpace}__option--focused focused focus`); - toFocus.setAttribute('aria-selected', 'true'); + // omit the aria-selected attribute from non-selectable options + if (toFocus.getAttribute('aria-disabled') !== 'true') { + toFocus.setAttribute('aria-selected', 'true'); + } if (triggerDomFocus) { toFocus.focus(); } @@ -657,6 +663,11 @@ export default class Autocomplete { // generate new object from the selected item in case the original source gets altered const option: any = mergeObjects(this.filteredSource[index]); + // check if option or linked element is disabled + if (option.disabled || (option.element && option.element.disabled)) { + return; + } + // detect if selected option is already in selected array let alreadySelected: boolean = false; for (let i = 0, l = this.selected.length; i < l; i += 1) { @@ -819,7 +830,7 @@ export default class Autocomplete { // when function, check the result first... if (typeof create === 'function') { - const result = create(trimmedTerm); + const result = this.triggerOptionCallback('create', [trimmedTerm]); const resultType = typeof result; // check that the result was a string or object // if devs want to add multiple entries, they can use the `onResponse` callback @@ -847,15 +858,15 @@ export default class Autocomplete { /** * final filtering and render for list options - * @todo add handling for disabled results */ setListOptions(results: any[]) { const { sourceMapping: mapping } = this.options; this.prependEntryInCreateMode(this.term, results); // if in multiple mode, exclude items already in the selected array const updated: any[] = this.removeSelectedFromResults(results); - // allow callback to alter the response before rendering - const callback: any = this.triggerOptionCallback('onResponse', [updated]); + // allow callback to alter the response before rendering; + // only provide a shallow copy of the source so that the callback cannot modify it + const callback: any = this.triggerOptionCallback('onResponse', [updated.slice()]); // at last, set the fully filtered source this.filteredSource = Array.isArray(callback) ? processSourceArray(callback, mapping) : updated; @@ -870,12 +881,15 @@ export default class Autocomplete { const toShow: string[] = []; for (let i = 0; i < lengthToUse; i += 1) { - const thisSource: any = this.filteredSource[i]; - const callbackResponse = checkCallback && this.triggerOptionCallback('onItemRender', [thisSource]); - const itemContent = typeof callbackResponse === 'string' ? callbackResponse : thisSource.label; + const entry: any = this.filteredSource[i]; + const callbackResponse = checkCallback && this.triggerOptionCallback('onItemRender', [entry]); + const itemContent = typeof callbackResponse === 'string' ? callbackResponse : entry.label; + const disabled: boolean = !!(entry.disabled || (entry.element && entry.element.disabled)); + // omit the aria-selected attribute from non-selectable options + const ariaSelected = !disabled ? ' aria-selected="false"' : ''; toShow.push( - `
  • ${escapeHtml(itemContent)}
  • ` ); } @@ -1209,7 +1223,10 @@ export default class Autocomplete { if (typeof toUse !== 'number' || toUse === -1) { // otherwise check for exact match of cleaned values // between current input value and available items - const cleanedTerm = cleanString(this.term); + const copiedSource = this.filteredSource.slice(); + const onConfirmVal = this.triggerOptionCallback('confirmOnBlur', [this.term, copiedSource]); + const useOnConfirmVal = onConfirmVal && typeof onConfirmVal === 'string'; + const cleanedTerm = cleanString(useOnConfirmVal ? onConfirmVal : this.term); toUse = this.indexOfValueIn.call(this, this.filteredSource, cleanedTerm, CLEANED_LABEL_PROP); } this.handleOptionSelect({}, toUse, false); @@ -1306,6 +1323,29 @@ export default class Autocomplete { } } + /** + * page up key handling within the component; move focus up by 10 + */ + handlePageUpKey(event: KeyboardEvent) { + if (!this.disabled && this.menuOpen && event.target !== this.input) { + event.preventDefault(); + const current: number = this.currentSelectedIndex; + const index = current > 0 && current - 10 < 0 ? 0 : current === 0 ? -1 : current - 10; + this.setOptionFocus(event, index); + } + } + + /** + * page down key handling within the component; move down by 10 + */ + handlePageDownKey(event: KeyboardEvent) { + if (!this.disabled && this.menuOpen && event.target !== this.input) { + event.preventDefault(); + const current: number = this.currentSelectedIndex; + this.setOptionFocus(event, current < 0 ? 0 : current + 10); + } + } + /** * enter keydown anywhere within the component */ @@ -1414,6 +1454,12 @@ export default class Autocomplete { case KEYCODES.ENTER: this.handleEnterKey(event); break; + case KEYCODES.PAGEUP: + this.handlePageUpKey(event); + break; + case KEYCODES.PAGEDOWN: + this.handlePageDownKey(event); + break; case KEYCODES.ESCAPE: this.handleComponentBlur(event, true); break;