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

296 lines
8.6 KiB
TypeScript
Raw Normal View History

2023-12-08 18:15:40 +01:00
import {
customElement,
type TemplateResult,
property,
state,
html,
css,
cssManager,
} from '@design.estate/dees-element';
2023-11-29 17:20:32 +01:00
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';
2020-09-13 16:24:48 +00:00
2021-03-06 15:48:02 +00:00
declare global {
interface HTMLElementTagNameMap {
'dees-input-dropdown': DeesInputDropdown;
}
}
2020-09-13 16:24:48 +00:00
@customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
2023-12-08 18:15:40 +01:00
public static demo = demoFunc;
public static demoGroups = ['Input'];
2020-12-09 23:05:13 +00:00
2021-08-20 00:25:14 +02:00
// INSTANCE
2020-09-13 16:24:48 +00:00
@property()
accessor options: { option: string; key: string; payload?: any }[] = [];
2020-09-13 16:24:48 +00:00
@property()
accessor selectedOption: { option: string; key: string; payload?: any } | null = null;
2020-09-13 16:24:48 +00:00
// 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;
}
2021-08-26 21:30:35 +02:00
@property({
2023-12-08 18:15:40 +01:00
type: Boolean,
})
accessor enableSearch: boolean = true;
2023-12-08 18:15:40 +01:00
2023-12-20 19:09:55 +01:00
@state()
accessor isOpened = false;
2023-12-20 19:09:55 +01:00
private popupInstance: DeesInputDropdownPopup | null = null;
2025-06-27 16:20:06 +00:00
2023-03-25 17:30:41 +01:00
public static styles = [
themeDefaultStyles,
...DeesInputBase.baseStyles,
2023-03-25 17:30:41 +01:00
cssManager.defaultStyles,
css`
* {
2023-12-08 18:15:40 +01:00
box-sizing: border-box;
}
2020-09-13 16:24:48 +00:00
2023-12-08 18:15:40 +01:00
:host {
2025-06-27 17:32:01 +00:00
font-family: ${cssGeistFontFamily};
2023-12-08 18:15:40 +01:00
position: relative;
2025-06-27 16:20:06 +00:00
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
2023-12-08 18:15:40 +01:00
}
2020-09-13 16:24:48 +00:00
2023-12-08 18:15:40 +01:00
.maincontainer {
display: block;
2025-06-27 16:20:06 +00:00
position: relative;
2023-12-08 18:15:40 +01:00
}
2020-09-13 16:24:48 +00:00
2023-12-08 18:15:40 +01:00
.selectedBox {
user-select: none;
position: relative;
2025-06-27 16:20:06 +00:00
width: 100%;
2023-12-08 18:15:40 +01:00
height: 40px;
2025-06-27 16:20:06 +00:00
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;
2023-12-08 18:15:40 +01:00
}
2023-03-25 20:56:12 +01:00
2025-06-27 16:20:06 +00:00
.selectedBox:hover:not(.disabled) {
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
2023-12-26 21:21:18 +01:00
}
2025-06-27 16:20:06 +00:00
.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)')};
2023-12-08 18:15:40 +01:00
}
2020-09-13 16:24:48 +00:00
2025-06-27 16:20:06 +00:00
.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);
2023-12-08 18:15:40 +01:00
}
`,
];
2023-03-25 17:30:41 +01:00
public render(): TemplateResult {
return html`
<div class="input-wrapper">
2025-06-27 16:20:06 +00:00
<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>
2020-09-13 16:24:48 +00:00
</div>
</div>
`;
}
firstUpdated() {
2024-01-09 13:57:53 +01:00
this.selectedOption = this.selectedOption || null;
2020-09-13 16:24:48 +00:00
}
2025-06-27 16:20:06 +00:00
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('options') && this.popupInstance && this.isOpened) {
this.popupInstance.updateOptions(this.options);
2025-06-27 16:20:06 +00:00
}
}
public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) {
2020-09-13 16:24:48 +00:00
this.selectedOption = selectedOption;
this.closePopup();
2020-09-13 16:24:48 +00:00
2023-12-08 18:15:40 +01:00
this.dispatchEvent(
new CustomEvent('selectedOption', {
detail: selectedOption,
bubbles: true,
})
);
2021-08-20 00:25:14 +02:00
this.changeSubject.next(this);
2020-09-13 16:24:48 +00:00
}
2023-12-08 18:15:40 +01:00
public async toggleSelectionBox() {
2025-06-27 16:20:06 +00:00
if (this.isOpened) {
this.closePopup();
return;
2023-12-08 18:15:40 +01:00
}
2023-12-20 19:09:55 +01:00
this.isOpened = true;
2023-12-20 19:09:55 +01:00
// 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;
2023-12-20 19:09:55 +01:00
// 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 (appends to document.body)
this.popupInstance.show();
// Focus search input
if (this.enableSearch) {
this.popupInstance.focusSearchInput();
2025-06-27 16:20:06 +00:00
}
}
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();
2025-06-27 16:20:06 +00:00
}
}
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;
};
2025-06-27 16:20:06 +00:00
private handleSelectedBoxKeydown(event: KeyboardEvent) {
if (this.disabled) return;
2025-06-27 16:20:06 +00:00
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();
2025-06-27 16:20:06 +00:00
}
2023-12-20 19:09:55 +01:00
}
}
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;
}
2025-06-27 16:20:06 +00:00
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;
}
2025-06-27 16:20:06 +00:00
}
}