@salla.sa/twilight-components
Version:
Salla Web Component
274 lines (271 loc) • 12.9 kB
JavaScript
/*!
* 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 };