mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
[CL-50] Form controls (checkbox and radio) (#4066)
* [CL-50] feat: scaffold checkbox component * [CL-50] feat: implement control value accessor for checbox * [CL-50] feat: add form-field support to checkbox * [CL-50] feat: implement non-selected checkbox styling * [CL-50] feat: implement checkbox checked styles * [CL-50] feat: improve checkbox form-field compat * [CL-50] fix: checkbox border hover wrong color * [CL-50] feat: use svg instead of bwi font * [CL-50] feat: scaffold radio button * [EC-50] feat: implement radio logic * [CL-50] feat: add radio group tests * [CL-50] feat: add radio-button tests * [CL-50] feat: implement radio button styles * [CL-50] fix: checkbox style tweaks * [CL-50] feat: smooth radio button selection transition * [CL-50] chore: various fixes and cleanups * [CL-50] feat: add form field support * [EC-50] feat-wip: simplify checkbox styling * [EC-50] feat: extract checkbox into separate component * [CL-50] feat: add standalone form control component * [CL-50] feat: remove unnecessary checkbox-control It wasn't really doing anything, might as well use form control directly * [CL-50] chore: create separate folder with form examples * [CL-50] feat: switch to common bit-label * [CL-50] feat: let radio group act as form control * [CL-50] chore: restore form-field component * [CL-50] feat: add support for hint and error * [CL-50] fix: storybook build issue * [CL-50] fix: radio group label wrong text color * [CL-50] fix: translation * [CL-50] fix: put hint and errors outside label * [CL-50] feat: * [CL-50] feat: add custom checkbox example story * [CL-50] chore: remove 1 from full example name * [CL-50] chore: clean up unused icon * [CL-50] chore: clean up unused tailwind plugin * [CL-50] fix: ring offset color in custom example * [CL-50] chore: clean up unused icon * [CL-50] chore: add design link * [CL-50] chore: remove unused import * [CL-50] fix: pr review comments * [CL-50] fix: improve id handling
This commit is contained in:
104
libs/components/src/checkbox/checkbox.component.ts
Normal file
104
libs/components/src/checkbox/checkbox.component.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Component, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { BitFormControlAbstraction } from "../form-control";
|
||||
|
||||
@Component({
|
||||
selector: "input[type=checkbox][bitCheckbox]",
|
||||
template: "",
|
||||
providers: [{ provide: BitFormControlAbstraction, useExisting: CheckboxComponent }],
|
||||
styles: [
|
||||
`
|
||||
:host:checked:before {
|
||||
-webkit-mask-image: url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E');
|
||||
mask-image: url('data:image/svg+xml,%3Csvg class="svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="8" height="8" viewBox="0 0 10 10"%3E%3Cpath d="M0.5 6.2L2.9 8.6L9.5 1.4" fill="none" stroke="white" stroke-width="2"%3E%3C/path%3E%3C/svg%3E');
|
||||
-webkit-mask-position: center;
|
||||
mask-position: center;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-repeat: no-repeat;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class CheckboxComponent implements BitFormControlAbstraction {
|
||||
@HostBinding("class")
|
||||
protected inputClasses = [
|
||||
"tw-appearance-none",
|
||||
"tw-outline-none",
|
||||
"tw-relative",
|
||||
"tw-transition",
|
||||
"tw-cursor-pointer",
|
||||
"tw-inline-block",
|
||||
"tw-rounded",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-secondary-500",
|
||||
"tw-h-3.5",
|
||||
"tw-w-3.5",
|
||||
"tw-mr-1.5",
|
||||
"tw-bottom-[-1px]", // Fix checkbox looking off-center
|
||||
|
||||
"before:tw-content-['']",
|
||||
"before:tw-block",
|
||||
"before:tw-absolute",
|
||||
"before:tw-inset-0",
|
||||
|
||||
"hover:tw-border-2",
|
||||
"[&>label]:tw-border-2",
|
||||
|
||||
"focus-visible:tw-ring-2",
|
||||
"focus-visible:tw-ring-offset-2",
|
||||
"focus-visible:tw-ring-primary-700",
|
||||
|
||||
"disabled:tw-cursor-auto",
|
||||
"disabled:tw-border",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
|
||||
"checked:tw-bg-primary-500",
|
||||
"checked:tw-border-primary-500",
|
||||
|
||||
"checked:hover:tw-bg-primary-700",
|
||||
"checked:hover:tw-border-primary-700",
|
||||
"[&>label:hover]:checked:tw-bg-primary-700",
|
||||
"[&>label:hover]:checked:tw-border-primary-700",
|
||||
|
||||
"checked:before:tw-bg-text-contrast",
|
||||
|
||||
"checked:disabled:tw-border-secondary-100",
|
||||
"checked:disabled:tw-bg-secondary-100",
|
||||
|
||||
"checked:disabled:before:tw-bg-text-muted",
|
||||
];
|
||||
|
||||
constructor(@Optional() @Self() private ngControl?: NgControl) {}
|
||||
|
||||
@HostBinding()
|
||||
@Input()
|
||||
get disabled() {
|
||||
return this._disabled ?? this.ngControl?.disabled ?? false;
|
||||
}
|
||||
set disabled(value: any) {
|
||||
this._disabled = value != null && value !== false;
|
||||
}
|
||||
private _disabled: boolean;
|
||||
|
||||
@Input()
|
||||
get required() {
|
||||
return (
|
||||
this._required ?? this.ngControl?.control?.hasValidator(Validators.requiredTrue) ?? false
|
||||
);
|
||||
}
|
||||
set required(value: any) {
|
||||
this._required = value != null && value !== false;
|
||||
}
|
||||
private _required: boolean;
|
||||
|
||||
get hasError() {
|
||||
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||
}
|
||||
|
||||
get error(): [string, any] {
|
||||
const key = Object.keys(this.ngControl.errors)[0];
|
||||
return [key, this.ngControl.errors[key]];
|
||||
}
|
||||
}
|
||||
14
libs/components/src/checkbox/checkbox.module.ts
Normal file
14
libs/components/src/checkbox/checkbox.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { FormControlModule } from "../form-control";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
import { CheckboxComponent } from "./checkbox.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, CommonModule, FormControlModule],
|
||||
declarations: [CheckboxComponent],
|
||||
exports: [CheckboxComponent],
|
||||
})
|
||||
export class CheckboxModule {}
|
||||
104
libs/components/src/checkbox/checkbox.stories.ts
Normal file
104
libs/components/src/checkbox/checkbox.stories.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/src/abstractions/i18n.service";
|
||||
|
||||
import { FormControlModule } from "../form-control";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
import { CheckboxModule } from "./checkbox.module";
|
||||
|
||||
const template = `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="checkbox">
|
||||
<bit-label>Click me</bit-label>
|
||||
</bit-form-control>
|
||||
</form>`;
|
||||
|
||||
@Component({
|
||||
selector: "app-example",
|
||||
template,
|
||||
})
|
||||
class ExampleComponent {
|
||||
protected formObj = this.formBuilder.group({
|
||||
checkbox: [false, Validators.requiredTrue],
|
||||
});
|
||||
|
||||
@Input() set checked(value: boolean) {
|
||||
this.formObj.patchValue({ checkbox: value });
|
||||
}
|
||||
|
||||
@Input() set disabled(disable: boolean) {
|
||||
if (disable) {
|
||||
this.formObj.disable();
|
||||
} else {
|
||||
this.formObj.enable();
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Component Library/Form/Checkbox",
|
||||
component: ExampleComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
declarations: [ExampleComponent],
|
||||
imports: [FormsModule, ReactiveFormsModule, FormControlModule, CheckboxModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
return new I18nMockService({
|
||||
required: "required",
|
||||
inputRequired: "Input is required.",
|
||||
inputEmail: "Input is not an email-address.",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=3930%3A16850&t=xXPx6GJYsJfuMQPE-4",
|
||||
},
|
||||
},
|
||||
args: {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const DefaultTemplate: Story<ExampleComponent> = (args: ExampleComponent) => ({
|
||||
props: args,
|
||||
template: `<app-example [checked]="checked" [disabled]="disabled"></app-example>`,
|
||||
});
|
||||
|
||||
export const Default = DefaultTemplate.bind({});
|
||||
|
||||
const CustomTemplate: Story = (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<div class="tw-flex tw-flex-col tw-w-32">
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
A-Z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
a-z
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
<label class="tw-text-main tw-flex tw-bg-secondary-300 tw-p-2 tw-items-baseline">
|
||||
0-9
|
||||
<input class="tw-ml-auto focus-visible:tw-ring-offset-secondary-300" type="checkbox" bitCheckbox>
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Custom = CustomTemplate.bind({});
|
||||
1
libs/components/src/checkbox/index.ts
Normal file
1
libs/components/src/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./checkbox.module";
|
||||
Reference in New Issue
Block a user