Skip to content

Commit 1b9714b

Browse files
authored
feat(module:carousel): support nzArrows (#9355)
1 parent cbecebf commit 1b9714b

8 files changed

Lines changed: 286 additions & 40 deletions

File tree

components/carousel/carousel.component.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'carousel';
6666
[class.slick-vertical]="nzDotPosition === 'left' || nzDotPosition === 'right'"
6767
[dir]="'ltr'"
6868
>
69+
@if (nzArrows) {
70+
<button
71+
type="button"
72+
aria-label="prev"
73+
class="slick-prev slick-arrow"
74+
[class.slick-disabled]="this.activeIndex === 0"
75+
(click)="pre()"
76+
></button>
77+
}
6978
<div
7079
#slickList
7180
class="slick-list"
@@ -78,6 +87,15 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'carousel';
7887
<ng-content></ng-content>
7988
</div>
8089
</div>
90+
@if (nzArrows) {
91+
<button
92+
type="button"
93+
aria-label="next"
94+
class="slick-next slick-arrow"
95+
[class.slick-disabled]="this.activeIndex === this.carouselContents.length - 1"
96+
(click)="next()"
97+
></button>
98+
}
8199
<!-- Render dots. -->
82100
@if (nzDots) {
83101
<ul
@@ -136,6 +154,7 @@ export class NzCarouselComponent implements AfterContentInit, AfterViewInit, OnC
136154
@Input({ transform: numberAttribute }) @WithConfig() nzAutoPlaySpeed: number = 3000;
137155
@Input({ transform: numberAttribute }) nzTransitionSpeed = 500;
138156
@Input() @WithConfig() nzLoop: boolean = true;
157+
@Input({ transform: booleanAttribute }) nzArrows = false;
139158

140159
/**
141160
* this property is passed directly to an NzCarouselBaseStrategy

components/carousel/carousel.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,80 @@ describe('carousel custom strategies', () => {
423423
}));
424424
});
425425

426+
describe('carousel arrows', () => {
427+
let fixture: ComponentFixture<NzTestCarouselArrowsComponent>;
428+
let testComponent: NzTestCarouselArrowsComponent;
429+
let carouselWrapper: DebugElement;
430+
let carouselContents: DebugElement[];
431+
432+
beforeEach(() => {
433+
fixture = TestBed.createComponent(NzTestCarouselArrowsComponent);
434+
fixture.detectChanges();
435+
testComponent = fixture.debugElement.componentInstance;
436+
carouselWrapper = fixture.debugElement.query(By.directive(NzCarouselComponent));
437+
carouselContents = fixture.debugElement.queryAll(By.directive(NzCarouselContentDirective));
438+
});
439+
440+
it('should render arrows when nzArrows is true', () => {
441+
const prev = carouselWrapper.nativeElement.querySelector('.slick-prev');
442+
const next = carouselWrapper.nativeElement.querySelector('.slick-next');
443+
expect(prev).not.toBeNull();
444+
expect(next).not.toBeNull();
445+
});
446+
447+
it('should navigate via arrows', fakeAsync(() => {
448+
expect(carouselContents[0].nativeElement.classList).toContain('slick-active');
449+
carouselWrapper.nativeElement.querySelector('.slick-next').click();
450+
tickMilliseconds(fixture, 700);
451+
expect(carouselContents[1].nativeElement.classList).toContain('slick-active');
452+
carouselWrapper.nativeElement.querySelector('.slick-prev').click();
453+
tickMilliseconds(fixture, 700);
454+
expect(carouselContents[0].nativeElement.classList).toContain('slick-active');
455+
}));
456+
457+
it('should disable arrows at edges when loop is false', fakeAsync(() => {
458+
testComponent.loop = false;
459+
fixture.detectChanges();
460+
const prev = carouselWrapper.nativeElement.querySelector('.slick-prev');
461+
const next = carouselWrapper.nativeElement.querySelector('.slick-next');
462+
expect(prev.classList).toContain('slick-disabled');
463+
expect(next.classList).not.toContain('slick-disabled');
464+
465+
// Go to last slide
466+
for (let i = 0; i < 3; i++) {
467+
next.click();
468+
tickMilliseconds(fixture, 700);
469+
}
470+
expect(carouselContents[3].nativeElement.classList).toContain('slick-active');
471+
expect(next.classList).toContain('slick-disabled');
472+
473+
// Clicking next should not move beyond last
474+
next.click();
475+
tickMilliseconds(fixture, 700);
476+
expect(carouselContents[3].nativeElement.classList).toContain('slick-active');
477+
}));
478+
});
479+
480+
describe('carousel no swipe', () => {
481+
let fixture: ComponentFixture<NzTestCarouselNoSwipeComponent>;
482+
let testComponent: NzTestCarouselNoSwipeComponent;
483+
let carouselContents: DebugElement[];
484+
485+
beforeEach(() => {
486+
fixture = TestBed.createComponent(NzTestCarouselNoSwipeComponent);
487+
fixture.detectChanges();
488+
testComponent = fixture.debugElement.componentInstance;
489+
carouselContents = fixture.debugElement.queryAll(By.directive(NzCarouselContentDirective));
490+
});
491+
492+
it('should not change slide on swipe when nzEnableSwipe is false', fakeAsync(() => {
493+
expect(carouselContents[0].nativeElement.classList).toContain('slick-active');
494+
swipe(testComponent.nzCarouselComponent, 500);
495+
tickMilliseconds(fixture, 700);
496+
expect(carouselContents[0].nativeElement.classList).toContain('slick-active');
497+
}));
498+
});
499+
426500
function tickMilliseconds<T>(fixture: ComponentFixture<T>, seconds: number = 1): void {
427501
fixture.detectChanges();
428502
tick(seconds);
@@ -508,6 +582,43 @@ export class NzTestCarouselActiveIndexComponent {
508582
}
509583
}
510584

585+
@Component({
586+
selector: 'nz-test-carousel-arrows',
587+
imports: [NzCarouselModule],
588+
template: `
589+
<nz-carousel [nzArrows]="true" [nzLoop]="loop">
590+
@for (index of array; track index) {
591+
<div nz-carousel-content>
592+
<h3>{{ index }}</h3>
593+
</div>
594+
}
595+
</nz-carousel>
596+
`
597+
})
598+
export class NzTestCarouselArrowsComponent {
599+
@ViewChild(NzCarouselComponent, { static: true }) nzCarouselComponent!: NzCarouselComponent;
600+
array = [1, 2, 3, 4];
601+
loop = true;
602+
}
603+
604+
@Component({
605+
selector: 'nz-test-carousel-no-swipe',
606+
imports: [NzCarouselModule],
607+
template: `
608+
<nz-carousel [nzEnableSwipe]="false">
609+
@for (index of array; track index) {
610+
<div nz-carousel-content>
611+
<h3>{{ index }}</h3>
612+
</div>
613+
}
614+
</nz-carousel>
615+
`
616+
})
617+
export class NzTestCarouselNoSwipeComponent {
618+
@ViewChild(NzCarouselComponent, { static: true }) nzCarouselComponent!: NzCarouselComponent;
619+
array = [1, 2, 3, 4];
620+
}
621+
511622
class MockDirectionality {
512623
value = 'ltr';
513624
change = new Subject();

components/carousel/demo/arrow.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
order: 6
3+
title:
4+
zh-CN: 切换箭头
5+
en-US: Arrows for switching
6+
---
7+
8+
## zh-CN
9+
10+
显示切换箭头。
11+
12+
## en-US
13+
14+
Show the arrows for switching.

components/carousel/demo/arrow.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Component } from '@angular/core';
2+
3+
import { NzCarouselModule } from 'ng-zorro-antd/carousel';
4+
5+
@Component({
6+
selector: 'nz-demo-carousel-arrow',
7+
imports: [NzCarouselModule],
8+
template: ` <nz-carousel [nzEffect]="effect" nzArrows>
9+
@for (index of array; track index) {
10+
<div nz-carousel-content>
11+
<h3>{{ index }}</h3>
12+
</div>
13+
}
14+
</nz-carousel>`,
15+
styles: [
16+
`
17+
[nz-carousel-content] {
18+
text-align: center;
19+
height: 160px;
20+
line-height: 160px;
21+
background: #364d79;
22+
color: #fff;
23+
overflow: hidden;
24+
}
25+
26+
h3 {
27+
color: #fff;
28+
margin-bottom: 0;
29+
user-select: none;
30+
}
31+
`
32+
]
33+
})
34+
export class NzDemoCarouselArrowComponent {
35+
array = [1, 2, 3, 4];
36+
effect = 'scrollx';
37+
}

components/carousel/doc/index.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ description: A carousel component. Scales with its container.
2626
| `[nzEffect]` | Transition effect | `'scrollx' \| 'fade'` | `'scrollx'` ||
2727
| `[nzEnableSwipe]` | Whether to support swipe gesture | `boolean` | `true` ||
2828
| `[nzLoop]` | Whether to enable the carousel to go in a loop | `boolean` | `true` ||
29+
| `[nzArrows]` | Whether to show the arrow buttons | `boolean` | `false` | - |
2930
| `(nzAfterChange)` | Callback function called after the current index changes | `EventEmitter<number>` | - |
3031
| `(nzBeforeChange)` | Callback function called before the current index changes | `EventEmitter{ from: number; to: number }>` | - |
3132

components/carousel/doc/index.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ description: 一组轮播的区域。
2727
| `[nzEffect]` | 动画效果函数,可取 `scrollx`, `fade` | `'scrollx' \| 'fade'` | `'scrollx'` ||
2828
| `[nzEnableSwipe]` | 是否支持手势划动切换 | `boolean` | `true` ||
2929
| `[nzLoop]` | 是否支持循环 | `boolean` | `true` ||
30+
| `[nzArrows]` | 是否显示箭头按钮 | `boolean` | `false` | - |
3031
| `(nzAfterChange)` | 切换面板的回调 | `EventEmitter<number>` | - |
3132
| `(nzBeforeChange)` | 切换面板的回调 | `EventEmitter<{ from: number; to: number }>` | - |
3233

components/carousel/style/index.less

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
@carousel-prefix-cls: ~'@{ant-prefix}-carousel';
55
@carousel-dot-margin: 4px;
6+
@carousel-arrow-size: 16px;
7+
@carousel-arrow-offset: 12px;
8+
@carousel-arrow-bg: fade(#000, 20%);
9+
@carousel-arrow-bg-hover: fade(#000, 30%);
10+
@carousel-arrow-color: @white;
611

712
.@{carousel-prefix-cls} {
813
.reset-component();
@@ -126,52 +131,98 @@
126131
.slick-next {
127132
position: absolute;
128133
top: 50%;
129-
display: block;
130-
width: 20px;
131-
height: 20px;
132-
margin-top: -10px;
134+
z-index: 1;
135+
display: inline-block;
136+
width: @carousel-arrow-size;
137+
height: @carousel-arrow-size;
138+
margin: 0;
133139
padding: 0;
134-
color: transparent;
135-
font-size: 0;
140+
color: @white;
141+
font-size: 0; // keep no text
136142
line-height: 0;
137143
background: transparent;
138144
border: 0;
145+
border-radius: 0;
139146
outline: none;
147+
transform: translateY(-50%);
140148
cursor: pointer;
149+
opacity: 0.3;
150+
transition:
151+
color 0.3s,
152+
opacity 0.3s;
153+
154+
&::after {
155+
position: absolute;
156+
top: 2.3px;
157+
display: inline-block;
158+
box-sizing: border-box;
159+
width: 11.3px;
160+
height: 11.3px;
161+
border: 0 solid currentcolor;
162+
border-radius: 1px;
163+
opacity: 1;
164+
content: '';
165+
inset-inline-start: 2.3px;
166+
border-inline-start-width: 2px;
167+
border-block-start-width: 2px;
168+
}
141169

142170
&:hover,
143171
&:focus {
144-
color: transparent;
145-
background: transparent;
172+
color: @white;
146173
outline: none;
147-
148-
&::before {
149-
opacity: 1;
150-
}
174+
opacity: 1;
151175
}
152176

153-
&.slick-disabled::before {
154-
opacity: 0.25;
177+
&.slick-disabled {
178+
display: none;
155179
}
156180
}
157181

158182
.slick-prev {
159-
left: -25px;
183+
right: auto;
184+
left: @carousel-arrow-offset;
160185

161-
&::before {
162-
content: '';
186+
&::after {
187+
transform: rotate(-45deg);
163188
}
164189
}
165190

166191
.slick-next {
167-
right: -25px;
192+
right: @carousel-arrow-offset;
193+
left: auto;
168194

169-
&::before {
170-
content: '';
195+
&::after {
196+
transform: rotate(135deg);
197+
}
198+
}
199+
200+
// Vertical mode (dots on left or right)
201+
.slick-vertical {
202+
.slick-prev {
203+
top: @carousel-dot-margin * 3;
204+
right: auto;
205+
left: 50%;
206+
transform: translateX(-50%);
207+
208+
&::after {
209+
transform: rotate(45deg);
210+
}
211+
}
212+
213+
.slick-next {
214+
top: auto;
215+
right: auto;
216+
bottom: @carousel-dot-margin * 3;
217+
left: 50%;
218+
transform: translateX(-50%);
219+
220+
&::after {
221+
transform: rotate(-135deg);
222+
}
171223
}
172224
}
173225

174-
// Dots
175226
.slick-dots {
176227
position: absolute;
177228
right: 0;

0 commit comments

Comments
 (0)