• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atinc / ngx-tethys / 68ef226c-f83e-44c1-b8ed-e420a83c5d84

28 May 2025 10:31AM UTC coverage: 10.352% (-80.0%) from 90.316%
68ef226c-f83e-44c1-b8ed-e420a83c5d84

Pull #3460

circleci

pubuzhixing8
chore: xxx
Pull Request #3460: refactor(icon): migrate signal input #TINFR-1476

132 of 6823 branches covered (1.93%)

Branch coverage included in aggregate %.

10 of 14 new or added lines in 1 file covered. (71.43%)

11648 existing lines in 344 files now uncovered.

2078 of 14525 relevant lines covered (14.31%)

6.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.49
/src/dialog/dialog-container.component.ts
1
import { reqAnimFrame, ThyAbstractOverlayContainer, ThyClickPositioner, ThyPortalOutlet } from 'ngx-tethys/core';
2
import { Observable } from 'rxjs';
3
import { filter } from 'rxjs/operators';
4

5
import { AnimationEvent } from '@angular/animations';
6
import { FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y';
7
import { PortalModule } from '@angular/cdk/portal';
8
import { DOCUMENT } from '@angular/common';
9
import {
10
    ChangeDetectionStrategy,
11
    ChangeDetectorRef,
12
    Component,
13
    ElementRef,
14
    EventEmitter,
1✔
15
    HostBinding,
UNCOV
16
    NgZone,
×
UNCOV
17
    OnDestroy,
×
18
    Renderer2,
UNCOV
19
    ViewChild,
×
20
    inject
21
} from '@angular/core';
UNCOV
22

×
23
import { thyDialogAnimations } from './dialog-animations';
24
import { ThyDialogConfig } from './dialog.config';
25
import { dialogAbstractOverlayOptions } from './dialog.options';
26

27
/**
28
 * @private
29
 */
UNCOV
30
@Component({
×
31
    selector: 'thy-dialog-container',
32
    template: ` <ng-template thyPortalOutlet></ng-template> `,
33
    // Using OnPush for dialogs caused some sync issues, e.g. custom ngModel can't to detect changes
34
    // Disabled until we can track them down.
35
    changeDetection: ChangeDetectionStrategy.Default,
UNCOV
36
    animations: [thyDialogAnimations.dialogContainer],
×
UNCOV
37
    host: {
×
UNCOV
38
        class: 'thy-dialog-container',
×
39
        tabindex: '-1',
40
        'aria-modal': 'true',
41
        '[attr.id]': 'id',
42
        '[attr.role]': 'config.role',
UNCOV
43
        '[attr.aria-labelledby]': 'config.ariaLabel ? null : ariaLabelledBy',
×
UNCOV
44
        '[attr.aria-label]': 'config.ariaLabel',
×
45
        '[attr.aria-describedby]': 'config.ariaDescribedBy || null',
46
        '[@dialogContainer]': 'animationState',
UNCOV
47
        '(@dialogContainer.start)': 'onAnimationStart($event)',
×
48
        '(@dialogContainer.done)': 'onAnimationDone($event)'
49
    },
50
    imports: [PortalModule, ThyPortalOutlet]
51
})
52
export class ThyDialogContainer extends ThyAbstractOverlayContainer implements OnDestroy {
UNCOV
53
    private elementRef = inject(ElementRef);
×
UNCOV
54
    private document = inject(DOCUMENT);
×
55
    config = inject(ThyDialogConfig);
56
    private clickPositioner = inject(ThyClickPositioner);
57
    private focusTrapFactory = inject(FocusTrapFactory);
58
    private ngZone = inject(NgZone);
UNCOV
59
    private renderer = inject(Renderer2);
×
60

UNCOV
61
    animationOpeningDone: Observable<AnimationEvent>;
×
UNCOV
62
    animationClosingDone: Observable<AnimationEvent>;
×
63

UNCOV
64
    @ViewChild(ThyPortalOutlet, { static: true })
×
UNCOV
65
    portalOutlet: ThyPortalOutlet;
×
66

67
    @HostBinding(`attr.id`)
68
    id: string;
UNCOV
69

×
UNCOV
70
    /** State of the dialog animation. */
×
UNCOV
71
    animationState: 'void' | 'enter' | 'exit' = 'void';
×
UNCOV
72

×
UNCOV
73
    /** Emits when an animation state changes. */
×
74
    animationStateChanged = new EventEmitter<AnimationEvent>();
75

UNCOV
76
    /** ID of the element that should be considered as the dialog's label. */
×
UNCOV
77
    ariaLabelledBy: string | null = null;
×
78

79
    /** Element that was focused before the dialog was opened. Save this to restore upon close. */
80
    private elementFocusedBeforeDialogWasOpened: HTMLElement | null = null;
UNCOV
81

×
UNCOV
82
    /** The class that traps and manages focus within the dialog. */
×
UNCOV
83
    private focusTrap: FocusTrap;
×
UNCOV
84

×
UNCOV
85
    private savePreviouslyFocusedElement() {
×
UNCOV
86
        if (this.document) {
×
UNCOV
87
            this.elementFocusedBeforeDialogWasOpened = this.document.activeElement as HTMLElement;
×
UNCOV
88

×
UNCOV
89
            // Note that there is no focus method when rendering on the server.
×
90
            if (this.elementRef.nativeElement.focus) {
UNCOV
91
                // Note: this is being run outside of the Angular zone because `element.focus()` doesn't require
×
92
                // running change detection.
UNCOV
93
                this.ngZone.runOutsideAngular(() =>
×
94
                    // Move focus onto the dialog immediately in order to prevent the user from accidentally
UNCOV
95
                    // opening multiple dialogs at the same time. Needs to be async, because the element
×
96
                    // may not be focusable immediately.
UNCOV
97

×
UNCOV
98
                    // Note: `element.focus()` causes re-layout and this may lead to frame drop on slower devices.
×
UNCOV
99
                    // https://gist.github.com/paulirish/5d52fb081b3570c81e3a#setting-focus
×
100
                    // `setTimeout` is a macrotask and macrotasks are executed within the current rendering frame.
UNCOV
101
                    // Animation tasks are executed within the next rendering frame.
×
UNCOV
102
                    reqAnimFrame(() => this.elementRef.nativeElement.focus())
×
103
                );
104
            }
UNCOV
105
        }
×
106
    }
UNCOV
107

×
108
    /** Moves the focus inside the focus trap. */
109
    private trapFocus() {
UNCOV
110
        const element = this.elementRef.nativeElement;
×
111

112
        if (!this.focusTrap) {
113
            this.focusTrap = this.focusTrapFactory.create(element);
UNCOV
114
        }
×
UNCOV
115

×
116
        // If we were to attempt to focus immediately, then the content of the dialog would not yet be
117
        // ready in instances where change detection has to run first. To deal with this, we simply
118
        // wait for the microtask queue to be empty.
UNCOV
119
        if (this.config.autoFocus) {
×
UNCOV
120
            this.focusTrap.focusInitialElementWhenReady();
×
121
        } else {
UNCOV
122
            const activeElement = this.document.activeElement;
×
UNCOV
123

×
124
            // Otherwise ensure that focus is on the dialog container. It's possible that a different
UNCOV
125
            // component tried to move focus while the open animation was running. See:
×
126
            // https://github.com/angular/components/issues/16215. Note that we only want to do this
127
            // if the focus isn't inside the dialog already, because it's possible that the consumer
128
            // turned off `autoFocus` in order to move focus themselves.
UNCOV
129
            if (activeElement !== element && !element.contains(activeElement)) {
×
130
                element.focus();
131
            }
UNCOV
132
        }
×
133
    }
134

1✔
135
    private restoreFocus() {
1✔
136
        const toFocus = this.elementFocusedBeforeDialogWasOpened;
137

138
        // We need the extra check, because IE can set the `activeElement` to null in some cases.
139
        if (this.config.restoreFocus && toFocus && typeof toFocus.focus === 'function') {
140
            toFocus.focus(this.config.restoreFocusOptions);
1✔
141
        }
142

143
        if (this.focusTrap) {
144
            this.focusTrap.destroy();
145
        }
146
    }
147

148
    private setTransformOrigin() {
149
        this.clickPositioner.runTaskUseLastPosition(lastPosition => {
150
            if (lastPosition) {
151
                const containerElement: HTMLElement = this.elementRef.nativeElement;
152
                const transformOrigin = `${lastPosition.x - containerElement.offsetLeft}px ${
153
                    lastPosition.y - containerElement.offsetTop
154
                }px 0px`;
155
                containerElement.style.transformOrigin = transformOrigin;
156
                // 手动修改动画状态为从 void 到 enter, 开启动画
157
            }
158
            this.animationState = 'enter';
159
            this.changeDetectorRef.markForCheck();
160
        });
161
    }
162

163
    constructor() {
164
        const changeDetectorRef = inject(ChangeDetectorRef);
165

166
        super(dialogAbstractOverlayOptions, changeDetectorRef);
167
        this.animationOpeningDone = this.animationStateChanged.pipe(
168
            filter((event: AnimationEvent) => {
169
                return event.phaseName === 'done' && event.toState === 'void';
170
            })
171
        );
172
        this.animationClosingDone = this.animationStateChanged.pipe(
173
            filter((event: AnimationEvent) => {
174
                return event.phaseName === 'done' && event.toState === 'exit';
175
            })
176
        );
177
        /* Prohibit operations on elements inside the container during animation execution */
178
        this.animationStateChanged
179
            .pipe(
180
                filter((event: AnimationEvent) => {
181
                    return event.phaseName === 'start' && event.toState === 'exit';
182
                })
183
            )
184
            .subscribe(() => {
185
                this.renderer.setStyle(this.elementRef.nativeElement, 'pointer-events', 'none');
186
            });
187
    }
188

189
    beforeAttachPortal(): void {
190
        this.setTransformOrigin();
191
        this.savePreviouslyFocusedElement();
192
    }
193

194
    /** Callback, invoked whenever an animation on the host completes. */
195
    onAnimationDone(event: AnimationEvent) {
196
        if (event.toState === 'void') {
197
            this.trapFocus();
198
        } else if (event.toState === 'exit') {
199
            this.restoreFocus();
200
        }
201
        this.animationStateChanged.emit(event);
202
    }
203

204
    /** Callback, invoked when an animation on the host starts. */
205
    onAnimationStart(event: AnimationEvent) {
206
        this.animationStateChanged.emit(event);
207
    }
208

209
    ngOnDestroy() {
210
        super.destroy();
211
    }
212
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc