Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,10 @@ Both entry points declare `[tedi-floating-button]`. The Community component is
### Clear buttons

- **`showClearOnInteraction` belongs on the component that renders the clear button.** For a text
field or textarea wrapped in `<tedi-form-field>`, set it on the form field, not the
control. `<tedi-search>` owns its form field and exposes the input itself; `<tedi-select>` exposes
it for its own clear button. In each case, `clearable` must be true and the field must have a value.
field wrapped in `<tedi-form-field>`, set it on the form field, not the control (a textarea
never gets a clear button). `<tedi-search>` owns its form field and exposes the input itself;
`<tedi-select>` exposes it for its own clear button. In each case, `clearable` must be true and
the field must have a value.
- With the option enabled, the clear button appears while the field is hovered, active, or contains
focus (including keyboard focus). The default is false, which keeps the clear button visible
whenever the clearable field has a value.
Expand Down
32 changes: 32 additions & 0 deletions src/community/text-editor/text-editor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { By } from "@angular/platform-browser";
import { QuillModules } from "ngx-quill";
import type Quill from "quill";
import {
FormFieldComponent,
TEDI_TRANSLATION_DEFAULT_TOKEN,
TediTranslationService,
} from "@tedi-design-system/angular/tedi";
Expand Down Expand Up @@ -373,3 +374,34 @@ describe("TextEditorComponent", () => {
});
});
});

@Component({
standalone: true,
imports: [FormFieldComponent, TextEditorComponent, ReactiveFormsModule],
template: `
<tedi-form-field [clearable]="true">
<tedi-text-editor [formControl]="control" inputId="in-field" />
</tedi-form-field>
`,
})
class FormFieldHostComponent {
control = new FormControl<string>("<p>Text</p>", { nonNullable: true });
}

describe("TextEditorComponent inside tedi-form-field", () => {
it("gets no clear button or field box, even when clearable", () => {
TestBed.configureTestingModule({
imports: [FormFieldHostComponent],
providers: [
{ provide: TediTranslationService, useClass: TranslationMock },
{ provide: TEDI_TRANSLATION_DEFAULT_TOKEN, useValue: "et" },
],
});
const fixture = TestBed.createComponent(FormFieldHostComponent);
fixture.detectChanges();

const el: HTMLElement = fixture.nativeElement;
expect(el.querySelector(".tedi-form-field__clear")).toBeNull();
expect(el.querySelector(".tedi-form-field__box")).toBeNull();
});
});
3 changes: 3 additions & 0 deletions src/community/text-editor/text-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ export class TextEditorComponent
readonly touched = this.derived.touched;
readonly dirty = this.derived.dirty;

/** Rich text takes no clear button, even inside a `clearable` form field. */
readonly clearButton = false;

readonly disabled = computed(
() => this.formDisabled() || (this.fieldContext?.disabled() ?? false),
);
Expand Down
2 changes: 2 additions & 0 deletions src/community/text-editor/text-editor.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import { TextEditorComponent } from "./text-editor.component";

/**
* <a href="https://www.figma.com/design/jWiRIXhHRxwVdMSimKX2FF/TEDI-READY-2.77.99?node-id=9938-87562&m=dev" target="_blank">Figma ↗</a>
*
* Rich text editor built on <a href="https://github.com/KillerCodeMonkey/ngx-quill" target="_blank">ngx-quill ↗</a>.
*
* Install `ngx-quill` and `quill` yourself — they are optional peer dependencies
Expand Down
1 change: 1 addition & 0 deletions src/tedi/components/content/table/table.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@
(input)="
handleColumnFilter(column, $any($event.target).value)
"
(clear)="handleColumnFilter(column, '')"
/>
</tedi-form-field>
}
Expand Down
28 changes: 28 additions & 0 deletions src/tedi/components/content/table/table.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,34 @@ describe("TediTableComponent", () => {
expect(rows[0].textContent).toContain("Anna");
});

it("resets the column filter from the clear button", () => {
const fixture = setupHost();
fixture.componentInstance.enableColumnFilters.set(true);
fixture.detectChanges();
const rows = () =>
fixture.nativeElement.querySelectorAll(
".tedi-table__body .tedi-table__row",
).length;
const total = rows();
const input = fixture.nativeElement.querySelector(
'tr.tedi-table__row--filter input[aria-label="Filter Name"]',
) as HTMLInputElement;
input.value = "Anna";
input.dispatchEvent(new Event("input"));
fixture.detectChanges();
expect(rows()).toBe(1);

(
input
.closest("tedi-form-field")!
.querySelector(".tedi-form-field__clear") as HTMLButtonElement
).click();
fixture.detectChanges();

expect(input.value).toBe("");
expect(rows()).toBe(total);
});

it("clearFilters() resets every column filter at once", () => {
const fixture = setupHost();
fixture.componentInstance.enableColumnFilters.set(true);
Expand Down
76 changes: 74 additions & 2 deletions src/tedi/components/form/date-field/date-field.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,7 @@ describe("DateFieldComponent", () => {
fixture.componentRef.setInput("value", new Date(2026, 4, 14));
fixture.detectChanges();

expect(component.clearableResolved()).toBe(true);
expect(component.canClear()).toBe(true);
expect(getClear(el)).not.toBeNull();
});
Expand Down Expand Up @@ -1470,6 +1471,31 @@ describe("DateFieldComponent", () => {
expect(component.useNativePickerEffective()).toBe(false);
expect(getClear(el)).not.toBeNull();
});

it("converts static attribute values to booleans", () => {
const { component, fixture } = createField();
fixture.componentRef.setInput("value", new Date(2026, 4, 14));

fixture.componentRef.setInput("clearable", "false");
fixture.detectChanges();
expect(component.clearable()).toBe(false);
expect(component.canClear()).toBe(false);

fixture.componentRef.setInput("clearable", "");
fixture.detectChanges();
expect(component.clearable()).toBe(true);
expect(component.canClear()).toBe(true);
});

it("hides the clear button when a standalone field opts out", () => {
const { component, el, fixture } = createField();
fixture.componentRef.setInput("clearable", false);
fixture.componentRef.setInput("value", new Date(2026, 4, 14));
fixture.detectChanges();

expect(component.canClear()).toBe(false);
expect(el.querySelector(".tedi-date-input__clear")).toBeNull();
});
});

describe("native picker parse rejection", () => {
Expand Down Expand Up @@ -1695,15 +1721,21 @@ describe("DateFieldComponent with ReactiveFormsModule", () => {
ReactiveFormsModule,
],
template: `
<tedi-form-field>
<tedi-form-field [clearable]="clearable">
<label tedi-label for="composite-date">Date</label>
<tedi-date-field inputId="composite-date" [formControl]="control" />
<tedi-date-field
inputId="composite-date"
[formControl]="control"
[clearable]="fieldClearable"
/>
<tedi-feedback-text text="Error" type="error" />
</tedi-form-field>
`,
})
class CompositeHostComponent {
control = new FormControl<Date | Date[] | DateRange | null>(null);
clearable = false;
fieldClearable?: boolean;
}

describe("DateFieldComponent inside FormFieldComponent", () => {
Expand Down Expand Up @@ -1738,6 +1770,46 @@ describe("DateFieldComponent inside FormFieldComponent", () => {
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("lets the date field's own clearable override the form field", () => {
fixture.componentInstance.fieldClearable = true;
seed(false);
expect(el.querySelectorAll(".tedi-date-input__clear")).toHaveLength(1);

fixture.componentInstance.fieldClearable = false;
seed(true);
expect(el.querySelectorAll(".tedi-date-input__clear")).toHaveLength(0);
});

it("still clears programmatically when opted out", () => {
seed(false);
const component = fixture.debugElement.query(
By.directive(DateFieldComponent),
).componentInstance as DateFieldComponent;

component.reset();
expect(component.value()).toBeNull();
});
});

it("associates the feedback text with the input via aria-describedby", () => {
const input = el.querySelector(
"input.tedi-date-input__input",
Expand Down
25 changes: 22 additions & 3 deletions src/tedi/components/form/date-field/date-field.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
Expand Down Expand Up @@ -69,7 +70,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;
Expand All @@ -93,7 +93,6 @@ export type DateFieldSize = "default" | "small";
imports: [
CalendarComponent,
DateInputComponent,
ButtonComponent,
OverlayModule,
A11yModule,
TediTranslationPipe,
Expand Down Expand Up @@ -201,6 +200,15 @@ export class DateFieldComponent
* set here.
*/
readonly size = input<DateFieldSize | undefined>();
/**
* Whether the clear button shows once the field has a value. Falls back to the
* wrapping `tedi-form-field`'s `clearable` when not set here — set it on the
* wrapper, and use this only for a standalone field.
*/
readonly clearable = input<boolean | undefined, unknown>(undefined, {
// Unset stays `undefined` so the wrapper's `clearable` still applies.
transform: (v: unknown) => (v == null ? undefined : booleanAttribute(v)),
});
/**
* Forces the error state on, or off, regardless of the reactive-forms state.
* Leave unset to let the control derive it.
Expand Down Expand Up @@ -430,12 +438,21 @@ export class DateFieldComponent
invalid: computed(() => this.invalid()),
valid: computed(() => this.valid()),
disabled: computed(() => this.disabled()),
clearable: computed(() => this.clearableResolved()),
};

ngOnInit(): void {
this.derived.connect();
}

/** The clear button sits in the date input's action row, beside the calendar. */
readonly ownsClearButton = true;

/** Own `clearable` wins, then the wrapping `tedi-form-field`'s. */
readonly clearableResolved = computed(
() => this.clearable() ?? this.fieldContext?.clearable() ?? true,
);

readonly resolvedDisabledMatchers = computed<Matcher[]>(() => {
const result: Matcher[] = [];
const explicit = this.disabledMatchers();
Expand Down Expand Up @@ -559,7 +576,9 @@ export class DateFieldComponent
() => !this.fieldDisabled() && (!this.readOnly() || this.showCalendar()),
);

readonly canClear = computed(() => !!this.value() && this.valueEditable());
readonly canClear = computed(
() => this.clearableResolved() && !!this.value() && this.valueEditable(),
);

readonly inputIsTrigger = computed(
() => this.showCalendar() && this.calendarTriggerResolved() === "input",
Expand Down
Loading
Loading