@ng-zen/cli 22.4.1-next.1 → 22.5.0-next.1

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.
Files changed (47) hide show
  1. package/README.md +37 -3
  2. package/eslint-config/index.js +130 -0
  3. package/package.json +14 -1
  4. package/schematics/ng-add/eslint-config.js +87 -0
  5. package/schematics/ng-add/eslint-config.js.map +1 -0
  6. package/schematics/ng-add/eslint-config.ts +98 -0
  7. package/schematics/ng-add/icon-library/icon-library.js +2 -1
  8. package/schematics/ng-add/icon-library/icon-library.js.map +1 -1
  9. package/schematics/ng-add/icon-library/icon-library.ts +2 -1
  10. package/schematics/ng-add/index.js +14 -2
  11. package/schematics/ng-add/index.js.map +1 -1
  12. package/schematics/ng-add/index.ts +14 -3
  13. package/schematics/ng-add/schema.js.map +1 -1
  14. package/schematics/ng-add/schema.json +5 -0
  15. package/schematics/ng-add/schema.ts +1 -0
  16. package/schematics/ui/files/avatar/avatar.ts +1 -1
  17. package/schematics/ui/files/checkbox/checkbox.stories.ts +1 -1
  18. package/schematics/ui/files/checkbox/checkbox.ts +2 -2
  19. package/schematics/ui/files/dialog/dialog.service.stories.ts +3 -3
  20. package/schematics/ui/files/dialog/dialog.service.ts +2 -2
  21. package/schematics/ui/files/dialog/dialog.spec.ts +6 -6
  22. package/schematics/ui/files/dialog/dialog.ts +1 -1
  23. package/schematics/ui/files/form-control/form-control.ts +5 -5
  24. package/schematics/ui/files/icon/icon.ts +5 -5
  25. package/schematics/ui/files/input/input.stories.ts +4 -4
  26. package/schematics/ui/files/input/input.ts +1 -1
  27. package/schematics/ui/files/pin/index.ts +3 -0
  28. package/schematics/ui/files/pin/pin-item.spec.ts +151 -0
  29. package/schematics/ui/files/pin/pin-item.ts +115 -0
  30. package/schematics/ui/files/pin/pin.spec.ts +153 -0
  31. package/schematics/ui/files/pin/pin.stories.ts +164 -0
  32. package/schematics/ui/files/pin/pin.ts +49 -0
  33. package/schematics/ui/files/pin/pin.types.ts +15 -0
  34. package/schematics/ui/files/popover/popover.spec.ts +10 -7
  35. package/schematics/ui/files/popover/popover.stories.ts +1 -1
  36. package/schematics/ui/files/radio/radio.stories.ts +3 -3
  37. package/schematics/ui/files/radio/radio.ts +3 -3
  38. package/schematics/ui/files/switch/switch.stories.ts +1 -1
  39. package/schematics/ui/index.js +2 -4
  40. package/schematics/ui/index.js.map +1 -1
  41. package/schematics/ui/index.ts +2 -4
  42. package/schematics/ui/schema.js.map +1 -1
  43. package/schematics/ui/schema.json +1 -0
  44. package/schematics/ui/schema.ts +1 -0
  45. package/utils/apply-file-template.util.js.map +1 -1
  46. package/utils/apply-file-template.util.ts +3 -3
  47. package/CHANGELOG.md +0 -8
@@ -8,13 +8,13 @@ import { ZenDialogService } from './dialog.service';
8
8
  @Component({
9
9
  template: `
10
10
  <dialog
11
+ zen-dialog
11
12
  [backdrop]="backdrop()"
12
13
  [closeOnEscape]="closeOnEscape()"
13
14
  [header]="header()"
14
15
  [id]="id()"
15
16
  [size]="size()"
16
17
  [(open)]="isOpen"
17
- zen-dialog
18
18
  >
19
19
  <p>Dialog content</p>
20
20
  </dialog>
@@ -34,8 +34,8 @@ class DialogTestComponent {
34
34
  @Component({
35
35
  template: `
36
36
  <p>{{ message() }}</p>
37
- <button (click)="onConfirm()">Confirm</button>
38
- <button (click)="onCancel()">Cancel</button>
37
+ <button type="button" (click)="onConfirm()">Confirm</button>
38
+ <button type="button" (click)="onCancel()">Cancel</button>
39
39
  `,
40
40
  standalone: true,
41
41
  })
@@ -58,7 +58,7 @@ function getDialogEl(fixture: ComponentFixture<unknown>): HTMLDialogElement | nu
58
58
  }
59
59
 
60
60
  function getDialogElFromBody(): HTMLDialogElement | null {
61
- return document.body.querySelector('dialog[zen-dialog]') as HTMLDialogElement | null;
61
+ return document.body.querySelector('dialog[zen-dialog]');
62
62
  }
63
63
 
64
64
  describe('ZenDialog', () => {
@@ -212,7 +212,7 @@ describe('ZenDialogService', () => {
212
212
  const service = TestBed.inject(ZenDialogService);
213
213
 
214
214
  const ref = service.open(TestDialogContent, {
215
- inputs: { message: 'Hello from service' } as Record<string, unknown>,
215
+ inputs: { message: 'Hello from service' },
216
216
  });
217
217
 
218
218
  expect(ref.componentInstance.message()).toBe('Hello from service');
@@ -230,7 +230,7 @@ describe('ZenDialogService', () => {
230
230
  },
231
231
  });
232
232
 
233
- const confirmBtn = document.body.querySelector('button') as HTMLButtonElement;
233
+ const confirmBtn = document.body.querySelector('button')!;
234
234
  confirmBtn?.click();
235
235
 
236
236
  expect(confirmHandler).toHaveBeenCalledWith('confirmed');
@@ -44,7 +44,7 @@ type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';
44
44
  <header class="zen-dialog-header">
45
45
  <h2>{{ header() }}</h2>
46
46
  @if (closable()) {
47
- <button (click)="onClose()" aria-label="Close dialog" class="zen-dialog-close" type="button">
47
+ <button aria-label="Close dialog" class="zen-dialog-close" type="button" (click)="onClose()">
48
48
  <zen-icon [icon]="closeIcon" [size]="20" />
49
49
  </button>
50
50
  }
@@ -40,9 +40,6 @@ import { FormValueControl } from '@angular/forms/signals';
40
40
  },
41
41
  })
42
42
  export abstract class ZenFormControl<Value> implements FormValueControl<Value> {
43
- /** The underlying value of the control. Subclasses must provide their own implementation using `model()`. */
44
- abstract readonly value: ModelSignal<Value>;
45
-
46
43
  /** Validation errors for the field. Auto-bound by the Signal Forms directive. */
47
44
  readonly errors = input<readonly ValidationError.WithOptionalFieldTree[]>([]);
48
45
  /** Whether the control is disabled. Auto-bound by the Signal Forms directive. */
@@ -68,12 +65,15 @@ export abstract class ZenFormControl<Value> implements FormValueControl<Value> {
68
65
  /** Emitted when the field is blurred. The {@link FormField} directive listens to this output to mark the field as touched. */
69
66
  readonly touch = output<void>();
70
67
 
68
+ /** The underlying value of the control. Subclasses must provide their own implementation using `model()`. */
69
+ abstract readonly value: ModelSignal<Value>;
70
+
71
71
  // @ignore
72
- // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
72
+
73
73
  focus(_options?: FocusOptions): void {}
74
74
 
75
75
  // @ignore
76
- // eslint-disable-next-line @typescript-eslint/no-empty-function
76
+
77
77
  reset(): void {}
78
78
 
79
79
  /** Should be called by the subclass when the control's value changes as a result of user interaction. */
@@ -29,17 +29,17 @@ export type IconSvgObject = readonly (readonly [string, Record<string, string |
29
29
  selector: 'zen-icon',
30
30
  template: `
31
31
  <svg
32
+ fill="none"
33
+ stroke="currentColor"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ xmlns="http://www.w3.org/2000/svg"
32
37
  [attr.height]="size()"
33
38
  [attr.stroke-width]="calculatedStrokeWidth()"
34
39
  [attr.viewBox]="'0 0 ' + BASE_SIZE + ' ' + BASE_SIZE"
35
40
  [attr.width]="size()"
36
41
  [innerHTML]="safeSvgContent()"
37
42
  [style.color]="color()"
38
- fill="none"
39
- stroke="currentColor"
40
- stroke-linecap="round"
41
- stroke-linejoin="round"
42
- xmlns="http://www.w3.org/2000/svg"
43
43
  ></svg>
44
44
  `,
45
45
  styles: [':host { display: inline-flex; }'],
@@ -89,7 +89,7 @@ export const WithFormRoot: Story = {
89
89
  standalone: true,
90
90
  template: `
91
91
  <div style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;">
92
- <zen-input [formField]="form.name" placeholder="Type something" />
92
+ <zen-input placeholder="Type something" [formField]="form.name" />
93
93
  @if (form.name().invalid()) {
94
94
  <p style="color: red; margin: 0; font-size: 0.875rem;">
95
95
  {{ form.name().errors()![0].message }}
@@ -108,9 +108,9 @@ class InputSignalFormComponent {
108
108
  @Component({
109
109
  standalone: true,
110
110
  template: `
111
- <form [formRoot]="loginForm" style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;">
112
- <zen-input [formField]="loginForm.email" placeholder="Email" />
113
- <zen-input [formField]="loginForm.password" placeholder="Password" type="password" />
111
+ <form style="display: flex; flex-direction: column; gap: 0.5rem; max-width: 300px;" [formRoot]="loginForm">
112
+ <zen-input placeholder="Email" [formField]="loginForm.email" />
113
+ <zen-input placeholder="Password" type="password" [formField]="loginForm.password" />
114
114
  <button type="submit">Sign in</button>
115
115
  </form>
116
116
  `,
@@ -37,6 +37,7 @@ import { ZenFormControl } from '../form-control';
37
37
  selector: 'zen-input',
38
38
  template: `
39
39
  <input
40
+ #inputRef
40
41
  [aria-invalid]="invalid() || null"
41
42
  [disabled]="disabled()"
42
43
  [placeholder]="placeholder()"
@@ -44,7 +45,6 @@ import { ZenFormControl } from '../form-control';
44
45
  [type]="type()"
45
46
  [value]="value()"
46
47
  (input)="onInput(inputRef.value)"
47
- #inputRef
48
48
  />
49
49
  `,
50
50
  styleUrls: ['./input.scss'],
@@ -0,0 +1,3 @@
1
+ export * from './pin';
2
+ export * from './pin.types';
3
+ export * from './pin-item';
@@ -0,0 +1,151 @@
1
+ import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { ZenButton } from '../button';
6
+ import { ZenPin } from './pin';
7
+ import type { PinPosition } from './pin.types';
8
+ import { ZenPinItem } from './pin-item';
9
+
10
+ @Component({
11
+ template: `
12
+ <button #trigger id="trigger" type="button" zen-button zenPin>
13
+ Trigger
14
+ <span zenPinItem [zenPinFlush]="flushed()" [zenPinOffset]="offset()" [zenPinPosition]="position()">
15
+ Pinned content
16
+ </span>
17
+ </button>
18
+ `,
19
+ imports: [ZenPin, ZenButton, ZenPinItem],
20
+ })
21
+ class HostComponent {
22
+ readonly position = signal<PinPosition>('top right');
23
+ readonly offset = signal<string | number>('0');
24
+ readonly flushed = signal<boolean>(false);
25
+ }
26
+
27
+ function getWrapper(fixture: ComponentFixture<unknown>): HTMLElement | null {
28
+ return fixture.nativeElement.querySelector('.zen-pin-wrapper') as HTMLElement | null;
29
+ }
30
+
31
+ describe('ZenPinItem', () => {
32
+ beforeEach(async () => {
33
+ await TestBed.configureTestingModule({
34
+ imports: [HostComponent],
35
+ providers: [provideZonelessChangeDetection()],
36
+ }).compileComponents();
37
+ });
38
+
39
+ function createFixture(): ComponentFixture<HostComponent> {
40
+ const fixture = TestBed.createComponent(HostComponent);
41
+ fixture.detectChanges();
42
+ return fixture;
43
+ }
44
+
45
+ it('sets default position-area to top right', () => {
46
+ expect(getWrapper(createFixture())!.style.getPropertyValue('position-area')).toBe('top right');
47
+ });
48
+
49
+ it('applies overlap translate by default', () => {
50
+ expect(getWrapper(createFixture())!.style.getPropertyValue('translate')).toBe('-50% 50%');
51
+ });
52
+
53
+ it('does not set margin by default', () => {
54
+ expect(getWrapper(createFixture())!.style.getPropertyValue('margin')).toBe('');
55
+ });
56
+
57
+ it.each([
58
+ ['top', '0 50%'],
59
+ ['bottom', '0 -50%'],
60
+ ['left', '50% 0'],
61
+ ['right', '-50% 0'],
62
+ ['center', '0 0'],
63
+ ['top left', '50% 50%'],
64
+ ['top right', '-50% 50%'],
65
+ ['bottom left', '50% -50%'],
66
+ ['bottom right', '-50% -50%'],
67
+ ['top center', '0 50%'],
68
+ ['center left', '50% 0'],
69
+ ['center right', '-50% 0'],
70
+ ['bottom center', '0 -50%'],
71
+ ] as const)('computes %s translate as %s', (position, expected) => {
72
+ const fixture = createFixture();
73
+ fixture.componentInstance.position.set(position);
74
+ fixture.detectChanges();
75
+
76
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe(expected);
77
+ });
78
+
79
+ it('removes translate when flush is true', () => {
80
+ const fixture = createFixture();
81
+ fixture.componentInstance.flushed.set(true);
82
+ fixture.detectChanges();
83
+
84
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe('');
85
+ });
86
+
87
+ it('sets margin from string offset', () => {
88
+ const fixture = createFixture();
89
+ fixture.componentInstance.offset.set('10px');
90
+ fixture.detectChanges();
91
+
92
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('10px');
93
+ });
94
+
95
+ it('sets margin from numeric offset', () => {
96
+ const fixture = createFixture();
97
+ fixture.componentInstance.offset.set(15);
98
+ fixture.detectChanges();
99
+
100
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('15px');
101
+ });
102
+
103
+ it('removes margin when offset is zero string', () => {
104
+ const fixture = createFixture();
105
+ fixture.componentInstance.offset.set('10px');
106
+ fixture.detectChanges();
107
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('10px');
108
+
109
+ fixture.componentInstance.offset.set('0');
110
+ fixture.detectChanges();
111
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('');
112
+ });
113
+
114
+ it('removes margin when offset is zero number', () => {
115
+ const fixture = createFixture();
116
+ fixture.componentInstance.offset.set(15);
117
+ fixture.detectChanges();
118
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('15px');
119
+
120
+ fixture.componentInstance.offset.set(0);
121
+ fixture.detectChanges();
122
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('');
123
+ });
124
+
125
+ it('keeps overlap translate when offset is set', () => {
126
+ const fixture = createFixture();
127
+ fixture.componentInstance.offset.set('10px');
128
+ fixture.detectChanges();
129
+
130
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe('-50% 50%');
131
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('10px');
132
+ });
133
+
134
+ it('sets margin without translate when flush and offset', () => {
135
+ const fixture = createFixture();
136
+ fixture.componentInstance.flushed.set(true);
137
+ fixture.componentInstance.offset.set('8px');
138
+ fixture.detectChanges();
139
+
140
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe('');
141
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('8px');
142
+ });
143
+
144
+ it('removes wrapper from DOM on destroy', () => {
145
+ const fixture = createFixture();
146
+ expect(getWrapper(fixture)).toBeTruthy();
147
+
148
+ fixture.destroy();
149
+ expect(getWrapper(fixture)).toBeNull();
150
+ });
151
+ });
@@ -0,0 +1,115 @@
1
+ import { booleanAttribute, DestroyRef, Directive, effect, ElementRef, inject, input, Renderer2 } from '@angular/core';
2
+
3
+ import { ZenPin } from './pin';
4
+ import type { PinPosition } from './pin.types';
5
+
6
+ /**
7
+ * ZenPinItem is a directive that pins an element as an overlay relative to a
8
+ * `[zenPin]` anchor. Each pinned element gets its own absolutely-positioned
9
+ * wrapper using CSS Anchor Positioning.
10
+ *
11
+ * @example
12
+ *
13
+ * ```html
14
+ * <button zen-button zenPin>
15
+ * Notifications
16
+ * <zen-badge zenPinItem zenPinPosition="top right" [zenPinOffset]="5px">3</zen-badge>
17
+ * <zen-icon zenPinItem zenPinPosition="bottom right" />
18
+ * </button>
19
+ * ```
20
+ *
21
+ * @author Konrad Stępień
22
+ * @license {@link https://github.com/kstepien3/ng-zen/blob/master/LICENSE|BSD-2-Clause}
23
+ * @see [GitHub](https://github.com/kstepien3/ng-zen)
24
+ * @see [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning)
25
+ */
26
+ @Directive({
27
+ selector: '[zenPinItem]',
28
+ })
29
+ export class ZenPinItem {
30
+ /** Position of the pin relative to the anchor element. */
31
+ readonly zenPinPosition = input<PinPosition>('top right');
32
+
33
+ /** Fine-tune the margin to offset the pin from the position-area edge. Accepts CSS value or number (px). */
34
+ readonly zenPinOffset = input<string | number>('0');
35
+
36
+ /** Whether the pin sits flush at the edge without overlapping the anchor. */
37
+ readonly zenPinFlush = input<boolean, unknown>(false, { transform: booleanAttribute });
38
+
39
+ private readonly el = inject(ElementRef);
40
+ private readonly renderer = inject(Renderer2);
41
+ private readonly zenPin = inject(ZenPin);
42
+ private readonly destroyRef = inject(DestroyRef);
43
+
44
+ private wrapper: HTMLElement | null = null;
45
+
46
+ constructor() {
47
+ effect(() => {
48
+ if (!this.wrapper) {
49
+ this.createWrapper();
50
+ }
51
+ this.updatePosition();
52
+ });
53
+
54
+ this.destroyRef.onDestroy(() => this.cleanup());
55
+ }
56
+
57
+ private createWrapper(): void {
58
+ const wrapper = this.renderer.createElement('div');
59
+ this.renderer.addClass(wrapper, 'zen-pin-wrapper');
60
+ this.renderer.setStyle(wrapper, 'position', 'absolute');
61
+ this.renderer.setStyle(wrapper, 'position-anchor', this.zenPin.anchorName);
62
+ this.renderer.setStyle(wrapper, 'align-self', 'anchor-center');
63
+ this.renderer.setStyle(wrapper, 'justify-self', 'anchor-center');
64
+
65
+ const itemEl = this.el.nativeElement;
66
+ const parent = this.renderer.parentNode(itemEl);
67
+ this.renderer.removeChild(parent, itemEl);
68
+ this.renderer.appendChild(wrapper, itemEl);
69
+
70
+ const hostParent = this.renderer.parentNode(this.zenPin.hostEl);
71
+ this.renderer.appendChild(hostParent, wrapper);
72
+
73
+ this.wrapper = wrapper;
74
+ }
75
+
76
+ private updatePosition(): void {
77
+ if (!this.wrapper) return;
78
+
79
+ this.renderer.setStyle(this.wrapper, 'position-area', this.zenPinPosition());
80
+
81
+ if (this.zenPinFlush()) {
82
+ this.renderer.removeStyle(this.wrapper, 'translate');
83
+ } else {
84
+ this.renderer.setStyle(this.wrapper, 'translate', this.computeCenterOffset(this.zenPinPosition(), 50));
85
+ }
86
+
87
+ const o = this.zenPinOffset();
88
+ if (o !== '0' && o !== 0) {
89
+ this.renderer.setStyle(this.wrapper, 'margin', typeof o === 'number' ? `${o}px` : o);
90
+ } else {
91
+ this.renderer.removeStyle(this.wrapper, 'margin');
92
+ }
93
+ }
94
+
95
+ private cleanup(): void {
96
+ if (!this.wrapper) return;
97
+ const parent = this.renderer.parentNode(this.wrapper);
98
+ if (parent) {
99
+ this.renderer.removeChild(parent, this.wrapper);
100
+ }
101
+ this.wrapper = null;
102
+ }
103
+
104
+ private computeCenterOffset(position: PinPosition, pct: number): string {
105
+ const parts = position.split(' ') as [string] | [string, string];
106
+ const verticals = new Set(['top', 'bottom', 'center']);
107
+ const [v, h] = parts.length === 2 ? parts : verticals.has(parts[0]) ? [parts[0], 'center'] : ['center', parts[0]];
108
+
109
+ const hFactor = h === 'right' ? -1 : h === 'left' ? 1 : 0;
110
+ const vFactor = v === 'top' ? 1 : v === 'bottom' ? -1 : 0;
111
+ const x = hFactor * pct;
112
+ const y = vFactor * pct;
113
+ return `${x ? x + '%' : '0'} ${y ? y + '%' : '0'}`;
114
+ }
115
+ }
@@ -0,0 +1,153 @@
1
+ import { Component, provideZonelessChangeDetection, signal } from '@angular/core';
2
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { ZenButton } from '../button';
6
+ import { ZenPin } from './pin';
7
+ import type { PinPosition } from './pin.types';
8
+ import { ZenPinItem } from './pin-item';
9
+
10
+ @Component({
11
+ template: `
12
+ <button #trigger id="trigger" type="button" zen-button zenPin>
13
+ Trigger
14
+ <span zenPinItem [zenPinFlush]="flushed()" [zenPinOffset]="offset()" [zenPinPosition]="position()">
15
+ Pinned content
16
+ </span>
17
+ </button>
18
+ `,
19
+ imports: [ZenPin, ZenButton, ZenPinItem],
20
+ })
21
+ class HostComponent {
22
+ readonly position = signal<PinPosition>('top right');
23
+ readonly offset = signal<string | number>('0');
24
+ readonly flushed = signal<boolean>(false);
25
+ }
26
+
27
+ @Component({
28
+ template: `
29
+ <button zen-button zenPin>
30
+ Trigger
31
+ <span zenPinItem>First pin</span>
32
+ <span zenPinItem>Second pin</span>
33
+ </button>
34
+ `,
35
+ imports: [ZenPin, ZenButton, ZenPinItem],
36
+ })
37
+ class MultiHostComponent {}
38
+
39
+ function getWrappers(fixture: ComponentFixture<unknown>): HTMLElement[] {
40
+ return Array.from(fixture.nativeElement.querySelectorAll('.zen-pin-wrapper') as NodeListOf<HTMLElement>);
41
+ }
42
+
43
+ function getWrapper(fixture: ComponentFixture<unknown>, index = 0): HTMLElement | null {
44
+ return getWrappers(fixture)[index] ?? null;
45
+ }
46
+
47
+ describe('ZenPin', () => {
48
+ beforeEach(async () => {
49
+ await TestBed.configureTestingModule({
50
+ imports: [HostComponent, MultiHostComponent],
51
+ providers: [provideZonelessChangeDetection()],
52
+ }).compileComponents();
53
+ });
54
+
55
+ function createFixture(): ComponentFixture<HostComponent> {
56
+ const fixture = TestBed.createComponent(HostComponent);
57
+ fixture.detectChanges();
58
+ return fixture;
59
+ }
60
+
61
+ it('creates a wrapper element with class zen-pin-wrapper', () => {
62
+ const fixture = createFixture();
63
+ expect(getWrapper(fixture)).toBeTruthy();
64
+ });
65
+
66
+ it('moves element content into wrapper', () => {
67
+ const fixture = createFixture();
68
+ expect(getWrapper(fixture)!.textContent).toContain('Pinned content');
69
+ });
70
+
71
+ it('sets anchor-name on host and position-anchor on wrapper', () => {
72
+ const fixture = createFixture();
73
+ const trigger = fixture.nativeElement.querySelector('#trigger') as HTMLElement;
74
+ const wrapper = getWrapper(fixture)!;
75
+
76
+ expect(trigger.style.getPropertyValue('anchor-name')).toMatch(/^--zen-pin-/);
77
+ expect(wrapper.style.getPropertyValue('position-anchor')).toBe(trigger.style.getPropertyValue('anchor-name'));
78
+ });
79
+
80
+ it('sets align-self and justify-self to anchor-center', () => {
81
+ const fixture = createFixture();
82
+ const wrapper = getWrapper(fixture)!;
83
+
84
+ expect(wrapper.style.getPropertyValue('align-self')).toBe('anchor-center');
85
+ expect(wrapper.style.getPropertyValue('justify-self')).toBe('anchor-center');
86
+ });
87
+
88
+ it('sets position-area from zenPinPosition input', () => {
89
+ const fixture = createFixture();
90
+ const wrapper = getWrapper(fixture)!;
91
+
92
+ expect(wrapper.style.getPropertyValue('position-area')).toBe('top right');
93
+ });
94
+
95
+ it('updates position-area when position changes', () => {
96
+ const fixture = createFixture();
97
+ fixture.componentInstance.position.set('bottom left');
98
+ fixture.detectChanges();
99
+
100
+ expect(getWrapper(fixture)!.style.getPropertyValue('position-area')).toBe('bottom left');
101
+ });
102
+
103
+ it('sets margin from zenPinOffset string input', () => {
104
+ const fixture = createFixture();
105
+ fixture.componentInstance.offset.set('10px');
106
+ fixture.detectChanges();
107
+
108
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('10px');
109
+ });
110
+
111
+ it('sets margin from zenPinOffset number input', () => {
112
+ const fixture = createFixture();
113
+ fixture.componentInstance.offset.set(15);
114
+ fixture.detectChanges();
115
+
116
+ expect(getWrapper(fixture)!.style.getPropertyValue('margin')).toBe('15px');
117
+ });
118
+
119
+ it.each([
120
+ ['top right', '-50% 50%'],
121
+ ['top left', '50% 50%'],
122
+ ['bottom right', '-50% -50%'],
123
+ ] as const)('computes center-directed translate for position %s when not flush', (position, expected) => {
124
+ const fixture = createFixture();
125
+ fixture.componentInstance.position.set(position);
126
+ fixture.detectChanges();
127
+
128
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe(expected);
129
+ });
130
+
131
+ it('removes translate when flush is true', () => {
132
+ const fixture = createFixture();
133
+ fixture.componentInstance.flushed.set(true);
134
+ fixture.detectChanges();
135
+
136
+ expect(getWrapper(fixture)!.style.getPropertyValue('translate')).toBe('');
137
+ });
138
+
139
+ it('creates a wrapper per zen-pin-item', () => {
140
+ const fixture = TestBed.createComponent(MultiHostComponent);
141
+ fixture.detectChanges();
142
+
143
+ expect(getWrappers(fixture)).toHaveLength(2);
144
+ });
145
+
146
+ it('removes wrapper on host destroy', () => {
147
+ const fixture = createFixture();
148
+ expect(getWrapper(fixture)).toBeTruthy();
149
+
150
+ fixture.destroy();
151
+ expect(getWrapper(fixture)).toBeNull();
152
+ });
153
+ });