diff --git a/lib/jquery.uls/css/jquery.uls.lcd.css b/lib/jquery.uls/css/jquery.uls.lcd.css
index 4b04e139..fd2b9784 100644
--- a/lib/jquery.uls/css/jquery.uls.lcd.css
+++ b/lib/jquery.uls/css/jquery.uls.lcd.css
@@ -138,3 +138,7 @@
bottom: 0;
left: 0;
}
+
+.uls-language-option--highlighted {
+ background-color: #eaeff7;
+}
diff --git a/lib/jquery.uls/src/jquery.uls.languagefilter.js b/lib/jquery.uls/src/jquery.uls.languagefilter.js
index c87c680b..5a838642 100644
--- a/lib/jquery.uls/src/jquery.uls.languagefilter.js
+++ b/lib/jquery.uls/src/jquery.uls.languagefilter.js
@@ -106,7 +106,12 @@
query = ( this.$element.val() || '' ).trim().toLowerCase();
- if ( this.selectedLanguage ) {
+ // Check if a language is currently highlighted, and if so select that
+ // if user presses enter.
+ var highlightedLanguage = this.options.lcd.getHighlightedLanguageCode();
+ if ( highlightedLanguage ) {
+ this.options.onSelect( highlightedLanguage, e );
+ } else if ( this.selectedLanguage ) {
// this.selectLanguage will be populated from a matching search
this.options.onSelect( this.selectedLanguage, e );
} else if ( this.options.languages[ query ] ) {
@@ -115,6 +120,16 @@
this.options.onSelect( query, e );
}
+ break;
+ case 38: // arrow up
+ this.options.lcd.navigateUp();
+ e.preventDefault();
+
+ break;
+ case 40: // arrow down
+ this.options.lcd.navigateDown();
+ e.preventDefault();
+
break;
}
},
@@ -163,6 +178,10 @@
results = [],
query = ( this.$element.val() || '' ).trim().toLowerCase();
+ // Reset the keyboard navigation index inside LanguageCategoryDisplay (lcd)
+ // before re-rendering the language options
+ this.options.lcd.resetNavigationIndex();
+
if ( query === '' ) {
this.options.lcd.setGroupByRegionOverride( null );
this.resultHandler( query, languages );
diff --git a/lib/jquery.uls/src/jquery.uls.lcd.js b/lib/jquery.uls/src/jquery.uls.lcd.js
index 47f08200..c61dd304 100644
--- a/lib/jquery.uls/src/jquery.uls.lcd.js
+++ b/lib/jquery.uls/src/jquery.uls.lcd.js
@@ -59,13 +59,119 @@
this.$cachedQuicklist = null;
this.groupByRegionOverride = null;
+ // The index of the language option that is currently visited using arrow key navigation
+ // Can take values in the [0, language options list item length - 1] range for top to bottom
+ // navigation, or in the [-1, -language options list item length + 1] range for bottom to
+ // top navigation.
+ this.navigationIndex = null;
+
this.render();
this.listen();
}
+ // Adapted from https://stackoverflow.com/a/41754707/903324
+ function isLanguageFullyVisible( $el, $holder ) {
+ var elementRect = $el.get( 0 ).getBoundingClientRect();
+ var holderRect = $holder.get( 0 ).getBoundingClientRect();
+
+ return elementRect.top <= holderRect.top ?
+ holderRect.top <= elementRect.top :
+ holderRect.bottom <= elementRect.height;
+ }
+
LanguageCategoryDisplay.prototype = {
constructor: LanguageCategoryDisplay,
+ /**
+ * Returns a jQuery object containing a collection of all the visible
+ * language option
elements
+ *
+ * @return {jQuery}
+ */
+ getLanguageOptionListItems: function () {
+ return this.$element.find( '.uls-lcd-region-section:not(.hide)' ).find( 'li[data-code]' );
+ },
+
+ /**
+ * Increases the keyboard navigation index by one and applies a specific
+ * class to the n-th language option element (where n = navigation index)
+ * Currently used as event handler for the arrow down 'keydown' event, inside
+ * LanguageFilter.
+ */
+ navigateDown: function () {
+ var maxIndex = this.getLanguageOptionListItems().length - 1;
+ // We support navigation starting both from the top and the bottom of the language list.
+ // The navigation should stop when the last language option is already highlighted (for
+ // top to bottom navigation). For top to bottom navigation, that happens when navigation
+ // index is equal to language options list item length - 1. For bottom to top
+ // navigation, that happens when navigation index is equal to -1.
+ if ( this.navigationIndex === maxIndex || this.navigationIndex === -1 ) {
+ return;
+ }
+
+ if ( this.navigationIndex === null ) {
+ this.navigationIndex = 0;
+ } else {
+ this.navigationIndex++;
+ }
+ this.highlightLanguageOption();
+ },
+
+ /**
+ * Decreases the keyboard navigation index by one and applies a specific
+ * class to the n-th language option element (where n = navigation index)
+ * Currently used as event handler for the arrow down 'keydown' event, inside
+ * LanguageFilter.
+ */
+ navigateUp: function () {
+ var maxIndex = this.getLanguageOptionListItems().length - 1;
+ // We support navigation starting both from the top and the bottom of the language list.
+ // The navigation should stop when the first language option is already highlighted (for
+ // bottom to top navigation). For top to bottom navigation, that happens when navigation
+ // index is equal to 0. For bottom to top navigation, that happens when navigation index
+ // is equal to -languageOptionListItemsLength + 1.
+ if ( this.navigationIndex === 0 || this.navigationIndex === -maxIndex ) {
+ return;
+ }
+
+ this.navigationIndex--;
+ this.highlightLanguageOption();
+ },
+
+ /**
+ * Adds a specific class ("uls-language-option--highlighted") only to the n-th
+ * language option element (where n = navigation index)
+ */
+ highlightLanguageOption: function () {
+ var $listItems = this.getLanguageOptionListItems();
+ $listItems.removeClass( 'uls-language-option--highlighted' );
+
+ var $selectedItem = $listItems.eq( this.navigationIndex );
+ $selectedItem.addClass( 'uls-language-option--highlighted' );
+
+ // If the selected item is not visible, then scroll the container to display it
+ if ( !isLanguageFullyVisible( $selectedItem, this.$element ) ) {
+ $selectedItem.get( 0 ).scrollIntoView( false );
+ }
+ },
+
+ getHighlightedLanguageCode: function () {
+ if ( this.navigationIndex ) {
+ var $selectedItem = this.getLanguageOptionListItems().eq( this.navigationIndex );
+ return $selectedItem.data( 'code' );
+ }
+
+ return null;
+ },
+
+ /**
+ * Resets the navigation index to null.
+ * Currently used inside LanguageFilter search method, to reset the keyboard navigation
+ */
+ resetNavigationIndex: function () {
+ this.navigationIndex = null;
+ },
+
/**
* Adds language to the language list.
*
@@ -419,6 +525,18 @@
listen: function () {
var lcd = this;
+ this.$element.on( 'mouseenter', 'li[data-code]', function () {
+ var $listItems = lcd.getLanguageOptionListItems();
+ // Remove the previous option, and then highlight the current one.
+ $listItems.removeClass( 'uls-language-option--highlighted' );
+ var $self = $( this );
+ $self.addClass( 'uls-language-option--highlighted' );
+ lcd.navigationIndex = $listItems.index( $self );
+ } ).on( 'mouseleave', 'li[data-code]', function () {
+ $( this ).removeClass( 'uls-language-option--highlighted' );
+ lcd.navigationIndex = null;
+ } );
+
if ( this.options.clickhandler ) {
this.$element.on( 'click', '.row li', function ( event ) {
lcd.options.clickhandler.call( this, $( this ).data( 'code' ), event );