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
13 changes: 13 additions & 0 deletions skills/tedi-angular/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ Both entry points declare `[tedi-floating-button]`. The Community component is
`icon` input, not from projection, so you cannot swap in your own `tedi-icon` or add trailing
content. It is decorative and `aria-hidden`, which means the projected text is the whole accessible
name — if the icon carries meaning the text doesn't, compose a heading and an icon yourself.
- **`tedi-sheet` content goes in `<ng-template tediSheetContent>`.** The sheet renders it into an
overlay only while open, so content placed directly in `<tedi-sheet>` stays on the page. The
header, body, footer, `tediSheetTrigger` and `tediSheetClose` find the sheet through DI and must be
inside it. Header slots are attributes on the projected elements (`tedi-sheet-header-start`,
`tedi-sheet-header-end`, `tedi-sheet-header-actions`), and so is the footer's end edge
(`tedi-sheet-footer-end`). The exception is `tedi-sheet-header-content`, which replaces the whole
header layout: it is a directive, so import `SheetHeaderContentDirective` or the element is
silently dropped, and the sheet is then unlabelled until you set `ariaLabelledBy` or `ariaLabel`
on `tediSheetContent`.

### Clear buttons

Expand Down Expand Up @@ -258,6 +267,10 @@ Both entry points declare `[tedi-floating-button]`. The Community component is
can wrap: it centres the icon on the *first line* rather than the middle of the block. A heading
value in `modifiers` overrides `element`'s typography and the icon follows it, so
`element="h2" modifiers="h4"` is an `<h2>` that looks like an `h4` throughout.
- **`tedi-sheet` for mobile bottom sheets, not `ModalService` with `position: 'bottom'`.** The sheet
adds drag to close, snap points and a collapsed state, and with `showOverlay`, `trapFocus` and
`lockScroll` off (plus `closeOnBackdropClick` off) it stays docked while the page behind remains
usable.

### Responsive behaviour that isn't an input

Expand Down
1 change: 1 addition & 0 deletions src/tedi/components/overlay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./modal";
export * from "./tooltip";
export * from "./info-tooltip";
export * from "./popover";
export * from "./sheet";
8 changes: 8 additions & 0 deletions src/tedi/components/overlay/sheet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from "./sheet.component";
export * from "./sheet-content.directive";
export * from "./sheet-trigger.directive";
export * from "./sheet-close.directive";
export * from "./sheet-header/sheet-header.component";
export * from "./sheet-header/sheet-header-content.directive";
export * from "./sheet-body/sheet-body.component";
export * from "./sheet-footer/sheet-footer.component";
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
ChangeDetectionStrategy,
Component,
inject,
input,
ViewEncapsulation,
} from "@angular/core";
import { CdkScrollable } from "@angular/cdk/scrolling";
import { SheetComponent } from "../sheet.component";

export type SheetBodyPadding = "default" | "none";

/** Sheet body. Scrolls when its content is taller than the sheet. */
@Component({
standalone: true,
selector: "tedi-sheet-body",
template: "<ng-content />",
styleUrl: "../sheet.component.scss",
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [CdkScrollable],
host: {
class: "tedi-sheet-body",
"[class.tedi-sheet-body--no-padding]": "padding() === 'none'",
"[hidden]": "sheet.collapsed()",
},
})
export class SheetBodyComponent {
/**
* Use `none` for content that brings its own padding, e.g. tabs or a full-width list.
* @default default
*/
readonly padding = input<SheetBodyPadding>("default");

protected readonly sheet = inject(SheetComponent);
}
14 changes: 14 additions & 0 deletions src/tedi/components/overlay/sheet/sheet-close.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Directive, inject } from "@angular/core";
import { SheetComponent } from "./sheet.component";

/** Closes the surrounding `<tedi-sheet>` on click, e.g. from a footer button. */
@Directive({
standalone: true,
selector: "[tediSheetClose]",
host: {
"(click)": "sheet.close()",
},
})
export class SheetCloseDirective {
protected readonly sheet = inject(SheetComponent);
}
144 changes: 144 additions & 0 deletions src/tedi/components/overlay/sheet/sheet-content.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
booleanAttribute,
computed,
Directive,
inject,
input,
output,
TemplateRef,
} from "@angular/core";
import {
breakpointInput,
BreakpointInput,
BreakpointService,
} from "../../../services/breakpoint/breakpoint.service";

export type SheetRadius = "default" | "card" | "none";
export type SheetPadding = "default" | "small";

/**
* The sheet panel's content and settings. Holds `<tedi-sheet-header>`,
* `<tedi-sheet-body>` and `<tedi-sheet-footer>`. Put a `cdkFocusInitial`
* attribute on the element that should get focus when the sheet opens.
*/
@Directive({
standalone: true,
selector: "ng-template[tediSheetContent]",
})
export class SheetContentDirective {
/** @internal */
readonly templateRef = inject(TemplateRef);

/**
* Show the drag handle. While it's shown, the sheet can be dragged from the
* handle or from anywhere on `<tedi-sheet-header>` except its interactive content.
* @default true
*/
readonly showHandle = input(true, { transform: booleanAttribute });

/**
* Trap focus inside the sheet while it's open.
* @default true
*/
readonly trapFocus = input(true, { transform: booleanAttribute });

/**
* Return focus to the element that had it before the sheet opened.
* @default true
*/
readonly returnFocus = input(true, { transform: booleanAttribute });

/**
* Show the dimmed backdrop. For a docked sheet that leaves the page usable,
* also set `trapFocus` and `lockScroll` to `false`, and the sheet's
* `closeOnBackdropClick` to `false`.
* @default true
*/
readonly showOverlay = input(true, { transform: booleanAttribute });

/**
* Block page scrolling while the sheet is open.
* @default true
*/
readonly lockScroll = input(true, { transform: booleanAttribute });

/**
* Keep the content rendered while the sheet is closed, so form values and
* scroll position survive closing and reopening.
* @default false
*/
readonly keepMounted = input(false, { transform: booleanAttribute });

/**
* Rest heights as fractions of the viewport height (`0`–`1`), e.g. `[0.4, 0.9]`.
* Releasing a drag snaps to the nearest one; dragging below the lowest closes the sheet.
*/
readonly snapPoints = input<number[]>();

/**
* Index into the sorted `snapPoints` to open at. The last snap point the
* user dragged to is kept across closing and reopening.
* @default the tallest snap point
*/
readonly defaultSnapPoint = input<number>();

/** Emits the fraction the sheet snapped to after a drag. */
readonly snapPointChange = output<number>();

/**
* Render visually hidden close buttons at the start and end of the sheet,
* so touch screen reader users can leave it without an Escape key. Only
* applies while `trapFocus` is `true`.
* @default true
*/
readonly visuallyHiddenDismiss = input(true, { transform: booleanAttribute });

/**
* Id of the element that labels the sheet. Only needed when the label is
* outside `<tedi-sheet-header>`, whose `title` labels the sheet automatically.
*/
readonly ariaLabelledBy = input<string>();

/**
* Accessible name for a sheet without a visible title. Ignored when
* `ariaLabelledBy` is set or the header has a `title`.
*/
readonly ariaLabel = input<string>();

/** Minimum panel height (any CSS length, e.g. `60dvh`). Not applied while collapsed. */
readonly minHeight = input<string>();

/**
* Maximum panel height (any CSS length). The body scrolls past it.
* Ignored while `snapPoints` are set.
* @default 90dvh
*/
readonly maxHeight = input<string>();

/**
* Padding of the header, body and footer. To remove only the body padding,
* use `padding="none"` on `<tedi-sheet-body>`.
* @default default
*/
readonly padding = input<SheetPadding>("default");

/**
* Top corner radius. `card` matches a card's rounded corners.
* Accepts a plain value or a breakpoint object, e.g. `{ xs: 'none', md: 'card' }`.
* @default default
*/
readonly radius = input(
{ xs: "default" as SheetRadius },
{ transform: (v: BreakpointInput<SheetRadius>) => breakpointInput(v) },
);

/** Extra class on the sheet panel. */
readonly panelClass = input<string>();

private readonly breakpointService = inject(BreakpointService);

/** @internal */
readonly resolvedRadius = computed(() =>
this.breakpointService.resolveBreakpointInput(this.radius()),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
inject,
input,
ViewEncapsulation,
} from "@angular/core";
import { SheetComponent } from "../sheet.component";

export type SheetFooterAlign = "left" | "center" | "right";

/**
* Sheet footer. Add `tedi-sheet-footer-end` to the elements that should sit
* at the end edge, e.g. an overflow menu button; the rest then align to the start.
*/
@Component({
standalone: true,
selector: "tedi-sheet-footer",
template: "<ng-content />",
styleUrl: "../sheet.component.scss",
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
"[class]": "'tedi-sheet-footer tedi-sheet-footer--' + align()",
"[class.tedi-sheet-footer--full-width]": "fullWidth()",
"[hidden]": "sheet.collapsed()",
},
})
export class SheetFooterComponent {
/**
* Placement of the actions. Ignored when an element has `tedi-sheet-footer-end`.
* @default left
*/
readonly align = input<SheetFooterAlign>("left");

/**
* Stretch the actions to share the footer width equally. Elements with
* `tedi-sheet-footer-end` keep their own width.
* @default false
*/
readonly fullWidth = input(false, { transform: booleanAttribute });

protected readonly sheet = inject(SheetComponent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Directive } from "@angular/core";

/**
* Custom header content. Replaces the header's whole default layout: title,
* slots, and the collapse and close buttons. The content then labels nothing,
* so set `ariaLabelledBy` or `ariaLabel` on `tediSheetContent`, and add your
* own `tediSheetClose` control if the sheet should be closable from it.
*/
@Directive({
standalone: true,
selector: "[tedi-sheet-header-content]",
host: {
class: "tedi-sheet-header__content",
},
})
export class SheetHeaderContentDirective {}
Loading
Loading