feat: Implement unified input component architecture with standardized margins and layout modes

This commit is contained in:
Juergen Kunz
2025-06-19 09:41:00 +00:00
parent 8fb5e2e2a2
commit 79b1a4ea9f
7 changed files with 482 additions and 357 deletions

View File

@ -1,202 +1,174 @@
# dees-appui-appbar Improvement Plan # Input Component Unification Plan
## Phase 1: Core Menu System Command to reread guidelines: `cat /home/philkunz/.claude/CLAUDE.md`
### Menu Data Structure ## Problem Summary
- [x] Extend existing `plugins.tsclass.website.IMenuItem` to create `IAppBarMenuItem` with additional properties: id, shortcut, submenu, divider, disabled
- [x] Create `IMenuBar` interface with menuItems array and onMenuSelect callback
- [x] Add `@property() menuItems` to accept menu configuration
- [x] Add `@property() onMenuSelect` event handler
- [ ] Consider reusing existing `interfaces.ITab` for simpler menu scenarios
### Basic Menu Rendering The dees-input components have inconsistent margin behavior causing vertical alignment issues in horizontal flexbox layouts:
- [x] Replace hardcoded menu items with dynamic rendering from menuItems property
- [x] Add support for menu item icons
- [x] Implement menu item disabled state styling
- [x] Add menu separator/divider support
### Dropdown Implementation - **dees-input-text**: 8px top, 24px bottom margin
- [x] Create dropdown container component (consider reusing logic from dees-contextmenu) - **dees-input-dropdown**: 0px top, 24px bottom margin
- [x] Implement click to open/close dropdown - **dees-input-checkbox/radio**: 20px top, 20px bottom margin
- [x] Add dropdown positioning logic (below menu item) - Different components use different label implementations (some use dees-label, others have built-in labels)
- [x] Implement click outside to close
- [x] Add dropdown arrow/caret indicator
- [x] Style dropdown with shadows and borders
- [ ] Ensure visual consistency with existing dees-contextmenu component
### Keyboard Navigation ## Proposed Solution
- [x] Add tabindex to menu items
- [x] Implement Tab navigation between top-level items
- [x] Add Enter key to open dropdown
- [x] Implement arrow keys for dropdown navigation
- [x] Add Escape key to close dropdown
- [x] Implement Home/End keys for first/last item
### Submenu Support ### 1. Standardize Margin System
- [ ] Detect submenu items and add arrow indicator
- [ ] Implement submenu positioning (to the right)
- [ ] Add hover delay before opening submenu
- [ ] Handle nested keyboard navigation
- [ ] Prevent submenus from going off-screen
## Phase 2: Breadcrumb Navigation & Theming Create a unified margin approach for all input components:
### Breadcrumb System ```css
- [ ] Define `IBreadcrumb` interface with label, path, icon /* Default vertical stacking mode (for forms) */
- [x] Add `@property() breadcrumbs` array :host {
- [x] Add `@property() breadcrumbSeparator` (default '>') margin: 0;
- [x] Implement breadcrumb rendering with separators margin-bottom: 16px; /* Reduced from 24px for better density */
- [x] Add click handlers for navigation }
- [x] Emit 'breadcrumb-navigate' custom event
- [ ] Add breadcrumb truncation for long paths
- [ ] Implement breadcrumb overflow with horizontal scroll
### Theme Support /* Last child in container should have no bottom margin */
- [x] Add CSS variables for all colors and sizes :host(:last-child) {
- [x] Create `--appbar-height` variable (default 40px) margin-bottom: 0;
- [x] Add `--appbar-bg`, `--appbar-text`, `--appbar-border` variables }
- [x] Implement `--appbar-hover` and `--appbar-active` states
- [x] Add `@property() theme` with 'light' | 'dark' options
- [x] Create light theme CSS variables
- [x] Add theme toggle to demo
### Visual Improvements /* Horizontal layout mode - activated by parent context or attribute */
- [x] Add smooth transitions for hover states :host([horizontal-layout]) {
- [ ] Implement ripple effect on click margin: 0;
- [x] Add focus ring styles for accessibility margin-right: 16px;
- [x] Improve menu item padding and spacing margin-bottom: 0;
- [ ] Add subtle gradient or texture to appbar }
## Phase 3: Search & User Account :host([horizontal-layout]:last-child) {
margin-right: 0;
}
```
### Search Integration ### 2. Unified Label Architecture
- [x] Add search icon in center section
- [ ] Create expandable search input
- [x] Add `@property() showSearch` boolean
- [ ] Implement Cmd/Ctrl+K keyboard shortcut
- [ ] Add search input with placeholder
- [x] Emit 'search-submit' event
- [ ] Add search suggestions dropdown
- [ ] Implement recent searches storage
### User Account Section All input components should use the `dees-label` component for consistency:
- [ ] Define `IUserAccount` interface
- [x] Add `@property() user` for user data
- [x] Render user avatar (with fallback to initials)
- [x] Display user name
- [x] Add status indicator (online/offline/busy/away)
- [ ] Create user dropdown menu
- [ ] Add logout/settings options
- [x] Emit 'user-menu-open' event
## Phase 4: Platform & Accessibility - Move label rendering from built-in implementations to `dees-label` usage
- Add a `labelPosition` property to all inputs: `'top' | 'left' | 'right' | 'none'`
- Default to 'top' for text/dropdown, 'right' for checkbox/radio
### Platform-Specific Features ### 3. Layout Mode Support
- [ ] Add `@property() platform` detection
- [x] Conditionally show window controls
- [ ] Implement platform-specific styling (macOS/Windows/Linux)
- [ ] Add platform-specific keyboard shortcuts
- [ ] Handle window dragging per platform
- [ ] Add fullscreen toggle button
### Window Controls Integration Add a `layoutMode` property to all input components:
- [ ] Make window controls position configurable
- [x] Add `@property() showWindowControls`
- [ ] Handle window controls on different platforms
- [ ] Add minimize/maximize/close functionality
- [ ] Style window controls to match theme
### Accessibility (A11Y) ```typescript
- [x] Add proper ARIA roles (menubar, menuitem) @property({ type: String })
- [x] Implement aria-haspopup for dropdowns public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
- [x] Add aria-expanded state ```
- [ ] Include aria-label for navigation
- [ ] Support screen reader announcements
- [ ] Add high contrast mode support
- [ ] Implement focus trap in dropdowns
- [ ] Add skip navigation link
### Responsive Design - `vertical`: Traditional form layout (label on top)
- [ ] Add breakpoint detection - `horizontal`: Inline layout (label position configurable)
- [ ] Implement hamburger menu for mobile - `auto`: Detect from parent context
- [ ] Create slide-out menu drawer
- [ ] Make breadcrumbs responsive
- [ ] Hide non-essential items on small screens
- [ ] Add touch gesture support
## Phase 5: Advanced Features ### 4. Implementation Steps
### Notification System 1. **Create base input class** (`DeesInputBase`):
- [ ] Add notification icon with badge - Common margin styles
- [ ] Create `@property() notifications` array - Layout mode detection
- [ ] Implement notification dropdown - Label position handling
- [ ] Add notification actions (mark read, dismiss) - Shared properties (key, required, disabled, value)
- [ ] Emit notification events
- [ ] Add notification sound option
- [ ] Implement notification grouping
### Plugin System 2. **Update dees-input-text**:
- [ ] Define `IAppBarPlugin` interface - Extend from DeesInputBase
- [ ] Add plugin registration method - Remove hardcoded margins
- [ ] Implement plugin rendering slots - Keep using dees-label component
- [ ] Add plugin positioning (left/center/right)
- [ ] Support plugin weight for ordering
- [ ] Create plugin lifecycle hooks
### Custom Slots 3. **Update dees-input-dropdown**:
- [ ] Add named slots for sections - Extend from DeesInputBase
- [ ] Implement slot change detection - Remove hardcoded margins
- [ ] Style slotted content appropriately - Switch from built-in label to dees-label
- [ ] Document slot usage
### Context Menus 4. **Update dees-input-checkbox**:
- [ ] Add right-click context menu support - Extend from DeesInputBase
- [ ] Implement context menu positioning - Remove hardcoded margins
- [ ] Add context-specific menu items - Add support for label position (keep default as 'right')
- [ ] Support custom context menus - Switch to dees-label component
## Phase 6: Performance & Polish 5. **Update dees-input-radio**:
- Same as checkbox
### Performance Optimizations 6. **Update dees-form**:
- [ ] Implement virtual scrolling for long menus - Add property to control child input layout mode
- [ ] Add lazy loading for submenu content - Ensure proper spacing context
- [ ] Debounce search input
- [ ] Memoize menu rendering
- [ ] Optimize re-renders with lit's `guard` directive
- [ ] Add loading states for async operations
### Testing ### 5. CSS Variable System
- [x] Create comprehensive demo page
- [ ] Add unit tests for menu logic
- [ ] Test keyboard navigation
- [ ] Test platform-specific behavior
- [ ] Add visual regression tests
- [ ] Test accessibility with screen readers
- [ ] Performance benchmark tests
### Documentation Introduce CSS variables for consistent spacing:
- [ ] Document all properties and methods
- [x] Create usage examples
- [ ] Add migration guide from current version
- [ ] Document keyboard shortcuts
- [ ] Create accessibility guide
- [ ] Add troubleshooting section
### Polish ```css
- [ ] Add loading skeletons :host {
- [ ] Implement error states --dees-input-spacing-unit: 8px;
- [ ] Add empty states --dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
- [ ] Create onboarding tooltips --dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
- [ ] Add animation preferences (reduced motion) --dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
- [ ] Implement print styles }
```
## Completion Tracking ### 6. Backward Compatibility
- Phase 1: 20/22 tasks - Keep existing properties and methods
- Phase 2: 10/15 tasks - Add deprecation notices for properties that will be removed
- Phase 3: 6/16 tasks - Provide migration guide in documentation
- Phase 4: 6/24 tasks
- Phase 5: 0/18 tasks
- Phase 6: 2/20 tasks
**Total: 44/115 tasks completed** ### 7. Testing Requirements
- Test all inputs in vertical form layouts
- Test all inputs in horizontal flexbox containers
- Test mixed input types in same container
- Test with and without labels
- Test theme switching (light/dark)
- Test responsive behavior
## Expected Outcome
- All input components will align properly in horizontal layouts
- Consistent spacing in vertical forms
- Unified label handling across all inputs
- Better developer experience with predictable behavior
- Maintained backward compatibility
## Timeline
1. Phase 1: Create DeesInputBase class and update dees-input-text ✅
2. Phase 2: Update remaining input components ✅
3. Phase 3: Update documentation and examples
4. Phase 4: Testing and refinement ✅
## Implementation Status
### Completed:
1. **Created DeesInputBase class** (`dees-input-base.ts`):
- Generic base class with unified margin system
- Layout mode support (vertical/horizontal/auto)
- Label position control
- Common properties and methods
- CSS variables for consistent spacing
2. **Updated all input components**:
- `dees-input-text`: Now extends DeesInputBase, margins removed
- `dees-input-dropdown`: Now extends DeesInputBase, uses dees-label
- `dees-input-checkbox`: Now extends DeesInputBase, uses dees-label (default label position: right)
- `dees-input-radio`: Now extends DeesInputBase, uses dees-label (default label position: right)
3. **Updated dees-form**:
- Added `horizontal-layout` property
- Auto-detection of layout mode for child inputs
- Added dropdown to form input types
4. **Fixed TypeScript errors**:
- Added value property to dropdown for form compatibility
- Fixed changeSubject typing
- Updated form value type to include dropdown options
### Result:
All input components now have:
- Unified 16px bottom margin in vertical layouts
- 16px right margin in horizontal layouts
- No margin on last child
- Consistent label handling via dees-label
- Flexible layout modes
- Better alignment in flexbox containers

View File

@ -4,6 +4,7 @@ import {
type TemplateResult, type TemplateResult,
DeesElement, DeesElement,
type CSSResult, type CSSResult,
property,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
@ -11,6 +12,7 @@ import { DeesInputCheckbox } from './dees-input-checkbox.js';
import { DeesInputText } from './dees-input-text.js'; import { DeesInputText } from './dees-input-text.js';
import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js';
import { DeesInputRadio } from './dees-input-radio.js'; import { DeesInputRadio } from './dees-input-radio.js';
import { DeesInputDropdown } from './dees-input-dropdown.js';
import { DeesFormSubmit } from './dees-form-submit.js'; import { DeesFormSubmit } from './dees-form-submit.js';
import { DeesTable } from './dees-table.js'; import { DeesTable } from './dees-table.js';
import { demoFunc } from './dees-form.demo.js'; import { demoFunc } from './dees-form.demo.js';
@ -19,6 +21,7 @@ import { DeesInputIban } from './dees-input-iban.js';
// Unified set for form input types // Unified set for form input types
const FORM_INPUT_TYPES = [ const FORM_INPUT_TYPES = [
DeesInputCheckbox, DeesInputCheckbox,
DeesInputDropdown,
DeesInputIban, DeesInputIban,
DeesInputText, DeesInputText,
DeesInputQuantitySelector, DeesInputQuantitySelector,
@ -28,6 +31,7 @@ const FORM_INPUT_TYPES = [
export type TFormInputElement = export type TFormInputElement =
| DeesInputCheckbox | DeesInputCheckbox
| DeesInputDropdown
| DeesInputIban | DeesInputIban
| DeesInputText | DeesInputText
| DeesInputQuantitySelector | DeesInputQuantitySelector
@ -48,6 +52,13 @@ export class DeesForm extends DeesElement {
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject(); public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
public readyDeferred = domtools.plugins.smartpromise.defer(); public readyDeferred = domtools.plugins.smartpromise.defer();
/**
* Controls the layout mode of child input components
* When true, sets all child inputs to horizontal layout
*/
@property({ type: Boolean, reflect: true, attribute: 'horizontal-layout' })
public horizontalLayout: boolean = false;
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<style> <style>
@ -62,6 +73,7 @@ export class DeesForm extends DeesElement {
public async firstUpdated() { public async firstUpdated() {
const formChildren = this.getFormElements(); const formChildren = this.getFormElements();
this.updateRequiredStatus(); this.updateRequiredStatus();
this.updateChildrenLayoutMode();
for (const child of formChildren) { for (const child of formChildren) {
child.changeSubject.subscribe(async () => { child.changeSubject.subscribe(async () => {
@ -107,7 +119,7 @@ export class DeesForm extends DeesElement {
*/ */
public async collectFormData() { public async collectFormData() {
const children = this.getFormElements(); const children = this.getFormElements();
const valueObject: { [key: string]: string | number | boolean | any[] } = {}; const valueObject: { [key: string]: string | number | boolean | any[] | { option: string; key: string; payload?: any } } = {};
for (const child of children) { for (const child of children) {
if (!child.key) { if (!child.key) {
console.log(`form element with label "${child.label}" has no key. skipping.`); console.log(`form element with label "${child.label}" has no key. skipping.`);
@ -202,4 +214,28 @@ export class DeesForm extends DeesElement {
} }
}); });
} }
/**
* Updates the layout mode of child input components based on form's horizontalLayout property
*/
private updateChildrenLayoutMode() {
const formChildren = this.getFormElements();
for (const child of formChildren) {
if ('layoutMode' in child) {
// The child's auto mode will detect this form's horizontal-layout attribute
(child as any).layoutMode = 'auto';
}
}
}
/**
* Called when properties change
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('horizontalLayout')) {
this.updateChildrenLayoutMode();
}
}
} }

View File

@ -0,0 +1,184 @@
import {
DeesElement,
property,
css,
type CSSResult,
cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
/**
* Base class for all dees-input components
* Provides unified margin system and layout mode support
*/
export abstract class DeesInputBase<T = any> extends DeesElement {
/**
* Layout mode for the input component
* - vertical: Traditional form layout (label on top)
* - horizontal: Inline layout (label position configurable)
* - auto: Detect from parent context
*/
@property({ type: String })
public layoutMode: 'vertical' | 'horizontal' | 'auto' = 'auto';
/**
* Position of the label relative to the input
*/
@property({ type: String })
public labelPosition: 'top' | 'left' | 'right' | 'none' = 'top';
/**
* Common properties for all inputs
*/
@property({ type: String })
public key: string;
@property({ type: String })
public label: string;
@property({ type: Boolean })
public required: boolean = false;
@property({ type: Boolean })
public disabled: boolean = false;
@property({ type: String })
public description: string;
/**
* Common styles for all input components
*/
public static get baseStyles(): CSSResult[] {
return [
css`
/* CSS Variables for consistent spacing */
:host {
--dees-input-spacing-unit: 8px;
--dees-input-vertical-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-horizontal-gap: calc(var(--dees-input-spacing-unit) * 2); /* 16px */
--dees-input-label-gap: var(--dees-input-spacing-unit); /* 8px */
}
/* Default vertical stacking mode (for forms) */
:host {
display: block;
margin: 0;
margin-bottom: var(--dees-input-vertical-gap);
}
/* Last child in container should have no bottom margin */
:host(:last-child) {
margin-bottom: 0;
}
/* Horizontal layout mode - activated by attribute */
:host([layout-mode="horizontal"]) {
display: inline-block;
margin: 0;
margin-right: var(--dees-input-horizontal-gap);
margin-bottom: 0;
}
:host([layout-mode="horizontal"]:last-child) {
margin-right: 0;
}
/* Auto mode - inherit from parent dees-form if present */
/* Label position variations */
:host([label-position="left"]) .input-wrapper {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="right"]) .input-wrapper {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--dees-input-label-gap);
align-items: center;
}
:host([label-position="top"]) .input-wrapper {
display: block;
}
:host([label-position="none"]) dees-label {
display: none;
}
`,
];
}
/**
* Subject for value changes that all inputs should implement
*/
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<T>();
/**
* Called when the element is connected to the DOM
* Sets up layout mode detection
*/
async connectedCallback() {
await super.connectedCallback();
this.detectLayoutMode();
}
/**
* Detects the appropriate layout mode based on parent context
*/
private detectLayoutMode() {
if (this.layoutMode !== 'auto') {
this.setAttribute('layout-mode', this.layoutMode);
return;
}
// Check if parent is a form with horizontal layout
const parentForm = this.closest('dees-form');
if (parentForm && parentForm.hasAttribute('horizontal-layout')) {
this.setAttribute('layout-mode', 'horizontal');
} else {
this.setAttribute('layout-mode', 'vertical');
}
}
/**
* Updates the layout mode attribute when property changes
*/
updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('layoutMode')) {
this.detectLayoutMode();
}
if (changedProperties.has('labelPosition')) {
this.setAttribute('label-position', this.labelPosition);
}
}
/**
* Standard method for freezing input (disabling)
*/
public async freeze() {
this.disabled = true;
}
/**
* Standard method for unfreezing input (enabling)
*/
public async unfreeze() {
this.disabled = false;
}
/**
* Abstract method that child classes must implement to get their value
*/
public abstract getValue(): any;
/**
* Abstract method that child classes must implement to set their value
*/
public abstract setValue(value: any): void;
}

View File

@ -1,14 +1,13 @@
import { import {
customElement, customElement,
DeesElement,
type TemplateResult, type TemplateResult,
property, property,
html, html,
css, css,
cssManager, cssManager,
type CSSResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -17,51 +16,33 @@ declare global {
} }
@customElement('dees-input-checkbox') @customElement('dees-input-checkbox')
export class DeesInputCheckbox extends DeesElement { export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
// STATIC // STATIC
public static demo = () => html`<dees-input-checkbox></dees-input-checkbox>`; public static demo = () => html`<dees-input-checkbox></dees-input-checkbox>`;
// INSTANCE // INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property({
type: String,
})
public label: string = 'Label';
@property({ @property({
type: Boolean, type: Boolean,
}) })
public value: boolean = false; public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({ constructor() {
type: Boolean super();
}) this.labelPosition = 'right'; // Checkboxes default to label on the right
public disabled: boolean = false; }
public render(): TemplateResult { public static styles = [
return html` ...DeesInputBase.baseStyles,
${domtools.elementBasic.styles} cssManager.defaultStyles,
<style> css`
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
:host { :host {
display: block;
position: relative; position: relative;
margin: 20px 0px;
cursor: default; cursor: default;
} }
:host(:hover) { :host(:hover) {
@ -69,21 +50,12 @@ export class DeesInputCheckbox extends DeesElement {
} }
.maincontainer { .maincontainer {
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px; padding: 5px 0px;
color: ${this.goBright ? '#333' : '#ccc'}; color: ${cssManager.bdTheme('#333', '#ccc')};
} }
.maincontainer:hover { .maincontainer:hover {
${this.goBright ? '#000' : '#ccc'}; color: ${cssManager.bdTheme('#000', '#fff')};
}
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
} }
input:focus { input:focus {
@ -94,12 +66,12 @@ export class DeesInputCheckbox extends DeesElement {
.checkbox { .checkbox {
transition: all 0.1s; transition: all 0.1s;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid ${this.goBright ? '#CCC' : '#999'}; border: 1px solid ${cssManager.bdTheme('#CCC', '#999')};
border-radius: 2px; border-radius: 2px;
height: 24px; height: 24px;
width: 24px; width: 24px;
display: inline-block; display: inline-block;
background: ${this.goBright ? '#fafafa' : '#222'}; background: ${cssManager.bdTheme('#fafafa', '#222')};
} }
.checkbox.selected { .checkbox.selected {
@ -146,19 +118,25 @@ export class DeesInputCheckbox extends DeesElement {
img { img {
padding: 4px; padding: 4px;
} }
</style> `,
<div class="maincontainer" @click="${this.toggleSelected}"> ];
<div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
${this.value public render(): TemplateResult {
? html` return html`
<span class="checkmark"> <div class="input-wrapper">
<div class="checkmark_stem"></div> <div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkmark_kick"></div> <div class="checkbox ${this.value ? 'selected' : ''} ${this.disabled ? 'disabled' : ''}" tabindex="0">
</span> ${this.value
` ? html`
: html``} <span class="checkmark">
<div class="checkmark_stem"></div>
<div class="checkmark_kick"></div>
</span>
`
: html``}
</div>
</div> </div>
<div class="label">${this.label}</div> <dees-label .label=${this.label}></dees-label>
</div> </div>
`; `;
} }
@ -177,6 +155,14 @@ export class DeesInputCheckbox extends DeesElement {
this.changeSubject.next(this); this.changeSubject.next(this);
} }
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
public focus(): void { public focus(): void {
const checkboxDiv = this.shadowRoot.querySelector('.checkbox'); const checkboxDiv = this.shadowRoot.querySelector('.checkbox');
if (checkboxDiv) { if (checkboxDiv) {

View File

@ -1,17 +1,16 @@
import { import {
customElement, customElement,
DeesElement,
type TemplateResult, type TemplateResult,
property, property,
state, state,
html, html,
css, css,
cssManager, cssManager,
type CSSResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js'; import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesWindowLayer } from './dees-windowlayer.js'; import { DeesWindowLayer } from './dees-windowlayer.js';
import { DeesInputBase } from './dees-input-base.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -20,20 +19,10 @@ declare global {
} }
@customElement('dees-input-dropdown') @customElement('dees-input-dropdown')
export class DeesInputDropdown extends DeesElement { export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
public static demo = demoFunc; public static demo = demoFunc;
// INSTANCE // INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public label: string = 'Label';
@property()
public key: string;
@property() @property()
public options: { option: string; key: string; payload?: any }[] = []; public options: { option: string; key: string; payload?: any }[] = [];
@ -41,20 +30,21 @@ export class DeesInputDropdown extends DeesElement {
@property() @property()
public selectedOption: { option: string; key: string; payload?: any } = null; public selectedOption: { option: string; key: string; payload?: any } = null;
@property({ // Add value property for form compatibility
type: Boolean, public get value() {
}) return this.selectedOption;
public required: boolean = false; }
public set value(val: { option: string; key: string; payload?: any }) {
this.selectedOption = val;
}
@property({ @property({
type: Boolean, type: Boolean,
}) })
public enableSearch: boolean = true; public enableSearch: boolean = true;
@property({
type: Boolean,
})
public disabled: boolean = false;
@state() @state()
public opensToTop: boolean = false; public opensToTop: boolean = false;
@ -69,6 +59,7 @@ export class DeesInputDropdown extends DeesElement {
public isOpened = false; public isOpened = false;
public static styles = [ public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
* { * {
@ -78,19 +69,13 @@ export class DeesInputDropdown extends DeesElement {
:host { :host {
font-family: Roboto; font-family: Roboto;
position: relative; position: relative;
display: block;
color: ${cssManager.bdTheme('#222', '#fff')}; color: ${cssManager.bdTheme('#222', '#fff')};
margin-bottom: 24px;
} }
.maincontainer { .maincontainer {
display: block; display: block;
} }
.label {
font-size: 14px;
margin-bottom: 8px;
}
.selectedBox { .selectedBox {
user-select: none; user-select: none;
@ -205,9 +190,10 @@ export class DeesInputDropdown extends DeesElement {
public render(): TemplateResult { public render(): TemplateResult {
return html` return html`
<div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}"> <div class="input-wrapper">
${this.label ? html`<div class="label">${this.label}</div>` : html``} <dees-label .label=${this.label}></dees-label>
<div class="selectionBox"> <div class="maincontainer" @keydown="${this.isOpened ? this.handleKeyDown : undefined}">
<div class="selectionBox">
${this.enableSearch && !this.opensToTop ${this.enableSearch && !this.opensToTop
? html` ? html`
<div class="search top"> <div class="search top">
@ -247,6 +233,7 @@ export class DeesInputDropdown extends DeesElement {
}}" }}"
> >
${this.selectedOption?.option || 'Select...'} ${this.selectedOption?.option || 'Select...'}
</div>
</div> </div>
</div> </div>
`; `;
@ -372,4 +359,12 @@ export class DeesInputDropdown extends DeesElement {
event.preventDefault(); event.preventDefault();
} }
} }
public getValue(): { option: string; key: string; payload?: any } {
return this.selectedOption;
}
public setValue(value: { option: string; key: string; payload?: any }): void {
this.selectedOption = value;
}
} }

View File

@ -1,5 +1,6 @@
import {customElement, DeesElement, type TemplateResult, property, html, type CSSResult,} from '@design.estate/dees-element'; import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools'; import * as domtools from '@design.estate/dees-domtools';
import { DeesInputBase } from './dees-input-base.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -8,55 +9,34 @@ declare global {
} }
@customElement('dees-input-radio') @customElement('dees-input-radio')
export class DeesInputRadio extends DeesElement { export class DeesInputRadio extends DeesInputBase<DeesInputRadio> {
public static demo = () => html`<dees-input-radio></dees-input-radio>`; public static demo = () => html`<dees-input-radio></dees-input-radio>`;
// INSTANCE // INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject();
@property({
type: String,
reflect: true,
})
public key: string;
@property()
public label: string = 'Label';
@property() @property()
public value: boolean = false; public value: boolean = false;
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean
})
public disabled: boolean = false;
constructor() { constructor() {
super(); super();
this.labelPosition = 'right'; // Radio buttons default to label on the right
} }
public render(): TemplateResult { public static styles = [
return html ` ...DeesInputBase.baseStyles,
<style> cssManager.defaultStyles,
css`
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
:host { :host {
display: block;
position: relative; position: relative;
margin: 20px 0px;
} }
.maincontainer { .maincontainer {
transition: all 0.3s; transition: all 0.3s;
display: grid;
grid-template-columns: 25px auto;
padding: 5px 0px; padding: 5px 0px;
color: #ccc; color: #ccc;
} }
@ -65,14 +45,6 @@ export class DeesInputRadio extends DeesElement {
color: #fff; color: #fff;
} }
.label {
margin-left: 15px;
line-height: 25px;
font-size: 14px;
font-weight: normal;
}
input:focus { input:focus {
outline: none; outline: none;
border-bottom: 1px solid #e4002b; border-bottom: 1px solid #e4002b;
@ -106,12 +78,18 @@ export class DeesInputRadio extends DeesElement {
height: 10px; height: 10px;
border-radius: 10px; border-radius: 10px;
} }
</style> `,
<div class="maincontainer" @click="${this.toggleSelected}"> ];
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``} public render(): TemplateResult {
return html`
<div class="input-wrapper">
<div class="maincontainer" @click="${this.toggleSelected}">
<div class="checkbox ${this.value ? 'selected' : ''}">
${this.value ? html`<div class="innercircle"></div>`: html``}
</div>
</div> </div>
<div class="label">${this.label}</div> <dees-label .label=${this.label}></dees-label>
</div> </div>
`; `;
} }
@ -124,4 +102,12 @@ export class DeesInputRadio extends DeesElement {
})); }));
this.changeSubject.next(this); this.changeSubject.next(this);
} }
public getValue(): boolean {
return this.value;
}
public setValue(value: boolean): void {
this.value = value;
}
} }

View File

@ -1,16 +1,14 @@
import * as colors from './00colors.js'; import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { import {
customElement, customElement,
DeesElement,
type TemplateResult, type TemplateResult,
property, property,
html, html,
cssManager, cssManager,
css, css,
type CSSResult,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -19,47 +17,19 @@ declare global {
} }
@customElement('dees-input-text') @customElement('dees-input-text')
export class DeesInputText extends DeesElement { export class DeesInputText extends DeesInputBase {
public static demo = () => html` public static demo = () => html`
<dees-input-text .label=${'this is a label'} .value=${'test'}></dees-input-text> <dees-input-text .label=${'this is a label'} .value=${'test'}></dees-input-text>
<dees-input-text .isPasswordBool=${true}></dees-input-text> <dees-input-text .isPasswordBool=${true}></dees-input-text>
`; `;
// INSTANCE // INSTANCE
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesInputText>();
@property({
type: String,
})
public label: string;
@property({
type: String,
})
public description: string;
@property({
type: String,
reflect: true,
})
public key: string;
@property({ @property({
type: String, type: String,
reflect: true, reflect: true,
}) })
public value: string = ''; public value: string = '';
@property({
type: Boolean,
})
public required: boolean = false;
@property({
type: Boolean,
})
public disabled: boolean = false;
@property({ @property({
type: Boolean, type: Boolean,
reflect: true, reflect: true,
@ -87,6 +57,7 @@ export class DeesInputText extends DeesElement {
validationFunction: (value: string) => boolean; validationFunction: (value: string) => boolean;
public static styles = [ public static styles = [
...DeesInputBase.baseStyles,
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
* { * {
@ -95,9 +66,6 @@ export class DeesInputText extends DeesElement {
:host { :host {
position: relative; position: relative;
display: grid;
margin: 8px 0px;
margin-bottom: 24px;
z-index: auto; z-index: auto;
} }
@ -193,33 +161,32 @@ export class DeesInputText extends DeesElement {
} }
`} `}
</style> </style>
<div class="maincontainer"> <div class="input-wrapper">
<dees-label .label=${this.label} .description=${this.description}></dees-label> <dees-label .label=${this.label} .description=${this.description}></dees-label>
<input <div class="maincontainer">
type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}" <input
.value=${this.value} type="${this.isPasswordBool && !this.showPasswordBool ? 'password' : 'text'}"
@input="${this.updateValue}" .value=${this.value}
.disabled=${this.disabled} @input="${this.updateValue}"
/> .disabled=${this.disabled}
<div class="validationContainer"> />
${this.validationText} <div class="validationContainer">
${this.validationText}
</div>
${this.isPasswordBool
? html`
<div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .iconFA=${this.showPasswordBool ? 'eye' : 'eyeSlash'}></dees-icon>
</div>
`
: html``}
</div> </div>
${this.isPasswordBool
? html`
<div class="showPassword" @click=${this.togglePasswordView}>
<dees-icon .iconFA=${this.showPasswordBool ? 'eye' : 'eyeSlash'}></dees-icon>
</div>
`
: html``}
</div> </div>
`; `;
} }
firstUpdated() { firstUpdated() {
const input = this.shadowRoot.querySelector('input'); // Input event handling is already done in updateValue method
input.addEventListener('input', (eventArg: InputEvent) => {
});
} }
public async updateValue(eventArg: Event) { public async updateValue(eventArg: Event) {
@ -228,16 +195,15 @@ export class DeesInputText extends DeesElement {
this.changeSubject.next(this); this.changeSubject.next(this);
} }
public async freeze() { public getValue(): string {
this.disabled = true; return this.value;
} }
public async unfreeze() { public setValue(value: string): void {
this.disabled = false; this.value = value;
} }
public async togglePasswordView() { public async togglePasswordView() {
const domtools = await this.domtoolsPromise;
this.showPasswordBool = !this.showPasswordBool; this.showPasswordBool = !this.showPasswordBool;
console.log(`this.showPasswordBool is: ${this.showPasswordBool}`); console.log(`this.showPasswordBool is: ${this.showPasswordBool}`);
} }