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; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; } .maincontainer { display: flex; flex-direction: column; gap: 10px; } .maincontainer.horizontal { flex-direction: row; flex-wrap: wrap; gap: 20px; } .radio-option { display: flex; align-items: center; gap: 10px; padding: 6px 0; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); user-select: none; position: relative; border-radius: 4px; } .maincontainer.horizontal .radio-option { padding: 6px 20px 6px 0; } .radio-option:hover .radio-circle { border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')}; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')}; } .radio-option:hover .radio-label { color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; } .radio-circle { width: 20px; height: 20px; border-radius: 50%; border: 2px solid ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')}; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } .radio-option.selected .radio-circle { border-color: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; background: ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; } .radio-option.selected .radio-circle::after { content: ''; position: absolute; width: 8px; height: 8px; border-radius: 50%; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')}; transform: scale(0); transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .radio-option.selected .radio-circle::after { transform: scale(1); } .radio-circle:focus-visible { outline: none; box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 3.9%)')}, 0 0 0 4px ${cssManager.bdTheme('hsl(217.2 91.2% 59.8%)', 'hsl(213.1 93.9% 67.8%)')}; } .radio-label { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')}; transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1); letter-spacing: -0.006em; line-height: 20px; } .radio-option.selected .radio-label { color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; } :host([disabled]) .radio-option { cursor: not-allowed; opacity: 0.5; } :host([disabled]) .radio-option:hover .radio-circle { border-color: ${cssManager.bdTheme('hsl(215 20.2% 65.1%)', 'hsl(215 20.2% 35.1%)')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 30% 6.8%)')}; } :host([disabled]) .radio-option:hover .radio-label { color: ${cssManager.bdTheme('hsl(215.3 25% 26.7%)', 'hsl(217.9 10.6% 74.9%)')}; } .label-text { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; margin-bottom: 10px; letter-spacing: -0.006em; line-height: 20px; } .description-text { font-size: 13px; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; margin-top: 10px; line-height: 1.5; letter-spacing: -0.003em; } /* Validation styles */ :host([validationState="invalid"]) .radio-circle { border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; } :host([validationState="invalid"]) .radio-option.selected .radio-circle { border-color: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; background: ${cssManager.bdTheme('hsl(0 72.2% 50.6%)', 'hsl(0 62.8% 30.6%)')}; } :host([validationState="valid"]) .radio-option.selected .radio-circle { border-color: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')}; background: ${cssManager.bdTheme('hsl(142.1 70.6% 45.3%)', 'hsl(142.1 76.2% 36.3%)')}; } :host([validationState="warn"]) .radio-option.selected .radio-circle { border-color: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')}; background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')}; } /* 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); } } private handleKeydown(event: KeyboardEvent, optionKey: string) { if (this.disabled) return; if (event.key === ' ' || event.key === 'Enter') { event.preventDefault(); this.selectOption(optionKey); } else if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { event.preventDefault(); this.focusNextOption(); } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { event.preventDefault(); this.focusPreviousOption(); } } private focusNextOption() { const radioCircles = Array.from(this.shadowRoot.querySelectorAll('.radio-circle')); const currentIndex = radioCircles.findIndex(el => el === this.shadowRoot.activeElement); const nextIndex = (currentIndex + 1) % radioCircles.length; (radioCircles[nextIndex] as HTMLElement).focus(); } private focusPreviousOption() { const radioCircles = Array.from(this.shadowRoot.querySelectorAll('.radio-circle')); const currentIndex = radioCircles.findIndex(el => el === this.shadowRoot.activeElement); const prevIndex = currentIndex <= 0 ? radioCircles.length - 1 : currentIndex - 1; (radioCircles[prevIndex] as HTMLElement).focus(); } }