@compsych-ui-components/angular 7.0.9 → 7.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dialog/dialog.ts","../../../../../../packages/angular/src/lib/dialog/dialog.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n OnDestroy,\r\n TemplateRef,\r\n ViewChild,\r\n ViewContainerRef,\r\n effect,\r\n inject,\r\n input,\r\n isDevMode,\r\n model,\r\n output,\r\n} from '@angular/core';\r\nimport { Dialog, DialogRef } from '@angular/cdk/dialog';\r\nimport { Directionality } from '@angular/cdk/bidi';\r\nimport { Overlay } from '@angular/cdk/overlay';\r\nimport { nextDialogIds } from './dialog-id';\r\nimport { DialogSurfaceComponent } from './dialog-surface';\r\nimport { DialogVariant } from './dialog-types';\r\n\r\n/** How a dialog was closed — surfaced by the `closed` output. */\r\nexport type DialogCloseReason = 'programmatic' | 'backdrop' | 'escape' | 'close-button';\r\n\r\n/**\r\n * A themeable modal dialog built on `@angular/cdk/dialog` (centered, scrimmed, focus-trapped).\r\n *\r\n * Figma: Core Components — \"Dialog\" organism (file VFBn7KCDy3FSIlvhNo3ylq, 873:9194).\r\n *\r\n * One component, switched by `variant`:\r\n * - `modal-leading-icon` — leading icon + title header, projected body, split footer.\r\n * - `modal-centered-icon` — centered icon badge, centered title + description, split footer.\r\n * - `alert` — compact title + description, right-aligned footer.\r\n *\r\n * Open state is a two-way `[(open)]` model wrapped around the CDK `Dialog` service. The\r\n * CDK dialog container provides `role=\"dialog\"`, `aria-modal`, the focus trap, initial\r\n * focus (`autoFocus: 'dialog'`) and focus restoration. The dialog closes on Escape /\r\n * backdrop click (when `dismissible`); those are handled here so `closed` can report a\r\n * reason. Title/description come from inputs; project the body as default content and each\r\n * action button with the `cmpDialogActions` attribute (the footer lays them out — split\r\n * full-width for modals, right-aligned for alerts).\r\n *\r\n * <compsych-dialog [(open)]=\"showDialog\" variant=\"modal-leading-icon\"\r\n * icon=\"globe\" title=\"Update your details\">\r\n * <p>Body content…</p>\r\n * <compsych-button cmpDialogActions variant=\"outlined\" fullWidth label=\"Cancel\" (click)=\"showDialog = false\" />\r\n * <compsych-button cmpDialogActions variant=\"filled\" fullWidth label=\"Save\" (click)=\"save()\" />\r\n * </compsych-dialog>\r\n *\r\n * Consumers must load `@angular/cdk/overlay-prebuilt.css` (overlay positioning) and the\r\n * `@compsych-ui-components/shared` design tokens globally. The scrim color is injected by\r\n * the dialog itself — no extra stylesheet import is needed.\r\n */\r\n@Component({\r\n selector: 'compsych-dialog',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [DialogSurfaceComponent],\r\n templateUrl: './dialog.html',\r\n styleUrl: './dialog.css',\r\n host: { class: 'cmp-dialog-host' },\r\n})\r\nexport class DialogComponent implements OnDestroy {\r\n /** Two-way open state. Set to `true` to show the dialog, `false` to close it. */\r\n readonly open = model<boolean>(false);\r\n /** Layout type. Defaults to `modal-leading-icon`. */\r\n readonly variant = input<DialogVariant>('modal-leading-icon');\r\n /**\r\n * Optional width override — any CSS length (e.g. `'1000px'`). Defaults to the variant\r\n * width (800px modal / 600px alert). Always clamped to the viewport on small screens.\r\n */\r\n readonly width = input<string>('');\r\n /** Heading text. Also drives the accessible name via `aria-labelledby`. */\r\n readonly title = input<string>('');\r\n /** Supporting text under the title (rendered for `centered` and `alert` variants). */\r\n readonly description = input<string>('');\r\n /** Lucide icon name — the leading header icon (`leading`) or centered badge glyph (`centered`). */\r\n readonly icon = input<string>('');\r\n /**\r\n * When true (default) the dialog shows a close button and closes on Escape / backdrop click.\r\n * When false, the dialog is a forced choice — provide a focusable action (e.g. a\r\n * `[cmpDialogActions]` button) so keyboard users can dismiss it (WCAG 2.1.2, no keyboard trap).\r\n */\r\n readonly dismissible = input<boolean>(true);\r\n /** Accessible label for the close button. */\r\n readonly closeLabel = input<string>('Close dialog');\r\n /** Accessible name used when no `title` is provided (e.g. a fully custom body). */\r\n readonly ariaLabel = input<string>('');\r\n /** Renders the action footer. Set false to hide it when there are no actions. */\r\n readonly showActions = input<boolean>(true);\r\n\r\n /** Emits after the dialog has been opened and shown. */\r\n readonly opened = output<void>();\r\n /** Emits after the dialog has closed, with the reason it closed. */\r\n readonly closed = output<DialogCloseReason>();\r\n\r\n /** The dialog surface template, projected into the CDK dialog container when open. */\r\n @ViewChild('panel', { static: true }) private panelTemplate!: TemplateRef<unknown>;\r\n\r\n private readonly dialog = inject(Dialog);\r\n private readonly overlay = inject(Overlay);\r\n private readonly directionality = inject(Directionality);\r\n private readonly viewContainerRef = inject(ViewContainerRef);\r\n\r\n private dialogRef?: DialogRef<unknown, unknown>;\r\n private closeReason: DialogCloseReason = 'programmatic';\r\n\r\n private readonly _ids = nextDialogIds();\r\n protected readonly titleId = this._ids.titleId;\r\n protected readonly descId = this._ids.descId;\r\n\r\n constructor() {\r\n // Reflect the open model onto the CDK dialog lifecycle.\r\n effect(() => {\r\n if (this.open() && !this.dialogRef) {\r\n this.attach();\r\n } else if (!this.open() && this.dialogRef) {\r\n this.detach();\r\n }\r\n });\r\n }\r\n\r\n ngOnDestroy(): void {\r\n this.dialogRef?.close();\r\n }\r\n\r\n /** Close request from the close button. */\r\n protected requestClose(): void {\r\n this.closeWith('close-button');\r\n }\r\n\r\n private closeWith(reason: DialogCloseReason): void {\r\n if (!this.open()) {\r\n return;\r\n }\r\n this.closeReason = reason;\r\n this.open.set(false);\r\n }\r\n\r\n private attach(): void {\r\n this.closeReason = 'programmatic';\r\n\r\n // Dev-only guardrail: a dialog with no title and no ariaLabel has no accessible\r\n // name (WCAG 2.2 §4.1.2). Stripped from production builds by isDevMode().\r\n if (isDevMode() && !this.title() && !this.ariaLabel()) {\r\n console.warn(\r\n '[compsych-dialog] Opened with no accessible name — set `title` or `ariaLabel` ' +\r\n 'so assistive technology can identify the dialog (WCAG 2.2 §4.1.2).',\r\n );\r\n }\r\n\r\n const ref = this.dialog.open(this.panelTemplate, {\r\n viewContainerRef: this.viewContainerRef,\r\n hasBackdrop: true,\r\n // Scrim color is injected by the dialog surface itself; positioning/fade come from\r\n // @angular/cdk/overlay-prebuilt.css (which consumers load).\r\n backdropClass: 'cmp-dialog-scrim',\r\n panelClass: 'cmp-dialog-pane',\r\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\r\n scrollStrategy: this.overlay.scrollStrategies.block(),\r\n // Forward the local reading direction so a body-portaled overlay mirrors correctly\r\n // even when only a subtree (not the document) is RTL.\r\n direction: this.directionality.value,\r\n // The `alert` variant is a decision/confirmation dialog → assertive alertdialog role.\r\n role: this.variant() === 'alert' ? 'alertdialog' : 'dialog',\r\n // We handle Escape / backdrop ourselves so `closed` can report a reason.\r\n disableClose: true,\r\n // Focus the dialog container on open (screen reader announces it; first Tab then\r\n // lands on the close button with its visible ring) and restore focus on close.\r\n autoFocus: 'dialog',\r\n restoreFocus: true,\r\n ariaModal: true,\r\n ariaLabelledBy: this.title() ? this.titleId : null,\r\n ariaDescribedBy:\r\n this.variant() !== 'modal-leading-icon' && this.description() ? this.descId : null,\r\n // Accessible name for the no-title custom-body case (CDK nulls aria-labelledby when set).\r\n ariaLabel: !this.title() ? this.ariaLabel() || null : null,\r\n });\r\n this.dialogRef = ref;\r\n\r\n ref.backdropClick.subscribe(() => {\r\n if (this.dismissible()) {\r\n this.closeWith('backdrop');\r\n }\r\n });\r\n ref.keydownEvents.subscribe((event) => {\r\n if (event.key === 'Escape' && this.dismissible()) {\r\n event.preventDefault();\r\n this.closeWith('escape');\r\n }\r\n });\r\n ref.closed.subscribe(() => {\r\n this.dialogRef = undefined;\r\n const reason = this.closeReason;\r\n this.closeReason = 'programmatic';\r\n // Keep the model in sync if the dialog was torn down by CDK rather than the model.\r\n if (this.open()) {\r\n this.open.set(false);\r\n }\r\n this.closed.emit(reason);\r\n });\r\n\r\n this.opened.emit();\r\n }\r\n\r\n private detach(): void {\r\n // Triggers `ref.closed`, which clears state, restores focus, and emits `closed`.\r\n this.dialogRef?.close();\r\n }\r\n}\r\n","<ng-template #panel>\r\n <cmp-dialog-surface\r\n [variant]=\"variant()\"\r\n [width]=\"width()\"\r\n [title]=\"title()\"\r\n [description]=\"description()\"\r\n [icon]=\"icon()\"\r\n [dismissible]=\"dismissible()\"\r\n [closeLabel]=\"closeLabel()\"\r\n [showActions]=\"showActions()\"\r\n [titleId]=\"titleId\"\r\n [descId]=\"descId\"\r\n (closeClick)=\"requestClose()\"\r\n >\r\n <ng-content />\r\n <ng-content select=\"[cmpDialogActions]\" ngProjectAs=\"[cmpDialogActions]\" />\r\n </cmp-dialog-surface>\r\n</ng-template>\r\n"]}
@@ -0,0 +1,93 @@
1
+ import { Injectable, inject, isDevMode } from '@angular/core';
2
+ import { Dialog } from '@angular/cdk/dialog';
3
+ import { Directionality } from '@angular/cdk/bidi';
4
+ import { Overlay } from '@angular/cdk/overlay';
5
+ import { CMP_DIALOG_CHROME, CmpDialogContainer } from './dialog-container';
6
+ import { nextDialogIds } from './dialog-id';
7
+ import { CompsychDialogRef } from './dialog-ref';
8
+ import * as i0 from "@angular/core";
9
+ /**
10
+ * Opens a component or template as a modal dialog inside the compsych-dialog chrome,
11
+ * imperatively (like Angular Material's `MatDialog`). Built on `@angular/cdk/dialog`.
12
+ *
13
+ * The opened content is placed in the dialog body; the header (icon/title/close) and
14
+ * behavior come from the config. The opened component can inject the CDK `DialogRef`
15
+ * (to close, optionally with a result) and `DIALOG_DATA` (to read `config.data`) — both
16
+ * re-exported from `@compsych-ui-components/angular`.
17
+ *
18
+ * const ref = dialogService.open(MyComponent, {
19
+ * title: 'Edit details', variant: 'modal-leading-icon', icon: 'globe', data: { id },
20
+ * });
21
+ * ref.closed.subscribe((result) => { ... });
22
+ *
23
+ * Requires `@angular/cdk/overlay-prebuilt.css` + the `@compsych-ui-components/shared` tokens
24
+ * loaded globally. The scrim color is injected by the dialog — no extra stylesheet needed.
25
+ */
26
+ export class DialogService {
27
+ dialog = inject(Dialog);
28
+ overlay = inject(Overlay);
29
+ directionality = inject(Directionality);
30
+ /** Opens `content` as a modal dialog. Returns a {@link CompsychDialogRef} (close / closed). */
31
+ open(content, config = {}) {
32
+ // Dev-only guardrail (parity with the declarative component): no title + no ariaLabel
33
+ // means the dialog has no accessible name (WCAG 2.2 §4.1.2).
34
+ if (isDevMode() && !config.title && !config.ariaLabel) {
35
+ console.warn('[compsych-dialog] Opened via DialogService with no accessible name — set `title` ' +
36
+ 'or `ariaLabel` so assistive technology can identify the dialog (WCAG 2.2 §4.1.2).');
37
+ }
38
+ const ids = nextDialogIds();
39
+ const chrome = {
40
+ variant: config.variant ?? 'modal-leading-icon',
41
+ title: config.title ?? '',
42
+ description: config.description ?? '',
43
+ icon: config.icon ?? '',
44
+ dismissible: config.dismissible ?? true,
45
+ closeLabel: config.closeLabel ?? 'Close dialog',
46
+ width: config.width ?? '',
47
+ showActions: config.showActions ?? true,
48
+ titleId: ids.titleId,
49
+ descId: ids.descId,
50
+ };
51
+ let ref;
52
+ const cdkConfig = {
53
+ // Object form so chrome params reach the container via DI (not spread onto the
54
+ // config, which would clobber real fields like `width`).
55
+ container: {
56
+ type: CmpDialogContainer,
57
+ providers: () => [{ provide: CMP_DIALOG_CHROME, useValue: chrome }],
58
+ },
59
+ hasBackdrop: true,
60
+ backdropClass: 'cmp-dialog-scrim',
61
+ panelClass: config.panelClass ?? 'cmp-dialog-pane',
62
+ positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
63
+ scrollStrategy: this.overlay.scrollStrategies.block(),
64
+ direction: this.directionality.value,
65
+ role: chrome.variant === 'alert' ? 'alertdialog' : 'dialog',
66
+ // dismissible → CDK closes on Escape / backdrop; non-dismissible → forced choice.
67
+ disableClose: !chrome.dismissible,
68
+ autoFocus: 'dialog',
69
+ restoreFocus: true,
70
+ ariaModal: true,
71
+ ariaLabelledBy: chrome.title ? chrome.titleId : null,
72
+ ariaDescribedBy: chrome.variant !== 'modal-leading-icon' && chrome.description ? chrome.descId : null,
73
+ ariaLabel: !chrome.title ? config.ariaLabel || null : null,
74
+ data: config.data,
75
+ // Wrap the CDK ref (async-safe `closed`) and expose it to the opened content via DI,
76
+ // so the content can inject `CompsychDialogRef` to close with a result.
77
+ providers: (cdkRef) => {
78
+ ref = new CompsychDialogRef(cdkRef);
79
+ return [{ provide: CompsychDialogRef, useValue: ref }];
80
+ },
81
+ };
82
+ const cdkRef = this.dialog.open(content, cdkConfig);
83
+ // `providers` runs synchronously during open(), so `ref` is set; fall back defensively.
84
+ return ref ?? new CompsychDialogRef(cdkRef);
85
+ }
86
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DialogService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
87
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DialogService, providedIn: 'root' });
88
+ }
89
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DialogService, decorators: [{
90
+ type: Injectable,
91
+ args: [{ providedIn: 'root' }]
92
+ }] });
93
+ //# sourceMappingURL=dialog.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dialog.service.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dialog/dialog.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable, TemplateRef, Type, inject, isDevMode } from '@angular/core';\r\nimport { Dialog, DialogConfig, DialogRef } from '@angular/cdk/dialog';\r\nimport { Directionality } from '@angular/cdk/bidi';\r\nimport { Overlay } from '@angular/cdk/overlay';\r\nimport { CMP_DIALOG_CHROME, CmpDialogContainer, DialogChrome } from './dialog-container';\r\nimport { nextDialogIds } from './dialog-id';\r\nimport { CompsychDialogRef } from './dialog-ref';\r\nimport { DialogVariant } from './dialog-types';\r\n\r\n/** Configuration for {@link DialogService.open}. */\r\nexport interface CompsychDialogConfig<D = unknown> {\r\n /** Layout type. Defaults to `modal-leading-icon`. */\r\n variant?: DialogVariant;\r\n /** Heading text (drives the accessible name via `aria-labelledby`). */\r\n title?: string;\r\n /** Supporting text (rendered for `centered` / `alert` variants). */\r\n description?: string;\r\n /** Lucide icon name (leading header icon / centered badge glyph). */\r\n icon?: string;\r\n /** When true (default) shows a close button and closes on Escape / backdrop click. */\r\n dismissible?: boolean;\r\n /** Accessible label for the close button. */\r\n closeLabel?: string;\r\n /** Accessible name used when no `title` is provided. */\r\n ariaLabel?: string;\r\n /** Width override — any CSS length. Defaults to the variant width, viewport-clamped. */\r\n width?: string;\r\n /** Whether to render the projected actions footer. */\r\n showActions?: boolean;\r\n /** Data injected into the opened component via the CDK `DIALOG_DATA` token. */\r\n data?: D;\r\n /** Extra class(es) for the overlay pane. */\r\n panelClass?: string | string[];\r\n}\r\n\r\n/**\r\n * Opens a component or template as a modal dialog inside the compsych-dialog chrome,\r\n * imperatively (like Angular Material's `MatDialog`). Built on `@angular/cdk/dialog`.\r\n *\r\n * The opened content is placed in the dialog body; the header (icon/title/close) and\r\n * behavior come from the config. The opened component can inject the CDK `DialogRef`\r\n * (to close, optionally with a result) and `DIALOG_DATA` (to read `config.data`) — both\r\n * re-exported from `@compsych-ui-components/angular`.\r\n *\r\n * const ref = dialogService.open(MyComponent, {\r\n * title: 'Edit details', variant: 'modal-leading-icon', icon: 'globe', data: { id },\r\n * });\r\n * ref.closed.subscribe((result) => { ... });\r\n *\r\n * Requires `@angular/cdk/overlay-prebuilt.css` + the `@compsych-ui-components/shared` tokens\r\n * loaded globally. The scrim color is injected by the dialog — no extra stylesheet needed.\r\n */\r\n@Injectable({ providedIn: 'root' })\r\nexport class DialogService {\r\n private readonly dialog = inject(Dialog);\r\n private readonly overlay = inject(Overlay);\r\n private readonly directionality = inject(Directionality);\r\n\r\n /** Opens `content` as a modal dialog. Returns a {@link CompsychDialogRef} (close / closed). */\r\n open<R = unknown, D = unknown, C = unknown>(\r\n content: Type<C> | TemplateRef<C>,\r\n config: CompsychDialogConfig<D> = {},\r\n ): CompsychDialogRef<R> {\r\n // Dev-only guardrail (parity with the declarative component): no title + no ariaLabel\r\n // means the dialog has no accessible name (WCAG 2.2 §4.1.2).\r\n if (isDevMode() && !config.title && !config.ariaLabel) {\r\n console.warn(\r\n '[compsych-dialog] Opened via DialogService with no accessible name — set `title` ' +\r\n 'or `ariaLabel` so assistive technology can identify the dialog (WCAG 2.2 §4.1.2).',\r\n );\r\n }\r\n\r\n const ids = nextDialogIds();\r\n const chrome: DialogChrome = {\r\n variant: config.variant ?? 'modal-leading-icon',\r\n title: config.title ?? '',\r\n description: config.description ?? '',\r\n icon: config.icon ?? '',\r\n dismissible: config.dismissible ?? true,\r\n closeLabel: config.closeLabel ?? 'Close dialog',\r\n width: config.width ?? '',\r\n showActions: config.showActions ?? true,\r\n titleId: ids.titleId,\r\n descId: ids.descId,\r\n };\r\n\r\n let ref: CompsychDialogRef<R> | undefined;\r\n const cdkConfig = {\r\n // Object form so chrome params reach the container via DI (not spread onto the\r\n // config, which would clobber real fields like `width`).\r\n container: {\r\n type: CmpDialogContainer,\r\n providers: () => [{ provide: CMP_DIALOG_CHROME, useValue: chrome }],\r\n },\r\n hasBackdrop: true,\r\n backdropClass: 'cmp-dialog-scrim',\r\n panelClass: config.panelClass ?? 'cmp-dialog-pane',\r\n positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),\r\n scrollStrategy: this.overlay.scrollStrategies.block(),\r\n direction: this.directionality.value,\r\n role: chrome.variant === 'alert' ? ('alertdialog' as const) : ('dialog' as const),\r\n // dismissible → CDK closes on Escape / backdrop; non-dismissible → forced choice.\r\n disableClose: !chrome.dismissible,\r\n autoFocus: 'dialog',\r\n restoreFocus: true,\r\n ariaModal: true,\r\n ariaLabelledBy: chrome.title ? chrome.titleId : null,\r\n ariaDescribedBy:\r\n chrome.variant !== 'modal-leading-icon' && chrome.description ? chrome.descId : null,\r\n ariaLabel: !chrome.title ? config.ariaLabel || null : null,\r\n data: config.data,\r\n // Wrap the CDK ref (async-safe `closed`) and expose it to the opened content via DI,\r\n // so the content can inject `CompsychDialogRef` to close with a result.\r\n providers: (cdkRef: DialogRef<R, C>) => {\r\n ref = new CompsychDialogRef<R>(cdkRef as DialogRef<R, unknown>);\r\n return [{ provide: CompsychDialogRef, useValue: ref }];\r\n },\r\n } as DialogConfig<D, DialogRef<R, C>>;\r\n\r\n const cdkRef = this.dialog.open<R, D, C>(content, cdkConfig);\r\n // `providers` runs synchronously during open(), so `ref` is set; fall back defensively.\r\n return ref ?? new CompsychDialogRef<R>(cdkRef as DialogRef<R, unknown>);\r\n }\r\n}\r\n"]}
@@ -1,5 +1,6 @@
1
1
  import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, booleanAttribute, inject, } from '@angular/core';
2
2
  import { NgControl, Validators } from '@angular/forms';
3
+ import { ButtonComponent } from '../button/button';
3
4
  import * as i0 from "@angular/core";
4
5
  let nextId = 0;
5
6
  /**
@@ -42,6 +43,43 @@ export class TextInputComponent {
42
43
  * Forwarded to `aria-describedby` on the native `<input>`.
43
44
  */
44
45
  ariaDescribedBy;
46
+ /** Text displayed below the input. Shown in red when the input is in error state. */
47
+ supportingText = '';
48
+ /**
49
+ * Static error text shown in red when the input is in error state.
50
+ * Used when the input is outside a form (with `[error]="true"`).
51
+ * When inside a form, prefer `errorMessages` for per-validator messages.
52
+ */
53
+ errorMessage = '';
54
+ /**
55
+ * Map of Angular validator error keys to display strings.
56
+ * The component reads `ngControl.errors` to determine which validator failed
57
+ * and shows the matching message in red below the input.
58
+ * Example: `{ required: 'Username is required', minlength: 'Min 8 characters' }`
59
+ */
60
+ errorMessages = {};
61
+ _labelButton = null;
62
+ /**
63
+ * JSON string configuring the optional button at the right of the label row.
64
+ * Hidden when empty or omitted. Example: `'{"id":"show","label":"Show"}'`
65
+ * Clicking emits the button's `id` via the `labelButtonClick` output.
66
+ */
67
+ set labelButton(json) {
68
+ if (!json) {
69
+ this._labelButton = null;
70
+ return;
71
+ }
72
+ try {
73
+ this._labelButton = JSON.parse(json);
74
+ }
75
+ catch {
76
+ this._labelButton = null;
77
+ }
78
+ }
79
+ /** @internal Template access to the parsed label button config. */
80
+ get parsedLabelButton() {
81
+ return this._labelButton;
82
+ }
45
83
  /**
46
84
  * Adds a blue asterisk after the label. When used in a form, this is set
47
85
  * automatically if the bound `FormControl` has `Validators.required`.
@@ -61,6 +99,8 @@ export class TextInputComponent {
61
99
  readonly = false;
62
100
  /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */
63
101
  valueChange = new EventEmitter();
102
+ /** Emits the label button's `id` when it is clicked. */
103
+ labelButtonClick = new EventEmitter();
64
104
  get inputId() {
65
105
  return this.id ?? this._autoId;
66
106
  }
@@ -72,6 +112,31 @@ export class TextInputComponent {
72
112
  get isRequired() {
73
113
  return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));
74
114
  }
115
+ get activeErrorMessage() {
116
+ const errors = this.ngControl?.errors;
117
+ if (!errors)
118
+ return '';
119
+ for (const key of Object.keys(errors)) {
120
+ if (this.errorMessages[key])
121
+ return this.errorMessages[key];
122
+ }
123
+ return '';
124
+ }
125
+ get displayedSupportingText() {
126
+ if (this.hasError) {
127
+ return this.activeErrorMessage || this.errorMessage || this.supportingText;
128
+ }
129
+ return this.supportingText;
130
+ }
131
+ get describedBy() {
132
+ const parts = [];
133
+ if (this.ariaDescribedBy)
134
+ parts.push(this.ariaDescribedBy);
135
+ if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {
136
+ parts.push(this.inputId + '-supporting');
137
+ }
138
+ return parts.length ? parts.join(' ') : null;
139
+ }
75
140
  get wrapperClasses() {
76
141
  return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']
77
142
  .filter(Boolean)
@@ -104,62 +169,105 @@ export class TextInputComponent {
104
169
  this._onTouched();
105
170
  this.cdr.markForCheck();
106
171
  }
107
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
108
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TextInputComponent, isStandalone: true, selector: "compsych-text-input", inputs: { label: "label", placeholder: "placeholder", value: "value", type: "type", size: "size", name: "name", id: "id", ariaDescribedBy: "ariaDescribedBy", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute] }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
109
- <div [class]="wrapperClasses">
110
- <label class="label" [for]="inputId">
111
- {{ label }}
112
- @if (isRequired) {
113
- <span class="required-marker" aria-hidden="true"> *</span>
172
+ onLabelButtonClick() {
173
+ if (this._labelButton) {
174
+ this.labelButtonClick.emit(this._labelButton.id);
114
175
  }
115
- </label>
116
- <input
117
- class="input"
118
- [id]="inputId"
119
- [name]="name || inputId"
120
- [type]="type"
121
- [placeholder]="placeholder"
122
- [value]="value"
123
- [disabled]="disabled"
124
- [readOnly]="readonly"
125
- [attr.required]="isRequired ? '' : null"
126
- [class.error]="hasError"
127
- [attr.aria-invalid]="hasError ? 'true' : null"
128
- [attr.aria-describedby]="ariaDescribedBy || null"
129
- (input)="onInput($event)"
130
- (blur)="onBlur()"
131
- />
132
- </div>
133
- `, isInline: true, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
176
+ }
177
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
178
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TextInputComponent, isStandalone: true, selector: "compsych-text-input", inputs: { label: "label", placeholder: "placeholder", value: "value", type: "type", size: "size", name: "name", id: "id", ariaDescribedBy: "ariaDescribedBy", supportingText: "supportingText", errorMessage: "errorMessage", errorMessages: "errorMessages", labelButton: "labelButton", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], readonly: ["readonly", "readonly", booleanAttribute] }, outputs: { valueChange: "valueChange", labelButtonClick: "labelButtonClick" }, ngImport: i0, template: `
179
+ <div [class]="wrapperClasses">
180
+ <div class="label-row">
181
+ <label class="label" [for]="inputId">
182
+ {{ label }}
183
+ @if (isRequired) {
184
+ <span class="required-marker" aria-hidden="true"> *</span>
185
+ }
186
+ </label>
187
+ @if (parsedLabelButton) {
188
+ <compsych-button
189
+ [label]="parsedLabelButton.label"
190
+ [variant]="parsedLabelButton.variant ?? 'text'"
191
+ size="small"
192
+ [leadingIcon]="parsedLabelButton.leadingIcon"
193
+ [trailingIcon]="parsedLabelButton.trailingIcon"
194
+ [iconOnly]="!!parsedLabelButton.iconOnly"
195
+ [disabled]="disabled"
196
+ (click)="onLabelButtonClick()"
197
+ />
198
+ }
199
+ </div>
200
+ <input
201
+ class="input"
202
+ [id]="inputId"
203
+ [name]="name || inputId"
204
+ [type]="type"
205
+ [placeholder]="placeholder"
206
+ [value]="value"
207
+ [disabled]="disabled"
208
+ [readOnly]="readonly"
209
+ [attr.required]="isRequired ? '' : null"
210
+ [class.error]="hasError"
211
+ [attr.aria-invalid]="hasError ? 'true' : null"
212
+ [attr.aria-describedby]="describedBy"
213
+ (input)="onInput($event)"
214
+ (blur)="onBlur()"
215
+ />
216
+ @if (displayedSupportingText) {
217
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="inputId + '-supporting'">
218
+ {{ displayedSupportingText }}
219
+ </span>
220
+ }
221
+ </div>
222
+ `, isInline: true, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-text-field.disabled .supporting-text{opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "compsych-button", inputs: ["label", "variant", "size", "leadingIcon", "trailingIcon", "type", "ariaLabel", "iconOnly", "disabled", "loading", "fullWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
134
223
  }
135
224
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TextInputComponent, decorators: [{
136
225
  type: Component,
137
- args: [{ selector: 'compsych-text-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
138
- <div [class]="wrapperClasses">
139
- <label class="label" [for]="inputId">
140
- {{ label }}
141
- @if (isRequired) {
142
- <span class="required-marker" aria-hidden="true"> *</span>
143
- }
144
- </label>
145
- <input
146
- class="input"
147
- [id]="inputId"
148
- [name]="name || inputId"
149
- [type]="type"
150
- [placeholder]="placeholder"
151
- [value]="value"
152
- [disabled]="disabled"
153
- [readOnly]="readonly"
154
- [attr.required]="isRequired ? '' : null"
155
- [class.error]="hasError"
156
- [attr.aria-invalid]="hasError ? 'true' : null"
157
- [attr.aria-describedby]="ariaDescribedBy || null"
158
- (input)="onInput($event)"
159
- (blur)="onBlur()"
160
- />
161
- </div>
162
- `, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}}\n"] }]
226
+ args: [{ selector: 'compsych-text-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ButtonComponent], template: `
227
+ <div [class]="wrapperClasses">
228
+ <div class="label-row">
229
+ <label class="label" [for]="inputId">
230
+ {{ label }}
231
+ @if (isRequired) {
232
+ <span class="required-marker" aria-hidden="true"> *</span>
233
+ }
234
+ </label>
235
+ @if (parsedLabelButton) {
236
+ <compsych-button
237
+ [label]="parsedLabelButton.label"
238
+ [variant]="parsedLabelButton.variant ?? 'text'"
239
+ size="small"
240
+ [leadingIcon]="parsedLabelButton.leadingIcon"
241
+ [trailingIcon]="parsedLabelButton.trailingIcon"
242
+ [iconOnly]="!!parsedLabelButton.iconOnly"
243
+ [disabled]="disabled"
244
+ (click)="onLabelButtonClick()"
245
+ />
246
+ }
247
+ </div>
248
+ <input
249
+ class="input"
250
+ [id]="inputId"
251
+ [name]="name || inputId"
252
+ [type]="type"
253
+ [placeholder]="placeholder"
254
+ [value]="value"
255
+ [disabled]="disabled"
256
+ [readOnly]="readonly"
257
+ [attr.required]="isRequired ? '' : null"
258
+ [class.error]="hasError"
259
+ [attr.aria-invalid]="hasError ? 'true' : null"
260
+ [attr.aria-describedby]="describedBy"
261
+ (input)="onInput($event)"
262
+ (blur)="onBlur()"
263
+ />
264
+ @if (displayedSupportingText) {
265
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="inputId + '-supporting'">
266
+ {{ displayedSupportingText }}
267
+ </span>
268
+ }
269
+ </div>
270
+ `, styles: [":host{display:block}.cmp-text-field{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-4)}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface)}.required-marker{color:var(--accent-primary-sys-primary)}.cmp-text-field.disabled .label{opacity:.48}.input{box-sizing:border-box;width:100%;margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline);border-radius:var(--border-radius-sys-radius-sm);background-color:var(--surface-surface-container-sys-surface-container-lowest);color:var(--surface-surface-sys-on-surface);font-family:var(--body-medium-sys-font-family);font-size:var(--body-medium-sys-font-size);font-weight:var(--body-medium-sys-font-weight);line-height:var(--body-medium-sys-line-height);appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.input::placeholder{color:var(--surface-surface-sys-on-surface-variant)}.input:hover:not(:disabled):not(:focus){border-color:var(--outline-sys-outline-variant);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.input:focus{border-color:var(--accent-primary-sys-primary);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input.error:not(:disabled):not(:focus){border-color:var(--error-sys-error)}.input.error:focus{border-color:var(--error-sys-error);border-width:var(--border-width-sys-stroke-medium);box-shadow:0 0 0 4px var(--transparent-primary-sys-primary-08)}.input:disabled{background-color:var(--surface-surface-container-sys-surface-container-highest);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline);cursor:not-allowed;opacity:.48}.supporting-text{font-family:var(--body-small-sys-font-family);font-size:var(--body-small-sys-font-size);font-weight:var(--body-small-sys-font-weight);line-height:var(--body-small-sys-line-height);color:var(--surface-surface-sys-on-surface-variant)}.supporting-text--error{color:var(--error-sys-error)}.cmp-text-field.disabled .supporting-text{opacity:.48}.cmp-text-field.small .input{height:44px;padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);font-size:var(--body-small-sys-font-size);line-height:var(--body-small-sys-line-height)}.cmp-text-field.medium .input{height:52px;padding:var(--spacing-padding-sys-padding-12) var(--spacing-padding-sys-padding-16);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.cmp-text-field.large .input{height:64px;padding:var(--spacing-padding-sys-padding-20) var(--spacing-padding-sys-padding-16);font-size:var(--body-large-sys-font-size);line-height:var(--body-large-sys-line-height)}@media(forced-colors:active){.input{border:1px solid ButtonText;forced-color-adjust:none}.input:focus{outline:2px solid Highlight;outline-offset:2px}.input:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-text-field.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}}\n"] }]
163
271
  }], ctorParameters: () => [], propDecorators: { label: [{
164
272
  type: Input
165
273
  }], placeholder: [{
@@ -176,6 +284,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
176
284
  type: Input
177
285
  }], ariaDescribedBy: [{
178
286
  type: Input
287
+ }], supportingText: [{
288
+ type: Input
289
+ }], errorMessage: [{
290
+ type: Input
291
+ }], errorMessages: [{
292
+ type: Input
293
+ }], labelButton: [{
294
+ type: Input
179
295
  }], required: [{
180
296
  type: Input,
181
297
  args: [{ transform: booleanAttribute }]
@@ -190,5 +306,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
190
306
  args: [{ transform: booleanAttribute }]
191
307
  }], valueChange: [{
192
308
  type: Output
309
+ }], labelButtonClick: [{
310
+ type: Output
193
311
  }] } });
194
312
  //# sourceMappingURL=text-input.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/text-input/text-input.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n Input,\n Output,\n booleanAttribute,\n inject,\n} from '@angular/core';\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\n\n/** Size variant of a `compsych-text-input`. Controls input height, padding, and font size. */\nexport type TextInputSize = 'small' | 'medium' | 'large';\n\n/** Native input type. */\nexport type TextInputType = 'text' | 'password';\n\nlet nextId = 0;\n\n/**\n * A labelled text input that wraps a native `<input>` element.\n * Implements `ControlValueAccessor` so it works with both reactive forms\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`).\n * The error visual state is driven automatically by the bound `FormControl`'s\n * validity once the field has been touched, and can also be set explicitly via\n * the `error` input for use outside of Angular forms.\n */\n@Component({\n selector: 'compsych-text-input',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <div [class]=\"wrapperClasses\">\n <label class=\"label\" [for]=\"inputId\">\n {{ label }}\n @if (isRequired) {\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\n }\n </label>\n <input\n class=\"input\"\n [id]=\"inputId\"\n [name]=\"name || inputId\"\n [type]=\"type\"\n [placeholder]=\"placeholder\"\n [value]=\"value\"\n [disabled]=\"disabled\"\n [readOnly]=\"readonly\"\n [attr.required]=\"isRequired ? '' : null\"\n [class.error]=\"hasError\"\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\n [attr.aria-describedby]=\"ariaDescribedBy || null\"\n (input)=\"onInput($event)\"\n (blur)=\"onBlur()\"\n />\n </div>\n `,\n styleUrl: './text-input.css',\n})\nexport class TextInputComponent implements ControlValueAccessor {\n private readonly _autoId = `cmp-text-input-${++nextId}`;\n private readonly cdr = inject(ChangeDetectorRef);\n private _onChange: (value: string) => void = () => {};\n private _onTouched: () => void = () => {};\n\n /** Injected when the component is used inside a reactive or template-driven form. */\n protected readonly ngControl: NgControl | null;\n\n constructor() {\n const ctrl = inject(NgControl, { optional: true, self: true });\n if (ctrl) ctrl.valueAccessor = this;\n this.ngControl = ctrl;\n }\n\n /** Label displayed above the input. */\n @Input() label = '';\n /** Placeholder text shown inside the input when empty. */\n @Input() placeholder = '';\n /** Current value of the input. When used in a form, the form controls the value. */\n @Input() value = '';\n /** Native input type. Defaults to `'text'`. Use `'password'` to mask the value. */\n @Input() type: TextInputType = 'text';\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\n @Input() size: TextInputSize = 'medium';\n /** Native `name` attribute forwarded to the `<input>`. Defaults to the auto-generated id. */\n @Input() name?: string;\n /** Native `id` attribute for the `<input>`. Auto-generated if omitted. */\n @Input() id?: string;\n /**\n * Id(s) of external elements that describe the input, space-separated.\n * Forwarded to `aria-describedby` on the native `<input>`.\n */\n @Input() ariaDescribedBy?: string;\n\n /**\n * Adds a blue asterisk after the label. When used in a form, this is set\n * automatically if the bound `FormControl` has `Validators.required`.\n * Can also be set explicitly for non-form use. Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) required = false;\n /**\n * Applies the error visual state (red border) explicitly. When used in a form,\n * the error state is driven automatically from the `FormControl`'s validity once\n * the field is touched — this input is only needed outside of forms.\n * Supports boolean coercion.\n */\n @Input({ transform: booleanAttribute }) error = false;\n /** Disables the input. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) disabled = false;\n /** Makes the input read-only — focusable and copyable but not editable. Value is still submitted with the form. Supports boolean coercion. */\n @Input({ transform: booleanAttribute }) readonly = false;\n\n /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */\n @Output() readonly valueChange = new EventEmitter<string>();\n\n get inputId(): string {\n return this.id ?? this._autoId;\n }\n\n /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */\n get hasError(): boolean {\n return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);\n }\n\n /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */\n get isRequired(): boolean {\n return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));\n }\n\n get wrapperClasses(): string {\n return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']\n .filter(Boolean)\n .join(' ');\n }\n\n // ── ControlValueAccessor ────────────────────────────────────────────────────\n\n writeValue(value: string | null): void {\n this.value = value ?? '';\n this.cdr.markForCheck();\n }\n\n registerOnChange(fn: (value: string) => void): void {\n this._onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n\n /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.cdr.markForCheck();\n }\n\n // ── Event handlers ──────────────────────────────────────────────────────────\n\n protected onInput(event: Event): void {\n const value = (event.target as HTMLInputElement).value;\n this.value = value;\n this._onChange(value);\n this.valueChange.emit(value);\n }\n\n protected onBlur(): void {\n this._onTouched();\n this.cdr.markForCheck();\n }\n}\n"]}
1
+ {"version":3,"file":"text-input.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/text-input/text-input.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef,\r\n Component,\r\n EventEmitter,\r\n Input,\r\n Output,\r\n booleanAttribute,\r\n inject,\r\n} from '@angular/core';\r\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\r\nimport { ButtonComponent, ButtonVariant } from '../button/button';\r\n\r\n/** Size variant of a `compsych-text-input`. Controls input height, padding, and font size. */\r\nexport type TextInputSize = 'small' | 'medium' | 'large';\r\n\r\n/** Native input type. */\r\nexport type TextInputType = 'text' | 'password';\r\n\r\n/** Configuration for the optional button rendered at the right of the label row. */\r\nexport interface TextInputLabelButton {\r\n /** Stable identifier emitted by `labelButtonClick` when clicked. */\r\n id: string;\r\n /** Text displayed on the button. */\r\n label: string;\r\n /** Visual style of the button. Defaults to `'text'`. */\r\n variant?: ButtonVariant;\r\n /** Lucide icon name shown before the label. */\r\n leadingIcon?: string;\r\n /** Lucide icon name shown after the label. */\r\n trailingIcon?: string;\r\n /** Hides the label and renders a square icon-only button. Requires `leadingIcon`. */\r\n iconOnly?: boolean;\r\n}\r\n\r\nlet nextId = 0;\r\n\r\n/**\r\n * A labelled text input that wraps a native `<input>` element.\r\n * Implements `ControlValueAccessor` so it works with both reactive forms\r\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`).\r\n * The error visual state is driven automatically by the bound `FormControl`'s\r\n * validity once the field has been touched, and can also be set explicitly via\r\n * the `error` input for use outside of Angular forms.\r\n */\r\n@Component({\r\n selector: 'compsych-text-input',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [ButtonComponent],\r\n template: `\r\n <div [class]=\"wrapperClasses\">\r\n <div class=\"label-row\">\r\n <label class=\"label\" [for]=\"inputId\">\r\n {{ label }}\r\n @if (isRequired) {\r\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\r\n }\r\n </label>\r\n @if (parsedLabelButton) {\r\n <compsych-button\r\n [label]=\"parsedLabelButton.label\"\r\n [variant]=\"parsedLabelButton.variant ?? 'text'\"\r\n size=\"small\"\r\n [leadingIcon]=\"parsedLabelButton.leadingIcon\"\r\n [trailingIcon]=\"parsedLabelButton.trailingIcon\"\r\n [iconOnly]=\"!!parsedLabelButton.iconOnly\"\r\n [disabled]=\"disabled\"\r\n (click)=\"onLabelButtonClick()\"\r\n />\r\n }\r\n </div>\r\n <input\r\n class=\"input\"\r\n [id]=\"inputId\"\r\n [name]=\"name || inputId\"\r\n [type]=\"type\"\r\n [placeholder]=\"placeholder\"\r\n [value]=\"value\"\r\n [disabled]=\"disabled\"\r\n [readOnly]=\"readonly\"\r\n [attr.required]=\"isRequired ? '' : null\"\r\n [class.error]=\"hasError\"\r\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\r\n [attr.aria-describedby]=\"describedBy\"\r\n (input)=\"onInput($event)\"\r\n (blur)=\"onBlur()\"\r\n />\r\n @if (displayedSupportingText) {\r\n <span class=\"supporting-text\" [class.supporting-text--error]=\"hasError\" [id]=\"inputId + '-supporting'\">\r\n {{ displayedSupportingText }}\r\n </span>\r\n }\r\n </div>\r\n `,\r\n styleUrl: './text-input.css',\r\n})\r\nexport class TextInputComponent implements ControlValueAccessor {\r\n private readonly _autoId = `cmp-text-input-${++nextId}`;\r\n private readonly cdr = inject(ChangeDetectorRef);\r\n private _onChange: (value: string) => void = () => {};\r\n private _onTouched: () => void = () => {};\r\n\r\n /** Injected when the component is used inside a reactive or template-driven form. */\r\n protected readonly ngControl: NgControl | null;\r\n\r\n constructor() {\r\n const ctrl = inject(NgControl, { optional: true, self: true });\r\n if (ctrl) ctrl.valueAccessor = this;\r\n this.ngControl = ctrl;\r\n }\r\n\r\n /** Label displayed above the input. */\r\n @Input() label = '';\r\n /** Placeholder text shown inside the input when empty. */\r\n @Input() placeholder = '';\r\n /** Current value of the input. When used in a form, the form controls the value. */\r\n @Input() value = '';\r\n /** Native input type. Defaults to `'text'`. Use `'password'` to mask the value. */\r\n @Input() type: TextInputType = 'text';\r\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\r\n @Input() size: TextInputSize = 'medium';\r\n /** Native `name` attribute forwarded to the `<input>`. Defaults to the auto-generated id. */\r\n @Input() name?: string;\r\n /** Native `id` attribute for the `<input>`. Auto-generated if omitted. */\r\n @Input() id?: string;\r\n /**\r\n * Id(s) of external elements that describe the input, space-separated.\r\n * Forwarded to `aria-describedby` on the native `<input>`.\r\n */\r\n @Input() ariaDescribedBy?: string;\r\n /** Text displayed below the input. Shown in red when the input is in error state. */\r\n @Input() supportingText = '';\r\n /**\r\n * Static error text shown in red when the input is in error state.\r\n * Used when the input is outside a form (with `[error]=\"true\"`).\r\n * When inside a form, prefer `errorMessages` for per-validator messages.\r\n */\r\n @Input() errorMessage = '';\r\n /**\r\n * Map of Angular validator error keys to display strings.\r\n * The component reads `ngControl.errors` to determine which validator failed\r\n * and shows the matching message in red below the input.\r\n * Example: `{ required: 'Username is required', minlength: 'Min 8 characters' }`\r\n */\r\n @Input() errorMessages: Record<string, string> = {};\r\n\r\n private _labelButton: TextInputLabelButton | null = null;\r\n\r\n /**\r\n * JSON string configuring the optional button at the right of the label row.\r\n * Hidden when empty or omitted. Example: `'{\"id\":\"show\",\"label\":\"Show\"}'`\r\n * Clicking emits the button's `id` via the `labelButtonClick` output.\r\n */\r\n @Input() set labelButton(json: string) {\r\n if (!json) { this._labelButton = null; return; }\r\n try {\r\n this._labelButton = JSON.parse(json) as TextInputLabelButton;\r\n } catch {\r\n this._labelButton = null;\r\n }\r\n }\r\n\r\n /** @internal Template access to the parsed label button config. */\r\n protected get parsedLabelButton(): TextInputLabelButton | null {\r\n return this._labelButton;\r\n }\r\n\r\n /**\r\n * Adds a blue asterisk after the label. When used in a form, this is set\r\n * automatically if the bound `FormControl` has `Validators.required`.\r\n * Can also be set explicitly for non-form use. Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) required = false;\r\n /**\r\n * Applies the error visual state (red border) explicitly. When used in a form,\r\n * the error state is driven automatically from the `FormControl`'s validity once\r\n * the field is touched — this input is only needed outside of forms.\r\n * Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) error = false;\r\n /** Disables the input. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) disabled = false;\r\n /** Makes the input read-only — focusable and copyable but not editable. Value is still submitted with the form. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) readonly = false;\r\n\r\n /** Emits the new string value whenever the user types. For use outside Angular forms; inside forms use the `FormControl` value stream. */\r\n @Output() readonly valueChange = new EventEmitter<string>();\r\n /** Emits the label button's `id` when it is clicked. */\r\n @Output() readonly labelButtonClick = new EventEmitter<string>();\r\n\r\n get inputId(): string {\r\n return this.id ?? this._autoId;\r\n }\r\n\r\n /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */\r\n get hasError(): boolean {\r\n return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);\r\n }\r\n\r\n /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */\r\n get isRequired(): boolean {\r\n return this.required || !!(this.ngControl?.control?.hasValidator(Validators.required));\r\n }\r\n\r\n private get activeErrorMessage(): string {\r\n const errors = this.ngControl?.errors;\r\n if (!errors) return '';\r\n for (const key of Object.keys(errors)) {\r\n if (this.errorMessages[key]) return this.errorMessages[key];\r\n }\r\n return '';\r\n }\r\n\r\n get displayedSupportingText(): string {\r\n if (this.hasError) {\r\n return this.activeErrorMessage || this.errorMessage || this.supportingText;\r\n }\r\n return this.supportingText;\r\n }\r\n\r\n get describedBy(): string | null {\r\n const parts: string[] = [];\r\n if (this.ariaDescribedBy) parts.push(this.ariaDescribedBy);\r\n if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {\r\n parts.push(this.inputId + '-supporting');\r\n }\r\n return parts.length ? parts.join(' ') : null;\r\n }\r\n\r\n get wrapperClasses(): string {\r\n return ['cmp-text-field', this.size, this.disabled ? 'disabled' : '']\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // ── ControlValueAccessor ────────────────────────────────────────────────────\r\n\r\n writeValue(value: string | null): void {\r\n this.value = value ?? '';\r\n this.cdr.markForCheck();\r\n }\r\n\r\n registerOnChange(fn: (value: string) => void): void {\r\n this._onChange = fn;\r\n }\r\n\r\n registerOnTouched(fn: () => void): void {\r\n this._onTouched = fn;\r\n }\r\n\r\n /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */\r\n setDisabledState(isDisabled: boolean): void {\r\n this.disabled = isDisabled;\r\n this.cdr.markForCheck();\r\n }\r\n\r\n // ── Event handlers ──────────────────────────────────────────────────────────\r\n\r\n protected onInput(event: Event): void {\r\n const value = (event.target as HTMLInputElement).value;\r\n this.value = value;\r\n this._onChange(value);\r\n this.valueChange.emit(value);\r\n }\r\n\r\n protected onBlur(): void {\r\n this._onTouched();\r\n this.cdr.markForCheck();\r\n }\r\n\r\n protected onLabelButtonClick(): void {\r\n if (this._labelButton) {\r\n this.labelButtonClick.emit(this._labelButton.id);\r\n }\r\n }\r\n}\r\n"]}
package/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import * as i5 from "./lib/sidebar/sidebar";
7
7
  import * as i6 from "./lib/card-base/card";
8
8
  import * as i7 from "./lib/card-grid/card-grid";
9
9
  import * as i8 from "./lib/text-input/text-input";
10
+ import * as i9 from "./lib/dialog/dialog";
10
11
  export { ButtonComponent } from './lib/button/button';
11
12
  export type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';
12
13
  export { AccordionComponent } from './lib/accordion/accordion';
@@ -23,9 +24,16 @@ export { RippleDirective } from './lib/card-base/ripple.directive';
23
24
  export { CardGridComponent } from './lib/card-grid/card-grid';
24
25
  export type { CardGridLayout } from './lib/card-grid/card-grid';
25
26
  export { TextInputComponent } from './lib/text-input/text-input';
26
- export type { TextInputSize, TextInputType } from './lib/text-input/text-input';
27
+ export type { TextInputSize, TextInputType, TextInputLabelButton } from './lib/text-input/text-input';
28
+ export { DialogComponent } from './lib/dialog/dialog';
29
+ export type { DialogCloseReason } from './lib/dialog/dialog';
30
+ export type { DialogVariant } from './lib/dialog/dialog-types';
31
+ export { DialogService } from './lib/dialog/dialog.service';
32
+ export type { CompsychDialogConfig } from './lib/dialog/dialog.service';
33
+ export { CompsychDialogRef } from './lib/dialog/dialog-ref';
34
+ export { DIALOG_DATA } from '@angular/cdk/dialog';
27
35
  export declare class CompsychAngularModule {
28
36
  static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
29
- static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent]>;
37
+ static ɵmod: i0.ɵɵNgModuleDeclaration<CompsychAngularModule, never, [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent, typeof i9.DialogComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.CardGridComponent, typeof i8.TextInputComponent, typeof i9.DialogComponent]>;
30
38
  static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
31
39
  }
@@ -0,0 +1,43 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { CdkDialogContainer, DialogConfig, DialogRef } from '@angular/cdk/dialog';
3
+ import { DialogVariant } from './dialog-types';
4
+ import * as i0 from "@angular/core";
5
+ /** Chrome params supplied by `DialogService` to the container via {@link CMP_DIALOG_CHROME}. */
6
+ export interface DialogChrome {
7
+ variant: DialogVariant;
8
+ title: string;
9
+ description: string;
10
+ icon: string;
11
+ dismissible: boolean;
12
+ closeLabel: string;
13
+ width: string;
14
+ showActions: boolean;
15
+ titleId: string;
16
+ descId: string;
17
+ }
18
+ /**
19
+ * Chrome params injected into `CmpDialogContainer`. Provided via the CDK `container` object
20
+ * form (`{ type, providers }`) rather than spread onto the `DialogConfig` — that avoids
21
+ * clobbering real config fields (e.g. `width`, which CDK also maps onto the overlay pane).
22
+ */
23
+ export declare const CMP_DIALOG_CHROME: InjectionToken<DialogChrome>;
24
+ /**
25
+ * Container used by `DialogService` via CDK `Dialog.open(content, { container })`. Extends
26
+ * the CDK dialog container (focus trap, focus capture/restore, portal outlet) and renders the
27
+ * shared dialog surface around the attached content. The opened content is attached into the
28
+ * surface's body via the `cdkPortalOutlet`; chrome params (title/variant/etc.) ride on the
29
+ * `DialogConfig`. Internal — not part of the public API.
30
+ *
31
+ * Host aria bindings are redeclared here (component host metadata is not inherited) and read
32
+ * from the injected `DialogConfig`, matching `CdkDialogContainer`'s own bindings.
33
+ */
34
+ export declare class CmpDialogContainer extends CdkDialogContainer {
35
+ /** The dialog config (provided by CDK to the container injector); drives the aria host bindings. */
36
+ protected readonly config: DialogConfig<unknown, unknown, import("@angular/cdk/dialog").DialogContainer>;
37
+ /** Ref for the open dialog (provided by CDK to the container injector); closes on the ✕. */
38
+ protected readonly dialogRef: DialogRef<unknown, unknown>;
39
+ /** Chrome params supplied by `DialogService` via the container `providers`. */
40
+ protected readonly chrome: DialogChrome;
41
+ static ɵfac: i0.ɵɵFactoryDeclaration<CmpDialogContainer, never>;
42
+ static ɵcmp: i0.ɵɵComponentDeclaration<CmpDialogContainer, "cmp-dialog-container", never, {}, {}, never, never, true, never>;
43
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Returns a unique `titleId` / `descId` pair for one dialog instance. Shared by the
3
+ * declarative `compsych-dialog` and the imperative `DialogService` so ids never collide
4
+ * when both kinds of dialog are open at once (e.g. GRO's `ModalService` delegating here).
5
+ */
6
+ export declare function nextDialogIds(): {
7
+ titleId: string;
8
+ descId: string;
9
+ };