Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ const preview: Preview = {
},
docs: {
codePanel: true,
toc: true,
// The default is `h3` alone, so `h2` sections never reach the TOC.
// Excluding `#stories` keeps autodocs' own heading from nesting every
// story beneath it.
toc: { headingSelector: "h2:not(#stories), h3" },
page: () => (
<>
<Title />
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Guidance for AI coding agents working in this repository.

## Project Overview

TEDI Design System Angular component library (`@tedi-design-system/angular`). Angular 19 standalone component library published via ng-packagr, with Storybook for documentation.
TEDI Design System Angular component library (`@tedi-design-system/angular`). Angular 20 standalone component library published via ng-packagr, with Storybook for documentation.

Supported Angular versions: **20, 21 and 22** — the published peer range is `^20.0.0 || ^21.0.0 || ^22.0.0` (declared in both `package.json` and `src/package.json`). The repo itself develops against Angular 20, which is also the floor consumers may be on, so do not rely on APIs introduced after Angular 20.

## Commands

Expand Down
12 changes: 12 additions & 0 deletions skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ Both entry points declare `[tedi-floating-button]`. The Community component is
`brand-primary` independently.
- **`[tedi-label-row]` projects your own `<label tedi-label>`** plus trailing affixes as siblings, so
native label attributes (`for`, `id`, `aria-*`, handlers) keep working.
- **Configure the inline editor for its control.** Set `[closeOnEnter]="false"
when Enter belongs to the control, such as a textarea or select. The edit icon
is visible by default; use `hideEditIcon` only when another visible cue identifies
the value as editable. Use `fullWidth` to fill the container.
- **`tedi-inline-edit` needs an `<ng-template tediInlineEditControl>`.** The control goes in that
template, not as plain content — it is instantiated on entering edit mode and destroyed on
leaving. Without the template the value never becomes editable. The inline edit does **not** own
the value: your control keeps its own binding, and `displayValue` has to be bound to the same
source. `(editCancel)` therefore only reports that Escape was pressed; restoring the previous
value is yours to do. A projected `tedi-feedback-text` is linked to the trigger for you. Give the editor control its
own accessible label and feedback association. Use `updateOn: "change"` or `"blur"` when
persisting the bound value in `editCommit`; `"submit"` remains the parent form's responsibility.
- **`tedi-attachment` has no built-in action buttons.** Project neutral `tedi-button`s inside a single
`<tedi-attachment-actions>` and wire `(click)` and `disabled` yourself.
- **`tedi-skeleton-block` belongs inside `tedi-skeleton`.** The wrapper stacks whatever is projected
Expand Down
2 changes: 1 addition & 1 deletion skills/tedi-angular/references/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ component's `ɵɵComponentDeclaration` carries its real selector; see
| SelectComponent | `tedi-select` | `T \| T[]` |
| TextEditorComponent | `tedi-text-editor` | `string` (Quill HTML) — `/community/text-editor` entry point |

`CheckboxComponent` (`input[type=checkbox][tedi-checkbox]`) is **not** a TEDI value accessor — it styles a native checkbox, so `[formControl]` on it is handled by Angular's built-in `CheckboxControlValueAccessor` and yields a `boolean`. Inside a managed `<tedi-checkbox-group>`, its `value` input is a `string` identity instead. `DropdownComponent` (`tedi-dropdown`) lives in `overlay/` and is not a form control — it exposes `[(value)]` but implements no `ControlValueAccessor`.
`CheckboxComponent` (`input[type=checkbox][tedi-checkbox]`) is **not** a TEDI value accessor — it styles a native checkbox, so `[formControl]` on it is handled by Angular's built-in `CheckboxControlValueAccessor` and yields a `boolean`. Inside a managed `<tedi-checkbox-group>`, its `value` input is a `string` identity instead. `DropdownComponent` (`tedi-dropdown`) lives in `overlay/` and is not a form control — it exposes `[(value)]` but implements no `ControlValueAccessor`. `InlineEditComponent` (`tedi-inline-edit`) lives in `form/` but is likewise not a value accessor — it wraps a control you project into an `<ng-template tediInlineEditControl>`, and that control keeps its own `[formControl]` / `[(ngModel)]` binding.

## Basic Usage with Reactive Forms

Expand Down
1 change: 1 addition & 0 deletions src/tedi/components/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from "./slider/slider.component";
export * from "./toggle/toggle.component";
export * from "./form-field";
export * from "./input-group";
export * from "./inline-edit";
export * from "./text-field/text-field.component";
export * from "./textarea/textarea.component";
export * from "./time-field";
Expand Down
2 changes: 2 additions & 0 deletions src/tedi/components/form/inline-edit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./inline-edit.component";
export * from "./inline-edit-control.directive";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Directive, TemplateRef, inject } from "@angular/core";

/**
* Marks the template holding the control that `tedi-inline-edit` swaps in when
* the value is clicked. The template is only instantiated while editing, so the
* control is created on entry and destroyed on exit.
*
* The projected control keeps its own value binding and accessible label.
* Configure exit keys for the control (for example, disable closeOnEnter for
* a textarea). Overlays must be linked through aria-controls or aria-owns.
*
* @example
* ```html
* <tedi-inline-edit [displayValue]="name()" label="Name">
* <ng-template tediInlineEditControl>
* <input tedi-text-field [(ngModel)]="name" aria-label="Name" />
* </ng-template>
* </tedi-inline-edit>
* ```
*/
@Directive({
selector: "[tediInlineEditControl]",
standalone: true,
})
export class InlineEditControlDirective {
template = inject<TemplateRef<unknown>>(TemplateRef);
}
41 changes: 41 additions & 0 deletions src/tedi/components/form/inline-edit/inline-edit.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@if (readOnly()) {
<span class="tedi-inline-edit__static">
<span
class="tedi-inline-edit__value"
[class.tedi-inline-edit__value--placeholder]="!hasValue()"
>{{ displayText() }}</span
>
</span>
} @else if (editing() && template(); as controlTemplate) {
<div #editor class="tedi-inline-edit__editor">
<ng-container [ngTemplateOutlet]="controlTemplate" />
</div>
} @else {
<button
#trigger
type="button"
class="tedi-inline-edit__trigger"
[disabled]="disabled()"
[attr.aria-label]="triggerLabel()"
[attr.aria-describedby]="hasFeedback() ? feedbackId : null"
(click)="startEditing()"
>
<span
class="tedi-inline-edit__value"
[class.tedi-inline-edit__value--placeholder]="!hasValue()"
>{{ displayText() }}</span
>
@if (!hideEditIcon()) {
<tedi-icon
class="tedi-inline-edit__icon"
name="edit"
[size]="18"
color="brand"
/>
}
</button>
}

<div [id]="feedbackId" class="tedi-inline-edit__feedback">
<ng-content select="tedi-feedback-text" />
</div>
142 changes: 142 additions & 0 deletions src/tedi/components/form/inline-edit/inline-edit.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
@use "@tedi-design-system/core/mixins";

.tedi-inline-edit {
--_inline-edit-font-size: var(--body-regular-size);
--_inline-edit-line-height: var(--body-regular-line-height);

display: inline-flex;
flex-direction: column;
align-items: flex-start;
max-width: 100%;
}

.tedi-inline-edit--full-width {
display: flex;
align-items: stretch;
width: 100%;
}

.tedi-inline-edit .tedi-feedback-text {
margin-top: var(--form-field-outer-spacing);
}

.tedi-inline-edit--small {
--_inline-edit-font-size: var(--body-small-regular-size);
--_inline-edit-line-height: var(--body-small-regular-line-height);
}

.tedi-inline-edit__trigger {
@include mixins.button-reset;

display: flex;
gap: var(--tedi-dimensions-05);
align-items: center;
padding: var(--form-field-padding-y-sm) var(--tedi-dimensions-03);
font-size: var(--_inline-edit-font-size);
line-height: var(--_inline-edit-line-height);
color: var(--form-input-text-filled);
text-align: left;
background-color: var(--form-hidden-default);
border-radius: var(--form-field-radius);
}

.tedi-inline-edit__trigger:hover:not(:disabled) {
background-color: var(--form-hidden-hover);
}

.tedi-inline-edit__trigger:focus-visible {
@include mixins.focus-element;
}

.tedi-inline-edit__trigger:active:not(:disabled) {
outline: none;
background-color: var(--form-hidden-default);
box-shadow: inset 0 0 0 var(--tedi-borders-02) var(--form-input-border-active);
}

.tedi-inline-edit__trigger:active:not(:disabled) .tedi-inline-edit__icon {
display: none;
}

.tedi-inline-edit__trigger:disabled {
color: var(--form-input-text-disabled);
cursor: not-allowed;
}

// Match the trigger's height and text position when editing starts.
// Reduce padding by the editor's border width to keep the text aligned.
.tedi-inline-edit__editor :is(.tedi-field-surface, .tedi-input) {
--_field-height: calc(
var(--_inline-edit-line-height) + 2 * var(--form-field-padding-y-sm)
);

padding: calc(var(--form-field-padding-y-sm) - var(--tedi-borders-01))
calc(var(--tedi-dimensions-03) - var(--tedi-borders-01));
font-size: var(--_inline-edit-font-size);
line-height: var(--_inline-edit-line-height);
}

.tedi-inline-edit__editor .tedi-select__arrow .tedi-icon {
--_tedi-icon-size: min(var(--icon-05), var(--_inline-edit-line-height));
}

.tedi-inline-edit__editor .tedi-select__clear {
width: min(var(--button-xs-icon-size), var(--_inline-edit-line-height));
height: min(var(--button-xs-icon-size), var(--_inline-edit-line-height));
}

.tedi-inline-edit__editor .tedi-select__search-wrapper {
min-height: var(--_inline-edit-line-height);
}

.tedi-inline-edit__value {
flex: 0 1 auto;
min-width: 0;
overflow-wrap: break-word;
}

.tedi-inline-edit__value--placeholder {
color: var(--form-input-text-placeholder);
}

.tedi-inline-edit__icon {
flex-shrink: 0;
}

.tedi-inline-edit__trigger:disabled .tedi-inline-edit__icon {
color: var(--general-text-disabled);
}

.tedi-inline-edit--full-width .tedi-inline-edit__trigger,
.tedi-inline-edit--full-width .tedi-inline-edit__editor {
width: 100%;
}

.tedi-inline-edit--icon-end .tedi-inline-edit__value {
flex: 1 1 auto;
}

// Keep read-only values aligned with editable values.
.tedi-inline-edit__static {
display: flex;
gap: var(--tedi-dimensions-05);
align-items: center;
padding: var(--form-field-padding-y-sm) var(--tedi-dimensions-03);
font-size: var(--_inline-edit-font-size);
line-height: var(--_inline-edit-line-height);
color: var(--form-input-text-filled);
}

.tedi-inline-edit--invalid .tedi-inline-edit__trigger {
box-shadow: inset 0 0 0 var(--tedi-borders-01)
var(--form-general-feedback-error-border);
}

.tedi-inline-edit--invalid .tedi-inline-edit__trigger:active:not(:disabled) {
box-shadow: inset 0 0 0 var(--tedi-borders-02)
var(--form-general-feedback-error-border);
}

.tedi-inline-edit__trigger:focus-visible:not(:active) {
box-shadow: none;
}
Loading
Loading