feat(structure): adjust

This commit is contained in:
2025-12-05 10:19:11 +00:00
parent d07fec834f
commit 7adad49cb1
97 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import './dees-panel.js';
import './dees-form.js';
import './dees-form-submit.js';
export const demoFunc = () => html`
<style>
${css`
.demo-container {
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
dees-panel {
margin-bottom: 24px;
}
dees-panel:last-child {
margin-bottom: 0;
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.spacer {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
`}
</style>
<div class="demo-container">
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate programmatic interaction with basic dropdowns
const countryDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Country"]');
const roleDropdown = elementArg.querySelector('dees-input-dropdown[label="Select Role"]');
// Log when country changes
if (countryDropdown) {
countryDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Country selected:', event.detail);
});
}
// Log when role changes
if (roleDropdown) {
roleDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Role selected:', event.detail);
});
}
}}>
<dees-panel .title=${'1. Basic Dropdowns'} .subtitle=${'Standard dropdown with search functionality and various options'}>
<dees-input-dropdown
.label=${'Select Country'}
.options=${[
{ option: 'United States', key: 'us' },
{ option: 'Canada', key: 'ca' },
{ option: 'Germany', key: 'de' },
{ option: 'France', key: 'fr' },
{ option: 'United Kingdom', key: 'uk' },
{ option: 'Australia', key: 'au' },
{ option: 'Japan', key: 'jp' },
{ option: 'Brazil', key: 'br' }
]}
.selectedOption=${{ option: 'United States', key: 'us' }}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Select Role'}
.options=${[
{ option: 'Administrator', key: 'admin' },
{ option: 'Editor', key: 'editor' },
{ option: 'Viewer', key: 'viewer' },
{ option: 'Guest', key: 'guest' }
]}
></dees-input-dropdown>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate simpler dropdown without search
const priorityDropdown = elementArg.querySelector('dees-input-dropdown');
if (priorityDropdown) {
priorityDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log(`Priority changed to: ${event.detail.option}`);
});
}
}}>
<dees-panel .title=${'2. Without Search'} .subtitle=${'Dropdown with search functionality disabled for simpler selection'}>
<dees-input-dropdown
.label=${'Priority Level'}
.enableSearch=${false}
.options=${[
{ option: 'High', key: 'high' },
{ option: 'Medium', key: 'medium' },
{ option: 'Low', key: 'low' }
]}
.selectedOption=${{ option: 'Medium', key: 'medium' }}
></dees-input-dropdown>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate horizontal layout with multiple dropdowns
const dropdowns = elementArg.querySelectorAll('dees-input-dropdown');
// Log all changes from horizontal dropdowns
dropdowns.forEach((dropdown) => {
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const label = dropdown.getAttribute('label');
console.log(`${label}: ${event.detail.option}`);
});
});
}}>
<dees-panel .title=${'3. Horizontal Layout'} .subtitle=${'Multiple dropdowns in a horizontal layout for compact forms'}>
<div class="horizontal-group">
<dees-input-dropdown
.label=${'Department'}
.layoutMode=${'horizontal'}
.options=${[
{ option: 'Engineering', key: 'eng' },
{ option: 'Design', key: 'design' },
{ option: 'Marketing', key: 'marketing' },
{ option: 'Sales', key: 'sales' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Team Size'}
.layoutMode=${'horizontal'}
.enableSearch=${false}
.options=${[
{ option: '1-5', key: 'small' },
{ option: '6-20', key: 'medium' },
{ option: '21-50', key: 'large' },
{ option: '50+', key: 'xlarge' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Location'}
.layoutMode=${'horizontal'}
.options=${[
{ option: 'Remote', key: 'remote' },
{ option: 'On-site', key: 'onsite' },
{ option: 'Hybrid', key: 'hybrid' }
]}
></dees-input-dropdown>
</div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate state handling
const requiredDropdown = elementArg.querySelector('dees-input-dropdown[required]');
if (requiredDropdown) {
// Show validation state changes
requiredDropdown.addEventListener('blur', () => {
console.log('Required dropdown lost focus');
});
}
}}>
<dees-panel .title=${'4. States'} .subtitle=${'Different states and configurations'}>
<dees-input-dropdown
.label=${'Required Field'}
.required=${true}
.options=${[
{ option: 'Option A', key: 'a' },
{ option: 'Option B', key: 'b' },
{ option: 'Option C', key: 'c' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Disabled Dropdown'}
.disabled=${true}
.options=${[
{ option: 'Cannot Select', key: 'disabled' }
]}
.selectedOption=${{ option: 'Cannot Select', key: 'disabled' }}
></dees-input-dropdown>
</dees-panel>
</dees-demowrapper>
<div class="spacer">
(Spacer to test dropdown positioning)
</div>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// This dropdown demonstrates automatic positioning
const dropdown = elementArg.querySelector('dees-input-dropdown');
if (dropdown) {
dropdown.addEventListener('selectedOption', (event: CustomEvent) => {
console.log('Bottom dropdown selected:', event.detail);
});
// Note: The dropdown automatically detects available space
// and opens upward when near the bottom of the viewport
}
}}>
<dees-panel .title=${'5. Bottom Positioning'} .subtitle=${'Dropdown that opens upward when near bottom of viewport'}>
<dees-input-dropdown
.label=${'Opens Upward'}
.options=${[
{ option: 'First Option', key: 'first' },
{ option: 'Second Option', key: 'second' },
{ option: 'Third Option', key: 'third' },
{ option: 'Fourth Option', key: 'fourth' },
{ option: 'Fifth Option', key: 'fifth' }
]}
></dees-input-dropdown>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Setup the interactive payload display
const dropdown = elementArg.querySelector('dees-input-dropdown');
const output = elementArg.querySelector('#selection-output');
if (dropdown && output) {
// Initialize output
output.innerHTML = '<em>Select a product to see details...</em>';
// Handle dropdown changes
dropdown.addEventListener('change', (event: CustomEvent) => {
if (event.detail.value) {
output.innerHTML = `
<strong>Selected:</strong> ${event.detail.value.option}<br>
<strong>Key:</strong> ${event.detail.value.key}<br>
<strong>Price:</strong> $${event.detail.value.payload?.price || 'N/A'}<br>
<strong>Features:</strong> ${event.detail.value.payload?.features?.join(', ') || 'N/A'}
`;
}
});
}
}}>
<dees-panel .title=${'6. Event Handling & Payload'} .subtitle=${'Dropdown with payload data and change event handling'}>
<dees-input-dropdown
.label=${'Select Product'}
.options=${[
{ option: 'Basic Plan', key: 'basic', payload: { price: 9.99, features: ['Feature A'] } },
{ option: 'Pro Plan', key: 'pro', payload: { price: 19.99, features: ['Feature A', 'Feature B'] } },
{ option: 'Enterprise Plan', key: 'enterprise', payload: { price: 49.99, features: ['Feature A', 'Feature B', 'Feature C'] } }
]}
></dees-input-dropdown>
<div id="selection-output" style="margin-top: 16px; padding: 12px; background: rgba(0, 105, 242, 0.1); border-radius: 4px; font-size: 14px;"></div>
</dees-panel>
</dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Demonstrate form integration and validation
const form = elementArg.querySelector('dees-form');
const projectTypeDropdown = elementArg.querySelector('dees-input-dropdown[key="projectType"]');
const frameworkDropdown = elementArg.querySelector('dees-input-dropdown[key="framework"]');
if (form) {
form.addEventListener('formData', (event: CustomEvent) => {
console.log('Form submitted with data:', event.detail.data);
});
}
if (projectTypeDropdown && frameworkDropdown) {
// Filter frameworks based on project type
projectTypeDropdown.addEventListener('selectedOption', (event: CustomEvent) => {
const selectedType = event.detail.key;
console.log(`Project type changed to: ${selectedType}`);
// In a real app, you could filter the framework options based on project type
// For demo purposes, we just log the change
});
}
}}>
<dees-panel .title=${'7. Form Integration'} .subtitle=${'Dropdown working within a form with validation'}>
<dees-form>
<dees-input-dropdown
.label=${'Project Type'}
.key=${'projectType'}
.required=${true}
.options=${[
{ option: 'Web Application', key: 'web' },
{ option: 'Mobile Application', key: 'mobile' },
{ option: 'Desktop Application', key: 'desktop' },
{ option: 'API Service', key: 'api' }
]}
></dees-input-dropdown>
<dees-input-dropdown
.label=${'Development Framework'}
.key=${'framework'}
.required=${true}
.options=${[
{ option: 'React', key: 'react', payload: { type: 'web' } },
{ option: 'Vue.js', key: 'vue', payload: { type: 'web' } },
{ option: 'Angular', key: 'angular', payload: { type: 'web' } },
{ option: 'React Native', key: 'react-native', payload: { type: 'mobile' } },
{ option: 'Flutter', key: 'flutter', payload: { type: 'mobile' } },
{ option: 'Electron', key: 'electron', payload: { type: 'desktop' } }
]}
></dees-input-dropdown>
<dees-form-submit .text=${'Create Project'}></dees-form-submit>
</dees-form>
</dees-panel>
</dees-demowrapper>
</div>
`

View File

@@ -0,0 +1,466 @@
import {
customElement,
type TemplateResult,
property,
state,
html,
css,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesInputBase } from './dees-input-base.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
'dees-input-dropdown': DeesInputDropdown;
}
}
@customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public static demo = demoFunc;
// INSTANCE
@property()
accessor options: { option: string; key: string; payload?: any }[] = [];
@property()
accessor selectedOption: { option: string; key: string; payload?: any } = null;
// Add value property for form compatibility
public get value() {
return this.selectedOption;
}
public set value(val: { option: string; key: string; payload?: any }) {
this.selectedOption = val;
}
@property({
type: Boolean,
})
accessor enableSearch: boolean = true;
@state()
accessor opensToTop: boolean = false;
@state()
accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
@state()
accessor highlightedIndex: number = 0;
@state()
accessor isOpened = false;
@state()
accessor searchValue: string = '';
public static styles = [
...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);
}
.selectionBox {
will-change: transform, opacity;
pointer-events: none;
transition: all 0.15s ease;
opacity: 0;
transform: translateY(-8px) scale(0.98);
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
box-shadow: 0 4px 6px -1px hsl(0 0% 0% / 0.1), 0 2px 4px -2px hsl(0 0% 0% / 0.1);
min-height: 40px;
max-height: 300px;
overflow: hidden;
border-radius: 6px;
position: absolute;
user-select: none;
margin-top: 4px;
z-index: 50;
left: 0;
right: 0;
}
.selectionBox.top {
bottom: calc(100% + 4px);
top: auto;
margin-top: 0;
margin-bottom: 4px;
transform: translateY(8px) scale(0.98);
}
.selectionBox.bottom {
top: 100%;
}
.selectionBox.show {
pointer-events: all;
transform: translateY(0) scale(1);
opacity: 1;
}
/* Options container */
.options-container {
max-height: 250px;
overflow-y: auto;
padding: 4px;
}
/* Options */
.option {
transition: all 0.15s ease;
line-height: 32px;
padding: 0 8px;
border-radius: 4px;
margin: 2px 0;
cursor: pointer;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}
.option.highlighted {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
}
.option:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
/* No options message */
.no-options {
padding: 8px;
text-align: center;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
font-style: italic;
}
/* Search */
.search {
padding: 4px;
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin-bottom: 4px;
}
.search.bottom {
border-bottom: none;
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin-bottom: 0;
margin-top: 4px;
}
.search input {
display: block;
width: 100%;
height: 32px;
padding: 0 8px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
color: inherit;
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color 0.15s ease;
}
.search input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
}
.search input:focus {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
}
/* Scrollbar styling */
.options-container::-webkit-scrollbar {
width: 8px;
}
.options-container::-webkit-scrollbar-track {
background: transparent;
}
.options-container::-webkit-scrollbar-thumb {
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
}
.options-container::-webkit-scrollbar-thumb:hover {
background: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
`,
];
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 class="selectionBox ${this.isOpened ? 'show' : ''} ${this.opensToTop ? 'top' : 'bottom'}">
${this.enableSearch
? html`
<div class="search">
<input
type="text"
placeholder="Search options..."
.value="${this.searchValue}"
@input="${this.handleSearch}"
@click="${(e: Event) => e.stopPropagation()}"
@keydown="${this.handleSearchKeydown}"
/>
</div>
`
: null}
<div class="options-container">
${this.filteredOptions.length === 0
? html`<div class="no-options">No options found</div>`
: this.filteredOptions.map((option, index) => {
const isHighlighted = this.highlightedIndex === index;
return html`
<div
class="option ${isHighlighted ? 'highlighted' : ''}"
@click="${() => this.updateSelection(option)}"
@mouseenter="${() => this.highlightedIndex = index}"
>
${option.option}
</div>
`;
})
}
</div>
</div>
</div>
</div>
`;
}
async connectedCallback() {
super.connectedCallback();
this.handleClickOutside = this.handleClickOutside.bind(this);
}
firstUpdated() {
this.selectedOption = this.selectedOption || null;
this.filteredOptions = this.options;
}
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('options')) {
this.filteredOptions = this.options;
}
}
public async updateSelection(selectedOption: { option: string; key: string; payload?: any }) {
this.selectedOption = selectedOption;
this.isOpened = false;
this.searchValue = '';
this.filteredOptions = this.options;
this.highlightedIndex = 0;
this.dispatchEvent(
new CustomEvent('selectedOption', {
detail: selectedOption,
bubbles: true,
})
);
this.changeSubject.next(this);
}
private handleClickOutside = (event: MouseEvent) => {
const path = event.composedPath();
if (!path.includes(this)) {
this.isOpened = false;
this.searchValue = '';
this.filteredOptions = this.options;
document.removeEventListener('click', this.handleClickOutside);
}
};
public async toggleSelectionBox() {
this.isOpened = !this.isOpened;
if (this.isOpened) {
// Check available space and set position
const selectedBox = this.shadowRoot.querySelector('.selectedBox') as HTMLElement;
const rect = selectedBox.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
// Determine if we should open upwards
this.opensToTop = spaceBelow < 300 && spaceAbove > spaceBelow;
// Focus search input if present
await this.updateComplete;
const searchInput = this.shadowRoot.querySelector('.search input') as HTMLInputElement;
if (searchInput) {
searchInput.focus();
}
// Add click outside listener
setTimeout(() => {
document.addEventListener('click', this.handleClickOutside);
}, 0);
} else {
// Cleanup
this.searchValue = '';
this.filteredOptions = this.options;
document.removeEventListener('click', this.handleClickOutside);
}
}
private handleSearch(event: Event): void {
const searchTerm = (event.target as HTMLInputElement).value;
this.searchValue = searchTerm;
const searchLower = searchTerm.toLowerCase();
this.filteredOptions = this.options.filter((option) =>
option.option.toLowerCase().includes(searchLower)
);
this.highlightedIndex = 0;
}
private handleKeyDown(event: KeyboardEvent): void {
const key = event.key;
const maxIndex = this.filteredOptions.length - 1;
if (key === 'ArrowDown') {
event.preventDefault();
this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
} else if (key === 'ArrowUp') {
event.preventDefault();
this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
} else if (key === 'Enter') {
event.preventDefault();
if (this.filteredOptions[this.highlightedIndex]) {
this.updateSelection(this.filteredOptions[this.highlightedIndex]);
}
} else if (key === 'Escape') {
event.preventDefault();
this.isOpened = false;
}
}
private handleSearchKeydown(event: KeyboardEvent): void {
if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter') {
this.handleKeyDown(event);
}
}
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.isOpened = false;
}
}
}
public getValue(): { option: string; key: string; payload?: any } {
return this.selectedOption;
}
public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value;
}
async disconnectedCallback() {
await super.disconnectedCallback();
document.removeEventListener('click', this.handleClickOutside);
}
}