Skip to content

Commit

Permalink
BB-20361: Make Search Autocomplete drop-down accessible by keyboard a…
Browse files Browse the repository at this point in the history
…rrows (#31255)
  • Loading branch information
ValeriyYustunyk authored Dec 14, 2021
1 parent af1005b commit 707887e
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

.search-autocomplete {
position: $search-autocomplete-position;
display: $search-autocomplete-display;
width: $search-autocomplete-width;
z-index: $search-autocomplete-z-index;

&__content {
Expand All @@ -15,16 +13,21 @@
}

&__item {
margin: $search-autocomplete-item-offset;
padding: $search-autocomplete-item-inner-offset;
border-bottom: $search-autocomplete-item-border-bottom;

&:last-child {
border-bottom-width: 0;
}

&[aria-selected='true'] {
box-shadow: $search-autocomplete-selected-box-shadow;
}
}

&__highlight {
background: $search-autocomplete-highlight-background;
text-decoration: $search-autocomplete-highlight-text-decoration;
}

&__footer {
padding: $search-autocomplete-footer-inner-offset;
}

&__submit {
Expand All @@ -40,8 +43,6 @@
.search-autocomplete-product {
text-decoration: $search-autocomplete-product-text-decoration;
display: $search-autocomplete-product-display;
margin: $search-autocomplete-product-offset;
padding: $search-autocomplete-product-inner-offset;

&:hover {
text-decoration: $search-autocomplete-product-hover-text-decoration;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* @theme: blank; */

$search-autocomplete-position: absolute !default;
$search-autocomplete-display: block !default;
$search-autocomplete-width: 100% !default;
$search-autocomplete-z-index: z('dropdown') + 1 !default;
$search-autocomplete-selected-box-shadow: $focus-visible-style !default;

$search-autocomplete-content-min-width: 385px !default;
$search-autocomplete-content-display: block !default;
Expand All @@ -12,11 +11,10 @@ $search-autocomplete-content-float: none !default;
$search-autocomplete-content-position: static !default;

$search-autocomplete-item-border-bottom: 1px solid get-color('additional', 'light') !default;
$search-autocomplete-item-offset: 0 -#{$offset-y-m + $offset-y-s} !default;
$search-autocomplete-item-inner-offset: #{$offset-y-m + $offset-y-s} #{$offset-y-m + $offset-y-s} !default;

$search-autocomplete-highlight-background: get-color('ui', 'warning') !default;
$search-autocomplete-highlight-text-decoration: underline !default;

$search-autocomplete-footer-inner-offset: #{$offset-y-m + $offset-y-s} 0 !default;

$search-autocomplete-submit-line-height: $base-line-height !default;
$search-autocomplete-submit-border: none !default;
Expand All @@ -25,8 +23,6 @@ $search-autocomplete-no-found-inner-offset: #{$offset-y-m + $offset-y-s} 0 !defa

$search-autocomplete-product-text-decoration: none !default;
$search-autocomplete-product-display: flex !default;
$search-autocomplete-product-offset: 0 -#{$offset-y-m + $offset-y-s} !default;
$search-autocomplete-product-inner-offset: #{$offset-y-m + $offset-y-s} #{$offset-y-m + $offset-y-s} !default;
$search-autocomplete-product-hover-text-decoration: none !default;

$search-autocomplete-product-image-width: 40px !default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import _ from 'underscore';
import BaseView from 'oroui/js/app/views/base/view';
import routing from 'routing';
import template from 'tpl-loader!oroproduct/templates/search-autocomplete.html';
import 'jquery-ui/tabbable';

const SearchAutocompleteView = BaseView.extend({
optionNames: BaseView.prototype.optionNames.concat([
Expand Down Expand Up @@ -41,29 +42,62 @@ const SearchAutocompleteView = BaseView.extend({
events: {
change: '_onInputChange',
keyup: '_onInputChange',
focus: '_onInputRefresh'
focus: '_onInputRefresh',
keydown: '_onKeyDown'
},

previousValue: '',

autocompleteItems: '[role="option"]',

/**
* @inheritdoc
*/
constructor: function SearchAutocompleteView(options) {
this.renderSuggestions = _.debounce(this.renderSuggestions.bind(this), this.delay);
SearchAutocompleteView.__super__.constructor.call(this, options);
},

preinitialize() {
this.comboboxId = `combobox-${this.cid}`;
},

/**
* @inheritdoc
*/
initialize(options) {
this.$el.attr('autocomplete', 'off');
this.$el.attr({
'role': 'combobox',
'autocomplete': 'off',
'aria-haspopup': true,
'aria-expanded': false,
'aria-autocomplete': 'list',
'aria-controls': this.comboboxId
});

this.renderSuggestions = _.debounce(this.renderSuggestions.bind(this), this.delay);
SearchAutocompleteView.__super__.initialize.call(this, options);
},

/**
* @inheritdoc
*/
delegateEvents: function() {
SearchAutocompleteView.__super__.delegateEvents.call(this);

$('body').on(`click${this.eventNamespace()}`, this._onOutsideAction.bind(this));

SearchAutocompleteView.__super__.initialize.call(this, options);
return this;
},

/**
* @inheritdoc
*/
undelegateEvents: function() {
SearchAutocompleteView.__super__.undelegateEvents.call(this);

$('body').off(this.eventNamespace());

return this;
},

getInputString() {
Expand All @@ -75,7 +109,8 @@ const SearchAutocompleteView = BaseView.extend({
*/
getTemplateData: function(data = {}) {
return Object.assign(data, {
inputString: this.getInputString()
inputString: this.getInputString(),
comboboxId: this.comboboxId
});
},

Expand All @@ -86,20 +121,109 @@ const SearchAutocompleteView = BaseView.extend({
if (this.disposed) {
return;
}
this.close();
this.closeCombobox();

$('body').off(this.eventNamespace());
this.$el.attr('aria-expanded', null);

SearchAutocompleteView.__super__.dispose.call(this);
},

close() {
closeCombobox() {
if (!this.$popup) {
return;
}

this.$popup.remove();
this.$popup = null;
this.$el.attr({
'aria-expanded': false,
'aria-activedescendant': null
});
this.undoFocusStyle();
},

hideCombobox() {
if (!this.$popup) {
return;
}

this.$el.attr({
'aria-expanded': false,
'aria-activedescendant': null
});
this.$popup.hide();
this.gerSelectedOption().removeAttr('aria-selected');
this.undoFocusStyle();
},

showCombobox() {
if (!this.$popup) {
return;
}

this.$popup.show();
},

hasSelectedOption() {
return this.gerSelectedOption().length > 0;
},

getAutocompleteItems() {
return this.$el.next().find(this.autocompleteItems);
},

gerSelectedOption() {
return this.getAutocompleteItems().filter((i, el) => $(el).attr('aria-selected') === 'true');
},

getNextOption() {
const $options = this.getAutocompleteItems();
const $activeOption = this.gerSelectedOption();

if (
$activeOption.length === 0 ||
($options.length - 1 === $options.index($activeOption))
) {
return $options.first();
}

return $options.eq($options.index($activeOption) + 1);
},

getPreviousOption() {
const $options = this.getAutocompleteItems();
const $activeOption = this.gerSelectedOption();

if (
$activeOption.length === 0 ||
$options.index($activeOption) === 0
) {
return $options.last();
}

return $options.eq($options.index($activeOption) - 1);
},

/**
* @param {string} direction
*/
goToOption(direction = 'down') {
const $options = this.getAutocompleteItems();
const $activeOption = direction === 'down'
? this.getNextOption()
: this.getPreviousOption()
;

this.showCombobox();
$options.attr('aria-selected', false);
$activeOption.attr('aria-selected', true);
this.$el.attr('aria-activedescendant', $activeOption.attr('id'));
},

executeSelectedOption() {
if (this.hasSelectedOption()) {
this.gerSelectedOption().find(':first-child')[0].click();
}
},

_getSearchXHR(inputString) {
Expand All @@ -117,11 +241,19 @@ const SearchAutocompleteView = BaseView.extend({
* @inheritdoc
*/
render(suggestions) {
this.close();
this.closeCombobox();

if (this.getInputString().length) {
this.$popup = $(this.template(this.getTemplateData(suggestions)));
this.$el.after(this.$popup);
this.$el.attr('aria-expanded', true);

this.getAutocompleteItems().each((i, el) => {
$(el).attr({
'id': _.uniqueId('item-'),
'aria-selected': false
}).find(':tabbable').attr('tabindex', -1);
});
}

return this;
Expand All @@ -148,6 +280,35 @@ const SearchAutocompleteView = BaseView.extend({
;
},

_onKeyDown(event) {
switch (event.key) {
case 'Tab':
case 'Escape':
this.hideCombobox();
break;
case 'ArrowUp':
event.preventDefault();
this.goToOption('up');
break;
case 'ArrowDown':
event.preventDefault();
this.goToOption('down');
break;
case 'Enter':
case ' ':
this.executeSelectedOption();
break;
default:
break;
}

this.undoFocusStyle();
},

undoFocusStyle() {
this.$el.toggleClass('undo-focus', this.hasSelectedOption());
},

_onInputChange(event) {
const inputString = this.getInputString();
if (inputString === this.previousValue) {
Expand All @@ -156,7 +317,7 @@ const SearchAutocompleteView = BaseView.extend({

this._shouldShowPopup(inputString)
? this.renderSuggestions(inputString)
: this.close();
: this.closeCombobox();

this.previousValue = inputString;
},
Expand All @@ -166,16 +327,16 @@ const SearchAutocompleteView = BaseView.extend({

if (!inputString.length && this.searchXHR) {
this.searchXHR.abort();
};
}

this._shouldShowPopup(inputString)
? this.renderSuggestions(inputString)
: this.close();
: this.closeCombobox();
},

_onOutsideAction(event) {
if (!((event.target === this.el) || (this.$popup && $.contains(this.$popup[0], event.target)))) {
this.close();
this.closeCombobox();
}
}
});
Expand Down
Loading

0 comments on commit 707887e

Please sign in to comment.