1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[CL-707] Migrate CL codebase to signals (#15340)

This commit is contained in:
Vicki League
2025-07-16 08:39:37 -04:00
committed by GitHub
parent 97ec9a6339
commit 6811ea4c0b
124 changed files with 944 additions and 809 deletions

View File

@@ -3,8 +3,8 @@
type="radio"
bitRadio
[id]="inputId"
[disabled]="groupDisabled || disabled"
[value]="value"
[disabled]="groupDisabled || disabled()"
[value]="value()"
[checked]="selected"
(change)="onInputChange()"
(blur)="onBlur()"

View File

@@ -1,5 +1,6 @@
import { Component } from "@angular/core";
import { Component, DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { By } from "@angular/platform-browser";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -10,70 +11,62 @@ import { RadioButtonModule } from "./radio-button.module";
import { RadioGroupComponent } from "./radio-group.component";
describe("RadioButton", () => {
let mockGroupComponent: MockedButtonGroupComponent;
let fixture: ComponentFixture<TestApp>;
let testAppComponent: TestApp;
let radioButton: HTMLInputElement;
let fixture: ComponentFixture<TestComponent>;
let radioButtonGroup: RadioGroupComponent;
let radioButtons: DebugElement[];
beforeEach(async () => {
mockGroupComponent = new MockedButtonGroupComponent();
TestBed.configureTestingModule({
imports: [TestApp],
providers: [
{ provide: RadioGroupComponent, useValue: mockGroupComponent },
{ provide: I18nService, useValue: new I18nMockService({}) },
],
imports: [TestComponent],
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
});
await TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
testAppComponent = fixture.debugElement.componentInstance;
radioButton = fixture.debugElement.query(By.css("input[type=radio]")).nativeElement;
radioButtonGroup = fixture.debugElement.query(
By.directive(RadioGroupComponent),
).componentInstance;
radioButtons = fixture.debugElement.queryAll(By.css("input[type=radio]"));
});
it("should emit value when clicking on radio button", () => {
testAppComponent.value = "value";
const spyFn = jest.spyOn(radioButtonGroup, "onInputChange");
radioButtons[1].triggerEventHandler("change");
fixture.detectChanges();
radioButton.click();
fixture.detectChanges();
expect(mockGroupComponent.onInputChange).toHaveBeenCalledWith("value");
expect(spyFn).toHaveBeenCalledWith(1);
});
it("should check radio button when selected matches value", () => {
testAppComponent.value = "value";
it("should check radio button only when selected matches value", () => {
fixture.detectChanges();
mockGroupComponent.selected = "value";
expect(radioButtons[0].nativeElement.checked).toBe(true);
expect(radioButtons[1].nativeElement.checked).toBe(false);
radioButtons[1].triggerEventHandler("change");
fixture.detectChanges();
expect(radioButton.checked).toBe(true);
});
it("should not check radio button when selected does not match value", () => {
testAppComponent.value = "value";
fixture.detectChanges();
mockGroupComponent.selected = "nonMatchingValue";
fixture.detectChanges();
expect(radioButton.checked).toBe(false);
expect(radioButtons[0].nativeElement.checked).toBe(false);
expect(radioButtons[1].nativeElement.checked).toBe(true);
});
});
class MockedButtonGroupComponent implements Partial<RadioGroupComponent> {
onInputChange = jest.fn();
selected: unknown = null;
}
@Component({
selector: "test-app",
template: `<bit-radio-button [value]="value"><bit-label>Element</bit-label></bit-radio-button>`,
imports: [RadioButtonModule],
selector: "test-component",
template: `
<form [formGroup]="formObj">
<bit-radio-group formControlName="radio">
<bit-radio-button [value]="0"><bit-label>Element</bit-label></bit-radio-button>
<bit-radio-button [value]="1"><bit-label>Element</bit-label></bit-radio-button>
</bit-radio-group>
</form>
`,
imports: [FormsModule, ReactiveFormsModule, RadioButtonModule],
})
class TestApp {
value?: string;
class TestComponent {
formObj = new FormGroup({
radio: new FormControl(0),
});
}

View File

@@ -1,4 +1,4 @@
import { Component, HostBinding, Input } from "@angular/core";
import { Component, HostBinding, input } from "@angular/core";
import { FormControlModule } from "../form-control/form-control.module";
@@ -11,20 +11,23 @@ let nextId = 0;
selector: "bit-radio-button",
templateUrl: "radio-button.component.html",
imports: [FormControlModule, RadioInputComponent],
host: {
"[id]": "this.id()",
},
})
export class RadioButtonComponent {
@HostBinding("attr.id") @Input() id = `bit-radio-button-${nextId++}`;
readonly id = input(`bit-radio-button-${nextId++}`);
@HostBinding("class") get classList() {
return [this.block ? "tw-block" : "tw-inline-block", "tw-mb-1", "[&_bit-hint]:tw-mt-0"];
}
@Input() value: unknown;
@Input() disabled = false;
readonly value = input<unknown>();
readonly disabled = input(false);
constructor(private groupComponent: RadioGroupComponent) {}
get inputId() {
return `${this.id}-input`;
return `${this.id()}-input`;
}
get name() {
@@ -32,7 +35,7 @@ export class RadioButtonComponent {
}
get selected() {
return this.groupComponent.selected === this.value;
return this.groupComponent.selected === this.value();
}
get groupDisabled() {
@@ -40,11 +43,11 @@ export class RadioButtonComponent {
}
get block() {
return this.groupComponent.block;
return this.groupComponent.block();
}
protected onInputChange() {
this.groupComponent.onInputChange(this.value);
this.groupComponent.onInputChange(this.value());
}
protected onBlur() {

View File

@@ -177,15 +177,15 @@ export const Disabled: Story = {
<bit-radio-group formControlName="radio" aria-label="Example radio group">
<bit-label>Group of radio buttons</bit-label>
<bit-radio-button id="radio-first" [value]="0" [disabled]="true">
<bit-radio-button [value]="0" [disabled]="true">
<bit-label>First</bit-label>
</bit-radio-button>
<bit-radio-button id="radio-second" [value]="1" [disabled]="true">
<bit-radio-button [value]="1" [disabled]="true">
<bit-label>Second</bit-label>
</bit-radio-button>
<bit-radio-button id="radio-third" [value]="2" [disabled]="true">
<bit-radio-button [value]="2" [disabled]="true">
<bit-label>Third</bit-label>
</bit-radio-button>
</bit-radio-group>

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgTemplateOutlet } from "@angular/common";
import { Component, ContentChild, HostBinding, Input, Optional, Self } from "@angular/core";
import { Component, ContentChild, HostBinding, Optional, Input, Self, input } from "@angular/core";
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -14,11 +14,16 @@ let nextId = 0;
selector: "bit-radio-group",
templateUrl: "radio-group.component.html",
imports: [NgTemplateOutlet, I18nPipe],
host: {
"[id]": "id()",
},
})
export class RadioGroupComponent implements ControlValueAccessor {
selected: unknown;
disabled = false;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
private _name?: string;
@Input() get name() {
return this._name ?? this.ngControl?.name?.toString();
@@ -27,10 +32,10 @@ export class RadioGroupComponent implements ControlValueAccessor {
this._name = value;
}
@Input() block = false;
readonly block = input(false);
@HostBinding("attr.role") role = "radiogroup";
@HostBinding("attr.id") @Input() id = `bit-radio-group-${nextId++}`;
readonly id = input(`bit-radio-group-${nextId++}`);
@HostBinding("class") classList = ["tw-block", "tw-mb-4"];
@ContentChild(BitLabel) protected label: BitLabel;

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, HostBinding, Input, Optional, Self } from "@angular/core";
import { Component, HostBinding, input, Input, Optional, Self } from "@angular/core";
import { NgControl, Validators } from "@angular/forms";
import { BitFormControlAbstraction } from "../form-control";
@@ -11,9 +11,12 @@ let nextId = 0;
selector: "input[type=radio][bitRadio]",
template: "",
providers: [{ provide: BitFormControlAbstraction, useExisting: RadioInputComponent }],
host: {
"[id]": "this.id()",
},
})
export class RadioInputComponent implements BitFormControlAbstraction {
@HostBinding("attr.id") @Input() id = `bit-radio-input-${nextId++}`;
readonly id = input(`bit-radio-input-${nextId++}`);
@HostBinding("class")
protected inputClasses = [
@@ -73,6 +76,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
constructor(@Optional() @Self() private ngControl?: NgControl) {}
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@HostBinding()
@Input()
get disabled() {
@@ -83,6 +88,8 @@ export class RadioInputComponent implements BitFormControlAbstraction {
}
private _disabled: boolean;
// TODO: Skipped for signal migration because:
// Accessor inputs cannot be migrated as they are too complex.
@Input()
get required() {
return (