6

I have a lightning:input field which I use as a searchbox, while typing, I get query suggestions from x endpoint and populate them in an slds-dropdown component (mostly HTML using SLDS).

This works great when I want to select a querysuggestion using a click event, however, when using keys to make a selection, this can become a bit of a pain. enter image description here Is there a component that could be leveraged to avoid doing so that would allow me to input a query simultaneously?

current code for the input + dropdown:

 <!-- Component Markup -->
   <aura:attribute name='searchAsYouType' type='Boolean' default='false' access='global' />
   <aura:attribute name='searchInut' type='String' default='' access='global' />
   <aura:attribute name="activeQuerySuggestions" type="List" default="[]"/>
<div class="search-container" onkeyup="{!c.handlekeyPress}">

    <!-- LIGHTNING:INPUT  -->

    <lightning:input aura:id="query-box" id="query-box" type="search" name="search" placeholder="additional search terms" onchange="{!c.query}" value="{!v.searchInut}"/>


    <!-- DROPDOWN --> 

    <div id="listbox-suggestions" aura:id="query-suggest-box" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid slds-hide" role="listbox">
        <ul aura:id="query-suggest-list" class="slds-listbox slds-listbox_vertical" role="presentation" >
            <aura:iteration items="{!v.activeQuerySuggestions}" var="sugg" indexVar="index">
                <li aura:id="query-suggest-item" role="presentation" class="slds-listbox__item" >
                    <div aura:id="query-suggestions" id="{!sugg}" class="slds-media slds-listbox__option slds-listbox__option_plain slds-media_small" role="option" 
                         value="{!sugg}" onclick="{!c.handlesuggestionClick}">
                        {!sugg}
                    </div>
                </li>
            </aura:iteration>
        </ul>
    </div>

</div>   

controller.js:

    query  : function(cmp, event, helper) {

    const typedQuery = event.getSource().get('v.value');            
    try{
        helper.autoComplete(cmp, event, typedQuery);
        helper.executeQuery(cmp, event, typedQuery);
    }
    catch(e){
        console.log(e.message);
    }
},
handlekeyPress  : function(cmp, event, helper) {
  /* add custom logic for key press event */
},
handlesuggestionClick  : function(cmp, event, helper) {
    const qrySuggBox = cmp.find('query-suggest-box');
    const qryBox = cmp.find('query-box');
    $A.util.addClass(qrySuggBox, 'slds-hide');

    qryBox.set('v.value', event.target.id);
    helper.onSuggestionClick(cmp, event, event.target.id);

},

helper.js

({

querySuggest : function(cmp, event, inp) {
    /*query endpoint for suggestion */
},

autoComplete : function(cmp, event, inp){
    let activeSuggestionArray = [];
    const querySuggestBox = cmp.find("query-suggest-box");
    this.querySuggest(cmp, event, inp).then(function(suggestions){
        const theRes = JSON.parse(suggestions);
        const allRes = theRes.completions;
        allRes.forEach(function(element) {
            activeSuggestionArray.push(element.expression);
        });
        //can add sorting as per relevance indicator .sort()
        //also, can change collection type to receive a map in order
        //to keep leverage other attributes for sorting/displaying

        cmp.set('v.activeQuerySuggestions', activeSuggestionArray);

        /*---class validation to hide/display suggestion box---*/
        if(cmp.get('v.activeQuerySuggestions').length > 0 && 
$A.util.hasClass(querySuggestBox, "slds-hide")){
                $A.util.removeClass(querySuggestBox, 'slds-hide');
            }
        else if(cmp.get('v.activeQuerySuggestions').length == 0){
            $A.util.addClass(querySuggestBox, 'slds-hide');
        }
    });

},

onSuggestionClick : function(cmp, event, selection){
    this.executeQuery(cmp, event, selection)
},
/* -- community search -- */
executeQuery : function(cmp, event, theQuery){
    const urlEvent = $A.get("e.force:navigateToURL");
    urlEvent.setParams({
        "url": "/global-search/"+theQuery
    });
    urlEvent.fire();  
},

})

similar to Lookup Field Dual Keyboard Focus (Answered with working Autocomplete lookup component and JS example for VF/SLDS) but in lightning and trying to leverage lightning:components when possible.

1
  • Did you ever get this fixed? I'm looking for an implementation.
    – Brian
    Commented Nov 27, 2019 at 15:43

1 Answer 1

1

I made a few changes to the HTML to align with the Combobox blueprint from SLDS. Additionally, I'm using Apex to query Account records, simulating a search based on user input.

Here's a quick overview of the changes:

  • The key is to detect pressed keys and focus or blur specific elements based on the arrow keys.
  • I've also prevented the default event when the suggestions container is visible to avoid page scrolling while using the arrow keys.

You can see a demo and the updated code here:

Demo

Demo

CMP

<aura: component implements="forceCommunity:searchInterface" controller="InputWithDropdownController">
    <!-- Component Markup -->
    <aura:attribute name="searchAsYouType" type="Boolean" default="false" access="global" />
    <aura:attribute name="searchInut" type="String" default="" access="global" />
    <aura:attribute name="activeQuerySuggestions" type="List" />
    <aura:attribute name="activeQuerySuggestionsLength" type="Integer" />
    <aura:attribute name="selectedOptionIndex" type="Integer" />
    <aura:attribute name="whatKeyWasPressed" type="String" />
    <aura:attribute name="whatKeyIsPressed" type="String" />
    <aura:attribute name="error" type="String" />
    <aura:attribute name="allowedKeys" type="List" default="['ArrowDown', 'ArrowUp', 'Escape', 'Enter']" />
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    <aura:if isTrue="{!v.error}">
        <div class="slds-text-color_error"> {!v.error}</div>
    </aura:if>

    <div>Previous key: {!v.whatKeyWasPressed}</div>
    <div>Current key: {!v.whatKeyIsPressed}</div>

    <div class="slds-form-element search-container" onkeyup="{!c.handlekeyPress}">
        <div class="slds-form-element__control">
            <div class="slds-combobox_container">
                <div class="slds-combobox slds-dropdown-trigger slds-dropdown-trigger_click slds-is-open">
                    <div class="slds-combobox__form-element slds-input-has-icon slds-input-has-icon_right" role="none">
                        <input aura:id="query-box" type="text" class="slds-input slds-combobox__input "
                            id="combobox-id-16" aria-activedescendant="option1" aria-autocomplete="list"
                            aria-controls="listbox-id-12" aria-expanded="true" aria-haspopup="listbox"
                            autoComplete="off" role="combobox" placeholder="Search..." oninput="{!c.query}" />
                        <span
                            class="slds-icon_container slds-icon-utility-search slds-input__icon slds-input__icon_right">
                            <lightning:icon iconName="utility:search" size="x-small"
                                class="slds-icon slds-icon slds-icon_x-small slds-icon-text-default">
                            </lightning:icon>
                        </span>
                    </div>
                    <aura:if isTrue="{!v.activeQuerySuggestions.length > 0}">
                        <div id="listbox-suggestions" class="slds-dropdown slds-dropdown_length-5 slds-dropdown_fluid"
                            role="listbox">
                            <ul class="slds-listbox slds-listbox_vertical" role="presentation">
                                <aura:iteration items="{!v.activeQuerySuggestions}" var="sugg" indexVar="index">
                                    <li role="presentation" class="slds-listbox__item"
                                        onclick="{!c.handlesuggestionClick}" title="{!sugg}">
                                        <div class="suggested-option slds-media slds-listbox__option slds-listbox__option_plain slds-media_small"
                                            role="option">
                                            <span class="slds-media__figure slds-listbox__option-icon"></span>
                                            <span class="slds-media__body">
                                                <span class="slds-truncate" title="{!sugg}">
                                                    {!sugg}
                                                </span>
                                            </span>
                                        </div>
                                    </li>
                                </aura:iteration>
                            </ul>
                        </div>
                    </aura:if>
                </div>
            </div>
        </div>
    </div>
</aura: component>

Javascript Controller

({
  doInit: function (cmp, event, helper) {
    try {
      helper.preventScrolling();
    } catch (error) {
      cmp.set("v.error", error);
    }
  },

  query: function (cmp, event, helper) {
    var typedQuery = event.target.value;
    try {
      helper.querySuggest(cmp, event, typedQuery);
    } catch (e) {
      cmp.set("v.error", e);
    }
  },

  handlekeyPress: function (cmp, event, helper) {
    event.preventDefault();
    try {
      if (
        helper.isSuggestionsContainerVisible() &&
        helper.isValidKey(cmp, event.key)
      ) {
        helper.capturePressedKeys(cmp, event.key);
        helper.performKeyPressAction(cmp, event.key);
      }
    } catch (error) {
      cmp.set("v.error", error);
    }
  },

  handlesuggestionClick: function (cmp, event, helper) {
    try {
      var suggestion = helper.getClickedSuggestion(event);
      if (suggestion) {
        helper.setSelectedSuggestion(cmp, suggestion);
        helper.onSuggestionClick(cmp, event, suggestion);
      }
    } catch (error) {
      cmp.set("v.error", error);
    }
  }
});

Javascript Helper

({
  querySuggest: function (cmp, event, inp) {
    if (inp.length < 2) return;

    var findAccounts = cmp.get("c.findAccounts");
    findAccounts.setParams({ searchInput: inp });
    findAccounts.setCallback(this, function (response) {
      if (response.getState() === "SUCCESS") {
        var suggestions = response.getReturnValue();
        cmp.set("v.activeQuerySuggestions", suggestions);
        cmp.set("v.activeQuerySuggestionsLength", suggestions.length - 1);
      } else {
        cmp.set("v.error", response.getError());
      }
    });
    $A.enqueueAction(findAccounts);
  },

  preventScrolling: function () {
    var isSuggestionsContainerVisible = this.isSuggestionsContainerVisible();
    window.addEventListener("keydown", function (e) {
      var arrowKeys = ["ArrowLeft", "ArrowUp", "ArrowRight", "ArrowDown"];

      if (arrowKeys.includes(e.key) && isSuggestionsContainerVisible) {
        e.preventDefault();
      }
    });
  },

  isValidKey: function (cmp, key) {
    return cmp.get("v.allowedKeys").includes(key);
  },

  isSuggestionsContainerVisible: function () {
    var suggestions = document.getElementById("listbox-suggestions");
    return suggestions !== undefined || suggestions !== null;
  },

  performKeyPressAction: function (cmp, keyPressed) {
    var selectedOptionIndex = cmp.get("v.selectedOptionIndex");
    var displayedOptions = document.getElementsByClassName("suggested-option");

    switch (keyPressed) {
      case "ArrowDown":
        this.selectNextOption(cmp, selectedOptionIndex, displayedOptions);
        break;
      case "ArrowUp":
        this.selectPreviousOption(cmp, selectedOptionIndex, displayedOptions);
        break;
      case "Escape":
        this.hideSuggestions(cmp);
        break;
      case "Enter":
        var suggestion = cmp.get("v.activeQuerySuggestions")[
          selectedOptionIndex
        ];
        this.setSelectedSuggestion(cmp, suggestion);
        this.executeQuery(cmp, null, suggestion);
        break;
      default:
        break;
    }
  },

  selectNextOption: function (cmp, selectedOptionIndex, displayedOptions) {
    var optionsLength = cmp.get("v.activeQuerySuggestionsLength");
    if (selectedOptionIndex < optionsLength) {
      if (selectedOptionIndex + 1 > optionsLength) {
        this.selectOption(cmp, displayedOptions[0], 0);
      } else {
        this.selectOption(
          cmp,
          displayedOptions[selectedOptionIndex + 1],
          selectedOptionIndex + 1
        );
      }
    } else if (!selectedOptionIndex || selectedOptionIndex === optionsLength) {
      this.selectOption(cmp, displayedOptions[0], 0);
    }
  },

  selectPreviousOption: function (cmp, selectedOptionIndex, displayedOptions) {
    var previousOptionIndex = selectedOptionIndex - 1;
    if (previousOptionIndex >= 0) {
      this.selectOption(
        cmp,
        displayedOptions[previousOptionIndex],
        previousOptionIndex
      );
    }
  },

  selectOption: function (cmp, option, optionIndex) {
    cmp.set("v.selectedOptionIndex", optionIndex);
    option.setAttribute("tabindex", "0");
    option.focus();
  },

  hideSuggestions: function (cmp) {
    var input = cmp.find("query-box").getElement();
    input.blur();
    cmp.set("v.activeQuerySuggestions", []);
  },

  capturePressedKeys: function (cmp, keyPressed) {
    if (cmp.get("v.whatKeyIsPressed")) {
      cmp.set("v.whatKeyWasPressed", cmp.get("v.whatKeyIsPressed"));
    }
    cmp.set("v.whatKeyIsPressed", keyPressed);
  },

  getClickedSuggestion: function (event) {
    var clickedElement = event.target;
    return clickedElement.classList.contains("slds-truncate")
      ? clickedElement.title
      : function () {
          return clickedElement.querySelector('span[class="slds-truncate"]')
            .title;
        };
  },

  setSelectedSuggestion: function (cmp, suggestion) {
    cmp.set("v.activeQuerySuggestions", null);

    var input = cmp.find("query-box").getElement();
    input.value = suggestion;
  },

  onSuggestionClick: function (cmp, event, selection) {
    this.executeQuery(cmp, event, selection);
  },

  /* -- community search -- */
  executeQuery: function (cmp, event, theQuery) {
    var urlEvent = $A.get("e.force:navigateToURL");
    urlEvent.setParams({
      url: "/global-search/" + theQuery
    });
    urlEvent.fire();
  }
});

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .