import { LitElement, html, css, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { provide } from "@lit-labs/context"; import AutocompleteController from "./controllers/autocomplete-controller"; import { debounce } from "./utils/debounce"; import { AddressContext, addressContext } from "./context/address.context"; import "./input.ts"; import "./dropdown-menu.ts"; import { Address } from "./controllers/autocomplete.parser"; @customElement("hvs-widget") export default class HvsWidget extends LitElement { @property({ attribute: "apikey", type: String }) apikey: string = ""; @property({ attribute: "api-url", type: String }) apiURL?: string; @property({ attribute: "report-url", type: String }) reportUrl?: string; @property({ attribute: "states", type: String }) states?: string; @property({ attribute: "preferred-states", type: String }) preferredStates?: string; @property({ attribute: "limit", type: String }) limit?: string; @property({ attribute: "no-result-message", type: String }) noResultsMessage?: string = "No results found"; @property({ attribute: "source", type: String }) source?: string; @property({ attribute: "username", type: String }) userName?: string; @property({ attribute: "contact-id", type: String }) contactID?: string; @property({ attribute: "contact-first-name", type: String }) contactFirstName?: string; @property({ attribute: "contact-last-name", type: String }) contactLastName?: string; @property({ attribute: "contact-email", type: String }) contactEmail?: string; @property({ attribute: "new-window", type: Boolean }) newWindow?: boolean; @property({ attribute: "search-icon", type: String }) searchIcon?: string = ""; @property({ attribute: "placeholder", type: String }) placeholder?: string = ""; @property({ attribute: "is-mobile", type: Number }) isMobile: number = 0; @property({ attribute: "has-agent-details", type: Number }) hasAgentDetails: number = 0; @property({ attribute: "protocol", type: String }) protocol: string = ""; @property({ attribute: "redirect-user", type: String }) redirectUser: string = ""; @property({ attribute: "should-store-address", type: Boolean }) shouldStoreAddress: boolean = false; @property({ attribute: "value", type: String }) value?: string = ""; @property({ attribute: "country", type: String }) country: string = "us"; @property({ attribute: "environment", type: String }) environment: "production" | "staging" = "production"; @state() query: string = ""; @state() selectedAddress?: Address; @state() showDropdown: boolean = true; /** * Tracks whether the user has interacted with the input * after the dropdown was closed */ @state() private userWantsToReopenDropdown: boolean = false; @provide({ context: addressContext }) @property({ attribute: false }) public addressProvider: AddressContext = { setAddress: (address) => { this.selectedAddress = address; }, setShowDropdown: (value) => { this.showDropdown = value; // If we're closing the dropdown, reset the reopen flag if (!value) { this.userWantsToReopenDropdown = false; } }, reopenDropdownWithResults: () => { // Only set this flag if we have results to show if (this.autocomplete.addresses.length > 0) { this.userWantsToReopenDropdown = true; this.showDropdown = true; } }, hasSearchResults: () => { return this.autocomplete.addresses.length > 0; } }; private autocomplete = new AutocompleteController(this); debounceInputHandler = debounce(async (value: string) => { await this.onInputChanged(value); }, 1000); async onInputChanged(value: string) { // Always show dropdown during typing this.showDropdown = true; this.query = value; // Fetch autocomplete results await this.autocomplete.fetchAddressAutocomplete(value, { apikey: this.apikey, apiURL: this.apiURL ?? "https://api.percyx.com/v1/address/autocomplete-all-v2", states: this.states, preferredStates: this.preferredStates, limit: this.limit, v2: true, country: this.country, }); // If we got results, make sure the dropdown is open if (this.autocomplete.addresses.length > 0) { this.showDropdown = true; } } firstUpdated(): void { this.autocomplete.validateTenant(this.apikey, this.environment); } protected updated( _changedProperties: PropertyValues ): void { if (_changedProperties.has("apikey")) { this.autocomplete.validateTenant(this.apikey, this.environment); } } render() { // Get status of search results and loading const hasAddresses = this.autocomplete.addresses.length > 0; const isTyping = this.query.length > 1; const isLoading = this.autocomplete.isLoading; // Determine if the dropdown should be displayed based on these conditions: // 1. Standard dropdown behavior (showDropdown is true + conditions) // 2. User explicitly wants to view previous results const standardDropdownCondition = this.showDropdown && (hasAddresses || isTyping || isLoading); const userReopenCondition = this.userWantsToReopenDropdown && hasAddresses; const shouldOpenDropdown = standardDropdownCondition || userReopenCondition; return html`
`; } static styles = css` .hvs-container { width: var(--hvs-input-width, 100%); min-width: var(--hvs-input-width, 100%); position: relative; display: flex; flex-direction: column; align-items: center; /* Shadow/xs */ box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); border-radius: 8px; } `; } declare global { interface HTMLElementTagNameMap { "hvs-widget": HvsWidget; } }