From 17dd86d389a99864f705e4a2afb6b50348cde228 Mon Sep 17 00:00:00 2001 From: xelaint Date: Tue, 2 Dec 2025 10:00:07 -0500 Subject: [PATCH 1/3] feat(design): update radio implementation --- .../src/app/radio/radio.component.html | 2 + .../basic-radio/basic-radio.component.html | 15 +- .../src/basic-radio/basic-radio.component.ts | 14 +- libs/design-examples/radio/src/examples.ts | 2 + .../radio-with-control.component.html | 6 + .../radio-with-control.component.ts | 23 +++ .../radio/src/cva/radio-cva.directive.spec.ts | 56 ------- .../radio/src/cva/radio-cva.directive.ts | 110 -------------- .../src/helpers/radio-set-orientation.ts | 18 +++ libs/design/radio/src/public_api.ts | 1 - .../src/radio-set/radio-set.component.html | 7 +- .../src/radio-set/radio-set.component.scss | 31 ++++ .../src/radio-set/radio-set.component.spec.ts | 25 ++- .../src/radio-set/radio-set.component.ts | 143 +++++++++++++++++- libs/design/radio/src/radio-theme.scss | 27 ++++ libs/design/radio/src/radio.module.ts | 3 - libs/design/radio/src/radio.ts | 2 - .../radio/src/radio/radio.component.html | 28 ++-- .../radio/src/radio/radio.component.scss | 59 ++++++++ .../radio/src/radio/radio.component.spec.ts | 43 +----- .../design/radio/src/radio/radio.component.ts | 135 +++++++++-------- .../radio/src/registry/radio-registry.spec.ts | 17 --- .../radio/src/registry/radio-registry.ts | 60 -------- ...ibility.spec.ts => radio-defaults.spec.ts} | 33 ++-- .../src/specs/radio-set-with-cva.spec.ts | 76 ---------- .../specs/radio-set-without-control.spec.ts | 103 +++++++++++++ .../radio/src/specs/radio-with-set.spec.ts | 115 ++++++++++++-- .../radio/src/specs/radio-without-cva.spec.ts | 54 ------- libs/design/scss/theme.scss | 2 + 29 files changed, 662 insertions(+), 548 deletions(-) create mode 100644 libs/design-examples/radio/src/radio-with-control/radio-with-control.component.html create mode 100644 libs/design-examples/radio/src/radio-with-control/radio-with-control.component.ts delete mode 100644 libs/design/radio/src/cva/radio-cva.directive.spec.ts delete mode 100644 libs/design/radio/src/cva/radio-cva.directive.ts create mode 100644 libs/design/radio/src/helpers/radio-set-orientation.ts create mode 100644 libs/design/radio/src/radio-set/radio-set.component.scss create mode 100644 libs/design/radio/src/radio-theme.scss create mode 100644 libs/design/radio/src/radio/radio.component.scss delete mode 100644 libs/design/radio/src/registry/radio-registry.spec.ts delete mode 100644 libs/design/radio/src/registry/radio-registry.ts rename libs/design/radio/src/specs/{radio-accessibility.spec.ts => radio-defaults.spec.ts} (62%) delete mode 100644 libs/design/radio/src/specs/radio-set-with-cva.spec.ts create mode 100644 libs/design/radio/src/specs/radio-set-without-control.spec.ts delete mode 100644 libs/design/radio/src/specs/radio-without-cva.spec.ts diff --git a/apps/design-land/src/app/radio/radio.component.html b/apps/design-land/src/app/radio/radio.component.html index b74bda0e4b..0671763c66 100644 --- a/apps/design-land/src/app/radio/radio.component.html +++ b/apps/design-land/src/app/radio/radio.component.html @@ -1,2 +1,4 @@

Daff Radio

+ + diff --git a/libs/design-examples/radio/src/basic-radio/basic-radio.component.html b/libs/design-examples/radio/src/basic-radio/basic-radio.component.html index 15e38f6942..327ffeccbe 100644 --- a/libs/design-examples/radio/src/basic-radio/basic-radio.component.html +++ b/libs/design-examples/radio/src/basic-radio/basic-radio.component.html @@ -1,9 +1,8 @@ -

Basic Radio

- - Terran - Protoss - Zerg + + Card Type + Visa + MasterCard + American Express -
- The best race to play as is: {{this.radioGroup.get('race').value}} -
\ No newline at end of file + +
Selected value: {{ value }}
\ No newline at end of file diff --git a/libs/design-examples/radio/src/basic-radio/basic-radio.component.ts b/libs/design-examples/radio/src/basic-radio/basic-radio.component.ts index 34b5b94f68..1c99d1c4b2 100644 --- a/libs/design-examples/radio/src/basic-radio/basic-radio.component.ts +++ b/libs/design-examples/radio/src/basic-radio/basic-radio.component.ts @@ -2,11 +2,7 @@ import { ChangeDetectionStrategy, Component, } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormControl, - ReactiveFormsModule, -} from '@angular/forms'; +import { ReactiveFormsModule } from '@angular/forms'; import { DAFF_RADIO_COMPONENTS } from '@daffodil/design/radio'; @@ -20,9 +16,9 @@ import { DAFF_RADIO_COMPONENTS } from '@daffodil/design/radio'; ], }) export class BasicRadioExampleComponent { - radioGroup = new UntypedFormGroup({ - race: new UntypedFormControl('Zerg'), - }); + value: any; - constructor() {} + update(val: any) { + this.value = val; + } } diff --git a/libs/design-examples/radio/src/examples.ts b/libs/design-examples/radio/src/examples.ts index 1beed50165..74f728e46b 100644 --- a/libs/design-examples/radio/src/examples.ts +++ b/libs/design-examples/radio/src/examples.ts @@ -1,5 +1,7 @@ import { BasicRadioExampleComponent } from './basic-radio/basic-radio.component'; +import { RadioWithControlExampleComponent } from './radio-with-control/radio-with-control.component'; export const RADIO_EXAMPLES = [ BasicRadioExampleComponent, + RadioWithControlExampleComponent, ]; diff --git a/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.html b/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.html new file mode 100644 index 0000000000..5d5ae57faa --- /dev/null +++ b/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.html @@ -0,0 +1,6 @@ + + Card Type + Visa + MasterCard + American Express + diff --git a/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.ts b/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.ts new file mode 100644 index 0000000000..0e9415f997 --- /dev/null +++ b/libs/design-examples/radio/src/radio-with-control/radio-with-control.component.ts @@ -0,0 +1,23 @@ +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; +import { + UntypedFormControl, + ReactiveFormsModule, +} from '@angular/forms'; + +import { DAFF_RADIO_COMPONENTS } from '@daffodil/design/radio'; + +@Component({ + selector: 'radio-with-control-example', + templateUrl: './radio-with-control.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + DAFF_RADIO_COMPONENTS, + ReactiveFormsModule, + ], +}) +export class RadioWithControlExampleComponent { + ccType = new UntypedFormControl('visa'); +} diff --git a/libs/design/radio/src/cva/radio-cva.directive.spec.ts b/libs/design/radio/src/cva/radio-cva.directive.spec.ts deleted file mode 100644 index d63d2269d2..0000000000 --- a/libs/design/radio/src/cva/radio-cva.directive.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Component } from '@angular/core'; -import { - ComponentFixture, - waitForAsync, - TestBed, -} from '@angular/core/testing'; -import { - ReactiveFormsModule, - UntypedFormControl, -} from '@angular/forms'; -import { By } from '@angular/platform-browser'; - -import { DaffRadioControlValueAccessorDirective } from './radio-cva.directive'; -import { DaffRadioComponent } from '../radio/radio.component'; - -@Component({ - template: ` - - `, - imports: [ - DaffRadioComponent, - DaffRadioControlValueAccessorDirective, - ReactiveFormsModule, - ], -}) -class WrapperComponent { - radio = new UntypedFormControl(); -} - -describe('@daffodil/design/radio | DaffRadioControlValueAccessorDirective | Defaults', () => { - let fixture: ComponentFixture; - let wrapper: WrapperComponent; - let component: DaffRadioComponent; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - WrapperComponent, - ], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(WrapperComponent); - wrapper = fixture.componentInstance; - component = fixture.debugElement.query(By.css('daff-radio')).componentInstance; - fixture.detectChanges(); - }); - - it('has the writeValue function for formControls', async () => { - expect(component.checked).toEqual(false); - wrapper.radio.setValue('testValue'); - expect(component.checked).toEqual(true); - }); -}); diff --git a/libs/design/radio/src/cva/radio-cva.directive.ts b/libs/design/radio/src/cva/radio-cva.directive.ts deleted file mode 100644 index 8f51f87998..0000000000 --- a/libs/design/radio/src/cva/radio-cva.directive.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - Directive, - Input, - OnInit, - Self, - Optional, -} from '@angular/core'; -import { - NgControl, - ControlValueAccessor, -} from '@angular/forms'; - -import { DaffRadioComponent } from '../radio/radio.component'; -import { DaffRadioRegistry } from '../registry/radio-registry'; - -/** - * ControlValueAccessor functionality for the DaffRadio - */ -@Directive({ - // eslint-disable-next-line @angular-eslint/directive-selector - selector: 'daff-radio[ngModel], daff-radio[formControl], daff-radio[formControlName]', -}) -export class DaffRadioControlValueAccessorDirective implements OnInit, ControlValueAccessor { - _onChange: () => void; - _onTouched: () => void; - - /** - * The value of the ControlValueAccessor - */ - @Input() value: any; - - /** - * The name of the ControlValueAccessor - */ - @Input() name: string; - - constructor( - @Optional() @Self() public _control: NgControl, - private _registry: DaffRadioRegistry, - private _radio: DaffRadioComponent, - ) { - if (this._control != null) { - this._control.valueAccessor = this; - } - } - - /** - * @docs-private - */ - ngOnInit(): void { - if (this._control) { - this.writeValue(this._control.value); - this._registry.add(this._control, this); - } - - this._radio.selectionChange.subscribe( - value => value ? this._onChange() : null, - ); - } - /** - * - * writeValue function from the CVA interface - */ - writeValue(value: any): void { - // the this._onChange null check here is necessary because of an ongoing bug in angular forms - // where writeValue can be called before the component initializes: https://github.com/angular/angular/issues/29218 - if (this.value === value && this._onChange) { - this._onChange(); - this.fireSelect(); - } - } - - /** - * registerOnChange implemented from the CVA interface - */ - registerOnChange(fn: any): void { - this._onChange = () => { - fn(this.value); - this._registry.select(this); - }; - } - - /** - * registerOnTouch implemented from the CVA interface - */ - registerOnTouched(fn: any): void { - this._onTouched = fn; - } - - /** - * sets the disabled state. - */ - setDisabledState?(isDisabled: boolean): void { - this._radio.disabled = isDisabled; - } - - /** - calls select function for the radio - */ - fireSelect() { - this._radio.select(); - } - - /** - calls deselect function for the radio - */ - fireDeselect() { - this._radio.deselect(); - } -} diff --git a/libs/design/radio/src/helpers/radio-set-orientation.ts b/libs/design/radio/src/helpers/radio-set-orientation.ts new file mode 100644 index 0000000000..c41aae6e97 --- /dev/null +++ b/libs/design/radio/src/helpers/radio-set-orientation.ts @@ -0,0 +1,18 @@ +/** + * The available orientations for a radio set. + * + * | Orientation | Description | + * | -- | -- | + * | `vertical` | Stacks radio set content from top to bottom. This is the default orientation. | + * | `horizontal` | Places radio set content side-by-side. On smaller screens, horizontal radio sets automatically switch to vertical for responsiveness. | + */ +export type DaffRadioSetOrientation = 'vertical' | 'horizontal'; + +/** + * Enum for representing the available radio set orientations. + * See {@link DaffRadioSetOrientation} for descriptions of each orientation. + */ +export enum DaffRadioSetOrientationEnum { + Vertical = 'vertical', + Horizontal = 'horizontal', +} diff --git a/libs/design/radio/src/public_api.ts b/libs/design/radio/src/public_api.ts index 3797463164..763da43e19 100644 --- a/libs/design/radio/src/public_api.ts +++ b/libs/design/radio/src/public_api.ts @@ -2,4 +2,3 @@ export { DaffRadioModule } from './radio.module'; export { DaffRadioComponent } from './radio/radio.component'; export { DaffRadioSetComponent } from './radio-set/radio-set.component'; export { DAFF_RADIO_COMPONENTS } from './radio'; -export { DaffRadioControlValueAccessorDirective } from './cva/radio-cva.directive'; diff --git a/libs/design/radio/src/radio-set/radio-set.component.html b/libs/design/radio/src/radio-set/radio-set.component.html index 95a0b70bdc..d057ebe1b4 100644 --- a/libs/design/radio/src/radio-set/radio-set.component.html +++ b/libs/design/radio/src/radio-set/radio-set.component.html @@ -1 +1,6 @@ - \ No newline at end of file + +
+ +
\ No newline at end of file diff --git a/libs/design/radio/src/radio-set/radio-set.component.scss b/libs/design/radio/src/radio-set/radio-set.component.scss new file mode 100644 index 0000000000..e103226e9a --- /dev/null +++ b/libs/design/radio/src/radio-set/radio-set.component.scss @@ -0,0 +1,31 @@ +@use '../../../scss/typography' as t; + +:host { + $root: '.daff-radio-set'; + display: flex; + flex-direction: column; + gap: 1rem; + + #{$root}__label { + font-size: t.$font-size-base; + font-weight: 500; + } + + #{$root}__wrapper { + display: flex; + } + + &.horizontal { + #{$root}__wrapper { + flex-direction: row; + gap: 1rem; + } + } + + &.vertical { + #{$root}__wrapper { + flex-direction: column; + gap: 0.5rem; + } + } +} diff --git a/libs/design/radio/src/radio-set/radio-set.component.spec.ts b/libs/design/radio/src/radio-set/radio-set.component.spec.ts index b25fc66d86..a992e843b3 100644 --- a/libs/design/radio/src/radio-set/radio-set.component.spec.ts +++ b/libs/design/radio/src/radio-set/radio-set.component.spec.ts @@ -8,22 +8,22 @@ import { TestBed, } from '@angular/core/testing'; import { - UntypedFormGroup, UntypedFormControl, ReactiveFormsModule, } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { DAFF_RADIO_COMPONENTS } from '@daffodil/design/radio'; - -import { DaffRadioSetComponent } from './radio-set.component'; +import { + DAFF_RADIO_COMPONENTS, + DaffRadioSetComponent, +} from '@daffodil/design/radio'; @Component({ template: ` - - Apple - Grape - Peach + + Apple + Grape + Peach `, imports: [ @@ -32,9 +32,7 @@ import { DaffRadioSetComponent } from './radio-set.component'; ], }) class WrapperComponent { - radioGroup = new UntypedFormGroup({ - fruit: new UntypedFormControl(), - }); + fruits = new UntypedFormControl(); } describe('@daffodil/design/radio | DaffRadioSetComponent | Defaults', () => { @@ -65,11 +63,10 @@ describe('@daffodil/design/radio | DaffRadioSetComponent | Defaults', () => { }); it('should take a name as an input', () => { - expect(component.name).toBe('fruit'); + expect(component.name()).toBe('fruit'); }); it('should have a role of radiogroup', () => { - const roleAttribute = de.nativeElement.getAttribute('role'); - expect(roleAttribute).toBe('radiogroup'); + expect(de.nativeElement.getAttribute('role')).toBe('radiogroup'); }); }); diff --git a/libs/design/radio/src/radio-set/radio-set.component.ts b/libs/design/radio/src/radio-set/radio-set.component.ts index 32325d8f25..94013d049d 100644 --- a/libs/design/radio/src/radio-set/radio-set.component.ts +++ b/libs/design/radio/src/radio-set/radio-set.component.ts @@ -1,22 +1,155 @@ +/* eslint-disable @angular-eslint/no-input-rename */ import { Component, - Input, ChangeDetectionStrategy, + input, + signal, + effect, + inject, + output, } from '@angular/core'; +import { + ControlValueAccessor, + NgControl, +} from '@angular/forms'; + +import { + DaffRadioSetOrientation, + DaffRadioSetOrientationEnum, +} from '../helpers/radio-set-orientation'; + +const radioSetUniqueId = 0; @Component({ selector: 'daff-radio-set', templateUrl: './radio-set.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, + styleUrl: './radio-set.component.scss', host: { + class: 'daff-radio-set', role: 'radiogroup', + '[class.vertical]': 'orientation() === "vertical"', + '[class.horizontal]': 'orientation() === "horizontal"', }, + changeDetection: ChangeDetectionStrategy.OnPush, }) +export class DaffRadioSetComponent implements ControlValueAccessor { + private ngControl = inject(NgControl, { self: true, optional: true }); + + /** + * The unique id of a radio set. Defaults to an autogenerated value. When using this, + * it's your responsibility to ensure that the id for each radio set is unique. + */ + id = input(`daff-radio-set-${radioSetUniqueId}`); + + /** + * The form element name. + */ + name = input(); + + /** + * Value currently selected for a radio set. This is ignored when a form control is used. + */ + _inputValue = input(undefined, { + alias: 'value', + }); + + /** + * Whether or not a radio set is disabled. This is ignored if a form control is used. + */ + _inputDisabled = input(false, { + alias: 'disabled', + }); + + /** + * The orientation of a radio set. Defaults to `vertical`. + */ + orientation = input(DaffRadioSetOrientationEnum.Vertical, { + transform: (value: DaffRadioSetOrientation | null | undefined): DaffRadioSetOrientation => value || DaffRadioSetOrientationEnum.Vertical, + }); + + /** The tabindex passed down to each radio. Defaults to `0`. */ + tabIndex = input(0); + + /** + * Event fired when the value has changed. + */ + valueChange = output(); + + /** Value currently selected for a radio set. */ + value = signal(undefined); + + /** + * @docs-private + */ + disabled = signal(false); + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + _onChange: () => unknown; + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + _onTouch: () => unknown; + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + writeValue(obj: any): void { + this.value.set(obj); + } + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + registerOnChange(fn: any): void { + this._onChange = fn; + } + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + registerOnTouched(fn: any): void { + this._onTouch = fn; + } + + /** + * @docs-private + * + * Part of ControlValueAccessor. + */ + setDisabledState?(isDisabled: boolean): void { + this.disabled.set(isDisabled); + } -export class DaffRadioSetComponent { + constructor() { + if (this.ngControl) { + this.ngControl.valueAccessor = this; + } - @Input() name: string; + if (!this.ngControl) { + effect(() => { + this.value.set(this._inputValue()); + }); - constructor() { } + effect(() => { + this.disabled.set(this._inputDisabled()); + }); + } + effect(() => { + this.valueChange.emit(this.value()); + }); + } } diff --git a/libs/design/radio/src/radio-theme.scss b/libs/design/radio/src/radio-theme.scss new file mode 100644 index 0000000000..908623a8d1 --- /dev/null +++ b/libs/design/radio/src/radio-theme.scss @@ -0,0 +1,27 @@ +@use '../../scss/theming' as *; + +// stylelint-disable selector-class-pattern +@mixin daff-radio-theme($theme) { + $neutral: daff-get-palette($theme, neutral); + $primary: daff-get-palette($theme, primary); + $mode: daff-get-theme-mode($theme); + + .daff-radio { + $root: '.daff-radio'; + + #{$root}__outer-circle { + border: 1px solid daff-color($neutral, 90); + } + + #{$root}__inner-circle { + background-color: daff-color($neutral, 90); + } + + &.focused { + #{$root}__outer-circle { + outline: 2px solid daff-color($primary); + outline-offset: 1.5px; + } + } + } +} diff --git a/libs/design/radio/src/radio.module.ts b/libs/design/radio/src/radio.module.ts index 7f8bdd825b..64c85b6b8e 100644 --- a/libs/design/radio/src/radio.module.ts +++ b/libs/design/radio/src/radio.module.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { DaffRadioControlValueAccessorDirective } from './cva/radio-cva.directive'; import { DaffRadioComponent } from './radio/radio.component'; import { DaffRadioSetComponent } from './radio-set/radio-set.component'; @@ -10,12 +9,10 @@ import { DaffRadioSetComponent } from './radio-set/radio-set.component'; CommonModule, DaffRadioComponent, DaffRadioSetComponent, - DaffRadioControlValueAccessorDirective, ], exports: [ DaffRadioComponent, DaffRadioSetComponent, - DaffRadioControlValueAccessorDirective, ], }) export class DaffRadioModule { } diff --git a/libs/design/radio/src/radio.ts b/libs/design/radio/src/radio.ts index 1fca455da3..74a7e6fce8 100644 --- a/libs/design/radio/src/radio.ts +++ b/libs/design/radio/src/radio.ts @@ -1,4 +1,3 @@ -import { DaffRadioControlValueAccessorDirective } from './cva/radio-cva.directive'; import { DaffRadioComponent } from './radio/radio.component'; import { DaffRadioSetComponent } from './radio-set/radio-set.component'; /** @@ -7,5 +6,4 @@ import { DaffRadioSetComponent } from './radio-set/radio-set.component'; export const DAFF_RADIO_COMPONENTS = [ DaffRadioComponent, DaffRadioSetComponent, - DaffRadioControlValueAccessorDirective, ]; diff --git a/libs/design/radio/src/radio/radio.component.html b/libs/design/radio/src/radio/radio.component.html index 1ab7b3e096..e5d301bdc6 100644 --- a/libs/design/radio/src/radio/radio.component.html +++ b/libs/design/radio/src/radio/radio.component.html @@ -1,14 +1,18 @@ - -