1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 18:23:31 +00:00
Files
browser/libs/components/src/switch/switch.component.ts
Bryan Cunningham 6e97927db6 Uif/export switch (#17234)
* fix private variables used in template

* remove module

* fix test import
2025-11-05 13:28:28 -05:00

135 lines
3.7 KiB
TypeScript

import { NgClass } from "@angular/common";
import {
Component,
computed,
contentChild,
ElementRef,
inject,
input,
model,
AfterViewInit,
} from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { AriaDisableDirective } from "../a11y";
import { FormControlModule } from "../form-control/form-control.module";
import { BitHintComponent } from "../form-control/hint.component";
import { BitLabel } from "../form-control/label.component";
let nextId = 0;
/**
* Switch component for toggling between two states. Switch actions are meant to take place immediately and are not to be used in a form where saving/submiting actions are required.
*/
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "bit-switch",
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: SwitchComponent,
multi: true,
},
],
templateUrl: "switch.component.html",
imports: [FormControlModule, NgClass],
host: {
"[id]": "this.id()",
"[attr.aria-disabled]": "this.disabled()",
"[attr.title]": "this.disabled() ? this.disabledReasonText() : null",
},
hostDirectives: [AriaDisableDirective],
})
export class SwitchComponent implements ControlValueAccessor, AfterViewInit {
private el = inject(ElementRef<HTMLButtonElement>);
private readonly label = contentChild.required(BitLabel);
/**
* Model signal for selected state binding when used outside of a form
*/
protected readonly selected = model(false);
/**
* Model signal for disabled binding when used outside of a form
*/
protected readonly disabled = model(false);
protected readonly disabledReasonText = input<string | null>(null);
private readonly hintComponent = contentChild<BitHintComponent>(BitHintComponent);
protected readonly disabledReasonTextId = `bit-switch-disabled-text-${nextId++}`;
protected readonly describedByIds = computed(() => {
const ids: string[] = [];
if (this.disabledReasonText() && this.disabled()) {
ids.push(this.disabledReasonTextId);
} else {
const hintId = this.hintComponent()?.id;
if (hintId) {
ids.push(hintId);
}
}
return ids.join(" ");
});
// ControlValueAccessor functions
private notifyOnChange: (value: boolean) => void = () => {};
private notifyOnTouch: () => void = () => {};
writeValue(value: boolean): void {
this.selected.set(value);
}
onChange(value: boolean): void {
this.selected.set(value);
if (this.notifyOnChange != undefined) {
this.notifyOnChange(value);
}
}
onTouch() {
if (this.notifyOnTouch != undefined) {
this.notifyOnTouch();
}
}
registerOnChange(fn: (value: boolean) => void): void {
this.notifyOnChange = fn;
}
registerOnTouched(fn: () => void): void {
this.notifyOnTouch = fn;
}
setDisabledState(isDisabled: boolean) {
this.disabled.set(isDisabled);
}
// end ControlValueAccessor functions
readonly id = input(`bit-switch-${nextId++}`);
protected onInputChange(event: Event) {
const checked = (event.target as HTMLInputElement).checked;
this.onChange(checked);
this.onTouch();
}
get inputId() {
return `${this.id()}-input`;
}
ngAfterViewInit() {
if (!this.label()) {
// This is only here so Angular throws a compilation error if no label is provided.
// the `this.label()` value must try to be accessed for the required content child check to throw
// eslint-disable-next-line no-console
console.error("No label component provided. <bit-switch> must be used with a <bit-label>.");
}
}
}