diff --git a/community/components/form/checkbox/checkbox.stories.ts b/community/components/form/checkbox/checkbox.stories.ts index 6ae5e376a..849e6a231 100644 --- a/community/components/form/checkbox/checkbox.stories.ts +++ b/community/components/form/checkbox/checkbox.stories.ts @@ -14,6 +14,11 @@ import { CheckboxCardGroupComponent } from "./checkbox-card-group/checkbox-card- export default { title: "Community/Form/Checkbox", component: CheckboxComponent, + parameters: { + status: { + type: ["existsInTediReady"], + }, + }, args: { size: "default", disabled: false, diff --git a/tedi/components/form/checkbox/checkbox.component.scss b/tedi/components/form/checkbox/checkbox.component.scss new file mode 100644 index 000000000..b81ad31e1 --- /dev/null +++ b/tedi/components/form/checkbox/checkbox.component.scss @@ -0,0 +1,102 @@ +@use "@tedi-design-system/core/mixins"; +@use "@tedi-design-system/core/bootstrap-utility/breakpoints"; + +input[tedi-checkbox][type="checkbox"] { + --_checkbox-icon-size: 1.125rem; + + appearance: none; + position: relative; + cursor: pointer; + border: 1px solid var(--form-checkbox-radio-default-border-default); + background-color: var(--form-checkbox-radio-default-background-default); + vertical-align: middle; + padding: 0; + margin: 0; + + @include mixins.responsive-styles(width, form-checkbox-radio-size-responsive); + @include mixins.responsive-styles( + height, + form-checkbox-radio-size-responsive + ); + @include mixins.responsive-styles( + border-radius, + form-checkbox-radio-indicator-radius-checkbox + ); + + @include breakpoints.media-breakpoint-up(sm) { + --_checkbox-icon-size: 1rem; + } + + &:not(:disabled):hover { + border-color: var(--form-checkbox-radio-default-border-hover); + box-shadow: 0 0 0 1px var(--form-checkbox-radio-default-border-hover); + } + + &:not(:checked):disabled { + cursor: not-allowed; + border-color: var(--form-general-border-disabled); + background-color: var(--form-general-background-disabled); + } + + &:checked, + &:indeterminate { + border-color: var(--form-checkbox-radio-default-border-selected); + background-color: var(--form-checkbox-radio-default-background-selected); + + &:disabled { + cursor: not-allowed; + border-color: var(--form-checkbox-radio-default-border-selected-disabled); + background-color: var( + --form-checkbox-radio-default-background-selected-disabled + ); + } + + &::before { + position: absolute; + top: 50%; + left: 50%; + content: "check"; + font-size: var(--_checkbox-icon-size); + font-family: "Material Symbols Outlined", sans-serif; + -webkit-font-smoothing: antialiased; + color: var(--form-checkbox-radio-default-check-indicator-default); + line-height: 1; + transform: translate(-50%, -50%); + } + } + + &:checked { + &::before { + content: "check"; + } + } + + &:indeterminate { + &::before { + content: "remove"; + } + } + + &:focus-visible { + outline-width: 2px; + outline-offset: 2px; + outline-style: solid; + } + + &:not(:checked):not(:disabled).tedi-checkbox--invalid, + &:user-invalid, + &.ng-invalid.ng-touched { + border-color: var(--form-general-feedback-error-border); + + &:hover { + border-color: var(--form-checkbox-radio-default-border-hover); + } + } + + &.tedi-checkbox--large { + --_checkbox-icon-size: 1.125rem; + + @include mixins.responsive-styles(width, form-checkbox-radio-size-large); + @include mixins.responsive-styles(height, form-checkbox-radio-size-large); + } +} diff --git a/tedi/components/form/checkbox/checkbox.component.spec.ts b/tedi/components/form/checkbox/checkbox.component.spec.ts new file mode 100644 index 000000000..a0d4dbb50 --- /dev/null +++ b/tedi/components/form/checkbox/checkbox.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { CheckboxComponent } from "./checkbox.component"; + +describe("CheckboxComponent", () => { + let fixture: ComponentFixture; + let element: HTMLInputElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CheckboxComponent], + }); + + fixture = TestBed.createComponent(CheckboxComponent); + element = fixture.nativeElement; + fixture.detectChanges(); + }); + + it("should create component", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + it("should not have large class by default", () => { + expect(element.classList).not.toContain("tedi-checkbox--large"); + }); + + it("should apply large class", () => { + fixture.componentRef.setInput("size", "large"); + fixture.detectChanges(); + expect(element.classList).toContain("tedi-checkbox--large"); + }); + + it("should not have invalid class by default", () => { + expect(element.classList).not.toContain("tedi-checkbox--invalid"); + }); + + it("should apply invalid class", () => { + fixture.componentRef.setInput("invalid", true); + fixture.detectChanges(); + expect(element.classList).toContain("tedi-checkbox--invalid"); + }); +}); diff --git a/tedi/components/form/checkbox/checkbox.component.ts b/tedi/components/form/checkbox/checkbox.component.ts new file mode 100644 index 000000000..f6d20fb0b --- /dev/null +++ b/tedi/components/form/checkbox/checkbox.component.ts @@ -0,0 +1,33 @@ +import { + ChangeDetectionStrategy, + Component, + input, + ViewEncapsulation, +} from "@angular/core"; + +export type CheckboxSize = "default" | "large"; + +@Component({ + standalone: true, + selector: "input[type=checkbox][tedi-checkbox]", + template: "", + styleUrl: "./checkbox.component.scss", + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + "[class.tedi-checkbox--large]": "size() === 'large'", + "[class.tedi-checkbox--invalid]": "invalid()", + }, +}) +export class CheckboxComponent { + /** + * Size of the checkbox. + * @default default + */ + readonly size = input("default"); + /** + * Is checkbox invalid? + * @default false + */ + readonly invalid = input(false); +} diff --git a/tedi/components/form/checkbox/checkbox.stories.ts b/tedi/components/form/checkbox/checkbox.stories.ts new file mode 100644 index 000000000..8319f2de9 --- /dev/null +++ b/tedi/components/form/checkbox/checkbox.stories.ts @@ -0,0 +1,352 @@ +import { + argsToTemplate, + Meta, + moduleMetadata, + StoryObj, +} from "@storybook/angular"; +import { CheckboxComponent } from "./checkbox.component"; +import { RowComponent } from "../../helpers/grid/row/row.component"; +import { TextComponent } from "../../base/text/text.component"; +import { LabelComponent } from "../label/label.component"; +import { IconComponent } from "../../base/icon/icon.component"; +import { TooltipComponent } from "../../overlay/tooltip/tooltip.component"; +import { TooltipTriggerComponent } from "../../overlay/tooltip/tooltip-trigger/tooltip-trigger.component"; +import { TooltipContentComponent } from "../../overlay/tooltip/tooltip-content/tooltip-content.component"; +import { InfoButtonComponent } from "../../buttons/info-button/info-button.component"; +import { FeedbackTextComponent } from "../feedback-text/feedback-text.component"; + +/** + * Figma ↗
+ * Zeroheight ↗ + */ + +export default { + title: "TEDI-Ready/Components/Form/Checkbox", + component: CheckboxComponent, + decorators: [ + moduleMetadata({ + imports: [ + CheckboxComponent, + RowComponent, + TextComponent, + LabelComponent, + IconComponent, + TooltipComponent, + TooltipTriggerComponent, + TooltipContentComponent, + InfoButtonComponent, + FeedbackTextComponent, + ], + }), + ], + argTypes: { + size: { + control: "radio", + options: ["default", "large"], + description: "Size of the checkbox.", + table: { + type: { + summary: "CheckboxSize", + detail: "default \nlarge", + }, + defaultValue: { + summary: "default", + }, + }, + }, + invalid: { + control: "boolean", + description: "Is checkbox invalid?", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: "false", + }, + }, + }, + disabled: { + control: "boolean", + description: "Is checkbox disabled?", + table: { + type: { + summary: "boolean", + }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + args: { + size: "default", + invalid: false, + disabled: false, + }, + render: (args) => ({ + props: args, + template: ` + + `, + }), +}; + +export const Size: StoryObj = { + render: (args) => ({ + props: args, + template: ` + +
Default
+ +
Large
+ +
+ `, + }), +}; + +export const Vertical: StoryObj = { + render: (args) => ({ + props: args, + template: ` +

Label

+ + + + + + `, + }), +}; + +export const Horizontal: StoryObj = { + render: (args) => ({ + props: args, + template: ` +

Label

+
+ + + +
+ `, + }), +}; + +export const VerticalTree: StoryObj = { + render: (args) => { + setTimeout(() => { + const parent = document.querySelector( + "#parentCB input", + ) as HTMLInputElement; + const children = Array.from( + document.querySelectorAll("#childrenCB input"), + ) as HTMLInputElement[]; + + function updateParent() { + const checked = children.map((c) => c.checked); + const all = checked.every((v) => v === true); + const none = checked.every((v) => v === false); + + parent.checked = all; + parent.indeterminate = !all && !none; + } + + updateParent(); + + children.forEach((c) => c.addEventListener("change", updateParent)); + + parent.addEventListener("change", () => { + const targetState = parent.checked; + children.forEach((c) => (c.checked = targetState)); + updateParent(); + }); + }); + + return { + props: args, + template: ` +

Label

+ + + + + + + + + `, + }; + }, +}; + +export const Separate: StoryObj = { + render: (args) => ({ + props: args, + template: ` + + +
+ + +
+ +
+ + + + + + + Tooltip text + + +
+
+
+ +
+
+ +

+ Description +

+
+
+
+ `, + }), +}; + +export const Group: StoryObj = { + render: (args) => ({ + props: args, + template: ` + +
+

Label

+ + + + + + +
+
+

Label

+
+ + + +
+ +
+
+

Label

+ + + + + + +
+
+

Label

+
+ + + +
+ +
+
+ `, + }), +}; diff --git a/tedi/components/form/index.ts b/tedi/components/form/index.ts index 09935c35a..e8ac0177e 100644 --- a/tedi/components/form/index.ts +++ b/tedi/components/form/index.ts @@ -1,4 +1,5 @@ +export * from "./checkbox/checkbox.component"; export * from "./feedback-text/feedback-text.component"; export * from "./label/label.component"; export * from "./number-field/number-field.component"; -export * from "./toggle/toggle.component"; \ No newline at end of file +export * from "./toggle/toggle.component"; diff --git a/tedi/components/form/label/label.component.scss b/tedi/components/form/label/label.component.scss index 622e2c15f..adabd14f0 100644 --- a/tedi/components/form/label/label.component.scss +++ b/tedi/components/form/label/label.component.scss @@ -1,8 +1,6 @@ @use "@tedi-design-system/core/mixins"; .tedi-label { - color: var(--general-text-secondary); - @include mixins.responsive-styles( font-size, body-regular-size, @@ -28,4 +26,17 @@ color: var(--form-general-feedback-error-border); } } + + &--primary { + color: var(--general-text-primary); + } + + &--secondary { + color: var(--general-text-secondary); + } + + &:has(input[disabled]), + &[for]:has(+ input[disabled]) { + color: var(--general-text-disabled); + } } diff --git a/tedi/components/form/label/label.component.ts b/tedi/components/form/label/label.component.ts index 807e6f024..7f17f23af 100644 --- a/tedi/components/form/label/label.component.ts +++ b/tedi/components/form/label/label.component.ts @@ -7,6 +7,7 @@ import { } from "@angular/core"; export type LabelSize = "small" | "default"; +export type LabelColor = "primary" | "secondary"; @Component({ selector: "label[tedi-label]", @@ -30,9 +31,14 @@ export class LabelComponent { * @default false */ required = input(false); + /** + * Color of the label. + * @default secondary + */ + color = input("secondary"); classes = computed(() => { - const classList = ["tedi-label"]; + const classList = ["tedi-label", `tedi-label--${this.color()}`]; if (this.size() === "small") { classList.push("tedi-label--small"); diff --git a/tedi/components/form/label/label.stories.ts b/tedi/components/form/label/label.stories.ts index e18b7b909..fad1d2bb7 100644 --- a/tedi/components/form/label/label.stories.ts +++ b/tedi/components/form/label/label.stories.ts @@ -63,6 +63,19 @@ export default { }, }, }, + color: { + control: "radio", + description: "Color of the label", + options: ["primary", "secondary"], + table: { + category: "inputs", + type: { + summary: "LabelColor", + detail: "primary \nsecondary", + }, + defaultValue: { summary: "secondary" }, + }, + }, }, } as Meta; @@ -72,6 +85,7 @@ export const Default: LabelStory = { args: { ngContent: "Label", size: "default", + color: "secondary", }, render: ({ ngContent, ...args }) => ({ props: args,