Introducing Autocomplete Search to shiny.fluent's Dropdown Component

Estimated time:
time
min

The introduction of <strong><a href="https://appsilon.com/shiny-fluent-intro/" target="_blank" rel="noopener"><code>shiny.react</code></a></strong> has been a significant milestone in R Shiny's evolution,<strong> acting as a bridge to React.js</strong> and <strong>enabling the incorporation of React components</strong> within Shiny applications. <a href="https://appsilon.github.io/shiny.fluent/" target="_blank" rel="noopener"><code>shiny.fluent</code></a>, leveraging <code>shiny.react</code>, integrates <a href="https://appsilon.com/fluent-and-shiny-build-beautiful-shiny-apps-using-microsofts-fluent-ui/" target="_blank" rel="noopener">Microsoft's Fluent UI</a> components into Shiny, broadening the scope for UI design and functionality.

In a recent one-hour hackathon, we set out to explore the potential of <code>shiny.react</code> and <code>shiny.fluent</code> in delivering a customized user interface of a Shiny application. Our primary focus was integrating an autocomplete search feature into the <code>Dropdown</code> element of <code>shiny.fluent</code>.

In this article, we will explore the outcome of our hackathon and how you can implement it in your projects.
<h3>Table of Contents</h3><ul><li><strong><a href="#the-challenge">The Challenge</a></strong></li><li><strong><a href="#seamless-integration">Leveraging <code>shiny.react</code> for Seamless Integration</a></strong></li><li><a href="#conclusion"><strong>Conclusion</strong></a></li></ul>

<hr />

<h2 id="the-challenge">The Challenge</h2>
The <code>Dropdown</code> component, a part of <a href="https://developer.microsoft.com/en-us/fluentui" target="_blank" rel="noopener noreferrer">Fluent UI</a> exposed by the <code>shiny.fluent</code> package, stands out for its functionality and reliability. Yet, when users encounter long lists of options, navigating through them can be less than optimal.

To enhance this experience, we aimed to integrate an autocomplete search box within the <code>Dropdown</code>.This feature is designed to <strong>streamline the process, allowing users to quickly search and filter through the extensive list of options</strong>.

It's noteworthy that <code>shiny.fluent</code> also exposes Fluent UI's <a href="https://learn.microsoft.com/en-us/fluent-ui/web-components/components/combobox?pivots=typescript" target="_blank" rel="noopener noreferrer">ComboBox</a>, a component with built-in autocomplete functionality. However, our focus was different; we wanted to specifically enhance the <code>Dropdown</code> component by adding a <strong>search box that not only allows for searching but also filters the available options in real-time</strong>.
<blockquote>Interested in enhancing the user experience of your Shiny apps while optimizing handling of large datasets? <a href="https://appsilon.com/reactable-extras-enhancing-shiny-applications/" target="_blank" rel="noopener">Explore how our latest reactable.extras release can help you</a>.</blockquote>
<h2 id="seamless-integration">Leveraging <code>shiny.react</code> for Seamless Integration</h2>
Our breakthrough came when we discovered a React code snippet that perfectly suited our needs. This code provided a foundation for adding an autocomplete search functionality to the dropdown menu. The key task was adapting this React code to work seamlessly within our Shiny application.

Here's how we started by creating a basic Dropdown with a variety of fruit and vegetable options:
<pre><code class="language-r">
box::use(
shiny[div, moduleServer, NS, observeEvent],
shiny.fluent[JS, fluentPage, Dropdown.shinyInput],
)
<br>#' @export
ui &lt;- function(id) {
ns &lt;- NS(id)
fluentPage(
 div(
   style = "height: 100%; width: 50%; margin:auto",
   Dropdown.shinyInput(
     inputId = ns("fruit"),
     label = "Searchable Fruit Selector",
     multiSelect = TRUE,
     placeholder = "Fruit/Vegetable",
     options = list(
       list(key = "apple", text = "Apple"),
       list(key = "banana", text = "Banana"),
       list(key = "orange", text = "Orange"),
       list(key = "grape", text = "Grape"),
       list(key = "broccoli", text = "Broccoli"),
       list(key = "carrot", text = "Carrot"),
       list(key = "lettuce", text = "Lettuce")
      )
    )
  )
)
}
<br>#' @export
server &lt;- function(id) {
moduleServer(id, function(input, output, session) {
 observeEvent(input$fruit, {
   print(input$fruit)
 })
})
}
</code></pre>
<img class="size-full wp-image-22340" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b00c6b246137d111455a45_searchable-fruit-selector.webp" alt="searchable fruit selector" width="1334" height="467" /> Searchable Fruit Selector

<strong>We then added a filtering header using the SearchBox in the options list:</strong>
<pre><code class="language-r">
DropdownMenuItemType &lt;- function(type) { # nolint
JS(paste0("jsmodule['@fluentui/react'].DropdownMenuItemType."), type)
}
// Add __FilterHeader__ to Dropdown’s options
ui &lt;- function(id) {
ns &lt;- NS(id)
fluentPage(
  div(
    style = "height: 100%; width: 50%; margin:auto",
    Dropdown.shinyInput(
      ...,
      options = list(
        list(key = "__FilterHeader__", text = "-", itemType = DropdownMenuItemType("Header")),
       ...
       )
     )
   )
 )
}
</code></pre>
Finally, thanks to <code>shiny.fluent</code> exposing Fluent UI’s properties, we could customize our <code>Dropdown</code> with the <code>onRenderOption</code> prop.

<strong>This allowed us to tailor a custom renderer for the dropdown options:</strong>
<pre><code class="language-r">
render_search_box &lt;- JS(paste("(option) =&gt; {
 // if option is not the header, simply return option label
 if (option.key !== '__FilterHeader__') {
   return option.text;
 }
<br>  // handle onChange event
 const onChange = (event, newValue) =&gt; {
   // get the typed key in lowercase
   const query = newValue.toLocaleLowerCase();
<br>    // get the checkbox labels
   const checkboxLabels = document.querySelectorAll(
     'div.ms-Checkbox .ms-Checkbox-label'
   );
<br>    // filter options according to search
   checkboxLabels.forEach(label =&gt; {
     const text = label.innerText.replace('\\n', '').replace('', '').toLocaleLowerCase();
<br>      // if the label of the option does not start with the typed search, we hide it
     if (query === '' || text.startsWith(query)) {
       label.parentElement.style.display = 'flex';
     } else {
       label.parentElement.style.display = 'none';
     }
   });
 };
<br>  // finally, create the react element
 const props = { placeholder: 'Start typing', underlined: true, onChange };
 const element = React.createElement(jsmodule['@fluentui/react'].SearchBox, props)
 return element;
}"))
<br>// Add onRenderOption prop
ui &lt;- function(id) {
ns &lt;- NS(id)
fluentPage(
  div(
    style = "height: 100%; width: 50%; margin:auto",
    Dropdown.shinyInput(
      ...,
      onRenderOption = render_search_box
     )
   )
 )
}
</code></pre>
<img class="aligncenter size-full wp-image-22342" src="https://webflow-prod-assets.s3.amazonaws.com/6525256482c9e9a06c7a9d3c%2F65b00c6d6df929db0a7fc478_searchable_dropdown.gif" alt="" width="600" height="362" />

If you're keen on exploring the project's implementation in detail, you can find the complete code available in the <a href="https://gist.github.com/ttnsy/00ccb5978ec8dfe665440c5158cd628c" target="_blank" rel="noopener">Gist</a> below:

<h2 id="conclusion">Conclusion</h2>
As our one-hour hackathon concludes, we've successfully integrated an autocomplete search feature into the <code>shiny.fluent</code> <code>Dropdown</code> component, demonstrating a practical application of <code>shiny.react</code> and <code>shiny.fluent</code> in enhancing Shiny UIs.

Looking ahead, we see opportunities to refine this feature, especially in terms of accessibility and user experience. For instance, improving keyboard interactions, such as enabling more intuitive navigation and selection within the dropdown, could be a valuable enhancement.
<blockquote>Did you find this helpful? Leave us a comment and join us for <a href="https://www.shinyconf.com/shiny-gatherings" target="_blank" rel="noopener">R Shiny Trivia Night</a> at our next Shiny Gathering! Test your knowledge, meet fellow enthusiasts, and enjoy a night of fun and networking. <a href="https://share.hsforms.com/1BGdQEoCORSKeJ53DQyUrLw2rk4g" target="_blank" rel="noopener">Secure your spot for this exciting trivia event today</a>!</blockquote>

Contact us!
Damian's Avatar
Damian Rodziewicz
Head of Sales
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
UI/UX
r
shiny.react
shiny.fluent
user interface
tutorials