import { AnalyticsPayload, OptionWithNested, PopperPlacement, DropdownVariant } from "typings/custom"; import { LightningElement, api, track } from "lwc"; import cx from "classnames"; import get from "lodash.get"; import { toJson } from "dxUtils/normalizers"; import { deepmapOptions } from "dxUtils/options"; interface DropdownOption extends OptionWithNested { // link that gets shown as a highlighted link at end of option group calloutLink?: { href: string; target?: string; label: string; }; } // @ts-ignore Dark Magic (TM) we are adding a custom getter to the 'role' property and tsc doesn't like that export default class Dropdown extends LightningElement { @api value: string | null = null; // "active option" id @api valuePath: string = "id"; // path to match for the active value (useful for url matching) @api stayOpenAfterChange?: boolean = false; // props forwarded to popover: @api ariaLabel: string | null = null; @api tempId: string | null = null; @api offset?: "small" | "medium"; @api open: boolean | null = null; @api pagePadding?: number = 16; // padding between dropdown and the edge of the page @api placement?: PopperPlacement = "bottom-start"; @api small?: boolean = false; @api openOnHover?: boolean = false; // dropdown opens/closes with hover @api width?: string | null = null; @api fullWidth?: boolean | null; @api navItemLabel: string | null = null; @api variant: DropdownVariant = "base"; // props forwarded to dropdown option @api analyticsEvent?: string; @api analyticsPayload?: AnalyticsPayload; @api get options() { return deepmapOptions(this._options, (option: DropdownOption) => ({ ...option, active: this.value === get(option, this.valuePath), keyValue: get(option, this.valuePath) })); } set options(value: DropdownOption[]) { this._options = toJson(value); } @api get focusedValue() { return this._focusedValue; } @track private _options!: DropdownOption[]; // PRIVATE VARS private _focusedValue: string | null = null; private popoverEl: HTMLElement | null = null; constructor() { super(); // @ts-ignore this.addEventListener("keydown", this.onKeyDown); } // GETTERS private get role() { return this.areOptionsEmpty ? null : "menu"; } private get className() { return cx( this.isNested && "menu-nested", this.isNested && this.options.length >= 2 && "menu-nested-2-col", this.offset && `menu-offset-${this.offset}`, this.areOptionsEmpty && "menu-hidden" ); } private get isNested() { return this.options.some((option) => !!option.options); } private get optionsElements() { return this.template.querySelectorAll("dx-dropdown-option"); } private get areOptionsEmpty() { return this.options.length === 0; } // CLASS FNS private onKeyDown = (e: KeyboardEvent) => { switch (e.key) { case "ArrowDown": e.preventDefault(); this.incrementOptionFocus(1); break; case "ArrowUp": e.preventDefault(); this.incrementOptionFocus(-1); break; default: } }; private onOptionClick(e: CustomEvent) { if (e.detail !== this.value) { this.dispatchEvent(new CustomEvent("change", { detail: e.detail })); } if (!this.stayOpenAfterChange && this.popoverEl) { (this.popoverEl as any).closePopover(); } } private onOptionFocus(e: PointerEvent) { if (e.target) { // @ts-ignore this._focusedValue = e.target.option.id; } } private findOptionElementIndex(value: string) { return Array.from(this.optionsElements).findIndex( // @ts-ignore (el) => el.option.id === value ); } private incrementOptionFocus(indexChange: number) { if (!this._focusedValue) { this.focusDefaultOption(); return; } const focusedOptionIndex = this.findOptionElementIndex( this._focusedValue ); let nextIndex = focusedOptionIndex + indexChange; if (nextIndex < 0) { nextIndex = this.optionsElements.length - 1; } if (nextIndex >= this.optionsElements.length) { nextIndex = 0; } const element = this.optionsElements[nextIndex]; if (element) { element.focus(); } } private onClose(): void { this._focusedValue = null; } private focusDefaultOption() { const defaultIndex = this.value ? this.findOptionElementIndex(this.value) : 0; const optionToFocus: any = this.optionsElements[defaultIndex < 0 ? 0 : defaultIndex]; if (optionToFocus) { optionToFocus.focus(); this._focusedValue = optionToFocus.option.id; } } renderedCallback() { if (!this.popoverEl) { this.popoverEl = this.template.querySelector("dx-popover"); } } }