From 232dd898146f853688b07b51bc829205e4500cfa Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Tue, 2 Sep 2025 11:48:46 -0700 Subject: [PATCH] [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 --- apps/browser/src/_locales/en/messages.json | 3 ++ apps/desktop/src/locales/en/messages.json | 3 ++ .../carousel/carousel.component.html | 46 ++++++++++++++----- .../carousel/carousel.component.spec.ts | 34 +++++++++++++- .../components/carousel/carousel.component.ts | 19 +++++++- .../components/carousel/carousel.stories.ts | 2 + 6 files changed, 92 insertions(+), 15 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index bb2483daf3b..3c46085c3d7 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -5588,6 +5588,9 @@ "showLess": { "message": "Show less" }, + "next": { + "message": "Next" + }, "moreBreadcrumbs": { "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." diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index c805096189b..1094a8be26f 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -4080,5 +4080,8 @@ "moreBreadcrumbs": { "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." + }, + "next": { + "message": "Next" } } diff --git a/libs/vault/src/components/carousel/carousel.component.html b/libs/vault/src/components/carousel/carousel.component.html index 04c6e559056..778b70e15e2 100644 --- a/libs/vault/src/components/carousel/carousel.component.html +++ b/libs/vault/src/components/carousel/carousel.component.html @@ -6,18 +6,40 @@ #container > -
- +
+ +
+ +
+
diff --git a/libs/vault/src/components/carousel/carousel.component.spec.ts b/libs/vault/src/components/carousel/carousel.component.spec.ts index 1409aea0cb2..ebb38576813 100644 --- a/libs/vault/src/components/carousel/carousel.component.spec.ts +++ b/libs/vault/src/components/carousel/carousel.component.spec.ts @@ -2,6 +2,8 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; import { VaultCarouselComponent } from "./carousel.component"; @@ -33,6 +35,7 @@ describe("VaultCarouselComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [VaultCarouselComponent, VaultCarouselSlideComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], }).compileComponents(); }); @@ -48,7 +51,7 @@ describe("VaultCarouselComponent", () => { it("shows the active slides content", () => { // 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(); const heading = fixture.debugElement.query(By.css("h1")).nativeElement; @@ -63,10 +66,37 @@ describe("VaultCarouselComponent", () => { it('emits "slideChange" event when slide changes', () => { 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(); 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); + }); }); diff --git a/libs/vault/src/components/carousel/carousel.component.ts b/libs/vault/src/components/carousel/carousel.component.ts index f2d211697df..fdebbebc33b 100644 --- a/libs/vault/src/components/carousel/carousel.component.ts +++ b/libs/vault/src/components/carousel/carousel.component.ts @@ -20,7 +20,9 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; 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 { VaultCarouselContentComponent } from "./carousel-content/carousel-content.component"; @@ -32,9 +34,12 @@ import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.com imports: [ CdkPortalOutlet, CommonModule, + JslibModule, + IconButtonModule, ButtonModule, VaultCarouselContentComponent, VaultCarouselButtonComponent, + I18nPipe, ], }) export class VaultCarouselComponent implements AfterViewInit { @@ -97,6 +102,18 @@ export class VaultCarouselComponent implements AfterViewInit { 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() { this.keyManager = new FocusKeyManager(this.carouselButtons) .withHorizontalOrientation("ltr") diff --git a/libs/vault/src/components/carousel/carousel.stories.ts b/libs/vault/src/components/carousel/carousel.stories.ts index 521a561a19f..1e393779a6a 100644 --- a/libs/vault/src/components/carousel/carousel.stories.ts +++ b/libs/vault/src/components/carousel/carousel.stories.ts @@ -1,5 +1,6 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { ButtonComponent, TypographyModule } from "@bitwarden/components"; import { VaultCarouselSlideComponent } from "./carousel-slide/carousel-slide.component"; @@ -11,6 +12,7 @@ export default { decorators: [ moduleMetadata({ imports: [VaultCarouselSlideComponent, TypographyModule, ButtonComponent], + providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }], }), ], } as Meta;