|
4 | 4 | */ |
5 | 5 |
|
6 | 6 | import { NgTemplateOutlet } from '@angular/common'; |
7 | | -import { ChangeDetectionStrategy, Component, Input, TemplateRef, ViewEncapsulation } from '@angular/core'; |
| 7 | +import { |
| 8 | + ChangeDetectionStrategy, |
| 9 | + Component, |
| 10 | + computed, |
| 11 | + effect, |
| 12 | + ElementRef, |
| 13 | + inject, |
| 14 | + input, |
| 15 | + signal, |
| 16 | + TemplateRef, |
| 17 | + untracked, |
| 18 | + ViewEncapsulation |
| 19 | +} from '@angular/core'; |
8 | 20 |
|
9 | | -import { tabSwitchMotion } from 'ng-zorro-antd/core/animation'; |
| 21 | +import { isAnimationEnabled } from 'ng-zorro-antd/core/animation'; |
| 22 | +import { requestAnimationFrame } from 'ng-zorro-antd/core/polyfill'; |
| 23 | +import { generateClassName } from 'ng-zorro-antd/core/util'; |
| 24 | + |
| 25 | +type AnimationState = 'enter-start' | 'enter-active' | 'leave-start' | 'leave-active' | 'void' | 'hidden'; |
| 26 | + |
| 27 | +const CLASS_NAME = 'ant-tabs-tabpane'; |
| 28 | +const ANIMATION_PREFIX = 'ant-tabs-switch'; |
| 29 | +const ANIMATION_CLASS_MAP: Record<AnimationState, string[]> = { |
| 30 | + 'enter-start': [generateClassName(ANIMATION_PREFIX, 'enter'), generateClassName(ANIMATION_PREFIX, 'enter-start')], |
| 31 | + 'enter-active': [generateClassName(ANIMATION_PREFIX, 'enter'), generateClassName(ANIMATION_PREFIX, 'enter-active')], |
| 32 | + 'leave-start': [generateClassName(ANIMATION_PREFIX, 'leave'), generateClassName(ANIMATION_PREFIX, 'leave-start')], |
| 33 | + 'leave-active': [generateClassName(ANIMATION_PREFIX, 'leave'), generateClassName(ANIMATION_PREFIX, 'leave-active')], |
| 34 | + // If animation is enabled, we should hide the tabpane after the leave animation is done |
| 35 | + hidden: [generateClassName(CLASS_NAME, 'hidden')], |
| 36 | + void: [] |
| 37 | +}; |
10 | 38 |
|
11 | 39 | @Component({ |
12 | 40 | selector: '[nz-tab-body]', |
13 | 41 | exportAs: 'nzTabBody', |
14 | | - encapsulation: ViewEncapsulation.None, |
15 | | - changeDetection: ChangeDetectionStrategy.OnPush, |
16 | | - template: `<ng-template [ngTemplateOutlet]="content"></ng-template>`, |
| 42 | + imports: [NgTemplateOutlet], |
| 43 | + template: `<ng-template [ngTemplateOutlet]="content()"></ng-template>`, |
17 | 44 | host: { |
18 | | - class: 'ant-tabs-tabpane', |
19 | | - '[class.ant-tabs-tabpane-active]': 'active', |
20 | | - '[class.ant-tabs-tabpane-hidden]': 'animated ? null : !active', |
21 | | - '[attr.tabindex]': 'active ? 0 : -1', |
22 | | - '[attr.aria-hidden]': '!active', |
23 | | - '[style.overflow-y]': 'animated ? active ? null : "none" : null', |
24 | | - '[@tabSwitchMotion]': `active ? 'enter' : 'leave'`, |
25 | | - '[@.disabled]': `!animated` |
| 45 | + '[class]': 'class()', |
| 46 | + '[class.ant-tabs-tabpane-active]': 'active()', |
| 47 | + '[attr.tabindex]': 'active() ? 0 : -1', |
| 48 | + '[attr.aria-hidden]': '!active()', |
| 49 | + '(transitionend)': '_onTransitionEnd($event)' |
26 | 50 | }, |
27 | | - imports: [NgTemplateOutlet], |
28 | | - animations: [tabSwitchMotion] |
| 51 | + encapsulation: ViewEncapsulation.None, |
| 52 | + changeDetection: ChangeDetectionStrategy.OnPush |
29 | 53 | }) |
30 | 54 | export class NzTabBodyComponent { |
31 | | - @Input() content: TemplateRef<void> | null = null; |
32 | | - @Input() active = false; |
33 | | - @Input() animated = true; |
| 55 | + private readonly elementRef = inject(ElementRef); |
| 56 | + |
| 57 | + readonly content = input<TemplateRef<void> | null>(null); |
| 58 | + readonly active = input(false); |
| 59 | + readonly animated = input(true); |
| 60 | + |
| 61 | + protected readonly _animationState = signal<AnimationState>('void'); |
| 62 | + protected readonly _animationEnabled = isAnimationEnabled(() => this.animated()); |
| 63 | + |
| 64 | + protected readonly class = computed(() => { |
| 65 | + const cls: string[] = [CLASS_NAME]; |
| 66 | + if (this._animationEnabled()) { |
| 67 | + cls.push(...ANIMATION_CLASS_MAP[this._animationState()]); |
| 68 | + } else if (!this.active()) { |
| 69 | + cls.push(generateClassName(CLASS_NAME, 'hidden')); |
| 70 | + } |
| 71 | + return cls; |
| 72 | + }); |
| 73 | + |
| 74 | + constructor() { |
| 75 | + effect(() => { |
| 76 | + if (!this._animationEnabled()) { |
| 77 | + return; |
| 78 | + } |
| 79 | + |
| 80 | + if (!this.active()) { |
| 81 | + untracked(() => this._animationState.set('leave-start')); |
| 82 | + requestAnimationFrame(() => { |
| 83 | + this._animationState.set('leave-active'); |
| 84 | + }); |
| 85 | + } else { |
| 86 | + untracked(() => this._animationState.set('enter-start')); |
| 87 | + requestAnimationFrame(() => { |
| 88 | + this._animationState.set('enter-active'); |
| 89 | + }); |
| 90 | + } |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + protected _onTransitionEnd(event: TransitionEvent): void { |
| 95 | + // avoid event bubbling from child elements |
| 96 | + if (event.target !== this.elementRef.nativeElement) { |
| 97 | + return; |
| 98 | + } |
| 99 | + |
| 100 | + const currentState = this._animationState(); |
| 101 | + if (currentState === 'enter-active') { |
| 102 | + this._animationState.set('void'); |
| 103 | + } else if (currentState === 'leave-active') { |
| 104 | + this._animationState.set('hidden'); |
| 105 | + } |
| 106 | + } |
34 | 107 | } |
0 commit comments