Microsoft/fast-dna

View on GitHub
packages/web-components/fast-foundation/src/picker/picker.ts

Summary

Maintainability
F
5 days
Test Coverage
import {
    attr,
    html,
    HTMLView,
    nullableNumberConverter,
    observable,
    oneWay,
    ref,
    RepeatDirective,
    Updates,
    ViewTemplate,
} from "@microsoft/fast-element";
import { ViewBehaviorOrchestrator } from "@microsoft/fast-element/utilities.js";
import {
    keyArrowDown,
    keyArrowLeft,
    keyArrowRight,
    keyArrowUp,
    keyBackspace,
    keyDelete,
    keyEnter,
    keyEscape,
    uniqueId,
} from "@microsoft/fast-web-utilities";
import {
    AnchoredRegionConfig,
    FASTAnchoredRegion,
    FlyoutPosBottom,
    FlyoutPosBottomFill,
    FlyoutPosTallest,
    FlyoutPosTallestFill,
    FlyoutPosTop,
    FlyoutPosTopFill,
} from "../anchored-region/index.js";
import { getRootActiveElement } from "../utilities/index.js";
import { FASTPickerListItem } from "./picker-list-item.js";
import type { FASTPickerList } from "./picker-list.js";
import { FASTPickerMenuOption } from "./picker-menu-option.js";
import type { FASTPickerMenu } from "./picker-menu.js";
import { FormAssociatedPicker } from "./picker.form-associated.js";
import { MenuPlacement } from "./picker.options.js";

const pickerInputTemplate: ViewTemplate = html<FASTPicker>`
    <input
        slot="input-region"
        role="combobox"
        type="text"
        autocapitalize="off"
        autocomplete="off"
        haspopup="list"
        aria-label="${x => x.label}"
        aria-labelledby="${x => x.labelledBy}"
        placeholder="${x => x.placeholder}"
        ${ref("inputElement")}
    />
`;

/**
 * A Picker Custom HTML Element.  This is an early "alpha" version of the component.
 * Developers should expect the api to evolve, breaking changes are possible.
 *
 * @beta
 */
export class FASTPicker extends FormAssociatedPicker {
    /**
     * Currently selected items. Comma delineated string ie. "apples,oranges".
     *
     * @remarks
     * HTML Attribute: selection
     */
    @attr({ attribute: "selection" })
    public selection: string = "";
    protected selectionChanged(): void {
        if (this.$fastController.isConnected) {
            this.handleSelectionChange();
            if (this.proxy instanceof HTMLInputElement) {
                this.proxy.value = this.selection;
                this.validate();
            }
        }
    }

    /**
     * Currently available options. Comma delineated string ie. "apples,oranges".
     *
     * @remarks
     * HTML Attribute: options
     */
    @attr({ attribute: "options" })
    public options: string;
    protected optionsChanged(): void {
        this.optionsList = this.options
            .split(",")
            .map(opt => opt.trim())
            .filter(opt => opt !== "");
    }

    /**
     * Whether the component should remove an option from the list when it is in the selection
     *
     * @remarks
     * HTML Attribute: filter-selected
     */
    @attr({ attribute: "filter-selected", mode: "boolean" })
    public filterSelected: boolean = true;

    /**
     * Whether the component should remove options based on the current query
     *
     * @remarks
     * HTML Attribute: filter-query
     */
    @attr({ attribute: "filter-query", mode: "boolean" })
    public filterQuery: boolean = true;

    /**
     * The maximum number of items that can be selected.
     *
     * @remarks
     * HTML Attribute: max-selected
     */
    @attr({ attribute: "max-selected", converter: nullableNumberConverter })
    public maxSelected: number | null = null;

    /**
     * The text to present to assistive technolgies when no suggestions are available.
     *
     * @remarks
     * HTML Attribute: no-suggestions-text
     */
    @attr({ attribute: "no-suggestions-text" })
    public noSuggestionsText: string = "No suggestions available";

    /**
     *  The text to present to assistive technolgies when suggestions are available.
     *
     * @remarks
     * HTML Attribute: suggestions-available-text
     */
    @attr({ attribute: "suggestions-available-text" })
    public suggestionsAvailableText: string = "Suggestions available";

    /**
     * The text to present to assistive technologies when suggestions are loading.
     *
     * @remarks
     * HTML Attribute: loading-text
     */
    @attr({ attribute: "loading-text" })
    public loadingText: string = "Loading suggestions";

    /**
     * Applied to the aria-label attribute of the input element
     *
     * @remarks
     * HTML Attribute: label
     */
    @attr({ attribute: "label" })
    public label: string;

    /**
     * Applied to the aria-labelledby attribute of the input element
     *
     * @remarks
     * HTML Attribute: labelledby
     */
    @attr({ attribute: "labelledby" })
    public labelledBy: string;

    /**
     * Applied to the placeholder attribute of the input element
     *
     * @remarks
     * HTML Attribute: placholder
     */
    @attr({ attribute: "placeholder" })
    public placeholder: string;

    /**
     * Controls menu placement
     *
     * @remarks
     * HTML Attribute: menu-placement
     */
    @attr({ attribute: "menu-placement" })
    public menuPlacement: MenuPlacement = MenuPlacement.bottomFill;
    protected menuPlacementChanged(): void {
        if (this.$fastController.isConnected) {
            this.updateMenuConfig();
        }
    }

    /**
     * Whether to display a loading state if the menu is opened.
     *
     */
    @observable
    public showLoading: boolean = false;
    protected showLoadingChanged(): void {
        if (this.$fastController.isConnected) {
            Updates.enqueue(() => {
                this.setFocusedOption(0);
            });
        }
    }

    /**
     * Template used to generate selected items.
     * This is used in a repeat directive.
     *
     */
    @observable
    public listItemTemplate: ViewTemplate;
    protected listItemTemplateChanged(): void {
        this.updateListItemTemplate();
    }

    /**
     * Default template to use for selected items (usually specified in the component template).
     * This is used in a repeat directive.
     *
     */
    @observable
    public defaultListItemTemplate?: ViewTemplate;
    protected defaultListItemTemplateChanged(): void {
        this.updateListItemTemplate();
    }

    /**
     * The item template currently in use.
     *
     * @internal
     */
    @observable
    public activeListItemTemplate?: ViewTemplate;

    /**
     * Template to use for available options.
     * This is used in a repeat directive.
     *
     */
    @observable
    public menuOptionTemplate: ViewTemplate;
    protected menuOptionTemplateChanged(): void {
        this.updateOptionTemplate();
    }

    /**
     * Default template to use for available options (usually specified in the template).
     * This is used in a repeat directive.
     *
     */
    @observable
    public defaultMenuOptionTemplate?: ViewTemplate;
    protected defaultMenuOptionTemplateChanged(): void {
        this.updateOptionTemplate();
    }

    /**
     * The option template currently in use.
     *
     * @internal
     */
    @observable
    public activeMenuOptionTemplate?: ViewTemplate;

    /**
     *  Template to use for the contents of a selected list item
     *
     */
    @observable
    public listItemContentsTemplate: ViewTemplate;

    /**
     *  Template to use for the contents of menu options
     *
     */
    @observable
    public menuOptionContentsTemplate: ViewTemplate;

    /**
     *  Current list of options in array form
     *
     */
    @observable
    public optionsList: string[] = [];
    private optionsListChanged(): void {
        this.updateFilteredOptions();
    }

    /**
     * The text value currently in the input field
     *
     */
    @observable
    public query: string;
    protected queryChanged(): void {
        if (this.$fastController.isConnected) {
            if (this.inputElement.value !== this.query) {
                this.inputElement.value = this.query;
            }
            this.updateFilteredOptions();
            this.$emit("querychange", { bubbles: false });
        }
    }

    /**
     *  Current list of filtered options in array form
     *
     * @internal
     */
    @observable
    public filteredOptionsList: string[] = [];
    protected filteredOptionsListChanged(): void {
        if (this.$fastController.isConnected) {
            Updates.enqueue(() => {
                this.showNoOptions =
                    this.menuElement.querySelectorAll('[role="listitem"]').length === 0;
                this.setFocusedOption(this.showNoOptions ? -1 : 0);
            });
        }
    }

    /**
     *  Indicates if the flyout menu is open or not
     *
     * @internal
     */
    @observable
    public flyoutOpen: boolean = false;
    protected flyoutOpenChanged(): void {
        if (this.flyoutOpen) {
            Updates.enqueue(this.setRegionProps);
            this.$emit("menuopening", { bubbles: false });
        } else {
            this.$emit("menuclosing", { bubbles: false });
        }
    }

    /**
     *  The id of the menu element
     *
     * @internal
     */
    @observable
    public menuId: string;

    /**
     *  The tag for the selected list element (ie. "fast-picker-list" vs. "fluent-picker-list")
     *
     * @internal
     */
    @observable
    public selectedListTag: string;

    /**
     * The tag for the menu element (ie. "fast-picker-menu" vs. "fluent-picker-menu")
     *
     * @internal
     */
    @observable
    public menuTag: string;

    /**
     *  Index of currently active menu option
     *
     * @internal
     */
    @observable
    public menuFocusIndex: number = -1;

    /**
     *  Id of currently active menu option.
     *
     * @internal
     */
    @observable
    public menuFocusOptionId: string | undefined;

    /**
     *  Internal flag to indicate no options available display should be shown.
     *
     * @internal
     */
    @observable
    public showNoOptions: boolean = false;
    private showNoOptionsChanged(): void {
        if (this.$fastController.isConnected) {
            Updates.enqueue(() => {
                this.setFocusedOption(0);
            });
        }
    }

    /**
     *  The anchored region config to apply.
     *
     * @internal
     */
    @observable
    public menuConfig: AnchoredRegionConfig;

    /**
     *  Reference to the placeholder element for the repeat directive
     *
     */
    public itemsPlaceholderElement: Node;

    /**
     * reference to the input element
     *
     * @internal
     */
    public inputElement: HTMLInputElement;

    /**
     * reference to the selected list element
     *
     * @internal
     */
    public listElement: FASTPickerList;

    /**
     * reference to the menu element
     *
     * @internal
     */
    public menuElement: FASTPickerMenu;

    /**
     * reference to the anchored region element
     *
     * @internal
     */
    public region: FASTAnchoredRegion;

    /**
     *
     *
     * @internal
     */
    @observable
    public selectedItems: string[] = [];

    private optionsPlaceholder: Node;
    private inputElementView: HTMLView | null = null;
    private behaviorOrchestrator: ViewBehaviorOrchestrator | null = null;

    /**
     * @internal
     */
    public connectedCallback(): void {
        super.connectedCallback();

        if (!this.listElement) {
            this.listElement = document.createElement(
                this.selectedListTag
            ) as FASTPickerList;
            this.appendChild(this.listElement);
            this.itemsPlaceholderElement = document.createComment("");
            this.listElement.appendChild(this.itemsPlaceholderElement);
        }

        this.inputElementView = pickerInputTemplate.render(this, this.listElement);

        const match: string = this.menuTag.toUpperCase();
        this.menuElement = Array.from(this.children).find((element: HTMLElement) => {
            return element.tagName === match;
        }) as FASTPickerMenu;

        if (!this.menuElement) {
            this.menuElement = document.createElement(this.menuTag) as FASTPickerMenu;
            this.appendChild(this.menuElement);

            if (this.menuElement.id === "") {
                this.menuElement.id = uniqueId("listbox-");
            }

            this.menuId = this.menuElement.id;
        }

        if (!this.optionsPlaceholder) {
            this.optionsPlaceholder = document.createComment("");
            this.menuElement.appendChild(this.optionsPlaceholder);
        }

        this.updateMenuConfig();
        Updates.enqueue(() => this.initialize());
    }

    public disconnectedCallback() {
        super.disconnectedCallback();
        this.toggleFlyout(false);
        this.inputElement.removeEventListener("input", this.handleTextInput);
        this.inputElement.removeEventListener("click", this.handleInputClick);

        if (this.inputElementView !== null) {
            this.inputElementView.dispose();
            this.inputElementView = null;
        }
    }

    /**
     * Move focus to the input element
     * @public
     */
    public focus() {
        this.inputElement.focus();
    }

    /**
     * Initialize the component.  This is delayed a frame to ensure children are connected as well.
     */
    private initialize(): void {
        this.updateListItemTemplate();
        this.updateOptionTemplate();

        if (this.behaviorOrchestrator === null) {
            this.behaviorOrchestrator = ViewBehaviorOrchestrator.create(this);
            this.$fastController.addBehavior(this.behaviorOrchestrator);

            this.behaviorOrchestrator.addBehaviorFactory(
                new RepeatDirective(
                    oneWay(x => x.selectedItems),
                    oneWay(x => x.activeListItemTemplate),
                    { positioning: true }
                ),
                this.itemsPlaceholderElement
            );

            this.behaviorOrchestrator.addBehaviorFactory(
                new RepeatDirective(
                    oneWay(x => x.filteredOptionsList),
                    oneWay(x => x.activeMenuOptionTemplate),
                    { positioning: true }
                ),
                this.optionsPlaceholder
            );
        }

        this.inputElement.addEventListener("input", this.handleTextInput);
        this.inputElement.addEventListener("click", this.handleInputClick);
        this.menuElement.suggestionsAvailableText = this.suggestionsAvailableText;
        this.menuElement.addEventListener(
            "optionsupdated",
            this.handleMenuOptionsUpdated
        );

        this.handleSelectionChange();
    }

    /**
     * Toggles the menu flyout
     */
    private toggleFlyout(open: boolean): void {
        if (this.flyoutOpen === open) {
            return;
        }

        if (open && getRootActiveElement(this) === this.inputElement) {
            this.flyoutOpen = open;
            Updates.enqueue(() => {
                if (this.menuElement !== undefined) {
                    this.setFocusedOption(0);
                } else {
                    this.disableMenu();
                }
            });
            return;
        }

        this.flyoutOpen = false;
        this.disableMenu();
        return;
    }

    /**
     * Handle input event from input element
     */
    private handleTextInput = (e: InputEvent): void => {
        this.query = this.inputElement.value;
    };

    /**
     * Handle click event from input element
     */
    private handleInputClick = (e: MouseEvent): void => {
        e.preventDefault();
        this.toggleFlyout(true);
    };

    /**
     * Handle the menu options updated event from the child menu
     */
    private handleMenuOptionsUpdated(e: Event): void {
        e.preventDefault();
        if (this.flyoutOpen) {
            this.setFocusedOption(0);
        }
    }

    /**
     * Handle key down events.
     */
    public handleKeyDown(e: KeyboardEvent): boolean {
        if (e.defaultPrevented) {
            return false;
        }
        const activeElement = getRootActiveElement(this);
        switch (e.key) {
            // TODO: what should "home" and "end" keys do, exactly?
            //
            // case keyHome: {
            //     if (!this.flyoutOpen) {
            //         this.toggleFlyout(true);
            //     } else {
            //         if (this.menuElement.optionElements.length > 0) {
            //             this.setFocusedOption(0);
            //         }
            //     }
            //     return false;
            // }

            // case keyEnd: {
            //     if (!this.flyoutOpen) {
            //         this.toggleFlyout(true);
            //     } else {
            //         if (this.menuElement.optionElements.length > 0) {
            //             this.toggleFlyout(true);
            //             this.setFocusedOption(this.menuElement.optionElements.length - 1);
            //         }
            //     }
            //     return false;
            // }

            case keyArrowDown: {
                if (!this.flyoutOpen) {
                    this.toggleFlyout(true);
                } else {
                    const nextFocusOptionIndex = this.flyoutOpen
                        ? Math.min(
                              this.menuFocusIndex + 1,
                              this.menuElement.optionElements.length - 1
                          )
                        : 0;
                    this.setFocusedOption(nextFocusOptionIndex);
                }
                return false;
            }

            case keyArrowUp: {
                if (!this.flyoutOpen) {
                    this.toggleFlyout(true);
                } else {
                    const previousFocusOptionIndex = this.flyoutOpen
                        ? Math.max(this.menuFocusIndex - 1, 0)
                        : 0;
                    this.setFocusedOption(previousFocusOptionIndex);
                }
                return false;
            }

            case keyEscape: {
                this.toggleFlyout(false);
                return false;
            }

            case keyEnter: {
                if (
                    this.menuFocusIndex !== -1 &&
                    this.menuElement.optionElements.length > this.menuFocusIndex
                ) {
                    this.menuElement.optionElements[this.menuFocusIndex].click();
                }
                return false;
            }

            case keyArrowRight: {
                if (activeElement !== this.inputElement) {
                    this.incrementFocusedItem(1);
                    return false;
                }
                // don't block if arrow keys moving caret in input element
                return true;
            }

            case keyArrowLeft: {
                if (this.inputElement.selectionStart === 0) {
                    this.incrementFocusedItem(-1);
                    return false;
                }
                // don't block if arrow keys moving caret in input element
                return true;
            }

            case keyDelete:
            case keyBackspace: {
                if (activeElement === null) {
                    return true;
                }

                if (activeElement === this.inputElement) {
                    if (this.inputElement.selectionStart === 0) {
                        this.selection = this.selectedItems
                            .slice(0, this.selectedItems.length - 1)
                            .toString();
                        this.toggleFlyout(false);
                        return false;
                    }
                    // let text deletion proceed
                    return true;
                }

                const selectedItems: Element[] = Array.from(this.listElement.children);
                const currentFocusedItemIndex: number =
                    selectedItems.indexOf(activeElement);

                if (currentFocusedItemIndex > -1) {
                    // delete currently focused item
                    this.selection = this.selectedItems
                        .splice(currentFocusedItemIndex, 1)
                        .toString();
                    Updates.enqueue(() => {
                        (
                            selectedItems[
                                Math.min(selectedItems.length, currentFocusedItemIndex)
                            ] as HTMLElement
                        ).focus();
                    });
                    return false;
                }
                return true;
            }
        }
        this.toggleFlyout(true);
        return true;
    }

    /**
     * Handle focus in events.
     */
    public handleFocusIn(e: FocusEvent): boolean {
        return false;
    }

    /**
     * Handle focus out events.
     */
    public handleFocusOut(e: FocusEvent): boolean {
        if (
            this.menuElement === undefined ||
            !this.menuElement.contains(e.relatedTarget as Element)
        ) {
            this.toggleFlyout(false);
        }

        return false;
    }

    /**
     * The list of selected items has changed
     */
    public handleSelectionChange(): void {
        if (this.selectedItems.toString() === this.selection) {
            return;
        }

        this.selectedItems = this.selection === "" ? [] : this.selection.split(",");

        this.updateFilteredOptions();

        Updates.enqueue(() => {
            this.checkMaxItems();
        });
        this.$emit("selectionchange", { bubbles: false });
    }

    /**
     * Anchored region is loaded, menu and options exist in the DOM.
     */
    public handleRegionLoaded(e: Event): void {
        Updates.enqueue(() => {
            this.setFocusedOption(0);
            this.$emit("menuloaded", { bubbles: false });
        });
    }

    /**
     * Sets properties on the anchored region once it is instanciated.
     */
    private setRegionProps = (): void => {
        if (!this.flyoutOpen) {
            return;
        }
        if (this.region === null || this.region === undefined) {
            // TODO: limit this
            Updates.enqueue(this.setRegionProps);
            return;
        }
        this.region.anchorElement = this.inputElement;
    };

    /**
     * Checks if the maximum number of items has been chosen and updates the ui.
     */
    private checkMaxItems(): void {
        if (this.inputElement === undefined) {
            return;
        }
        if (
            this.maxSelected !== null &&
            this.maxSelected !== 0 &&
            this.selectedItems.length >= this.maxSelected
        ) {
            if (getRootActiveElement(this) === this.inputElement) {
                const selectedItemInstances: Element[] = Array.from(
                    this.listElement.querySelectorAll("[role='listitem']")
                );
                (
                    selectedItemInstances[selectedItemInstances.length - 1] as HTMLElement
                ).focus();
            }
            this.inputElement.hidden = true;
        } else {
            this.inputElement.hidden = false;
        }
    }

    /**
     * A list item has been invoked.
     */
    public handleItemInvoke(e: Event): boolean {
        if (e.defaultPrevented) {
            return false;
        }
        if (e.target instanceof FASTPickerListItem) {
            const listItems: Element[] = Array.from(
                this.listElement.querySelectorAll("[role='listitem']")
            );
            const itemIndex: number = listItems.indexOf(e.target as Element);
            if (itemIndex !== -1) {
                const newSelection: string[] = this.selectedItems.slice();
                newSelection.splice(itemIndex, 1);
                this.selection = newSelection.toString();
                Updates.enqueue(() => {
                    this.incrementFocusedItem(0);
                    this.toggleFlyout(true);
                });
            }
            return false;
        }
        return true;
    }

    /**
     * A menu option has been invoked.
     */
    public handleOptionInvoke(e: Event): boolean {
        if (e.defaultPrevented) {
            return false;
        }

        if (e.target instanceof FASTPickerMenuOption && e.target.value !== undefined) {
            if (this.maxSelected === 0) {
                // if we don't allow selection just update the query
                this.query = e.target.value;
            } else {
                this.query = "";
                this.selection = `${this.selection}${this.selection === "" ? "" : ","}${
                    e.target.value
                }`;
            }

            this.toggleFlyout(false);
            this.inputElement.focus();
            return false;
        }

        return true;
    }

    /**
     * Increments the focused list item by the specified amount
     */
    private incrementFocusedItem(increment: number) {
        if (this.selectedItems.length === 0) {
            this.inputElement.focus();
            return;
        }

        const selectedItemsAsElements: Element[] = Array.from(
            this.listElement.querySelectorAll("[role='listitem']")
        );

        const activeElement = getRootActiveElement(this);
        if (activeElement !== null) {
            let currentFocusedItemIndex: number =
                selectedItemsAsElements.indexOf(activeElement);
            if (currentFocusedItemIndex === -1) {
                // use the input element
                currentFocusedItemIndex = selectedItemsAsElements.length;
            }

            const newFocusedItemIndex = Math.min(
                selectedItemsAsElements.length,
                Math.max(0, currentFocusedItemIndex + increment)
            );
            if (newFocusedItemIndex === selectedItemsAsElements.length) {
                if (
                    this.maxSelected !== null &&
                    this.selectedItems.length >= this.maxSelected
                ) {
                    (
                        selectedItemsAsElements[newFocusedItemIndex - 1] as HTMLElement
                    ).focus();
                } else {
                    this.inputElement.focus();
                }
            } else {
                (selectedItemsAsElements[newFocusedItemIndex] as HTMLElement).focus();
            }
        }
    }

    /**
     * Disables the menu. Note that the menu can be open, just doens't have any valid options on display.
     */
    private disableMenu(): void {
        this.menuFocusIndex = -1;
        this.menuFocusOptionId = undefined;
        this.inputElement?.removeAttribute("aria-activedescendant");
        this.inputElement?.removeAttribute("aria-owns");
        this.inputElement?.removeAttribute("aria-expanded");
    }

    /**
     * Sets the currently focused menu option by index
     */
    private setFocusedOption(optionIndex: number): void {
        if (
            !this.flyoutOpen ||
            optionIndex === -1 ||
            this.showNoOptions ||
            this.showLoading
        ) {
            this.disableMenu();
            return;
        }

        if (this.menuElement.optionElements.length === 0) {
            return;
        }

        this.menuElement.optionElements.forEach((element: HTMLElement) => {
            element.setAttribute("aria-selected", "false");
        });

        this.menuFocusIndex = optionIndex;
        if (this.menuFocusIndex > this.menuElement.optionElements.length - 1) {
            this.menuFocusIndex = this.menuElement.optionElements.length - 1;
        }

        this.menuFocusOptionId = this.menuElement.optionElements[this.menuFocusIndex].id;

        this.inputElement.setAttribute("aria-owns", this.menuId);
        this.inputElement.setAttribute("aria-expanded", "true");
        this.inputElement.setAttribute("aria-activedescendant", this.menuFocusOptionId);

        const focusedOption = this.menuElement.optionElements[this.menuFocusIndex];

        focusedOption.setAttribute("aria-selected", "true");

        focusedOption.scrollIntoView(true);
    }

    /**
     * Updates the template used for the list item repeat behavior
     */
    private updateListItemTemplate(): void {
        this.activeListItemTemplate =
            this.listItemTemplate ?? this.defaultListItemTemplate;
    }

    /**
     * Updates the template used for the menu option repeat behavior
     */
    private updateOptionTemplate(): void {
        this.activeMenuOptionTemplate =
            this.menuOptionTemplate ?? this.defaultMenuOptionTemplate;
    }

    /**
     * Updates the filtered options array
     */
    private updateFilteredOptions(): void {
        this.filteredOptionsList = this.optionsList.slice(0);
        if (this.filterSelected) {
            this.filteredOptionsList = this.filteredOptionsList.filter(
                el => this.selectedItems.indexOf(el) === -1
            );
        }
        if (this.filterQuery && this.query !== "" && this.query !== undefined) {
            // compare case-insensitive
            const filterQuery = this.query.toLowerCase();
            this.filteredOptionsList = this.filteredOptionsList.filter(
                el => el.toLowerCase().indexOf(filterQuery) !== -1
            );
        }
    }

    /**
     * Updates the menu configuration
     */
    private updateMenuConfig(): void {
        let newConfig = this.configLookup[this.menuPlacement];

        if (newConfig === null) {
            newConfig = FlyoutPosBottomFill;
        }

        this.menuConfig = {
            ...newConfig,
            autoUpdateMode: "auto",
            fixedPlacement: true,
            horizontalViewportLock: false,
            verticalViewportLock: false,
        };
    }

    /**
     * matches menu placement values with the associated menu config
     */
    private configLookup: object = {
        top: FlyoutPosTop,
        bottom: FlyoutPosBottom,
        tallest: FlyoutPosTallest,
        "top-fill": FlyoutPosTopFill,
        "bottom-fill": FlyoutPosBottomFill,
        "tallest-fill": FlyoutPosTallestFill,
    };
}