From 167df241b7fe8becc369b02dfbef4df286058467 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 26 Jun 2025 15:08:14 +0000 Subject: [PATCH] update --- ts_web/elements/dees-form.ts | 23 +- ts_web/elements/dees-input-checkbox.ts | 50 ++- ts_web/elements/dees-input-radio.demo.ts | 267 ---------------- ts_web/elements/dees-input-radio.ts | 135 -------- ts_web/elements/dees-input-radiogroup.demo.ts | 200 ++++++++++++ ts_web/elements/dees-input-radiogroup.ts | 295 ++++++++++++++++++ ts_web/elements/dees-simple-appdash.demo.ts | 15 +- ts_web/elements/index.ts | 2 +- ts_web/pages/input-showcase.ts | 9 +- 9 files changed, 559 insertions(+), 437 deletions(-) delete mode 100644 ts_web/elements/dees-input-radio.demo.ts delete mode 100644 ts_web/elements/dees-input-radio.ts create mode 100644 ts_web/elements/dees-input-radiogroup.demo.ts create mode 100644 ts_web/elements/dees-input-radiogroup.ts diff --git a/ts_web/elements/dees-form.ts b/ts_web/elements/dees-form.ts index 55d3a21..1a835b3 100644 --- a/ts_web/elements/dees-form.ts +++ b/ts_web/elements/dees-form.ts @@ -11,7 +11,7 @@ import * as domtools from '@design.estate/dees-domtools'; import { DeesInputCheckbox } from './dees-input-checkbox.js'; import { DeesInputText } from './dees-input-text.js'; import { DeesInputQuantitySelector } from './dees-input-quantityselector.js'; -import { DeesInputRadio } from './dees-input-radio.js'; +import { DeesInputRadiogroup } from './dees-input-radiogroup.js'; import { DeesInputDropdown } from './dees-input-dropdown.js'; import { DeesInputFileupload } from './dees-input-fileupload.js'; import { DeesInputIban } from './dees-input-iban.js'; @@ -31,7 +31,7 @@ const FORM_INPUT_TYPES = [ DeesInputMultitoggle, DeesInputPhone, DeesInputQuantitySelector, - DeesInputRadio, + DeesInputRadiogroup, DeesInputText, DeesInputTypelist, DeesTable, @@ -45,7 +45,7 @@ export type TFormInputElement = | DeesInputMultitoggle | DeesInputPhone | DeesInputQuantitySelector - | DeesInputRadio + | DeesInputRadiogroup | DeesInputText | DeesInputTypelist | DeesTable; @@ -132,7 +132,6 @@ export class DeesForm extends DeesElement { public async collectFormData() { const children = this.getFormElements(); const valueObject: { [key: string]: string | number | boolean | any[] | File[] | { option: string; key: string; payload?: any } } = {}; - const radioGroups = new Map(); for (const child of children) { if (!child.key) { @@ -140,21 +139,7 @@ export class DeesForm extends DeesElement { continue; } - // Handle radio buttons specially - if (child instanceof DeesInputRadio && child.name) { - if (!radioGroups.has(child.name)) { - radioGroups.set(child.name, []); - } - radioGroups.get(child.name).push(child); - } else { - valueObject[child.key] = child.value; - } - } - - // Process radio groups - use the name as key and selected radio's key as value - for (const [groupName, radios] of radioGroups) { - const selectedRadio = radios.find(radio => radio.value === true); - valueObject[groupName] = selectedRadio ? selectedRadio.key : null; + valueObject[child.key] = child.value; } return valueObject; diff --git a/ts_web/elements/dees-input-checkbox.ts b/ts_web/elements/dees-input-checkbox.ts index 289538d..3a894a4 100644 --- a/ts_web/elements/dees-input-checkbox.ts +++ b/ts_web/elements/dees-input-checkbox.ts @@ -50,14 +50,24 @@ export class DeesInputCheckbox extends DeesInputBase { } .maincontainer { - padding: 5px 0px; + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0px; color: ${cssManager.bdTheme('#333', '#ccc')}; + cursor: pointer; + user-select: none; + transition: all 0.2s; } .maincontainer:hover { color: ${cssManager.bdTheme('#000', '#fff')}; } + .maincontainer:hover .checkbox { + border-color: ${cssManager.bdTheme('#999', '#888')}; + } + input:focus { outline: none; border-bottom: 1px solid #e4002b; @@ -72,6 +82,7 @@ export class DeesInputCheckbox extends DeesInputBase { width: 24px; display: inline-block; background: ${cssManager.bdTheme('#fafafa', '#222')}; + flex-shrink: 0; } .checkbox.selected { @@ -118,13 +129,43 @@ export class DeesInputCheckbox extends DeesInputBase { img { padding: 4px; } + + .checkbox-label { + font-size: 14px; + transition: color 0.2s ease; + } + + .maincontainer:hover .checkbox-label { + color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; + } + + .maincontainer.disabled { + cursor: not-allowed; + opacity: 0.5; + } + + .maincontainer.disabled:hover { + color: ${cssManager.bdTheme('#333', '#ccc')}; + } + + .maincontainer.disabled:hover .checkbox { + border-color: ${cssManager.bdTheme('#ccc', '#333')}; + } + + .description-text { + font-size: 12px; + color: ${cssManager.bdTheme('#666', '#999')}; + margin-top: 4px; + line-height: 1.4; + padding-left: 36px; + } `, ]; public render(): TemplateResult { return html`
-
+
${this.value ? html` @@ -135,8 +176,11 @@ export class DeesInputCheckbox extends DeesInputBase { ` : html``}
+ ${this.label ? html`
${this.label}
` : ''}
- + ${this.description ? html` +
${this.description}
+ ` : ''}
`; } diff --git a/ts_web/elements/dees-input-radio.demo.ts b/ts_web/elements/dees-input-radio.demo.ts deleted file mode 100644 index 027a0e0..0000000 --- a/ts_web/elements/dees-input-radio.demo.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { html, css } from '@design.estate/dees-element'; -import '@design.estate/dees-wcctools/demotools'; -import type { DeesInputRadio } from './dees-input-radio.js'; - -export const demoFunc = () => html` - - - -
-
-

Basic Radio Groups

-

Radio buttons for single-choice selections

- -
-
Select your subscription plan:
- - - -
- -
-
Task Priority:
- - - -
-
- -
-

Horizontal Layout

-

Radio buttons arranged horizontally for yes/no questions

- -
-
Do you agree?
- - - -
- -
-
Experience Level:
- - - -
-
- -
-

Survey Example

-

Multiple radio groups in a survey format

- -
-
-
How satisfied are you?
- - - - - -
- -
-
Would you recommend us?
- - - - - -
-
-
- -
-

States

-

Different radio button states

- -
- - - - -
-
- -
-

Settings Example

-

Common radio button patterns in settings

- -
-
Theme Preference:
- - - -
- -
-
Notification Frequency:
- - - -
-
-
-
-`; \ No newline at end of file diff --git a/ts_web/elements/dees-input-radio.ts b/ts_web/elements/dees-input-radio.ts deleted file mode 100644 index 44367fc..0000000 --- a/ts_web/elements/dees-input-radio.ts +++ /dev/null @@ -1,135 +0,0 @@ -import {customElement, type TemplateResult, property, html, css, cssManager} from '@design.estate/dees-element'; -import { DeesInputBase } from './dees-input-base.js'; -import { demoFunc } from './dees-input-radio.demo.js'; - -declare global { - interface HTMLElementTagNameMap { - 'dees-input-radio': DeesInputRadio; - } -} - -@customElement('dees-input-radio') -export class DeesInputRadio extends DeesInputBase { - public static demo = demoFunc; - - // INSTANCE - - @property() - public value: boolean = false; - - @property({ type: String }) - public name: string = ''; - - constructor() { - super(); - this.labelPosition = 'right'; // Radio buttons default to label on the right - } - - public static styles = [ - ...DeesInputBase.baseStyles, - cssManager.defaultStyles, - css` - * { - box-sizing: border-box; - } - - :host { - position: relative; - } - - .maincontainer { - transition: all 0.3s; - padding: 5px 0px; - color: #ccc; - } - - .maincontainer:hover { - color: #fff; - } - - input:focus { - outline: none; - border-bottom: 1px solid #e4002b; - } - - .checkbox { - transition: all 0.3s; - box-sizing: border-box; - border-radius: 20px; - border: 1px solid #999; - height: 24px; - width: 24px; - display: inline-block; - background: #222; - } - - .checkbox.selected { - background: #0050b9; - border: 1px solid #0050b9; - } - - .maincontainer:hover .checkbox.selected { - background: #03A9F4; - } - - .innercircle { - transition: all 0.3s; - margin: 6px 0px 0px 6px; - background: #222; - width: 10px; - height: 10px; - border-radius: 10px; - } - `, - ]; - - public render(): TemplateResult { - return html` -
-
-
- ${this.value ? html`
`: html``} -
-
- -
- `; - } - - public async toggleSelected () { - // Radio buttons can only be selected, not deselected by clicking - if (this.value) { - return; - } - - // If this radio has a name, find and deselect other radios in the same group - if (this.name) { - // Try to find a form container first, then fall back to document - const container = this.closest('dees-form') || - this.closest('dees-demowrapper') || - this.closest('.radio-group')?.parentElement || - document; - const allRadios = container.querySelectorAll(`dees-input-radio[name="${this.name}"]`); - allRadios.forEach((radio: DeesInputRadio) => { - if (radio !== this && radio.value) { - radio.value = false; - } - }); - } - - this.value = true; - this.dispatchEvent(new CustomEvent('newValue', { - detail: this.value, - bubbles: true - })); - this.changeSubject.next(this); - } - - public getValue(): boolean { - return this.value; - } - - public setValue(value: boolean): void { - this.value = value; - } -} \ No newline at end of file diff --git a/ts_web/elements/dees-input-radiogroup.demo.ts b/ts_web/elements/dees-input-radiogroup.demo.ts new file mode 100644 index 0000000..8434f35 --- /dev/null +++ b/ts_web/elements/dees-input-radiogroup.demo.ts @@ -0,0 +1,200 @@ +import { html, css } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import './dees-panel.js'; + +export const demoFunc = () => html` + + + +
+ +
+ + + +
+
+ + + + + + + + + { + const display = document.querySelector('#region-result'); + if (display) { + display.textContent = 'Selected: ' + JSON.stringify(e.detail.value, null, 2); + } + }} + > +
Selected: { "region": "eu-central-1", "latency": 50 }
+
+ + +
+ + + +
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+`; \ No newline at end of file diff --git a/ts_web/elements/dees-input-radiogroup.ts b/ts_web/elements/dees-input-radiogroup.ts new file mode 100644 index 0000000..14215b6 --- /dev/null +++ b/ts_web/elements/dees-input-radiogroup.ts @@ -0,0 +1,295 @@ +import { + customElement, + type TemplateResult, + property, + html, + css, + cssManager, +} from '@design.estate/dees-element'; +import { DeesInputBase } from './dees-input-base.js'; +import { demoFunc } from './dees-input-radiogroup.demo.js'; + +declare global { + interface HTMLElementTagNameMap { + 'dees-input-radiogroup': DeesInputRadiogroup; + } +} + +type RadioOption = string | { option: string; key: string; payload?: any }; + +@customElement('dees-input-radiogroup') +export class DeesInputRadiogroup extends DeesInputBase { + public static demo = demoFunc; + + // INSTANCE + + @property({ type: Array }) + public options: RadioOption[] = []; + + @property() + public selectedOption: string = ''; + + @property({ type: String }) + public direction: 'vertical' | 'horizontal' = 'vertical'; + + @property({ type: String, reflect: true }) + public validationState: 'valid' | 'invalid' | 'warn' | 'pending' = null; + + // Form compatibility + public get value() { + const option = this.getOptionByKey(this.selectedOption); + if (typeof option === 'object' && option.payload !== undefined) { + return option.payload; + } + return this.selectedOption; + } + + public set value(val: string | any) { + if (typeof val === 'string') { + this.selectedOption = val; + } else { + // Try to find option by payload + const option = this.options.find(opt => + typeof opt === 'object' && opt.payload === val + ); + if (option && typeof option === 'object') { + this.selectedOption = option.key; + } + } + } + + public static styles = [ + ...DeesInputBase.baseStyles, + cssManager.defaultStyles, + css` + * { + box-sizing: border-box; + } + + :host { + display: block; + position: relative; + } + + .maincontainer { + display: flex; + flex-direction: column; + gap: 8px; + } + + .maincontainer.horizontal { + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + } + + .radio-option { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + position: relative; + } + + .maincontainer.horizontal .radio-option { + padding: 8px 16px 8px 0; + } + + .radio-option:hover .radio-circle { + border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')}; + } + + .radio-option:hover .radio-label { + color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; + } + + .radio-circle { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid ${cssManager.bdTheme('#999', '#666')}; + background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; + transition: all 0.2s ease; + position: relative; + flex-shrink: 0; + } + + .radio-option.selected .radio-circle { + border-color: ${cssManager.bdTheme('#0050b9', '#0084ff')}; + background: ${cssManager.bdTheme('#0050b9', '#0084ff')}; + } + + .radio-option.selected .radio-circle::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: white; + } + + .radio-label { + font-size: 14px; + color: ${cssManager.bdTheme('#666', '#999')}; + transition: color 0.2s ease; + } + + .radio-option.selected .radio-label { + color: ${cssManager.bdTheme('#1a1a1a', '#ffffff')}; + font-weight: 500; + } + + :host([disabled]) .radio-option { + cursor: not-allowed; + opacity: 0.5; + } + + :host([disabled]) .radio-option:hover .radio-circle { + border-color: ${cssManager.bdTheme('#999', '#666')}; + } + + :host([disabled]) .radio-option:hover .radio-label { + color: ${cssManager.bdTheme('#666', '#999')}; + } + + .label-text { + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('#333', '#ccc')}; + margin-bottom: 8px; + } + + .description-text { + font-size: 12px; + color: ${cssManager.bdTheme('#666', '#999')}; + margin-top: 8px; + line-height: 1.4; + } + + /* Validation styles */ + :host([validationState="invalid"]) .radio-circle { + border-color: #e74c3c; + } + + :host([validationState="valid"]) .radio-option.selected .radio-circle { + border-color: #27ae60; + background: #27ae60; + } + + :host([validationState="warn"]) .radio-option.selected .radio-circle { + border-color: #f39c12; + background: #f39c12; + } + + /* Override base grid layout for radiogroup to prevent large gaps */ + :host([label-position="left"]) .input-wrapper { + grid-template-columns: auto auto; + } + + :host([label-position="right"]) .input-wrapper { + grid-template-columns: auto auto; + } + `, + ]; + + public render(): TemplateResult { + return html` +
+ ${this.label ? html`
${this.label}
` : ''} +
+ ${this.options.map((option) => { + const optionKey = this.getOptionKey(option); + const optionLabel = this.getOptionLabel(option); + const isSelected = this.selectedOption === optionKey; + + return html` +
+
+
${optionLabel}
+
+ `; + })} +
+ ${this.description ? html`
${this.description}
` : ''} +
+ `; + } + + private getOptionKey(option: RadioOption): string { + if (typeof option === 'string') { + return option; + } + return option.key; + } + + private getOptionLabel(option: RadioOption): string { + if (typeof option === 'string') { + return option; + } + return option.option; + } + + private getOptionByKey(key: string): RadioOption | undefined { + return this.options.find(opt => this.getOptionKey(opt) === key); + } + + private selectOption(key: string): void { + if (this.disabled) { + return; + } + + const oldValue = this.selectedOption; + this.selectedOption = key; + + if (oldValue !== key) { + this.dispatchEvent(new CustomEvent('change', { + detail: { value: this.value }, + bubbles: true, + composed: true, + })); + + this.dispatchEvent(new CustomEvent('input', { + detail: { value: this.value }, + bubbles: true, + composed: true, + })); + + this.changeSubject.next(this); + } + } + + public getValue(): string | any { + return this.value; + } + + public setValue(val: string | any): void { + this.value = val; + } + + public async validate(): Promise { + if (this.required && !this.selectedOption) { + this.validationState = 'invalid'; + return false; + } + + this.validationState = 'valid'; + return true; + } + + public async firstUpdated() { + // Auto-select first option if none selected and not required + if (!this.selectedOption && this.options.length > 0 && !this.required) { + const firstOption = this.options[0]; + this.selectedOption = this.getOptionKey(firstOption); + } + } +} \ No newline at end of file diff --git a/ts_web/elements/dees-simple-appdash.demo.ts b/ts_web/elements/dees-simple-appdash.demo.ts index bf227d0..6afa801 100644 --- a/ts_web/elements/dees-simple-appdash.demo.ts +++ b/ts_web/elements/dees-simple-appdash.demo.ts @@ -4,7 +4,7 @@ import './dees-form.js'; import './dees-input-text.js'; import './dees-input-checkbox.js'; import './dees-input-dropdown.js'; -import './dees-input-radio.js'; +import './dees-input-radiogroup.js'; import './dees-form-submit.js'; import './dees-statsgrid.js'; import type { IStatsTile } from './dees-statsgrid.js'; @@ -230,13 +230,12 @@ class DemoViewSettings extends DeesElement {

Notification Settings

-
-
Email Frequency:
- - - - -
+ Update Notifications diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index ae46b56..90fad72 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -34,7 +34,7 @@ export * from './dees-input-phone.js'; export * from './dees-input-wysiwyg.js'; export * from './dees-progressbar.js'; export * from './dees-input-quantityselector.js'; -export * from './dees-input-radio.js'; +export * from './dees-input-radiogroup.js'; export * from './dees-input-richtext.js'; export * from './dees-input-text.js'; export * from './dees-label.js'; diff --git a/ts_web/pages/input-showcase.ts b/ts_web/pages/input-showcase.ts index 3f37033..424405e 100644 --- a/ts_web/pages/input-showcase.ts +++ b/ts_web/pages/input-showcase.ts @@ -315,12 +315,12 @@ export const inputShowcase = () => html`
- + >
@@ -538,12 +538,13 @@ export const inputShowcase = () => html` .key=${'country'} > - + .selectedOption=${'Personal'} + >