From 7ff0f0c715963c7c962fba17080df85c7e778401 Mon Sep 17 00:00:00 2001 From: Ly Tempel Date: Thu, 30 Jul 2026 10:32:50 +0300 Subject: [PATCH 01/11] feat(form-field,date-field,time-field): single clearable prop on the field #572 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: tedi-time-field no longer accepts clearable — set it on the wrapping tedi-form-field instead. BREAKING CHANGE: date and time fields no longer show a clear button by default. tedi-time-field's clearable previously defaulted to true, so add clearable to the wrapping tedi-form-field to keep it. BREAKING CHANGE: tedi-date-field no longer accepts size. It had no effect — bind size on the wrapping tedi-form-field, which is where it always applied. --- .../date-field/date-field.component.spec.ts | 44 +++++- .../form/date-field/date-field.component.ts | 22 ++- .../form/date-field/date-field.stories.ts | 136 ++++++++++-------- .../form/form-field/form-field-context.ts | 15 ++ .../form/form-field/form-field-control.ts | 7 + .../form/form-field/form-field.component.html | 2 +- .../form-field/form-field.component.spec.ts | 42 ++++++ .../form/form-field/form-field.component.ts | 39 ++++- tedi/components/form/index.ts | 1 + .../time-field/time-field.component.spec.ts | 20 +++ .../form/time-field/time-field.component.ts | 14 +- .../form/time-field/time-field.stories.ts | 66 ++++++--- 12 files changed, 304 insertions(+), 104 deletions(-) create mode 100644 tedi/components/form/form-field/form-field-context.ts diff --git a/tedi/components/form/date-field/date-field.component.spec.ts b/tedi/components/form/date-field/date-field.component.spec.ts index a4b894a02..fb2ad1208 100644 --- a/tedi/components/form/date-field/date-field.component.spec.ts +++ b/tedi/components/form/date-field/date-field.component.spec.ts @@ -1104,16 +1104,15 @@ describe("DateFieldComponent", () => { }); }); - describe("clear button", () => { - it("renders once the field has a value", () => { + describe("clearable", () => { + it("renders no clear button until the form field opts in", () => { const { component, el, fixture } = createField(); - expect(el.querySelector(".tedi-date-input__clear")).toBeNull(); - fixture.componentRef.setInput("value", new Date(2026, 4, 14)); fixture.detectChanges(); - expect(component.canClear()).toBe(true); - expect(el.querySelector(".tedi-date-input__clear")).not.toBeNull(); + expect(component.clearableResolved()).toBe(false); + expect(component.canClear()).toBe(false); + expect(el.querySelector(".tedi-date-input__clear")).toBeNull(); }); }); @@ -1308,7 +1307,7 @@ describe("DateFieldComponent with ReactiveFormsModule", () => { ReactiveFormsModule, ], template: ` - + @@ -1317,6 +1316,7 @@ describe("DateFieldComponent with ReactiveFormsModule", () => { }) class CompositeHostComponent { control = new FormControl(null); + clearable = false; } describe("DateFieldComponent inside FormFieldComponent", () => { @@ -1350,6 +1350,36 @@ describe("DateFieldComponent inside FormFieldComponent", () => { it("renders feedback text", () => { expect(el.querySelector("tedi-feedback-text")).toBeTruthy(); }); + + describe("clearable driven by the form field", () => { + const seed = (clearable: boolean) => { + fixture.componentInstance.clearable = clearable; + fixture.componentInstance.control.setValue(new Date(2026, 4, 14)); + fixture.detectChanges(); + }; + + it("renders none when the form field opts out", () => { + seed(false); + expect(el.querySelectorAll(".tedi-date-input__clear")).toHaveLength(0); + expect(el.querySelectorAll(".tedi-form-field__clear")).toHaveLength(0); + }); + + it("renders exactly one when the form field opts in", () => { + seed(true); + expect(el.querySelectorAll(".tedi-date-input__clear")).toHaveLength(1); + expect(el.querySelectorAll(".tedi-form-field__clear")).toHaveLength(0); + }); + + it("still clears programmatically when opted out", () => { + seed(false); + const component = fixture.debugElement.query( + By.directive(DateFieldComponent), + ).componentInstance as DateFieldComponent; + + component.clearField(); + expect(component.value()).toBeNull(); + }); + }); }); // TODO: footer-projection cannot be asserted in jsdom; see date-field.stories.ts WithFooter diff --git a/tedi/components/form/date-field/date-field.component.ts b/tedi/components/form/date-field/date-field.component.ts index 15a94e608..e31082366 100644 --- a/tedi/components/form/date-field/date-field.component.ts +++ b/tedi/components/form/date-field/date-field.component.ts @@ -32,6 +32,7 @@ import { FormFieldControl, TEDI_FORM_FIELD_CONTROL, } from "../form-field/form-field-control"; +import { TEDI_FORM_FIELD } from "../form-field/form-field-context"; import { breakpointInput, BreakpointInput, @@ -53,7 +54,6 @@ import { DateFieldModalComponent, DateFieldModalData, } from "./date-field-modal/date-field-modal.component"; -import { ButtonComponent } from "../../buttons"; import { TediTranslationPipe } from "../../../services/translation/translation.pipe"; type DateFieldValue = Date | Date[] | DateRange | null; @@ -65,7 +65,6 @@ type YearPredicate = (year: Date) => boolean; type DateFieldCalendarTrigger = "input" | "button"; type DateFieldModalInput = boolean | "sm" | "md" | "lg" | "xl"; export type DateFieldUseNativePicker = boolean | "sm" | "md" | "lg" | "xl"; -export type DateFieldSize = "default" | "small"; @Component({ selector: "tedi-date-field", @@ -77,7 +76,6 @@ export type DateFieldSize = "default" | "small"; imports: [ CalendarComponent, DateInputComponent, - ButtonComponent, OverlayModule, A11yModule, TediTranslationPipe, @@ -157,8 +155,6 @@ export class DateFieldComponent * bind it there too, since DateField owns no label. */ readonly required = input(false); - /** Field size — matches the surrounding `tedi-form-field`. */ - readonly size = input("default"); /** Disables all dates before this date (inclusive boundary stays enabled). */ readonly minDate = input(undefined); /** Disables all dates after this date (inclusive boundary stays enabled). */ @@ -319,6 +315,16 @@ export class DateFieldComponent readonly invalid = computed(() => this.formInvalid()); + private readonly formField = inject(TEDI_FORM_FIELD, { optional: true }); + + /** The clear button sits in the date input's action row, beside the calendar. */ + readonly ownsClearButton = true; + + /** Driven by the wrapping `tedi-form-field`'s `clearable`. */ + readonly clearableResolved = computed( + () => this.formField?.clearable() ?? false, + ); + readonly resolvedDisabledMatchers = computed(() => { const result: Matcher[] = []; const explicit = this.disabledMatchers(); @@ -427,7 +433,11 @@ export class DateFieldComponent }); readonly canClear = computed( - () => !!this.value() && !this.fieldDisabled() && !this.readOnly(), + () => + this.clearableResolved() && + !!this.value() && + !this.fieldDisabled() && + !this.readOnly(), ); readonly inputIsTrigger = computed( diff --git a/tedi/components/form/date-field/date-field.stories.ts b/tedi/components/form/date-field/date-field.stories.ts index 83b980e5d..f55a743a4 100644 --- a/tedi/components/form/date-field/date-field.stories.ts +++ b/tedi/components/form/date-field/date-field.stories.ts @@ -10,7 +10,10 @@ import { Validators, } from "@angular/forms"; import { DateFieldComponent } from "./date-field.component"; -import { FormFieldComponent } from "../form-field/form-field.component"; +import { + FormFieldComponent, + type InputSize, +} from "../form-field/form-field.component"; import { LabelComponent } from "../label/label.component"; import { FeedbackTextComponent } from "../feedback-text/feedback-text.component"; import { ButtonComponent } from "../../buttons/button/button.component"; @@ -68,7 +71,6 @@ const parseUS = (value: string): Date | undefined => { */ const COMMON_INPUTS = [ "mode", - "size", "selectionLevel", "monthYearSelectType", "localeCode", @@ -101,6 +103,10 @@ const argBindings = (exclude: string[] = []): string => .join("\n "); type DateFieldStoryArgs = DateFieldComponent & { + /** Story-only: size applied to the wrapping `tedi-form-field`. */ + size?: InputSize; + /** Story-only: clearable applied to the wrapping `tedi-form-field`. */ + clearable?: boolean; /** Story-only: text rendered in the sibling `