296 lines
8.6 KiB
TypeScript
296 lines
8.6 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 (appends to document.body)
|
|
this.popupInstance.show();
|
|
|
|
// Focus search input
|
|
if (this.enableSearch) {
|
|
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;
|
|
}
|
|
}
|
|
}
|