@compsych-ui-components/angular 7.0.27 → 7.0.32

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.
package/esm2022/index.js CHANGED
@@ -7,6 +7,7 @@ import { SidebarComponent } from './lib/sidebar/sidebar';
7
7
  import { CardComponent } from './lib/card-base/card';
8
8
  import { TextInputComponent } from './lib/text-input/text-input';
9
9
  import { DialogComponent } from './lib/dialog/dialog';
10
+ import { DropdownComponent } from './lib/dropdown/dropdown';
10
11
  import * as i0 from "@angular/core";
11
12
  export { ButtonComponent } from './lib/button/button';
12
13
  export { AccordionComponent } from './lib/accordion/accordion';
@@ -21,6 +22,7 @@ export { DialogService } from './lib/dialog/dialog.service';
21
22
  export { CompsychDialogRef } from './lib/dialog/dialog-ref';
22
23
  // Re-exported from CDK so opened content components can inject the data token.
23
24
  export { DIALOG_DATA } from '@angular/cdk/dialog';
25
+ export { DropdownComponent } from './lib/dropdown/dropdown';
24
26
  export class CompsychAngularModule {
25
27
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
26
28
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, imports: [ButtonComponent,
@@ -30,14 +32,16 @@ export class CompsychAngularModule {
30
32
  SidebarComponent,
31
33
  CardComponent,
32
34
  TextInputComponent,
33
- DialogComponent], exports: [ButtonComponent,
35
+ DialogComponent,
36
+ DropdownComponent], exports: [ButtonComponent,
34
37
  AccordionComponent,
35
38
  FooterComponent,
36
39
  HeaderNavComponent,
37
40
  SidebarComponent,
38
41
  CardComponent,
39
42
  TextInputComponent,
40
- DialogComponent] });
43
+ DialogComponent,
44
+ DropdownComponent] });
41
45
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule });
42
46
  }
43
47
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CompsychAngularModule, decorators: [{
@@ -52,6 +56,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
52
56
  CardComponent,
53
57
  TextInputComponent,
54
58
  DialogComponent,
59
+ DropdownComponent,
55
60
  ],
56
61
  exports: [
57
62
  ButtonComponent,
@@ -62,6 +67,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
62
67
  CardComponent,
63
68
  TextInputComponent,
64
69
  DialogComponent,
70
+ DropdownComponent,
65
71
  ],
66
72
  }]
67
73
  }] });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/angular/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAwBlD,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YApB9B,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,kBAAkB;YAClB,eAAe,aAGf,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,kBAAkB;YAClB,eAAe;yGAGN,qBAAqB;;4FAArB,qBAAqB;kBAtBjC,QAAQ;mBAAC;oBACR,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,kBAAkB;wBAClB,eAAe;qBAChB;oBACD,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,kBAAkB;wBAClB,eAAe;qBAChB;iBACF","sourcesContent":["import { NgModule } from '@angular/core';\r\nimport { ButtonComponent } from './lib/button/button';\r\nimport { AccordionComponent } from './lib/accordion/accordion';\r\nimport { FooterComponent } from './lib/footer/footer';\r\nimport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nimport { SidebarComponent } from './lib/sidebar/sidebar';\r\nimport { CardComponent } from './lib/card-base/card';\r\nimport { TextInputComponent } from './lib/text-input/text-input';\r\nimport { DialogComponent } from './lib/dialog/dialog';\r\n\r\nexport { ButtonComponent } from './lib/button/button';\r\nexport type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';\r\nexport { AccordionComponent } from './lib/accordion/accordion';\r\nexport type { AccordionItem, AccordionButton, AccordionVariant, NestedAccordionItem } from './lib/accordion/accordion-types';\r\nexport { FooterComponent } from './lib/footer/footer';\r\nexport type { FooterLink } from './lib/footer/footer';\r\nexport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nexport type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';\r\nexport { SidebarComponent } from './lib/sidebar/sidebar';\r\nexport type { SidebarItem } from './lib/sidebar/sidebar';\r\nexport { CardComponent } from './lib/card-base/card';\r\nexport type { CardVariant, CardAppearance, CardRadius, CardPadding, CardOrientation, CardSize, CardHeadingLevel, CardCategoryStyle, CardIconStyle } from './lib/card-base/card';\r\nexport { RippleDirective } from './lib/card-base/ripple.directive';\r\nexport { TextInputComponent } from './lib/text-input/text-input';\r\nexport type { TextInputSize, TextInputType, TextInputLabelButton } from './lib/text-input/text-input';\r\nexport { DialogComponent } from './lib/dialog/dialog';\r\nexport type { DialogCloseReason } from './lib/dialog/dialog';\r\nexport type { DialogVariant } from './lib/dialog/dialog-types';\r\nexport { DialogService } from './lib/dialog/dialog.service';\r\nexport type { CompsychDialogConfig } from './lib/dialog/dialog.service';\r\nexport { CompsychDialogRef } from './lib/dialog/dialog-ref';\r\n// Re-exported from CDK so opened content components can inject the data token.\r\nexport { DIALOG_DATA } from '@angular/cdk/dialog';\r\n\r\n@NgModule({\r\n imports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n TextInputComponent,\r\n DialogComponent,\r\n ],\r\n exports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n TextInputComponent,\r\n DialogComponent,\r\n ],\r\n})\r\nexport class CompsychAngularModule {}\r\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/angular/src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;;AAE5D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AA2B5D,MAAM,OAAO,qBAAqB;wGAArB,qBAAqB;yGAArB,qBAAqB,YAtB9B,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,kBAAkB;YAClB,eAAe;YACf,iBAAiB,aAGjB,eAAe;YACf,kBAAkB;YAClB,eAAe;YACf,kBAAkB;YAClB,gBAAgB;YAChB,aAAa;YACb,kBAAkB;YAClB,eAAe;YACf,iBAAiB;yGAGR,qBAAqB;;4FAArB,qBAAqB;kBAxBjC,QAAQ;mBAAC;oBACR,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,kBAAkB;wBAClB,eAAe;wBACf,iBAAiB;qBAClB;oBACD,OAAO,EAAE;wBACP,eAAe;wBACf,kBAAkB;wBAClB,eAAe;wBACf,kBAAkB;wBAClB,gBAAgB;wBAChB,aAAa;wBACb,kBAAkB;wBAClB,eAAe;wBACf,iBAAiB;qBAClB;iBACF","sourcesContent":["import { NgModule } from '@angular/core';\r\nimport { ButtonComponent } from './lib/button/button';\r\nimport { AccordionComponent } from './lib/accordion/accordion';\r\nimport { FooterComponent } from './lib/footer/footer';\r\nimport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nimport { SidebarComponent } from './lib/sidebar/sidebar';\r\nimport { CardComponent } from './lib/card-base/card';\r\nimport { TextInputComponent } from './lib/text-input/text-input';\r\nimport { DialogComponent } from './lib/dialog/dialog';\r\nimport { DropdownComponent } from './lib/dropdown/dropdown';\r\n\r\nexport { ButtonComponent } from './lib/button/button';\r\nexport type { ButtonVariant, ButtonSize, ButtonType } from './lib/button/button';\r\nexport { AccordionComponent } from './lib/accordion/accordion';\r\nexport type { AccordionItem, AccordionButton, AccordionVariant, NestedAccordionItem } from './lib/accordion/accordion-types';\r\nexport { FooterComponent } from './lib/footer/footer';\r\nexport type { FooterLink } from './lib/footer/footer';\r\nexport { HeaderNavComponent } from './lib/header-nav/header-nav';\r\nexport type { HeaderNavLevel, NavAction } from './lib/header-nav/header-nav';\r\nexport { SidebarComponent } from './lib/sidebar/sidebar';\r\nexport type { SidebarItem } from './lib/sidebar/sidebar';\r\nexport { CardComponent } from './lib/card-base/card';\r\nexport type { CardVariant, CardAppearance, CardRadius, CardPadding, CardOrientation, CardSize, CardHeadingLevel, CardCategoryStyle, CardIconStyle } from './lib/card-base/card';\r\nexport { RippleDirective } from './lib/card-base/ripple.directive';\r\nexport { TextInputComponent } from './lib/text-input/text-input';\r\nexport type { TextInputSize, TextInputType, TextInputLabelButton } from './lib/text-input/text-input';\r\nexport { DialogComponent } from './lib/dialog/dialog';\r\nexport type { DialogCloseReason } from './lib/dialog/dialog';\r\nexport type { DialogVariant } from './lib/dialog/dialog-types';\r\nexport { DialogService } from './lib/dialog/dialog.service';\r\nexport type { CompsychDialogConfig } from './lib/dialog/dialog.service';\r\nexport { CompsychDialogRef } from './lib/dialog/dialog-ref';\r\n// Re-exported from CDK so opened content components can inject the data token.\r\nexport { DIALOG_DATA } from '@angular/cdk/dialog';\r\nexport { DropdownComponent } from './lib/dropdown/dropdown';\r\nexport type { DropdownSize, DropdownOption } from './lib/dropdown/dropdown-types';\r\n\r\n@NgModule({\r\n imports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n TextInputComponent,\r\n DialogComponent,\r\n DropdownComponent,\r\n ],\r\n exports: [\r\n ButtonComponent,\r\n AccordionComponent,\r\n FooterComponent,\r\n HeaderNavComponent,\r\n SidebarComponent,\r\n CardComponent,\r\n TextInputComponent,\r\n DialogComponent,\r\n DropdownComponent,\r\n ],\r\n})\r\nexport class CompsychAngularModule {}\r\n"]}
@@ -118,7 +118,7 @@ export class DialogComponent {
118
118
  backdropClass: 'cmp-dialog-scrim',
119
119
  panelClass: 'cmp-dialog-pane',
120
120
  positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
121
- scrollStrategy: this.overlay.scrollStrategies.block(),
121
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
122
122
  // Forward the local reading direction so a body-portaled overlay mirrors correctly
123
123
  // even when only a subtree (not the document) is RTL.
124
124
  direction: this.directionality.value,
@@ -1 +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"]}
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.noop(),\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"]}
@@ -60,7 +60,7 @@ export class DialogService {
60
60
  backdropClass: 'cmp-dialog-scrim',
61
61
  panelClass: config.panelClass ?? 'cmp-dialog-pane',
62
62
  positionStrategy: this.overlay.position().global().centerHorizontally().centerVertically(),
63
- scrollStrategy: this.overlay.scrollStrategies.block(),
63
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
64
64
  direction: this.directionality.value,
65
65
  role: chrome.variant === 'alert' ? 'alertdialog' : 'dialog',
66
66
  // dismissible → CDK closes on Escape / backdrop; non-dismissible → forced choice.
@@ -1 +1 @@
1
- {"version":3,"file":"dialog.service.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dialog/dialog.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAqB,MAAM,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AACjF,OAAO,EAAE,MAAM,EAA2B,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAgB,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;;AA6BjD;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,OAAO,aAAa;IACP,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACxB,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1B,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IAEzD,+FAA+F;IAC/F,IAAI,CACF,OAAiC,EACjC,SAAkC,EAAE;QAEpC,sFAAsF;QACtF,6DAA6D;QAC7D,IAAI,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtD,OAAO,CAAC,IAAI,CACV,mFAAmF;gBACjF,mFAAmF,CACtF,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAiB;YAC3B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,oBAAoB;YAC/C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;YACzB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE;YACrC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE;YACvB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,IAAI;YACvC,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,cAAc;YAC/C,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE;YACzB,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,IAAI;YACvC,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,MAAM,EAAE,GAAG,CAAC,MAAM;SACnB,CAAC;QAEF,IAAI,GAAqC,CAAC;QAC1C,MAAM,SAAS,GAAG;YAChB,+EAA+E;YAC/E,yDAAyD;YACzD,SAAS,EAAE;gBACT,IAAI,EAAE,kBAAkB;gBACxB,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;aACpE;YACD,WAAW,EAAE,IAAI;YACjB,aAAa,EAAE,kBAAkB;YACjC,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,iBAAiB;YAClD,gBAAgB,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,kBAAkB,EAAE,CAAC,gBAAgB,EAAE;YAC1F,cAAc,EAAE,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,EAAE;YACrD,SAAS,EAAE,IAAI,CAAC,cAAc,CAAC,KAAK;YACpC,IAAI,EAAE,MAAM,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAE,aAAuB,CAAC,CAAC,CAAE,QAAkB;YACjF,kFAAkF;YAClF,YAAY,EAAE,CAAC,MAAM,CAAC,WAAW;YACjC,SAAS,EAAE,QAAQ;YACnB,YAAY,EAAE,IAAI;YAClB,SAAS,EAAE,IAAI;YACf,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;YACpD,eAAe,EACb,MAAM,CAAC,OAAO,KAAK,oBAAoB,IAAI,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;YACtF,SAAS,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI;YAC1D,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,qFAAqF;YACrF,wEAAwE;YACxE,SAAS,EAAE,CAAC,MAAuB,EAAE,EAAE;gBACrC,GAAG,GAAG,IAAI,iBAAiB,CAAI,MAA+B,CAAC,CAAC;gBAChE,OAAO,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;YACzD,CAAC;SACkC,CAAC;QAEtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAU,OAAO,EAAE,SAAS,CAAC,CAAC;QAC7D,wFAAwF;QACxF,OAAO,GAAG,IAAI,IAAI,iBAAiB,CAAI,MAA+B,CAAC,CAAC;IAC1E,CAAC;wGArEU,aAAa;4GAAb,aAAa,cADA,MAAM;;4FACnB,aAAa;kBADzB,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE","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
+ {"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.noop(),\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"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dropdown-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropdown-types.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dropdown/dropdown-types.ts"],"names":[],"mappings":"","sourcesContent":["/** Size variant of a `compsych-dropdown`. Controls trigger height, padding, and font size. */\r\nexport type DropdownSize = 'small' | 'medium' | 'large';\r\n\r\n/** A single selectable option in a `compsych-dropdown`. */\r\nexport interface DropdownOption {\r\n /** Value written to the bound form control / emitted by `valueChange`. */\r\n value: string;\r\n /** Text shown for the option in the panel and, when selected, in the trigger. */\r\n label: string;\r\n /** Prevents the option from being selected or focused. */\r\n disabled?: boolean;\r\n}\r\n"]}
@@ -0,0 +1,438 @@
1
+ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild, booleanAttribute, inject, } from '@angular/core';
2
+ import { DomSanitizer } from '@angular/platform-browser';
3
+ import { NgControl, Validators } from '@angular/forms';
4
+ import { CdkListbox, CdkOption } from '@angular/cdk/listbox';
5
+ import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
6
+ import { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';
7
+ import * as i0 from "@angular/core";
8
+ let nextId = 0;
9
+ /**
10
+ * A labelled single-select dropdown built on the CDK Listbox (`@angular/cdk/listbox`)
11
+ * and CDK Overlay. The trigger renders the closed field (label, placeholder/selected
12
+ * value and chevron); the options open in an overlay panel with full keyboard support
13
+ * (arrow keys, Home/End, typeahead) provided by the CDK listbox.
14
+ *
15
+ * Implements `ControlValueAccessor` so it works with both reactive forms
16
+ * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`). The error
17
+ * visual state is driven automatically by the bound `FormControl`'s validity once the
18
+ * field has been touched, and can also be set explicitly via the `error` input.
19
+ */
20
+ export class DropdownComponent {
21
+ _autoId = `cmp-dropdown-${++nextId}`;
22
+ cdr = inject(ChangeDetectorRef);
23
+ sanitizer = inject(DomSanitizer);
24
+ _onChange = () => { };
25
+ _onTouched = () => { };
26
+ /** Injected when the component is used inside a reactive or template-driven form. */
27
+ ngControl;
28
+ constructor() {
29
+ const ctrl = inject(NgControl, { optional: true, self: true });
30
+ if (ctrl)
31
+ ctrl.valueAccessor = this;
32
+ this.ngControl = ctrl;
33
+ this.chevronSvg = this.buildIconSvg('chevron-down');
34
+ this.checkSvg = this.buildIconSvg('check');
35
+ }
36
+ triggerBtn;
37
+ listboxEl;
38
+ /** Label displayed above the trigger. */
39
+ label = '';
40
+ /** Text shown in the trigger when no option is selected. */
41
+ placeholder = '';
42
+ /** Currently selected value. When used in a form, the form controls the value. */
43
+ value = '';
44
+ /** Selectable options rendered in the overlay panel. */
45
+ options = [];
46
+ /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */
47
+ size = 'medium';
48
+ /** Native `id` attribute for the trigger. Auto-generated if omitted. */
49
+ id;
50
+ /**
51
+ * Id(s) of external elements that describe the field, space-separated.
52
+ * Forwarded to `aria-describedby` on the trigger.
53
+ */
54
+ ariaDescribedBy;
55
+ /** Text displayed below the trigger. Shown in red when in error state. */
56
+ supportingText = '';
57
+ /**
58
+ * Static error text shown in red when in error state. Used when the dropdown is
59
+ * outside a form (with `[error]="true"`). When inside a form, prefer `errorMessages`.
60
+ */
61
+ errorMessage = '';
62
+ /**
63
+ * Map of Angular validator error keys to display strings. The component reads
64
+ * `ngControl.errors` to determine which validator failed and shows the matching
65
+ * message in red below the trigger. Example: `{ required: 'Please choose an option' }`
66
+ */
67
+ errorMessages = {};
68
+ /**
69
+ * Adds a blue asterisk after the label. When used in a form, this is set
70
+ * automatically if the bound `FormControl` has `Validators.required`.
71
+ * Supports boolean coercion.
72
+ */
73
+ required = false;
74
+ /**
75
+ * Applies the error visual state (red border) explicitly. When used in a form,
76
+ * the error state is driven automatically from the `FormControl`'s validity once
77
+ * the field is touched — this input is only needed outside of forms. Supports boolean coercion.
78
+ */
79
+ error = false;
80
+ /** Disables the dropdown. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */
81
+ disabled = false;
82
+ /** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */
83
+ hideLabel = false;
84
+ /** Emits the newly selected value whenever the user picks an option. */
85
+ valueChange = new EventEmitter();
86
+ /** Emitted when the overlay panel opens. */
87
+ opened = new EventEmitter();
88
+ /** Emitted when the overlay panel closes. */
89
+ closed = new EventEmitter();
90
+ chevronSvg;
91
+ checkSvg;
92
+ isOpen = false;
93
+ overlayWidth = 0;
94
+ overlayPositions = [
95
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },
96
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },
97
+ ];
98
+ buildIconSvg(name) {
99
+ const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', '<svg width="20" height="20" ');
100
+ return this.sanitizer.bypassSecurityTrustHtml(svg);
101
+ }
102
+ get triggerId() {
103
+ return this.id ?? this._autoId;
104
+ }
105
+ get labelId() {
106
+ return this.triggerId + '-label';
107
+ }
108
+ get listboxId() {
109
+ return this.triggerId + '-listbox';
110
+ }
111
+ get selectedOption() {
112
+ return this.options.find((o) => o.value === this.value);
113
+ }
114
+ /** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */
115
+ get listboxValue() {
116
+ return this.value ? [this.value] : [];
117
+ }
118
+ /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */
119
+ get hasError() {
120
+ return this.error || !!(this.ngControl?.invalid && this.ngControl.touched);
121
+ }
122
+ /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */
123
+ get isRequired() {
124
+ return this.required || !!this.ngControl?.control?.hasValidator(Validators.required);
125
+ }
126
+ get activeErrorMessage() {
127
+ const errors = this.ngControl?.errors;
128
+ if (!errors)
129
+ return '';
130
+ for (const key of Object.keys(errors)) {
131
+ if (this.errorMessages[key])
132
+ return this.errorMessages[key];
133
+ }
134
+ return '';
135
+ }
136
+ get displayedSupportingText() {
137
+ if (this.hasError) {
138
+ return this.activeErrorMessage || this.errorMessage || this.supportingText;
139
+ }
140
+ return this.supportingText;
141
+ }
142
+ get describedBy() {
143
+ const parts = [];
144
+ if (this.ariaDescribedBy)
145
+ parts.push(this.ariaDescribedBy);
146
+ if (this.supportingText || this.errorMessage || Object.keys(this.errorMessages).length) {
147
+ parts.push(this.triggerId + '-supporting');
148
+ }
149
+ return parts.length ? parts.join(' ') : null;
150
+ }
151
+ get wrapperClasses() {
152
+ return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '']
153
+ .filter(Boolean)
154
+ .join(' ');
155
+ }
156
+ // ── Open / close ──────────────────────────────────────────────────────────
157
+ toggle() {
158
+ if (this.isOpen) {
159
+ this.close(true);
160
+ }
161
+ else {
162
+ this.open();
163
+ }
164
+ }
165
+ open() {
166
+ if (this.disabled || this.isOpen)
167
+ return;
168
+ this.overlayWidth = this.triggerBtn?.nativeElement.offsetWidth ?? 0;
169
+ this.isOpen = true;
170
+ this.opened.emit();
171
+ this.cdr.markForCheck();
172
+ }
173
+ close(refocus = false) {
174
+ if (!this.isOpen)
175
+ return;
176
+ this.isOpen = false;
177
+ this._onTouched();
178
+ this.closed.emit();
179
+ if (refocus) {
180
+ setTimeout(() => this.triggerBtn?.nativeElement.focus());
181
+ }
182
+ this.cdr.markForCheck();
183
+ }
184
+ onPanelAttach() {
185
+ setTimeout(() => this.listboxEl?.nativeElement.focus());
186
+ }
187
+ // ── Keyboard ──────────────────────────────────────────────────────────────
188
+ onTriggerKeydown(event) {
189
+ if (this.disabled)
190
+ return;
191
+ if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && !this.isOpen) {
192
+ event.preventDefault();
193
+ this.open();
194
+ }
195
+ else if (event.key === 'Escape' && this.isOpen) {
196
+ event.preventDefault();
197
+ this.close(true);
198
+ }
199
+ }
200
+ onListboxKeydown(event) {
201
+ if (event.key === 'Escape') {
202
+ event.preventDefault();
203
+ this.close(true);
204
+ }
205
+ else if (event.key === 'Tab') {
206
+ event.preventDefault();
207
+ this.close(true);
208
+ }
209
+ }
210
+ onListboxValueChange(event) {
211
+ const next = event.value[0] ?? '';
212
+ this.value = next;
213
+ this._onChange(next);
214
+ this.valueChange.emit(next);
215
+ this.close(true);
216
+ }
217
+ // ── ControlValueAccessor ────────────────────────────────────────────────────
218
+ writeValue(value) {
219
+ this.value = value ?? '';
220
+ this.cdr.markForCheck();
221
+ }
222
+ registerOnChange(fn) {
223
+ this._onChange = fn;
224
+ }
225
+ registerOnTouched(fn) {
226
+ this._onTouched = fn;
227
+ }
228
+ /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */
229
+ setDisabledState(isDisabled) {
230
+ this.disabled = isDisabled;
231
+ this.cdr.markForCheck();
232
+ }
233
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: DropdownComponent, isStandalone: true, selector: "compsych-dropdown", inputs: { label: "label", placeholder: "placeholder", value: "value", options: "options", size: "size", id: "id", ariaDescribedBy: "ariaDescribedBy", supportingText: "supportingText", errorMessage: "errorMessage", errorMessages: "errorMessages", required: ["required", "required", booleanAttribute], error: ["error", "error", booleanAttribute], disabled: ["disabled", "disabled", booleanAttribute], hideLabel: ["hideLabel", "hideLabel", booleanAttribute] }, outputs: { valueChange: "valueChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "triggerBtn", first: true, predicate: ["triggerBtn"], descendants: true }, { propertyName: "listboxEl", first: true, predicate: ["listboxEl"], descendants: true }], ngImport: i0, template: `
235
+ <div [class]="wrapperClasses">
236
+ <div class="label-row" [class.sr-only]="hideLabel">
237
+ <label class="label" [id]="labelId" [for]="triggerId">
238
+ {{ label }}
239
+ @if (isRequired) {
240
+ <span class="required-marker" aria-hidden="true"> *</span>
241
+ }
242
+ </label>
243
+ </div>
244
+
245
+ <button
246
+ #triggerBtn
247
+ cdkOverlayOrigin
248
+ #origin="cdkOverlayOrigin"
249
+ type="button"
250
+ class="trigger"
251
+ [id]="triggerId"
252
+ [class.placeholder]="!selectedOption"
253
+ [class.error]="hasError"
254
+ [class.open]="isOpen"
255
+ [disabled]="disabled"
256
+ role="combobox"
257
+ aria-haspopup="listbox"
258
+ [attr.aria-expanded]="isOpen"
259
+ [attr.aria-controls]="listboxId"
260
+ [attr.aria-labelledby]="labelId + ' ' + triggerId"
261
+ [attr.aria-invalid]="hasError ? 'true' : null"
262
+ [attr.aria-required]="isRequired ? 'true' : null"
263
+ [attr.aria-describedby]="describedBy"
264
+ (click)="toggle()"
265
+ (keydown)="onTriggerKeydown($event)"
266
+ >
267
+ <span class="trigger-text">{{ selectedOption?.label || placeholder }}</span>
268
+ <span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
269
+ </button>
270
+
271
+ @if (displayedSupportingText) {
272
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="triggerId + '-supporting'">
273
+ {{ displayedSupportingText }}
274
+ </span>
275
+ }
276
+ </div>
277
+
278
+ <ng-template
279
+ cdkConnectedOverlay
280
+ [cdkConnectedOverlayOrigin]="origin"
281
+ [cdkConnectedOverlayOpen]="isOpen"
282
+ [cdkConnectedOverlayWidth]="overlayWidth"
283
+ [cdkConnectedOverlayPositions]="overlayPositions"
284
+ [cdkConnectedOverlayViewportMargin]="8"
285
+ (overlayOutsideClick)="close()"
286
+ (attach)="onPanelAttach()"
287
+ (detach)="close()"
288
+ >
289
+ <ul
290
+ #listboxEl
291
+ cdkListbox
292
+ class="panel"
293
+ [id]="listboxId"
294
+ [attr.aria-labelledby]="labelId"
295
+ [cdkListboxValue]="listboxValue"
296
+ (cdkListboxValueChange)="onListboxValueChange($event)"
297
+ (keydown)="onListboxKeydown($event)"
298
+ >
299
+ @for (opt of options; track opt.value) {
300
+ <li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
301
+ <span class="option-check" aria-hidden="true">
302
+ @if (opt.value === value) {
303
+ <span [innerHTML]="checkSvg"></span>
304
+ }
305
+ </span>
306
+ <span class="option-label">{{ opt.label }}</span>
307
+ </li>
308
+ }
309
+ </ul>
310
+ </ng-template>
311
+ `, isInline: true, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking);color:var(--surface-surface-sys-on-surface)}.cmp-dropdown.large .label{font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.required-marker{color:var(--custom-info-sys-info)}.cmp-dropdown.disabled .label{opacity:.48}.trigger{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);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-weight:var(--body-medium-sys-font-weight);text-align:start;cursor:pointer;appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.trigger-text{flex:1 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.trigger.placeholder .trigger-text{color:var(--surface-surface-sys-on-surface-variant)}.trigger-chevron{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant);transition:transform .15s ease}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger:hover:not(:disabled):not(.open){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.trigger:focus-visible,.trigger.open{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)}.trigger.error:not(:disabled):not(.open){border-color:var(--error-sys-error)}.trigger.error:focus-visible,.trigger.error.open{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)}.trigger:disabled{background-color:var(--surface-surface-container-sys-surface-container-low);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline-variant);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-dropdown.disabled .supporting-text{opacity:.48}.cmp-dropdown.small .trigger{height:40px;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-dropdown.medium .trigger{height:48px;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-dropdown.large .trigger{height:56px;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-20);border-radius:var(--border-radius-sys-radius-md);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.panel{box-sizing:border-box;list-style:none;margin:0;padding:var(--spacing-padding-sys-padding-4);max-height:280px;overflow-y:auto;background-color:var(--surface-surface-container-sys-surface-container-lowest);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014;outline:none;font-family:var(--body-medium-sys-font-family)}.option{box-sizing:border-box;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);border-radius:var(--border-radius-sys-radius-sm);color:var(--surface-surface-sys-on-surface);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-check{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;color:var(--accent-primary-sys-primary)}.option-label{flex:1 0 0;min-width:0}.option:hover:not([aria-disabled=true]){background-color:var(--transparent-primary-sys-primary-08)}.option.cdk-option-active{background-color:var(--transparent-primary-sys-primary-08);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-selected=true]{color:var(--accent-primary-sys-primary)}.option[aria-disabled=true]{color:var(--surface-surface-sys-on-surface-variant);cursor:not-allowed;opacity:.48}@media(forced-colors:active){.trigger{border:1px solid ButtonText;forced-color-adjust:none}.trigger:focus-visible,.trigger.open{outline:2px solid Highlight;outline-offset:2px}.trigger:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-dropdown.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}.panel{border:1px solid ButtonText}.option.cdk-option-active{outline:2px solid Highlight;outline-offset:-2px}}\n"], dependencies: [{ kind: "directive", type: CdkListbox, selector: "[cdkListbox]", inputs: ["id", "tabindex", "cdkListboxValue", "cdkListboxMultiple", "cdkListboxDisabled", "cdkListboxUseActiveDescendant", "cdkListboxOrientation", "cdkListboxCompareWith", "cdkListboxNavigationWrapDisabled", "cdkListboxNavigatesDisabledOptions"], outputs: ["cdkListboxValueChange"], exportAs: ["cdkListbox"] }, { kind: "directive", type: CdkOption, selector: "[cdkOption]", inputs: ["id", "cdkOption", "cdkOptionTypeaheadLabel", "cdkOptionDisabled", "tabindex"], exportAs: ["cdkOption"] }, { kind: "directive", type: CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation", "cdkConnectedOverlayUsePopover", "cdkConnectedOverlayMatchWidth", "cdkConnectedOverlay"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
312
+ }
313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: DropdownComponent, decorators: [{
314
+ type: Component,
315
+ args: [{ selector: 'compsych-dropdown', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CdkListbox, CdkOption, CdkConnectedOverlay, CdkOverlayOrigin], template: `
316
+ <div [class]="wrapperClasses">
317
+ <div class="label-row" [class.sr-only]="hideLabel">
318
+ <label class="label" [id]="labelId" [for]="triggerId">
319
+ {{ label }}
320
+ @if (isRequired) {
321
+ <span class="required-marker" aria-hidden="true"> *</span>
322
+ }
323
+ </label>
324
+ </div>
325
+
326
+ <button
327
+ #triggerBtn
328
+ cdkOverlayOrigin
329
+ #origin="cdkOverlayOrigin"
330
+ type="button"
331
+ class="trigger"
332
+ [id]="triggerId"
333
+ [class.placeholder]="!selectedOption"
334
+ [class.error]="hasError"
335
+ [class.open]="isOpen"
336
+ [disabled]="disabled"
337
+ role="combobox"
338
+ aria-haspopup="listbox"
339
+ [attr.aria-expanded]="isOpen"
340
+ [attr.aria-controls]="listboxId"
341
+ [attr.aria-labelledby]="labelId + ' ' + triggerId"
342
+ [attr.aria-invalid]="hasError ? 'true' : null"
343
+ [attr.aria-required]="isRequired ? 'true' : null"
344
+ [attr.aria-describedby]="describedBy"
345
+ (click)="toggle()"
346
+ (keydown)="onTriggerKeydown($event)"
347
+ >
348
+ <span class="trigger-text">{{ selectedOption?.label || placeholder }}</span>
349
+ <span class="trigger-chevron" aria-hidden="true" [innerHTML]="chevronSvg"></span>
350
+ </button>
351
+
352
+ @if (displayedSupportingText) {
353
+ <span class="supporting-text" [class.supporting-text--error]="hasError" [id]="triggerId + '-supporting'">
354
+ {{ displayedSupportingText }}
355
+ </span>
356
+ }
357
+ </div>
358
+
359
+ <ng-template
360
+ cdkConnectedOverlay
361
+ [cdkConnectedOverlayOrigin]="origin"
362
+ [cdkConnectedOverlayOpen]="isOpen"
363
+ [cdkConnectedOverlayWidth]="overlayWidth"
364
+ [cdkConnectedOverlayPositions]="overlayPositions"
365
+ [cdkConnectedOverlayViewportMargin]="8"
366
+ (overlayOutsideClick)="close()"
367
+ (attach)="onPanelAttach()"
368
+ (detach)="close()"
369
+ >
370
+ <ul
371
+ #listboxEl
372
+ cdkListbox
373
+ class="panel"
374
+ [id]="listboxId"
375
+ [attr.aria-labelledby]="labelId"
376
+ [cdkListboxValue]="listboxValue"
377
+ (cdkListboxValueChange)="onListboxValueChange($event)"
378
+ (keydown)="onListboxKeydown($event)"
379
+ >
380
+ @for (opt of options; track opt.value) {
381
+ <li class="option" [cdkOption]="opt.value" [cdkOptionDisabled]="!!opt.disabled">
382
+ <span class="option-check" aria-hidden="true">
383
+ @if (opt.value === value) {
384
+ <span [innerHTML]="checkSvg"></span>
385
+ }
386
+ </span>
387
+ <span class="option-label">{{ opt.label }}</span>
388
+ </li>
389
+ }
390
+ </ul>
391
+ </ng-template>
392
+ `, styles: [":host{display:block}.cmp-dropdown{display:flex;flex-direction:column;gap:var(--spacing-padding-sys-padding-8)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label-row{display:flex;justify-content:space-between;align-items:center;gap:var(--spacing-padding-sys-padding-8)}.label{font-family:var(--label-medium-sys-font-family);font-size:var(--label-medium-sys-font-size);font-weight:var(--label-medium-sys-font-weight-emphasized);line-height:var(--label-medium-sys-line-height);letter-spacing:var(--label-medium-sys-tracking);color:var(--surface-surface-sys-on-surface)}.cmp-dropdown.large .label{font-family:var(--label-large-sys-font-family);font-size:var(--label-large-sys-font-size);font-weight:var(--label-large-sys-font-weight-emphasized);line-height:var(--label-large-sys-line-height);letter-spacing:var(--label-large-sys-tracking)}.required-marker{color:var(--custom-info-sys-info)}.cmp-dropdown.disabled .label{opacity:.48}.trigger{box-sizing:border-box;width:100%;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);margin:0;border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);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-weight:var(--body-medium-sys-font-weight);text-align:start;cursor:pointer;appearance:none;-webkit-appearance:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background-color .15s ease}.trigger-text{flex:1 0 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.trigger.placeholder .trigger-text{color:var(--surface-surface-sys-on-surface-variant)}.trigger-chevron{display:flex;align-items:center;flex-shrink:0;color:var(--surface-surface-sys-on-surface-variant);transition:transform .15s ease}.trigger.open .trigger-chevron{transform:rotate(180deg)}.trigger:hover:not(:disabled):not(.open){box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014}.trigger:focus-visible,.trigger.open{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)}.trigger.error:not(:disabled):not(.open){border-color:var(--error-sys-error)}.trigger.error:focus-visible,.trigger.error.open{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)}.trigger:disabled{background-color:var(--surface-surface-container-sys-surface-container-low);color:var(--surface-surface-sys-on-surface-variant);border-color:var(--outline-sys-outline-variant);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-dropdown.disabled .supporting-text{opacity:.48}.cmp-dropdown.small .trigger{height:40px;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-dropdown.medium .trigger{height:48px;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-dropdown.large .trigger{height:56px;padding:var(--spacing-padding-sys-padding-16) var(--spacing-padding-sys-padding-20);border-radius:var(--border-radius-sys-radius-md);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height)}.panel{box-sizing:border-box;list-style:none;margin:0;padding:var(--spacing-padding-sys-padding-4);max-height:280px;overflow-y:auto;background-color:var(--surface-surface-container-sys-surface-container-lowest);border:var(--border-width-sys-stroke-thin) solid var(--outline-sys-outline-variant);border-radius:var(--border-radius-sys-radius-sm);box-shadow:0 4px 16px var(--transparent-neutral-sys-black-10),0 2px 4px #00000014;outline:none;font-family:var(--body-medium-sys-font-family)}.option{box-sizing:border-box;display:flex;align-items:center;gap:var(--spacing-padding-sys-padding-8);padding:var(--spacing-padding-sys-padding-8) var(--spacing-padding-sys-padding-12);border-radius:var(--border-radius-sys-radius-sm);color:var(--surface-surface-sys-on-surface);font-size:var(--body-medium-sys-font-size);line-height:var(--body-medium-sys-line-height);cursor:pointer;outline:none;-webkit-user-select:none;user-select:none}.option-check{display:flex;align-items:center;justify-content:center;width:20px;height:20px;flex-shrink:0;color:var(--accent-primary-sys-primary)}.option-label{flex:1 0 0;min-width:0}.option:hover:not([aria-disabled=true]){background-color:var(--transparent-primary-sys-primary-08)}.option.cdk-option-active{background-color:var(--transparent-primary-sys-primary-08);outline:var(--border-width-sys-stroke-medium) solid var(--accent-primary-sys-primary);outline-offset:-2px}.option[aria-selected=true]{color:var(--accent-primary-sys-primary)}.option[aria-disabled=true]{color:var(--surface-surface-sys-on-surface-variant);cursor:not-allowed;opacity:.48}@media(forced-colors:active){.trigger{border:1px solid ButtonText;forced-color-adjust:none}.trigger:focus-visible,.trigger.open{outline:2px solid Highlight;outline-offset:2px}.trigger:disabled{color:GrayText;border-color:GrayText;background:ButtonFace;opacity:1}.cmp-dropdown.disabled .label{color:GrayText;opacity:1}.supporting-text--error{color:LinkText}.panel{border:1px solid ButtonText}.option.cdk-option-active{outline:2px solid Highlight;outline-offset:-2px}}\n"] }]
393
+ }], ctorParameters: () => [], propDecorators: { triggerBtn: [{
394
+ type: ViewChild,
395
+ args: ['triggerBtn']
396
+ }], listboxEl: [{
397
+ type: ViewChild,
398
+ args: ['listboxEl']
399
+ }], label: [{
400
+ type: Input
401
+ }], placeholder: [{
402
+ type: Input
403
+ }], value: [{
404
+ type: Input
405
+ }], options: [{
406
+ type: Input
407
+ }], size: [{
408
+ type: Input
409
+ }], id: [{
410
+ type: Input
411
+ }], ariaDescribedBy: [{
412
+ type: Input
413
+ }], supportingText: [{
414
+ type: Input
415
+ }], errorMessage: [{
416
+ type: Input
417
+ }], errorMessages: [{
418
+ type: Input
419
+ }], required: [{
420
+ type: Input,
421
+ args: [{ transform: booleanAttribute }]
422
+ }], error: [{
423
+ type: Input,
424
+ args: [{ transform: booleanAttribute }]
425
+ }], disabled: [{
426
+ type: Input,
427
+ args: [{ transform: booleanAttribute }]
428
+ }], hideLabel: [{
429
+ type: Input,
430
+ args: [{ transform: booleanAttribute }]
431
+ }], valueChange: [{
432
+ type: Output
433
+ }], opened: [{
434
+ type: Output
435
+ }], closed: [{
436
+ type: Output
437
+ }] } });
438
+ //# sourceMappingURL=dropdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropdown.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/dropdown/dropdown.ts"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n ChangeDetectorRef,\r\n Component,\r\n ElementRef,\r\n EventEmitter,\r\n Input,\r\n Output,\r\n ViewChild,\r\n booleanAttribute,\r\n inject,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { ControlValueAccessor, NgControl, Validators } from '@angular/forms';\r\nimport { CdkListbox, CdkOption, ListboxValueChangeEvent } from '@angular/cdk/listbox';\r\nimport { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\nimport { DropdownOption, DropdownSize } from './dropdown-types';\r\n\r\nlet nextId = 0;\r\n\r\n/**\r\n * A labelled single-select dropdown built on the CDK Listbox (`@angular/cdk/listbox`)\r\n * and CDK Overlay. The trigger renders the closed field (label, placeholder/selected\r\n * value and chevron); the options open in an overlay panel with full keyboard support\r\n * (arrow keys, Home/End, typeahead) provided by the CDK listbox.\r\n *\r\n * Implements `ControlValueAccessor` so it works with both reactive forms\r\n * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`). The error\r\n * visual state is driven automatically by the bound `FormControl`'s validity once the\r\n * field has been touched, and can also be set explicitly via the `error` input.\r\n */\r\n@Component({\r\n selector: 'compsych-dropdown',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [CdkListbox, CdkOption, CdkConnectedOverlay, CdkOverlayOrigin],\r\n template: `\r\n <div [class]=\"wrapperClasses\">\r\n <div class=\"label-row\" [class.sr-only]=\"hideLabel\">\r\n <label class=\"label\" [id]=\"labelId\" [for]=\"triggerId\">\r\n {{ label }}\r\n @if (isRequired) {\r\n <span class=\"required-marker\" aria-hidden=\"true\"> *</span>\r\n }\r\n </label>\r\n </div>\r\n\r\n <button\r\n #triggerBtn\r\n cdkOverlayOrigin\r\n #origin=\"cdkOverlayOrigin\"\r\n type=\"button\"\r\n class=\"trigger\"\r\n [id]=\"triggerId\"\r\n [class.placeholder]=\"!selectedOption\"\r\n [class.error]=\"hasError\"\r\n [class.open]=\"isOpen\"\r\n [disabled]=\"disabled\"\r\n role=\"combobox\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-expanded]=\"isOpen\"\r\n [attr.aria-controls]=\"listboxId\"\r\n [attr.aria-labelledby]=\"labelId + ' ' + triggerId\"\r\n [attr.aria-invalid]=\"hasError ? 'true' : null\"\r\n [attr.aria-required]=\"isRequired ? 'true' : null\"\r\n [attr.aria-describedby]=\"describedBy\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onTriggerKeydown($event)\"\r\n >\r\n <span class=\"trigger-text\">{{ selectedOption?.label || placeholder }}</span>\r\n <span class=\"trigger-chevron\" aria-hidden=\"true\" [innerHTML]=\"chevronSvg\"></span>\r\n </button>\r\n\r\n @if (displayedSupportingText) {\r\n <span class=\"supporting-text\" [class.supporting-text--error]=\"hasError\" [id]=\"triggerId + '-supporting'\">\r\n {{ displayedSupportingText }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <ng-template\r\n cdkConnectedOverlay\r\n [cdkConnectedOverlayOrigin]=\"origin\"\r\n [cdkConnectedOverlayOpen]=\"isOpen\"\r\n [cdkConnectedOverlayWidth]=\"overlayWidth\"\r\n [cdkConnectedOverlayPositions]=\"overlayPositions\"\r\n [cdkConnectedOverlayViewportMargin]=\"8\"\r\n (overlayOutsideClick)=\"close()\"\r\n (attach)=\"onPanelAttach()\"\r\n (detach)=\"close()\"\r\n >\r\n <ul\r\n #listboxEl\r\n cdkListbox\r\n class=\"panel\"\r\n [id]=\"listboxId\"\r\n [attr.aria-labelledby]=\"labelId\"\r\n [cdkListboxValue]=\"listboxValue\"\r\n (cdkListboxValueChange)=\"onListboxValueChange($event)\"\r\n (keydown)=\"onListboxKeydown($event)\"\r\n >\r\n @for (opt of options; track opt.value) {\r\n <li class=\"option\" [cdkOption]=\"opt.value\" [cdkOptionDisabled]=\"!!opt.disabled\">\r\n <span class=\"option-check\" aria-hidden=\"true\">\r\n @if (opt.value === value) {\r\n <span [innerHTML]=\"checkSvg\"></span>\r\n }\r\n </span>\r\n <span class=\"option-label\">{{ opt.label }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </ng-template>\r\n `,\r\n styleUrl: './dropdown.css',\r\n})\r\nexport class DropdownComponent implements ControlValueAccessor {\r\n private readonly _autoId = `cmp-dropdown-${++nextId}`;\r\n private readonly cdr = inject(ChangeDetectorRef);\r\n private readonly sanitizer = inject(DomSanitizer);\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 this.chevronSvg = this.buildIconSvg('chevron-down');\r\n this.checkSvg = this.buildIconSvg('check');\r\n }\r\n\r\n @ViewChild('triggerBtn') private triggerBtn?: ElementRef<HTMLButtonElement>;\r\n @ViewChild('listboxEl') private listboxEl?: ElementRef<HTMLUListElement>;\r\n\r\n /** Label displayed above the trigger. */\r\n @Input() label = '';\r\n /** Text shown in the trigger when no option is selected. */\r\n @Input() placeholder = '';\r\n /** Currently selected value. When used in a form, the form controls the value. */\r\n @Input() value = '';\r\n /** Selectable options rendered in the overlay panel. */\r\n @Input() options: DropdownOption[] = [];\r\n /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */\r\n @Input() size: DropdownSize = 'medium';\r\n /** Native `id` attribute for the trigger. Auto-generated if omitted. */\r\n @Input() id?: string;\r\n /**\r\n * Id(s) of external elements that describe the field, space-separated.\r\n * Forwarded to `aria-describedby` on the trigger.\r\n */\r\n @Input() ariaDescribedBy?: string;\r\n /** Text displayed below the trigger. Shown in red when in error state. */\r\n @Input() supportingText = '';\r\n /**\r\n * Static error text shown in red when in error state. Used when the dropdown is\r\n * outside a form (with `[error]=\"true\"`). When inside a form, prefer `errorMessages`.\r\n */\r\n @Input() errorMessage = '';\r\n /**\r\n * Map of Angular validator error keys to display strings. The component reads\r\n * `ngControl.errors` to determine which validator failed and shows the matching\r\n * message in red below the trigger. Example: `{ required: 'Please choose an option' }`\r\n */\r\n @Input() errorMessages: Record<string, string> = {};\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 * 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. Supports boolean coercion.\r\n */\r\n @Input({ transform: booleanAttribute }) error = false;\r\n /** Disables the dropdown. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) disabled = false;\r\n /** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */\r\n @Input({ transform: booleanAttribute }) hideLabel = false;\r\n\r\n /** Emits the newly selected value whenever the user picks an option. */\r\n @Output() readonly valueChange = new EventEmitter<string>();\r\n /** Emitted when the overlay panel opens. */\r\n @Output() readonly opened = new EventEmitter<void>();\r\n /** Emitted when the overlay panel closes. */\r\n @Output() readonly closed = new EventEmitter<void>();\r\n\r\n protected readonly chevronSvg: SafeHtml;\r\n protected readonly checkSvg: SafeHtml;\r\n\r\n protected isOpen = false;\r\n protected overlayWidth = 0;\r\n protected readonly overlayPositions: ConnectedPosition[] = [\r\n { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4 },\r\n { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -4 },\r\n ];\r\n\r\n private buildIconSvg(name: string): SafeHtml {\r\n const svg = (LUCIDE_ICONS[name] ?? '').replace('<svg ', '<svg width=\"20\" height=\"20\" ');\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n\r\n get triggerId(): string {\r\n return this.id ?? this._autoId;\r\n }\r\n\r\n get labelId(): string {\r\n return this.triggerId + '-label';\r\n }\r\n\r\n get listboxId(): string {\r\n return this.triggerId + '-listbox';\r\n }\r\n\r\n get selectedOption(): DropdownOption | undefined {\r\n return this.options.find((o) => o.value === this.value);\r\n }\r\n\r\n /** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */\r\n get listboxValue(): readonly string[] {\r\n return this.value ? [this.value] : [];\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.triggerId + '-supporting');\r\n }\r\n return parts.length ? parts.join(' ') : null;\r\n }\r\n\r\n get wrapperClasses(): string {\r\n return ['cmp-dropdown', this.size, this.disabled ? 'disabled' : '']\r\n .filter(Boolean)\r\n .join(' ');\r\n }\r\n\r\n // ── Open / close ──────────────────────────────────────────────────────────\r\n\r\n protected toggle(): void {\r\n if (this.isOpen) {\r\n this.close(true);\r\n } else {\r\n this.open();\r\n }\r\n }\r\n\r\n protected open(): void {\r\n if (this.disabled || this.isOpen) return;\r\n this.overlayWidth = this.triggerBtn?.nativeElement.offsetWidth ?? 0;\r\n this.isOpen = true;\r\n this.opened.emit();\r\n this.cdr.markForCheck();\r\n }\r\n\r\n protected close(refocus = false): void {\r\n if (!this.isOpen) return;\r\n this.isOpen = false;\r\n this._onTouched();\r\n this.closed.emit();\r\n if (refocus) {\r\n setTimeout(() => this.triggerBtn?.nativeElement.focus());\r\n }\r\n this.cdr.markForCheck();\r\n }\r\n\r\n protected onPanelAttach(): void {\r\n setTimeout(() => this.listboxEl?.nativeElement.focus());\r\n }\r\n\r\n // ── Keyboard ──────────────────────────────────────────────────────────────\r\n\r\n protected onTriggerKeydown(event: KeyboardEvent): void {\r\n if (this.disabled) return;\r\n if ((event.key === 'ArrowDown' || event.key === 'ArrowUp') && !this.isOpen) {\r\n event.preventDefault();\r\n this.open();\r\n } else if (event.key === 'Escape' && this.isOpen) {\r\n event.preventDefault();\r\n this.close(true);\r\n }\r\n }\r\n\r\n protected onListboxKeydown(event: KeyboardEvent): void {\r\n if (event.key === 'Escape') {\r\n event.preventDefault();\r\n this.close(true);\r\n } else if (event.key === 'Tab') {\r\n event.preventDefault();\r\n this.close(true);\r\n }\r\n }\r\n\r\n protected onListboxValueChange(event: ListboxValueChangeEvent<string>): void {\r\n const next = event.value[0] ?? '';\r\n this.value = next;\r\n this._onChange(next);\r\n this.valueChange.emit(next);\r\n this.close(true);\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"]}
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/text-input/text-input";
9
9
  import * as i8 from "./lib/dialog/dialog";
10
+ import * as i9 from "./lib/dropdown/dropdown";
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';
@@ -29,8 +30,10 @@ export { DialogService } from './lib/dialog/dialog.service';
29
30
  export type { CompsychDialogConfig } from './lib/dialog/dialog.service';
30
31
  export { CompsychDialogRef } from './lib/dialog/dialog-ref';
31
32
  export { DIALOG_DATA } from '@angular/cdk/dialog';
33
+ export { DropdownComponent } from './lib/dropdown/dropdown';
34
+ export type { DropdownSize, DropdownOption } from './lib/dropdown/dropdown-types';
32
35
  export declare class CompsychAngularModule {
33
36
  static ɵfac: i0.ɵɵFactoryDeclaration<CompsychAngularModule, never>;
34
- 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.TextInputComponent, typeof i8.DialogComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent]>;
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.TextInputComponent, typeof i8.DialogComponent, typeof i9.DropdownComponent], [typeof i1.ButtonComponent, typeof i2.AccordionComponent, typeof i3.FooterComponent, typeof i4.HeaderNavComponent, typeof i5.SidebarComponent, typeof i6.CardComponent, typeof i7.TextInputComponent, typeof i8.DialogComponent, typeof i9.DropdownComponent]>;
35
38
  static ɵinj: i0.ɵɵInjectorDeclaration<CompsychAngularModule>;
36
39
  }
@@ -0,0 +1,11 @@
1
+ /** Size variant of a `compsych-dropdown`. Controls trigger height, padding, and font size. */
2
+ export type DropdownSize = 'small' | 'medium' | 'large';
3
+ /** A single selectable option in a `compsych-dropdown`. */
4
+ export interface DropdownOption {
5
+ /** Value written to the bound form control / emitted by `valueChange`. */
6
+ value: string;
7
+ /** Text shown for the option in the panel and, when selected, in the trigger. */
8
+ label: string;
9
+ /** Prevents the option from being selected or focused. */
10
+ disabled?: boolean;
11
+ }
@@ -0,0 +1,120 @@
1
+ import { EventEmitter } from '@angular/core';
2
+ import { SafeHtml } from '@angular/platform-browser';
3
+ import { ControlValueAccessor, NgControl } from '@angular/forms';
4
+ import { ListboxValueChangeEvent } from '@angular/cdk/listbox';
5
+ import { ConnectedPosition } from '@angular/cdk/overlay';
6
+ import { DropdownOption, DropdownSize } from './dropdown-types';
7
+ import * as i0 from "@angular/core";
8
+ /**
9
+ * A labelled single-select dropdown built on the CDK Listbox (`@angular/cdk/listbox`)
10
+ * and CDK Overlay. The trigger renders the closed field (label, placeholder/selected
11
+ * value and chevron); the options open in an overlay panel with full keyboard support
12
+ * (arrow keys, Home/End, typeahead) provided by the CDK listbox.
13
+ *
14
+ * Implements `ControlValueAccessor` so it works with both reactive forms
15
+ * (`formControlName`, `[formControl]`) and template-driven forms (`ngModel`). The error
16
+ * visual state is driven automatically by the bound `FormControl`'s validity once the
17
+ * field has been touched, and can also be set explicitly via the `error` input.
18
+ */
19
+ export declare class DropdownComponent implements ControlValueAccessor {
20
+ private readonly _autoId;
21
+ private readonly cdr;
22
+ private readonly sanitizer;
23
+ private _onChange;
24
+ private _onTouched;
25
+ /** Injected when the component is used inside a reactive or template-driven form. */
26
+ protected readonly ngControl: NgControl | null;
27
+ constructor();
28
+ private triggerBtn?;
29
+ private listboxEl?;
30
+ /** Label displayed above the trigger. */
31
+ label: string;
32
+ /** Text shown in the trigger when no option is selected. */
33
+ placeholder: string;
34
+ /** Currently selected value. When used in a form, the form controls the value. */
35
+ value: string;
36
+ /** Selectable options rendered in the overlay panel. */
37
+ options: DropdownOption[];
38
+ /** Size variant — controls height, padding, and font size. Defaults to `'medium'`. */
39
+ size: DropdownSize;
40
+ /** Native `id` attribute for the trigger. Auto-generated if omitted. */
41
+ id?: string;
42
+ /**
43
+ * Id(s) of external elements that describe the field, space-separated.
44
+ * Forwarded to `aria-describedby` on the trigger.
45
+ */
46
+ ariaDescribedBy?: string;
47
+ /** Text displayed below the trigger. Shown in red when in error state. */
48
+ supportingText: string;
49
+ /**
50
+ * Static error text shown in red when in error state. Used when the dropdown is
51
+ * outside a form (with `[error]="true"`). When inside a form, prefer `errorMessages`.
52
+ */
53
+ errorMessage: string;
54
+ /**
55
+ * Map of Angular validator error keys to display strings. The component reads
56
+ * `ngControl.errors` to determine which validator failed and shows the matching
57
+ * message in red below the trigger. Example: `{ required: 'Please choose an option' }`
58
+ */
59
+ errorMessages: Record<string, string>;
60
+ /**
61
+ * Adds a blue asterisk after the label. When used in a form, this is set
62
+ * automatically if the bound `FormControl` has `Validators.required`.
63
+ * Supports boolean coercion.
64
+ */
65
+ required: boolean;
66
+ /**
67
+ * Applies the error visual state (red border) explicitly. When used in a form,
68
+ * the error state is driven automatically from the `FormControl`'s validity once
69
+ * the field is touched — this input is only needed outside of forms. Supports boolean coercion.
70
+ */
71
+ error: boolean;
72
+ /** Disables the dropdown. When used in a form, also responds to `FormControl.disable()`. Supports boolean coercion. */
73
+ disabled: boolean;
74
+ /** Visually hides the label while keeping it accessible to screen readers. Supports boolean coercion. */
75
+ hideLabel: boolean;
76
+ /** Emits the newly selected value whenever the user picks an option. */
77
+ readonly valueChange: EventEmitter<string>;
78
+ /** Emitted when the overlay panel opens. */
79
+ readonly opened: EventEmitter<void>;
80
+ /** Emitted when the overlay panel closes. */
81
+ readonly closed: EventEmitter<void>;
82
+ protected readonly chevronSvg: SafeHtml;
83
+ protected readonly checkSvg: SafeHtml;
84
+ protected isOpen: boolean;
85
+ protected overlayWidth: number;
86
+ protected readonly overlayPositions: ConnectedPosition[];
87
+ private buildIconSvg;
88
+ get triggerId(): string;
89
+ get labelId(): string;
90
+ get listboxId(): string;
91
+ get selectedOption(): DropdownOption | undefined;
92
+ /** Value array bound to the CDK listbox (single-select, so 0 or 1 entry). */
93
+ get listboxValue(): readonly string[];
94
+ /** True when the bound FormControl is invalid and touched, or when `error` is explicitly set. */
95
+ get hasError(): boolean;
96
+ /** True when `required` is set explicitly, or when the bound FormControl has Validators.required. */
97
+ get isRequired(): boolean;
98
+ private get activeErrorMessage();
99
+ get displayedSupportingText(): string;
100
+ get describedBy(): string | null;
101
+ get wrapperClasses(): string;
102
+ protected toggle(): void;
103
+ protected open(): void;
104
+ protected close(refocus?: boolean): void;
105
+ protected onPanelAttach(): void;
106
+ protected onTriggerKeydown(event: KeyboardEvent): void;
107
+ protected onListboxKeydown(event: KeyboardEvent): void;
108
+ protected onListboxValueChange(event: ListboxValueChangeEvent<string>): void;
109
+ writeValue(value: string | null): void;
110
+ registerOnChange(fn: (value: string) => void): void;
111
+ registerOnTouched(fn: () => void): void;
112
+ /** Called by Angular when the bound FormControl is programmatically disabled or enabled. */
113
+ setDisabledState(isDisabled: boolean): void;
114
+ static ɵfac: i0.ɵɵFactoryDeclaration<DropdownComponent, never>;
115
+ static ɵcmp: i0.ɵɵComponentDeclaration<DropdownComponent, "compsych-dropdown", never, { "label": { "alias": "label"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "value": { "alias": "value"; "required": false; }; "options": { "alias": "options"; "required": false; }; "size": { "alias": "size"; "required": false; }; "id": { "alias": "id"; "required": false; }; "ariaDescribedBy": { "alias": "ariaDescribedBy"; "required": false; }; "supportingText": { "alias": "supportingText"; "required": false; }; "errorMessage": { "alias": "errorMessage"; "required": false; }; "errorMessages": { "alias": "errorMessages"; "required": false; }; "required": { "alias": "required"; "required": false; }; "error": { "alias": "error"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "hideLabel": { "alias": "hideLabel"; "required": false; }; }, { "valueChange": "valueChange"; "opened": "opened"; "closed": "closed"; }, never, never, true, never>;
116
+ static ngAcceptInputType_required: unknown;
117
+ static ngAcceptInputType_error: unknown;
118
+ static ngAcceptInputType_disabled: unknown;
119
+ static ngAcceptInputType_hideLabel: unknown;
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compsych-ui-components/angular",
3
- "version": "7.0.27",
3
+ "version": "7.0.32",
4
4
  "description": "Angular UI components for compsych",
5
5
  "publishConfig": {
6
6
  "access": "public",