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.

glls
  • 20,137
  • 19
  • 46
  • 82

1 Answers1

1

I changed the html a bit to work with the Combobox blueprint from SLDS. And I query the list of Account records from Apex to imitate querying by user's input.

A small breakdown:

  • the trick is to detect what keys were pressed and make certain elements be focused or blurred based on the arrow's keys
  • also, the default event is prevented when the suggestions container is visible to prevent the page from scrolling while arrow keys are pressed

Here's the demo and updated code:

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>
&lt;div&gt;Previous key: {!v.whatKeyWasPressed}&lt;/div&gt;
&lt;div&gt;Current key: {!v.whatKeyIsPressed}&lt;/div&gt;

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

</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(&quot;c.findAccounts&quot;);
findAccounts.setParams({ searchInput: inp });
findAccounts.setCallback(this, function (response) {
  if (response.getState() === &quot;SUCCESS&quot;) {
    var suggestions = response.getReturnValue();
    cmp.set(&quot;v.activeQuerySuggestions&quot;, suggestions);
    cmp.set(&quot;v.activeQuerySuggestionsLength&quot;, suggestions.length - 1);
  } else {
    cmp.set(&quot;v.error&quot;, 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) &amp;&amp; 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 &quot;ArrowDown&quot;:
    this.selectNextOption(cmp, selectedOptionIndex, displayedOptions);
    break;
  case &quot;ArrowUp&quot;:
    this.selectPreviousOption(cmp, selectedOptionIndex, displayedOptions);
    break;
  case &quot;Escape&quot;:
    this.hideSuggestions(cmp);
    break;
  case &quot;Enter&quot;:
    var suggestion = cmp.get(&quot;v.activeQuerySuggestions&quot;)[
      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(&quot;query-box&quot;).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(); } });

Anton Kutishevsky
  • 918
  • 1
  • 5
  • 17