1
0
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:
Nik Gilmore
2025-09-02 11:48:46 -07:00
committed by GitHub
parent 048d8a5f79
commit 232dd89814
6 changed files with 92 additions and 15 deletions

View File

@@ -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."

View File

@@ -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"
} }
} }

View File

@@ -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>

View File

@@ -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);
});
}); });

View File

@@ -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")

View File

@@ -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;