mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-19998] Add arrow icons to vault carousel component (#16041)
* Add arrow icons to vault carousel component * Fix carousel next button and update tests * Add new unit tests for back/next buttons * Copy 'next' string from web/src/locales to browser/src/_locales * Fix layout / spacing on carousel arrows * Remove 'next' string from non-en locales * Fix lint errors on carousel tests * Add I18n provider to storybook for carousel * Fix spacing for carousel button row * Update carousel arrows to use small icon variant * Add label attr to carousel buttons * Add next string to locales for Desktop
This commit is contained in:
@@ -5588,6 +5588,9 @@
|
|||||||
"showLess": {
|
"showLess": {
|
||||||
"message": "Show less"
|
"message": "Show less"
|
||||||
},
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Next"
|
||||||
|
},
|
||||||
"moreBreadcrumbs": {
|
"moreBreadcrumbs": {
|
||||||
"message": "More breadcrumbs",
|
"message": "More breadcrumbs",
|
||||||
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
||||||
|
|||||||
@@ -4080,5 +4080,8 @@
|
|||||||
"moreBreadcrumbs": {
|
"moreBreadcrumbs": {
|
||||||
"message": "More breadcrumbs",
|
"message": "More breadcrumbs",
|
||||||
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
"description": "This is used in the context of a breadcrumb navigation, indicating that there are more items in the breadcrumb trail that are not currently displayed."
|
||||||
|
},
|
||||||
|
"next": {
|
||||||
|
"message": "Next"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,18 +6,40 @@
|
|||||||
#container
|
#container
|
||||||
>
|
>
|
||||||
<vault-carousel-content [content]="slides.get(selectedIndex)?.content"></vault-carousel-content>
|
<vault-carousel-content [content]="slides.get(selectedIndex)?.content"></vault-carousel-content>
|
||||||
<div
|
<div class="tw-w-full tw-flex tw-justify-between tw-mt-auto tw-px-4 tw-pb-2 tw-pt-4">
|
||||||
class="tw-w-full tw-flex tw-gap-2 tw-justify-center tw-mt-auto tw-pt-4"
|
<button
|
||||||
role="tablist"
|
type="button"
|
||||||
(keydown)="keyManager.onKeydown($event)"
|
bitIconButton="bwi-angle-left"
|
||||||
#carouselButtonWrapper
|
class="tw-size-6 tw-p-0 tw-flex tw-items-center tw-justify-center"
|
||||||
>
|
size="small"
|
||||||
<vault-carousel-button
|
[attr.label]="'back' | i18n"
|
||||||
*ngFor="let slide of slides; let i = index"
|
(click)="prevSlide()"
|
||||||
[slide]="slide"
|
[disabled]="selectedIndex <= 0"
|
||||||
[isActive]="i === selectedIndex"
|
appA11yTitle="{{ 'back' | i18n }}"
|
||||||
(onClick)="selectSlide(i)"
|
></button>
|
||||||
></vault-carousel-button>
|
<div
|
||||||
|
class="tw-w-full tw-flex tw-gap-2 tw-justify-center tw-mt-auto"
|
||||||
|
role="tablist"
|
||||||
|
(keydown)="keyManager.onKeydown($event)"
|
||||||
|
#carouselButtonWrapper
|
||||||
|
>
|
||||||
|
<vault-carousel-button
|
||||||
|
*ngFor="let slide of slides; let i = index"
|
||||||
|
[slide]="slide"
|
||||||
|
[isActive]="i === selectedIndex"
|
||||||
|
(onClick)="selectSlide(i)"
|
||||||
|
></vault-carousel-button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-angle-right"
|
||||||
|
class="tw-size-6 tw-p-0 tw-flex tw-items-center tw-justify-center"
|
||||||
|
[attr.label]="'next' | i18n"
|
||||||
|
size="small"
|
||||||
|
(click)="nextSlide()"
|
||||||
|
[disabled]="selectedIndex >= slides.length - 1"
|
||||||
|
appA11yTitle="{{ 'next' | i18n }}"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-absolute tw-invisible" #tempSlideContainer *ngIf="minHeight === null">
|
<div class="tw-absolute tw-invisible" #tempSlideContainer *ngIf="minHeight === null">
|
||||||
<ng-template cdkPortalOutlet></ng-template>
|
<ng-template cdkPortalOutlet></ng-template>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Component } from "@angular/core";
|
|||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
|
||||||
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
|
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
|
||||||
import { VaultCarouselComponent } from "./carousel.component";
|
import { VaultCarouselComponent } from "./carousel.component";
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ describe("VaultCarouselComponent", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [VaultCarouselComponent, VaultCarouselSlideComponent],
|
imports: [VaultCarouselComponent, VaultCarouselSlideComponent],
|
||||||
|
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ describe("VaultCarouselComponent", () => {
|
|||||||
|
|
||||||
it("shows the active slides content", () => {
|
it("shows the active slides content", () => {
|
||||||
// Set the second slide as active
|
// Set the second slide as active
|
||||||
fixture.debugElement.queryAll(By.css("button"))[1].nativeElement.click();
|
fixture.debugElement.queryAll(By.css("button"))[2].nativeElement.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const heading = fixture.debugElement.query(By.css("h1")).nativeElement;
|
const heading = fixture.debugElement.query(By.css("h1")).nativeElement;
|
||||||
@@ -63,10 +66,37 @@ describe("VaultCarouselComponent", () => {
|
|||||||
it('emits "slideChange" event when slide changes', () => {
|
it('emits "slideChange" event when slide changes', () => {
|
||||||
jest.spyOn(component.slideChange, "emit");
|
jest.spyOn(component.slideChange, "emit");
|
||||||
|
|
||||||
const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[2];
|
const thirdSlideButton = fixture.debugElement.queryAll(By.css("button"))[3];
|
||||||
|
|
||||||
thirdSlideButton.nativeElement.click();
|
thirdSlideButton.nativeElement.click();
|
||||||
|
|
||||||
expect(component.slideChange.emit).toHaveBeenCalledWith(2);
|
expect(component.slideChange.emit).toHaveBeenCalledWith(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('advances to the next slide when the "next" button is pressed', () => {
|
||||||
|
const middleSlideButton = fixture.debugElement.queryAll(By.css("button"))[2];
|
||||||
|
const nextButton = fixture.debugElement.queryAll(By.css("button"))[4];
|
||||||
|
|
||||||
|
middleSlideButton.nativeElement.click();
|
||||||
|
|
||||||
|
jest.spyOn(component.slideChange, "emit");
|
||||||
|
|
||||||
|
nextButton.nativeElement.click();
|
||||||
|
|
||||||
|
expect(component.slideChange.emit).toHaveBeenCalledWith(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('advances to the previous slide when the "back" button is pressed', async () => {
|
||||||
|
const middleSlideButton = fixture.debugElement.queryAll(By.css("button"))[2];
|
||||||
|
const backButton = fixture.debugElement.queryAll(By.css("button"))[0];
|
||||||
|
|
||||||
|
middleSlideButton.nativeElement.click();
|
||||||
|
await new Promise((r) => setTimeout(r, 100)); // Give time for the DOM to update.
|
||||||
|
|
||||||
|
jest.spyOn(component.slideChange, "emit");
|
||||||
|
|
||||||
|
backButton.nativeElement.click();
|
||||||
|
|
||||||
|
expect(component.slideChange.emit).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import {
|
|||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { take } from "rxjs";
|
import { take } from "rxjs";
|
||||||
|
|
||||||
import { ButtonModule } from "@bitwarden/components";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
|
import { ButtonModule, IconButtonModule } from "@bitwarden/components";
|
||||||
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component";
|
import { VaultCarouselButtonComponent } from "./carousel-button/carousel-button.component";
|
||||||
import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component";
|
import { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component";
|
||||||
@@ -32,9 +34,12 @@ import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.com
|
|||||||
imports: [
|
imports: [
|
||||||
CdkPortalOutlet,
|
CdkPortalOutlet,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
JslibModule,
|
||||||
|
IconButtonModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
VaultCarouselContentComponent,
|
VaultCarouselContentComponent,
|
||||||
VaultCarouselButtonComponent,
|
VaultCarouselButtonComponent,
|
||||||
|
I18nPipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class VaultCarouselComponent implements AfterViewInit {
|
export class VaultCarouselComponent implements AfterViewInit {
|
||||||
@@ -97,6 +102,18 @@ export class VaultCarouselComponent implements AfterViewInit {
|
|||||||
this.slideChange.emit(index);
|
this.slideChange.emit(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected nextSlide() {
|
||||||
|
if (this.selectedIndex < this.slides.length - 1) {
|
||||||
|
this.selectSlide(this.selectedIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected prevSlide() {
|
||||||
|
if (this.selectedIndex > 0) {
|
||||||
|
this.selectSlide(this.selectedIndex - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ngAfterViewInit() {
|
async ngAfterViewInit() {
|
||||||
this.keyManager = new FocusKeyManager(this.carouselButtons)
|
this.keyManager = new FocusKeyManager(this.carouselButtons)
|
||||||
.withHorizontalOrientation("ltr")
|
.withHorizontalOrientation("ltr")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ButtonComponent, TypographyModule } from "@bitwarden/components";
|
import { ButtonComponent, TypographyModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
|
import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component";
|
||||||
@@ -11,6 +12,7 @@ export default {
|
|||||||
decorators: [
|
decorators: [
|
||||||
moduleMetadata({
|
moduleMetadata({
|
||||||
imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent],
|
imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent],
|
||||||
|
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|||||||
Reference in New Issue
Block a user