Files
dees-catalog/ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown.ts

296 lines
8.7 KiB
TypeScript

import {
customElement,
type TemplateResult,
property,
state,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesInputBase } from '../dees-input-base/dees-input-base.js';
import { cssGeistFontFamily } from '../../00fonts.js';
import { themeDefaultStyles } from '../../00theme.js';
import { DeesInputDropdownPopup } from './dees-input-dropdown-popup.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-dropdown': DeesInputDropdown;
}
}
@customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public static demo = demoFunc;
public static demoGroups = ['Input'];
// INSTANCE
@property()
accessor options: { option: string; key: string; payload?: any }[] = [];
@property()
accessor selectedOption: { option: string; key: string; payload?: any } | null = null;
// Add value property for form compatibility
public get value() {
return this.selectedOption;
}
public set value(val: { option: string; key: string; payload?: any } | null) {
this.selectedOption = val;
}
@property({
type: Boolean,
})
accessor enableSearch: boolean = true;
@state()
accessor isOpened = false;
private popupInstance: DeesInputDropdownPopup | null = null;
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
cssManager.defaultStyles,
css`
* {
box-sizing: border-box;
}
:host {
font-family: ${cssGeistFontFamily};
position: relative;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
.maincontainer {
display: block;
position: relative;
}
.selectedBox {
user-select: none;
position: relative;
width: 100%;
height: 40px;
line-height: 38px;
padding: 0 40px 0 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
transition: all 0.15s ease;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selectedBox:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.selectedBox:focus-visible {
outline: none;
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
}
.selectedBox.disabled {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
cursor: not-allowed;
opacity: 0.5;
}
/* Dropdown arrow */
.selectedBox::after {
content: '';
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: transform 0.15s ease;
}
.selectedBox.open::after {
transform: translateY(-50%) rotate(180deg);
}
`,
];
public render(): TemplateResult {
return html`
<div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description} .required=${this.required}></dees-label>
<div class="maincontainer">
<div
class="selectedBox ${this.isOpened ? 'open' : ''} ${this.disabled ? 'disabled' : ''}"
@click="${() => !this.disabled && this.toggleSelectionBox()}"
tabindex="${this.disabled ? '-1' : '0'}"
@keydown="${this.handleSelectedBoxKeydown}"
>
${this.selectedOption?.option || 'Select an option'}
</div>
</div>
</div>
`;
}
firstUpdated() {
this.selectedOption = this.selectedOption || null;
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('options') && this.popupInstance && this.isOpened) {
this.popupInstance.updateOptions(this.options);
}
}
public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) {
this.selectedOption = selectedOption;
this.closePopup();
this.dispatchEvent(
new CustomEvent('selectedOption', {
detail: selectedOption,
bubbles: true,
})
);
this.changeSubject.next(this);
}
public async toggleSelectionBox() {
if (this.isOpened) {
this.closePopup();
return;
}
this.isOpened = true;
// Get trigger position
const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement;
const rect = selectedBox.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
const opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
// Create popup if needed
if (!this.popupInstance) {
this.popupInstance = new DeesInputDropdownPopup();
}
// Configure popup
this.popupInstance.options = this.options;
this.popupInstance.enableSearch = this.enableSearch;
this.popupInstance.opensToTop = opensToTop;
this.popupInstance.triggerRect = rect;
this.popupInstance.ownerComponent = this;
// Listen for popup events
this.popupInstance.addEventListener('option-selected', this.handleOptionSelected);
this.popupInstance.addEventListener('close-request', this.handleCloseRequest);
this.popupInstance.addEventListener('reposition-request', this.handleRepositionRequest);
// Show popup (creates window layer, appends to document.body)
await this.popupInstance.show();
// Focus search input
if (this.enableSearch) {
await this.popupInstance.focusSearchInput();
}
}
private closePopup(): void {
this.isOpened = false;
if (this.popupInstance) {
this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected);
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
this.popupInstance.hide();
}
}
private handleOptionSelected = (event: Event): void => {
const detail = (event as CustomEvent).detail;
this.updateSelection(detail);
};
private handleCloseRequest = (): void => {
this.closePopup();
};
private handleRepositionRequest = (): void => {
if (!this.popupInstance || !this.isOpened) return;
const selectedBox = this.shadowRoot!.querySelector('.selectedBox') as HTMLElement;
if (!selectedBox) return;
const rect = selectedBox.getBoundingClientRect();
// Close if trigger scrolled off-screen
if (rect.bottom < 0 || rect.top > window.innerHeight) {
this.closePopup();
return;
}
// Update position
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
this.popupInstance.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
this.popupInstance.triggerRect = rect;
};
private handleSelectedBoxKeydown(event: KeyboardEvent) {
if (this.disabled) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.toggleSelectionBox();
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
if (!this.isOpened) {
this.toggleSelectionBox();
}
} else if (event.key === 'Escape') {
event.preventDefault();
if (this.isOpened) {
this.closePopup();
}
}
}
public getValue(): { option: string; key: string; payload?: any } | null {
return this.selectedOption;
}
public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value;
}
async disconnectedCallback() {
await super.disconnectedCallback();
if (this.popupInstance) {
this.popupInstance.removeEventListener('option-selected', this.handleOptionSelected);
this.popupInstance.removeEventListener('close-request', this.handleCloseRequest);
this.popupInstance.removeEventListener('reposition-request', this.handleRepositionRequest);
this.popupInstance.hide();
this.popupInstance = null;
}
}
}