Jazzing up Search & Filter dropdowns

The Search & Filter Pro plugin has some neat functionality to allow users to refine their searches in just about any way you might wish. Not only that, but it’s pretty slick in delivering updated results via ajax with very little configuration. So it’s been my go-to choice whenever a site needs anything more than the most basic search function.

It’s not too hot on the look though.

Here’s the standard implementation of a combobox multi-select dropdown (using the select2 option offered by the plugin).

I don’t like how the selected items are displayed and managed. Instead, I’d rather have checkboxes included within the dropdown and the selected items displayed separately. This is the end result of my tinkering (which you can also see on the site where I implemented it):

So, a few things I needed to do along the way:

  1. Hide the ugly selected items and add checkboxes in the dropdown.
  2. Add in translatable placeholders.
  3. Display selected items separately.
  4. Make sure everything is keyboard usable for accessibility.

Let’s get into it.

Styling the dropdowns

Here’s the full SASS with comments to explain the various bits. The handling of “placeholders” needs a little explanation – more on that in the next section.

/*==================================================================================*/
/* General layout styling */

.searchandfilter {

    > ul {
        margin: 0 0 1rem;
        padding: 0;

        > li {
            display: inline-block;
        }
    }

    /**
     * The three dropdowns I'm using are for taxonomies location, theme and partner. You'll have
     * to adjust for your use case...
     */
    .sf-field-taxonomy {
        &-location,
        &-theme,
        &-partner {
            width: 100%;
        }

        @include media-breakpoint-up(sm) {
            &-location {
                width: 49%;
                margin-right: 2%;
            }
            &-theme {
                width: 49%;
            }
            &-partner {
                width: 100%;
            }
        }

        @include media-breakpoint-up(lg) {
            &-location,
            &-theme {
                width: 23%;
                margin-right: 2%;
            }

            &-partner {
                width: 50%;
            }
        }
    }
}

/*==================================================================================*/
/* Specific styling */

.select2-container--default {
    width: 100% !important;
    max-width: 100%;

    &.select2-container--focus {
        .select2-selection--multiple {
            border-color: $primary-purple-blue;
            outline: 1px dotted $primary-purple-blue;
            outline-offset: -3px;
        }
    }

    /**
     * The Search & Filter plugin allows us to set the dropdown field placeholder that you can see in the
     * unstyled version (e.g. "All Countries"), but the plugin removes this using JS once an item is selected,
     * so this method is no good to us. So we hide the placeholder entirely and here and use inline CSS instead.
     */
    .select2-search__field {
        &::placeholder {
            color: transparent;
        }
    }

    .select2-selection--multiple {
        position: relative; // As reference point for absolute position dropdown arrow
        border-color: $primary-purple-blue;
        border-radius: 0;
        padding: 0 0.5em;

        // Here's where we hide the ugly selected items - by moving them off screen.
        .select2-selection {
            &__choice {
                position: absolute;
                top: -9999px;
            }
        }

        .select2-search--inline:not(:focus-within) {
            // Display dropdown arrow
            &::after {
                content: '';
                position: absolute;
                top: 50%;
                transform: translateY(-50%);
                right: 0.5em;
                width: 2em;
                height: 2em;
                background-size: 1.5em;
                background-position: center;
                background-repeat: no-repeat;
                background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 25 16' style='enable-background:new 0 0 25 16;' xml:space='preserve'%3E%3Cstyle type='text/css'%3E .st0%7Bfill:%2300B8C5;%7D%0A%3C/style%3E%3Cg%3E%3Cg%3E%3Cpath class='st0' d='M12.5,13.5c-0.4,0-0.8-0.1-1-0.4l-9-8.3c-0.6-0.5-0.6-1.4,0-1.9c0.6-0.5,1.5-0.5,2.1,0l8,7.3l8-7.3 c0.6-0.5,1.5-0.5,2.1,0c0.6,0.5,0.6,1.4,0,1.9l-9,8.3C13.3,13.4,12.9,13.5,12.5,13.5z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
            }

            // Style "placeholder" created by inline CSS.
            &::before {
                font-weight: 500;
            }
        }
    }

    .select2-results {
        &__option {
            position: relative;
            padding-left: 2.5rem; // Make space for checkbox.
            color: $color__text-main;

            // Add empty checkbox.
            &::before {
                content: '';
                position: absolute;
                top: 50%;
                transform: translateY(-50%);
                left: 0.5rem;
                width: 1.5rem;
                height: 1.5rem;
                display: inline-block;
                margin-right: 1rem;
                border: 1px solid $color__text-main;
            }

            &[aria-selected='true'] {
                background: none; // Don't highlight selected rows.
                color: $color__text-main;

                // Add a tick to checkbox when selected.
                &::before {
                    color: inherit;
                    font-family: 'Font Awesome 5 Free';
                    font-weight: 900;
                    content: '\f00c';
                    padding-left: 0.2em;
                    display: inline-block;
                    font-style: normal;
                    font-variant: normal;
                    text-rendering: auto;
                    -webkit-font-smoothing: antialiased;
                }
            }

            // Change hover colour.
            &--highlighted[aria-selected] {
                background-color: $secondary-grey;
                color: $color__text-main;
            }
        }

        // Don't have a checkbox before the results message.
        &__message {
            padding-left: 6px;
            &::before {
                display: none;
            }
        }
    }
}

Translatable placeholders

In the unstyled version (as shown in the first video above), you can see placeholders “All Countries”, All Themes”, etc. These are set using the S&F plugin interface. However (again shown in the first video), these disappear when an option is selected from the dropdown because that space is used to display the selected items.

In our new version, though, we need the placeholder to stay there – otherwise we just get left with an empty box whenever an item has been selected. And since S&F actually removes the placeholder using JS on item selection, I decided the easiest way to implement a placeholder was to hide the original S&F version entirely and create a new one using inline CSS. Why inline CSS rather than just adding it to the stylesheet? Because that way we can make the placeholders translatable.

Here’s the code to do that, which you could put in your functions.php file.

<?php
/**
 * Only add the css if we're on the right page, in my case the Projects archive -
 * you can adjust as needed for your situation...
 */
if ( is_post_type_archive( 'project' ) ) {
    
    /**
     * Set the CSS. Note that I'm getting the taxonomy labels for my use case. You'll
     * need to amend as required...
     */
    $css = '
.sf-field-taxonomy-location .select2-search--inline:not(:focus-within)::before {
    content: "' . get_taxonomy_labels( get_taxonomy( 'location' ) )->name . '";
}
.sf-field-taxonomy-theme .select2-search--inline:not(:focus-within)::before {
    content: "' . get_taxonomy_labels( get_taxonomy( 'theme' ) )->name . '";
}
.sf-field-taxonomy-partner .select2-search--inline:not(:focus-within)::before {
    content: "' . get_taxonomy_labels( get_taxonomy( 'partner' ) )->name . '";
}
';
    /**
     * Add the style. You'll need to change the '{my-theme}' handle to whatever handle
     * is used for your theme stylesheet.
     */
    wp_add_inline_style( '{mytheme}', $css );
}

Display selected items separately

The trick here is to access the Search and Filter query data, and that’s covered in the plugin documentation here. Then it’s just a question of including some code in the template in the right place. Here’s what I ended up using for my case. Obviously you’ll need to adapt significantly for your situation so I’m not going to pretend you’ll just be able to copy and paste this – rather it’s just to give a sense of how to go about it.

<?php
// Display selections made.
global $searchandfilter;
$sf_current_query = $searchandfilter->get(19)->current_query(); // 19 is the form ID.
$selection_data = $sf_current_query->get_array();
$selection_html = '';
if ( $selection_data ) {
    foreach ( $selection_data as $field ) {;
        $selected_values = array();
        foreach ( $field['active_terms'] as $selected_value ) {
            $name = $selected_value['name'];
            $selected_values[] = $name;
        }
        $selection_html .= '<div class = "sf-field-selection"><span class="sf-field--label">' . $field['name'] . ':&nbsp;</span><span class="sf-field--values">' . implode( '<span class="sf-field--separator"> | </span>', $selected_values ) . '</span></div>';
    }
}

if ( $selection_html ) : ?>
    <div class="sf-field-selections">
        <?php echo $selection_html; ?>
    </div>
<?php endif;

Accessibility

Everything looks and works great now – except for keyboard usage. Tabbing into the dropdown works fine, as does scrolling through the options using the arrow keys. Hitting Enter to select an option works too – but hitting Enter to de-select an option does nothing. Fixing this turned out to be a bit of a headache. S&F offers two options for implementing the combobox dropdown we’re using here – Select2 or Chosen – and I think either one makes all this a bit tricky, but I went with Select2, so I can’t know whether the Chosen option makes it easier or harder.

The Select2 package uses lots of whizzy JS to create the dropdowns, and the actual form inputs aren’t seen at all by the user. Long story short, it took me ages to figure out a way to get this working and I’m not going to walk you through all the whys and wherefores of how I ended up here. It works, and in the end, it’s not complex…

'use strict';
(function ($) {

    /**
     * On a filter close (which happens when Enter is pressed),
     * store the current highlighted item if it
     * is set and therefore available for deletion.
     */
    let selectedForDeletion = '';
    $(document).on('select2:closing', '.searchandfilter', function(evt) {
        const currentHighlighted = document.querySelector('.select2-results__option--highlighted');
        if ( currentHighlighted && 'true' == currentHighlighted.getAttribute('aria-selected') ) {
            selectedForDeletion = currentHighlighted.textContent;
        } else {
            selectedForDeletion = '';
        }
    });

    /**
     * If Enter is pressed when a filter item is selected,
     * check if that item is available for deletion
     * and if so, click on the hidden "Remove" option.
     */
    $(document).on('keyup', '.searchandfilter', function(evt) {
        if (evt.target.classList.contains('select2-search__field')) {
            if ( evt.originalEvent && 'Enter' == evt.originalEvent.code ) {
                if (selectedForDeletion) {
                    const itemForDeletion = evt.target.closest('.select2-selection__rendered').querySelector('li[title="' + selectedForDeletion + '"]');
                    if (itemForDeletion) {
                        itemForDeletion.firstChild.click();
                    }
                }
            }
        }
    });

})(jQuery);

And that’s it. One way, at least, to jazz up the extremely capable but decidedly un-pretty Search and Filter comboboxes…

Leave a Reply

Your email address will not be published. Required fields are marked *