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

atinc / ngx-tethys / 5682dbf5-1dfc-42ac-a7a6-192e525abbc5

pending completion
5682dbf5-1dfc-42ac-a7a6-192e525abbc5

Pull #2753

circleci

minlovehua
test(layout): improve sidebar test coverage
Pull Request #2753: feat(layout): merge thyHasBorderLeft and thyHasBorderRight into thyDivided, implement the divider on the left or right according to thyDirection INFR-8536

5096 of 6307 branches covered (80.8%)

Branch coverage included in aggregate %.

18 of 18 new or added lines in 1 file covered. (100.0%)

12895 of 13652 relevant lines covered (94.46%)

974.41 hits per line

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

92.56
/src/layout/sidebar.component.ts
1
import { NgClass, NgIf, NgStyle, NgTemplateOutlet } from '@angular/common';
2
import {
3
    Component,
4
    ElementRef,
5
    EventEmitter,
6
    Host,
7
    HostBinding,
8
    HostListener,
9
    Input,
10
    OnDestroy,
11
    OnInit,
12
    Optional,
1✔
13
    Output,
1✔
14
    TemplateRef
15
} from '@angular/core';
16
import { ThyHotkeyDispatcher } from '@tethys/cdk/hotkey';
17
import { isMacPlatform } from '@tethys/cdk/is';
18
import { InputBoolean, InputNumber } from 'ngx-tethys/core';
19
import { ThyIconComponent } from 'ngx-tethys/icon';
1✔
20
import { ThyResizableDirective, ThyResizeEvent, ThyResizeHandleComponent } from 'ngx-tethys/resizable';
21
import { ThyTooltipDirective } from 'ngx-tethys/tooltip';
122✔
22
import { coerceBooleanProperty } from 'ngx-tethys/util';
6✔
23
import { Subscription } from 'rxjs';
24
import { ThyLayoutComponent } from './layout.component';
25

116✔
26
const LG_WIDTH = 300;
27
const SIDEBAR_DEFAULT_WIDTH = 240;
28

29
export type ThySidebarTheme = 'white' | 'light' | 'dark';
2✔
30

31
export type ThySidebarDirection = 'left' | 'right';
32

2✔
33
/**
34
 * 布局侧边栏组件
35
 * @name thy-sidebar
29✔
36
 * @order 20
1✔
37
 */
38
@Component({
29✔
39
    selector: 'thy-sidebar',
40
    preserveWhitespaces: false,
41
    template: `
82✔
42
        <ng-content></ng-content>
82✔
43
        <div
44
            thyResizable
45
            class="sidebar-drag"
27✔
46
            *ngIf="thyDraggable"
47
            thyBounds="window"
48
            [thyMaxWidth]="thyDragMaxWidth"
27✔
49
            [thyMinWidth]="thyCollapsedWidth"
50
            (thyResize)="resizeHandler($event)"
51
            (thyResizeStart)="resizeStart()"
27✔
52
            (thyResizeEnd)="resizeEnd()"
53
            [style.display]="!isResizable ? 'contents' : null">
54
            <thy-resize-handle
35✔
55
                *ngIf="!thyCollapsed"
35✔
56
                [thyDirection]="thyDirection === 'right' ? 'left' : 'right'"
9✔
57
                class="sidebar-resize-handle"
58
                thyLine="true"
59
                (mouseenter)="toggleResizable($event, 'enter')"
26✔
60
                (mouseleave)="toggleResizable($event, 'leave')"
61
                (dblclick)="restoreToDefaultWidth()">
62
            </thy-resize-handle>
63
        </div>
375✔
64
        <div *ngIf="thyCollapsible" class="sidebar-collapse-line"></div>
65
        <div
66
            *ngIf="thyCollapsible && thyTrigger !== null"
37✔
67
            class="sidebar-collapse"
68
            [ngClass]="{ 'collapse-visible': collapseVisible, 'collapse-hidden': collapseHidden }"
69
            (click)="toggleCollapse($event)"
230✔
70
            [thyTooltip]="!thyTrigger && collapseTip">
71
            <ng-template [ngTemplateOutlet]="thyTrigger || defaultTrigger"></ng-template>
72
            <ng-template #defaultTrigger>
122✔
73
                <thy-icon class="sidebar-collapse-icon" [thyIconName]="this.thyCollapsed ? 'indent' : 'outdent'"></thy-icon>
74
            </ng-template>
75
        </div>
122✔
76
    `,
77
    host: {
78
        class: 'thy-layout-sidebar',
28✔
79
        '[class.thy-layout-sidebar-right]': 'thyDirection === "right"',
28✔
80
        '[class.thy-layout-sidebar--clear-border-right]': 'thyDirection === "left" && !isDivided',
28✔
81
        '[class.thy-layout-sidebar--clear-border-left]': 'thyDirection === "right" && !isDivided',
28✔
82
        '[class.sidebar-theme-light]': 'thyTheme === "light"',
28✔
83
        '[class.sidebar-theme-dark]': 'thyTheme === "dark"',
28✔
84
        '[class.thy-layout-sidebar-isolated]': 'sidebarIsolated'
28✔
85
    },
28✔
86
    standalone: true,
28✔
87
    imports: [
28✔
88
        NgTemplateOutlet,
28✔
89
        NgIf,
28✔
90
        ThyResizeHandleComponent,
28✔
91
        ThyResizableDirective,
92
        ThyIconComponent,
93
        ThyTooltipDirective,
28!
94
        NgClass,
28✔
95
        NgStyle
96
    ]
28✔
97
})
2✔
98
export class ThySidebarComponent implements OnInit, OnDestroy {
99
    thyLayoutSidebarWidth: number;
28✔
100

101
    isDivided = true;
102

9✔
103
    sidebarIsolated = false;
2✔
104

105
    @HostBinding('style.width.px') get sidebarWidth() {
106
        if (this.thyCollapsible && this.thyCollapsed) {
107
            return this.thyCollapsedWidth;
35✔
108
        } else {
35!
109
            return this.thyLayoutSidebarWidth;
110
        }
111
    }
4!
112

×
113
    @HostListener('mouseenter', ['$event'])
114
    mouseenter($event: MouseEvent) {
4!
115
        this.resizeHandleHover($event, 'enter');
×
116
    }
117

4✔
118
    @HostListener('mouseleave', ['$event'])
2✔
119
    mouseleave($event: MouseEvent) {
2✔
120
        this.resizeHandleHover($event, 'leave');
2✔
121
    }
2✔
122

2✔
123
    /**
2✔
124
     * 宽度,默认是 240px,传入 `lg` 大小时宽度是300px
125
     * @default 240px
2✔
126
     */
2✔
127
    @Input('thyWidth')
128
    set thyWidth(value: any) {
129
        if (value === 'lg') {
3✔
130
            value = LG_WIDTH;
3✔
131
        }
3✔
132
        this.thyLayoutSidebarWidth = value || SIDEBAR_DEFAULT_WIDTH;
133
    }
134

2✔
135
    /**
2✔
136
     * sidebar 位置,默认在左侧
137
     */
138
    @Input() thyDirection: ThySidebarDirection = 'left';
4✔
139

140
    /**
141
     * sidebar 是否有分割线。当`thyDirection`值为`left`时,控制右侧是否有分割线;当`thyDirection`值为`right`时,控制左侧是否有分割线。
5✔
142
     * @default true
5✔
143
     */
5✔
144
    @Input('thyDivided')
145
    set thyDivided(value: string) {
146
        console.log('value: ', value);
2✔
147
        this.isDivided = coerceBooleanProperty(value);
148
    }
149

1!
150
    /**
×
151
     * 右侧是否有边框,已废弃,请使用 thyDivided
152
     * @deprecated please use thyDivided
1!
153
     * @default true
1✔
154
     */
155
    @Input('thyHasBorderRight')
156
    set thyHasBorderRight(value: string) {
28✔
157
        this.thyDivided = value;
158
    }
1✔
159

160
    /**
161
     * 左侧是否有边框,已废弃,请使用 thyDivided
162
     * @deprecated please use thyDivided
163
     * @default true
1✔
164
     */
165
    @Input('thyHasBorderLeft')
166
    set thyHasBorderLeft(value: string) {
167
        this.thyDivided = value;
168
    }
169

170
    /**
171
     * 是否和右侧 /左侧隔离,当为 true 时距右侧 /左侧会有 margin,同时边框会去掉
172
     * @default false
173
     */
174
    @Input('thyIsolated')
175
    set thyIsolated(value: string) {
176
        this.sidebarIsolated = coerceBooleanProperty(value);
177
    }
178

179
    /**
180
     * 宽度是否可以拖拽
181
     * @default false
182
     */
183
    @Input() @InputBoolean() thyDraggable: boolean = false;
184

185
    /**
186
     * 拖拽的最大宽度
187
     */
1✔
188
    @Input() @InputNumber() thyDragMaxWidth: number;
189

190
    /**
191
     * 展示收起的触发器自定义模板,默认显示展开收起的圆形图标,设置为 null 表示不展示触发元素,手动控制展开收起状态
1✔
192
     * @type null | undefined | TemplateRef<any>
193
     * @default undefined
194
     */
195
    @Input() thyTrigger: null | undefined | TemplateRef<any> = undefined;
1✔
196

197
    /**
198
     * 收起状态改变后的事件
199
     */
200
    @Output()
1✔
201
    thyCollapsedChange = new EventEmitter<boolean>();
202

203
    /**
204
     * 拖拽宽度的修改事件
205
     */
1✔
206
    @Output()
207
    thyDragWidthChange = new EventEmitter<number>();
208

209
    /**
1✔
210
     * 开启收起/展开功能
211
     * @default false
212
     */
213
    @Input() @InputBoolean() set thyCollapsible(collapsible: boolean) {
214
        this.collapsible = collapsible;
215
        if (this.collapsible) {
216
            this.subscribeHotkeyEvent();
217
        } else {
218
            this.hotkeySubscription?.unsubscribe();
219
        }
220
    }
221

222
    get thyCollapsible() {
223
        return this.collapsible;
224
    }
225

226
    /**
227
     * 是否是收起
228
     * @default false
229
     */
230
    @Input() @InputBoolean() set thyCollapsed(value: boolean) {
231
        this.isCollapsed = value;
232
    }
233

234
    get thyCollapsed() {
235
        return this.isCollapsed;
236
    }
237

238
    /**
239
     * 收起后的宽度
240
     */
241
    @Input() @InputNumber() thyCollapsedWidth = 20;
242

243
    /**
244
     * 主题
245
     * @type white | light | dark
246
     * @default white
247
     */
248
    @Input() thyTheme: ThySidebarTheme;
249

250
    /**
251
     * 默认宽度,双击后可恢复到此宽度,默认是 240px,传入 lg 大小时宽度是300px
252
     */
253
    @Input() thyDefaultWidth: string | number;
254

255
    @HostBinding('class.sidebar-collapse-show')
256
    get collapseVisibility() {
257
        return this.thyCollapsed;
258
    }
259

260
    @HostBinding('class.remove-transition')
261
    get removeTransition() {
262
        return this.isRemoveTransition;
263
    }
264

265
    collapseTip: string;
266

267
    collapsible: boolean;
268

269
    isCollapsed = false;
270

271
    originWidth: number = SIDEBAR_DEFAULT_WIDTH;
272

273
    collapseVisible: boolean;
274

275
    collapseHidden: boolean;
276

277
    isRemoveTransition: boolean;
278

279
    isResizable: boolean;
280

281
    private hotkeySubscription: Subscription;
282

283
    constructor(
284
        @Optional() @Host() private thyLayoutComponent: ThyLayoutComponent,
285
        public elementRef: ElementRef,
286
        private hotkeyDispatcher: ThyHotkeyDispatcher
287
    ) {}
288

289
    ngOnInit() {
290
        if (this.thyLayoutComponent) {
291
            this.thyLayoutComponent.hasSidebar = true;
292
        }
293
        if (this.thyDirection === 'right') {
294
            this.thyLayoutComponent.isSidebarRight = true;
295
        }
296
        this.updateCollapseTip();
297
    }
298

299
    private subscribeHotkeyEvent() {
300
        this.hotkeySubscription = this.hotkeyDispatcher.keydown(['Control+/', 'Meta+/']).subscribe(() => {
301
            this.toggleCollapse();
302
        });
303
    }
304

305
    private updateCollapseTip() {
306
        this.collapseTip = this.thyCollapsed ? '展开' : '收起';
307
        this.collapseTip = this.collapseTip + (isMacPlatform() ? `(⌘ + /)` : `(Ctrl + /)`);
308
    }
309

310
    resizeHandler({ width }: ThyResizeEvent) {
311
        if (width === this.thyLayoutSidebarWidth) {
312
            return;
313
        }
314
        if (this.thyCollapsible && width < this.thyCollapsedWidth) {
315
            return;
316
        }
317
        if (this.thyCollapsible && width === this.thyCollapsedWidth) {
318
            this.thyCollapsed = true;
319
            setTimeout(() => this.updateCollapseTip(), 200);
320
            this.thyCollapsedChange.emit(this.isCollapsed);
321
            this.thyLayoutSidebarWidth = this.originWidth;
322
            this.collapseVisible = false;
323
            return;
324
        }
325
        this.thyLayoutSidebarWidth = width;
326
        this.thyDragWidthChange.emit(width);
327
    }
328

329
    resizeStart() {
330
        this.originWidth = this.thyLayoutSidebarWidth;
331
        this.collapseHidden = true;
332
        this.isRemoveTransition = true;
333
    }
334

335
    resizeEnd() {
336
        this.collapseHidden = false;
337
        this.isRemoveTransition = false;
338
    }
339

340
    resizeHandleHover(event: MouseEvent, type: 'enter' | 'leave') {
341
        this.collapseVisible = type === 'enter' && !this.thyCollapsed ? true : false;
342
    }
343

344
    toggleCollapse(event?: MouseEvent) {
345
        this.thyCollapsed = !this.thyCollapsed;
346
        setTimeout(() => this.updateCollapseTip(), 200);
347
        this.thyCollapsedChange.emit(this.isCollapsed);
348
    }
349

350
    public toggleResizable(event: MouseEvent, type: 'enter' | 'leave') {
351
        this.isResizable = type === 'enter' ? true : false;
352
    }
353

354
    restoreToDefaultWidth() {
355
        if (this.thyDefaultWidth === 'lg') {
356
            this.thyDefaultWidth = LG_WIDTH;
357
        }
358
        this.thyLayoutSidebarWidth = (this.thyDefaultWidth as number) || SIDEBAR_DEFAULT_WIDTH;
359
        this.thyDragWidthChange.emit(this.thyLayoutSidebarWidth);
360
    }
361

362
    ngOnDestroy(): void {
363
        this.hotkeySubscription?.unsubscribe();
364
    }
365
}
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