UNPKG

@salla.sa/twilight-components

Version:
274 lines (271 loc) 12.9 kB
/*! * Crafted with ❤ by Salla */ import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client'; const SallaSearchableDropdown = /*@__PURE__*/ proxyCustomElement(class SallaSearchableDropdown extends HTMLElement { constructor() { super(); this.__registerHost(); this.itemSelected = createEvent(this, "itemSelected", 7); this.searchInput = createEvent(this, "searchInput", 7); this.dropdownOpened = createEvent(this, "dropdownOpened", 7); this.dropdownClosed = createEvent(this, "dropdownClosed", 7); this.placeholder = ''; this.items = []; this.selectedItem = null; this.loading = false; this.searching = false; this.disabled = false; this.required = false; this.noResultsText = ''; this.inputId = ''; this.searchQuery = ''; this.clientSearch = false; this.dropUp = false; this.searchable = true; /** * When true, the dropdown will NOT override the nearest scrollable ancestor's * `overflow` to `visible` while open. Use this when the host (e.g. a modal * body) needs to keep its own scrolling intact while the dropdown is open. */ this.keepParentScroll = false; this.isOpen = false; this.focusedIndex = -1; this.clientSearchQuery = ''; this.scrollableAncestor = null; this.handleTriggerClick = () => { if (this.isOpen) { this.close(); } else { this.open(); } }; this.handleTriggerKeyDown = (e) => { if (!this.isOpen) { if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); this.open(); } else if (e.key === 'Escape') { this.close(); } return; } // When open without a search input, the trigger keeps focus and drives item navigation. this.navigateItems(e); }; this.handleSearchInputChange = (e) => { const value = e.target.value; if (this.clientSearch) { this.clientSearchQuery = value; } this.searchInput.emit(value); this.focusedIndex = -1; }; this.handleSearchKeyDown = (e) => { this.navigateItems(e); }; } connectedCallback() { this.boundHandleClickOutside = this.handleClickOutside.bind(this); document.addEventListener('mousedown', this.boundHandleClickOutside); } disconnectedCallback() { document.removeEventListener('mousedown', this.boundHandleClickOutside); if (this.scrollableAncestor) { this.scrollableAncestor.style.overflow = ''; this.scrollableAncestor = null; } } onItemsChange() { this.focusedIndex = -1; } normalizeArabic(text) { return text.replace(/[أإآا]/g, 'ا'); } get filteredItems() { if (!this.clientSearch || !this.clientSearchQuery.trim()) { return this.items; } const query = this.normalizeArabic(this.clientSearchQuery.trim().toLowerCase()); return this.items.filter(item => { const normalizedName = this.normalizeArabic(item.name.toLowerCase()); const normalizedNameEn = item.name_en ? item.name_en.toLowerCase() : ''; return normalizedName.includes(query) || normalizedNameEn.includes(query); }); } getDisplayName(item) { const lang = salla?.config?.get('user.language_code'); if (lang && lang !== 'ar' && item.name_en?.trim()) { return item.name_en.trim(); } return item.name; } handleClickOutside(e) { if (!this.isOpen) return; const target = e.target; if (!this.host.contains(target)) { this.close(); } } findScrollableAncestor() { let el = this.host.parentElement; while (el) { const style = getComputedStyle(el); const overflowY = style.overflowY; if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'hidden' || style.overflow === 'clip') { return el; } el = el.parentElement; } return null; } open() { if (this.disabled || this.loading) return; this.isOpen = true; this.focusedIndex = -1; if (!this.keepParentScroll) { this.scrollableAncestor = this.findScrollableAncestor(); if (this.scrollableAncestor) { this.scrollableAncestor.style.overflow = 'visible'; } } this.dropdownOpened.emit(); requestAnimationFrame(() => this.searchInputRef?.focus()); } close() { if (!this.isOpen) return; this.isOpen = false; this.focusedIndex = -1; this.clientSearchQuery = ''; if (this.scrollableAncestor) { this.scrollableAncestor.style.overflow = ''; this.scrollableAncestor = null; } this.dropdownClosed.emit(); } navigateItems(e) { const visibleItems = this.filteredItems; const itemCount = visibleItems.length; switch (e.key) { case 'ArrowDown': e.preventDefault(); this.focusedIndex = itemCount > 0 ? (this.focusedIndex + 1) % itemCount : -1; this.scrollFocusedIntoView(); break; case 'ArrowUp': e.preventDefault(); this.focusedIndex = itemCount > 0 ? (this.focusedIndex - 1 + itemCount) % itemCount : -1; this.scrollFocusedIntoView(); break; case 'Enter': e.preventDefault(); if (this.focusedIndex >= 0 && this.focusedIndex < itemCount) { this.selectItem(visibleItems[this.focusedIndex]); } break; case 'Escape': e.preventDefault(); this.close(); break; } } scrollFocusedIntoView() { if (this.focusedIndex < 0) return; requestAnimationFrame(() => { const listEl = this.panelRef?.querySelector('.s-searchable-dropdown-list'); const focused = listEl?.children[this.focusedIndex]; focused?.scrollIntoView({ block: 'nearest' }); }); } selectItem(item) { this.itemSelected.emit(item); this.close(); } render() { const hasSelection = this.selectedItem != null; const listboxId = `${this.inputId}-listbox`; return (h(Host, { key: 'c529f2b61adbc38aa1e5403da6fdb985fe256baa', class: "s-searchable-dropdown" }, h("div", { key: '88ab96afd36421fdf965b79298fa14f435adbcef', class: { 's-searchable-dropdown': true, 's-searchable-dropdown--open': this.isOpen } }, this.label && (h("label", { key: '6ee8a2d0428e1347e36eabec32a3129897481abc', class: "s-searchable-dropdown-label", htmlFor: this.inputId }, this.label, this.required && h("span", { key: '8473884ec18e73c72c79e7749264bd9c24c56ae9', class: "s-searchable-dropdown-required" }, " *"))), h("div", { key: 'bffbea7ff0cb9466c8a852a841a21ed8351ef92d', class: { 's-searchable-dropdown-trigger': true, 's-searchable-dropdown-trigger--disabled': this.disabled || this.loading, 's-searchable-dropdown-trigger--open': this.isOpen, }, role: "combobox", "aria-expanded": this.isOpen ? 'true' : 'false', "aria-haspopup": "listbox", "aria-controls": listboxId, "aria-disabled": this.disabled || this.loading ? 'true' : 'false', tabIndex: this.disabled || this.loading ? -1 : 0, onClick: this.handleTriggerClick, onKeyDown: this.handleTriggerKeyDown }, hasSelection ? (h("span", { class: "s-searchable-dropdown-trigger-text" }, this.getDisplayName(this.selectedItem))) : (h("span", { class: "s-searchable-dropdown-trigger-placeholder" }, this.placeholder)), h("i", { key: 'e3aee1ffe1b80a9ef21f4c9cca02de0a590b0825', class: { 'sicon-keyboard_arrow_down': true, 's-searchable-dropdown-trigger-icon': true, 's-searchable-dropdown-trigger-icon--open': this.isOpen, }, "aria-hidden": "true" })), this.isOpen && (h("div", { key: '7a19f2d6847b3e0dda5e0ea509df91a2f48fa098', class: { 's-searchable-dropdown-panel': true, 's-searchable-dropdown-panel--up': this.dropUp, }, ref: (el) => (this.panelRef = el) }, this.searchable && (h("div", { key: '601529f76a09762a57fe7b7663ecfa1505462367', class: "s-searchable-dropdown-search-wrap" }, h("i", { key: 'f91e464e7efed3759837880b5a7593daa4f84ecb', class: "sicon-search s-searchable-dropdown-search-icon", "aria-hidden": "true" }), h("input", { key: 'f72a7e12d0a6e90bab3983258c9b08aa286bc897', ref: (el) => (this.searchInputRef = el), id: this.inputId, type: "text", class: "s-searchable-dropdown-search-input", placeholder: this.placeholder, value: this.clientSearch ? this.clientSearchQuery : this.searchQuery, onInput: this.handleSearchInputChange, onKeyDown: this.handleSearchKeyDown, autocomplete: "off", "aria-autocomplete": "list", "aria-expanded": this.isOpen ? 'true' : 'false', "aria-controls": listboxId }))), h("div", { key: '495aa88fc76d3d17956db7277cc98aff08b4c1af', id: listboxId, class: "s-searchable-dropdown-list s-scrollbar", role: "listbox" }, this.renderListContent())))))); } renderListContent() { if (this.searching || this.loading) { return (h("div", { class: "s-searchable-dropdown-loading" }, h("svg", { class: "s-searchable-dropdown-spinner", viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, h("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", "stroke-width": "3", "stroke-linecap": "round", opacity: "0.25" }), h("path", { d: "M12 2a10 10 0 0 1 10 10", stroke: "currentColor", "stroke-width": "3", "stroke-linecap": "round" })))); } const visibleItems = this.filteredItems; if (visibleItems.length === 0) { return (h("div", { class: "s-searchable-dropdown-empty" }, this.noResultsText || salla?.lang?.get('common.elements.no_options') || 'No results found')); } return visibleItems.map((item, index) => { const isSelected = this.selectedItem?.id === item.id; const isFocused = this.focusedIndex === index; return (h("button", { key: item.id, type: "button", role: "option", class: { 's-searchable-dropdown-item': true, 's-searchable-dropdown-item--selected': isSelected, 's-searchable-dropdown-item--focused': isFocused && !isSelected, }, "aria-selected": isSelected ? 'true' : 'false', onClick: () => this.selectItem(item) }, h("span", { class: { 's-searchable-dropdown-item-name': true, 's-searchable-dropdown-item-name--selected': isSelected, } }, this.getDisplayName(item)), isSelected && (h("i", { class: "sicon-check s-searchable-dropdown-item-check", "aria-hidden": "true" })))); }); } get host() { return this; } static get watchers() { return { "items": ["onItemsChange"] }; } }, [0, "salla-searchable-dropdown", { "label": [1], "placeholder": [1], "items": [16], "selectedItem": [16, "selected-item"], "loading": [4], "searching": [4], "disabled": [4], "required": [4], "noResultsText": [1, "no-results-text"], "inputId": [1, "input-id"], "searchQuery": [1, "search-query"], "clientSearch": [4, "client-search"], "dropUp": [4, "drop-up"], "searchable": [4], "keepParentScroll": [4, "keep-parent-scroll"], "isOpen": [32], "focusedIndex": [32], "clientSearchQuery": [32] }, undefined, { "items": ["onItemsChange"] }]); function defineCustomElement() { if (typeof customElements === "undefined") { return; } const components = ["salla-searchable-dropdown"]; components.forEach(tagName => { switch (tagName) { case "salla-searchable-dropdown": if (!customElements.get(tagName)) { customElements.define(tagName, SallaSearchableDropdown); } break; } }); } defineCustomElement(); export { SallaSearchableDropdown as S, defineCustomElement as d };