1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-27711] Loading Skeleton page (#17224)

* convert popup page component to use inputs

* disable overflow on popup page to allow content to naturally overflow

* migrate popup-page to OnPush

* add vault-loading-skeleton component

* remove internal loading text

* hide entire skeleton from screen readers
This commit is contained in:
Nick Krantz
2025-11-05 14:05:15 -06:00
committed by GitHub
parent edae5e69c7
commit 8cd39690ed
5 changed files with 62 additions and 44 deletions

View File

@@ -29,11 +29,9 @@ import {
SearchModule, SearchModule,
SectionComponent, SectionComponent,
ScrollLayoutDirective, ScrollLayoutDirective,
SkeletonComponent,
SkeletonTextComponent,
SkeletonGroupComponent,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { VaultLoadingSkeletonComponent } from "../../../vault/popup/components/vault-loading-skeleton/vault-loading-skeleton.component";
import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service";
import { PopupFooterComponent } from "./popup-footer.component"; import { PopupFooterComponent } from "./popup-footer.component";
@@ -366,9 +364,7 @@ export default {
SectionComponent, SectionComponent,
IconButtonModule, IconButtonModule,
BadgeModule, BadgeModule,
SkeletonComponent, VaultLoadingSkeletonComponent,
SkeletonTextComponent,
SkeletonGroupComponent,
], ],
providers: [ providers: [
{ {
@@ -634,21 +630,9 @@ export const SkeletonLoading: Story = {
template: /* HTML */ ` template: /* HTML */ `
<extension-container> <extension-container>
<popup-tab-navigation> <popup-tab-navigation>
<popup-page> <popup-page hideOverflow>
<popup-header slot="header" pageTitle="Page Header"></popup-header> <popup-header slot="header" pageTitle="Page Header"></popup-header>
<div> <vault-loading-skeleton></vault-loading-skeleton>
<div class="tw-sr-only" role="status">Loading...</div>
<div class="tw-flex tw-flex-col tw-gap-4">
<bit-skeleton-text class="tw-w-1/3"></bit-skeleton-text>
@for (num of data; track $index) {
<bit-skeleton-group>
<bit-skeleton class="tw-size-8" slot="start"></bit-skeleton>
<bit-skeleton-text [lines]="2" class="tw-w-1/2"></bit-skeleton-text>
</bit-skeleton-group>
<bit-skeleton class="tw-w-full tw-h-[1px]"></bit-skeleton>
}
</div>
</div>
</popup-page> </popup-page>
</popup-tab-navigation> </popup-tab-navigation>
</extension-container> </extension-container>

View File

@@ -1,7 +1,7 @@
<ng-content select="[slot=header]"></ng-content> <ng-content select="[slot=header]"></ng-content>
<main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt"> <main class="tw-flex-1 tw-overflow-hidden tw-flex tw-flex-col tw-relative tw-bg-background-alt">
<ng-content select="[slot=full-width-notice]"></ng-content> <ng-content select="[slot=full-width-notice]"></ng-content>
<!-- <!--
x padding on this container is designed to always be a minimum of 0.75rem (equivalent to tailwind's tw-px-3), or 0.5rem (equivalent x padding on this container is designed to always be a minimum of 0.75rem (equivalent to tailwind's tw-px-3), or 0.5rem (equivalent
to tailwind's tw-px-2) in compact mode, but stretch to fill the remainder of the container when the content reaches a maximum of to tailwind's tw-px-2) in compact mode, but stretch to fill the remainder of the container when the content reaches a maximum of
640px in width (equivalent to tailwind's `sm` breakpoint) 640px in width (equivalent to tailwind's `sm` breakpoint)
@@ -10,26 +10,28 @@
#nonScrollable #nonScrollable
class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid tw-py-3 bit-compact:tw-py-2 tw-px-[max(0.75rem,calc((100%-(var(--tw-sm-breakpoint)))/2))] bit-compact:tw-px-[max(0.5rem,calc((100%-(var(--tw-sm-breakpoint)))/2))]" class="tw-transition-colors tw-duration-200 tw-border-0 tw-border-b tw-border-solid tw-py-3 bit-compact:tw-py-2 tw-px-[max(0.75rem,calc((100%-(var(--tw-sm-breakpoint)))/2))] bit-compact:tw-px-[max(0.5rem,calc((100%-(var(--tw-sm-breakpoint)))/2))]"
[ngClass]="{ [ngClass]="{
'tw-invisible !tw-p-0 !tw-border-none': loading || nonScrollable.childElementCount === 0, 'tw-invisible !tw-p-0 !tw-border-none': loading() || nonScrollable.childElementCount === 0,
'tw-border-secondary-300': scrolled(), 'tw-border-secondary-300': scrolled(),
'tw-border-transparent': !scrolled(), 'tw-border-transparent': !scrolled(),
}" }"
> >
<ng-content select="[slot=above-scroll-area]"></ng-content> <ng-content select="[slot=above-scroll-area]"></ng-content>
</div> </div>
<!-- <!--
x padding on this container is designed to always be a minimum of 0.75rem (equivalent to tailwind's tw-px-3), or 0.5rem (equivalent x padding on this container is designed to always be a minimum of 0.75rem (equivalent to tailwind's tw-px-3), or 0.5rem (equivalent
to tailwind's tw-px-2) in compact mode, but stretch to fill the remainder of the container when the content reaches a maximum of to tailwind's tw-px-2) in compact mode, but stretch to fill the remainder of the container when the content reaches a maximum of
640px in width (equivalent to tailwind's `sm` breakpoint) 640px in width (equivalent to tailwind's `sm` breakpoint)
--> -->
<div <div
class="tw-overflow-y-auto tw-size-full tw-styled-scrollbar" class="tw-size-full tw-styled-scrollbar"
data-testid="popup-layout-scroll-region" data-testid="popup-layout-scroll-region"
(scroll)="handleScroll($event)" (scroll)="handleScroll($event)"
[ngClass]="{ [ngClass]="{
'tw-invisible': loading, 'tw-overflow-hidden': hideOverflow(),
'tw-overflow-y-auto': !hideOverflow(),
'tw-invisible': loading(),
'tw-py-3 bit-compact:tw-py-2 tw-px-[max(0.75rem,calc((100%-(var(--tw-sm-breakpoint)))/2))] bit-compact:tw-px-[max(0.5rem,calc((100%-(var(--tw-sm-breakpoint)))/2))]': 'tw-py-3 bit-compact:tw-py-2 tw-px-[max(0.75rem,calc((100%-(var(--tw-sm-breakpoint)))/2))] bit-compact:tw-px-[max(0.5rem,calc((100%-(var(--tw-sm-breakpoint)))/2))]':
!disablePadding, !disablePadding(),
}" }"
bitScrollLayoutHost bitScrollLayoutHost
> >
@@ -37,9 +39,9 @@
</div> </div>
<span <span
class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-text-main" class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center tw-text-main"
[ngClass]="{ 'tw-invisible': !loading }" [ngClass]="{ 'tw-invisible': !loading() }"
> >
<i class="bwi bwi-spinner bwi-lg bwi-spin" [attr.aria-label]="loadingText"></i> <i class="bwi bwi-spinner bwi-lg bwi-spin" [attr.aria-label]="loadingText()"></i>
</span> </span>
</main> </main>
<ng-content select="[slot=footer]"></ng-content> <ng-content select="[slot=footer]"></ng-content>

View File

@@ -1,11 +1,16 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { booleanAttribute, Component, inject, Input, signal } from "@angular/core"; import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
inject,
input,
signal,
} from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ScrollLayoutHostDirective } from "@bitwarden/components"; import { ScrollLayoutHostDirective } from "@bitwarden/components";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({ @Component({
selector: "popup-page", selector: "popup-page",
templateUrl: "popup-page.component.html", templateUrl: "popup-page.component.html",
@@ -13,28 +18,23 @@ import { ScrollLayoutHostDirective } from "@bitwarden/components";
class: "tw-h-full tw-flex tw-flex-col tw-overflow-y-hidden", class: "tw-h-full tw-flex tw-flex-col tw-overflow-y-hidden",
}, },
imports: [CommonModule, ScrollLayoutHostDirective], imports: [CommonModule, ScrollLayoutHostDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PopupPageComponent { export class PopupPageComponent {
protected i18nService = inject(I18nService); protected i18nService = inject(I18nService);
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals readonly loading = input<boolean>(false);
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() loading = false;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals readonly disablePadding = input(false, { transform: booleanAttribute });
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input({ transform: booleanAttribute })
disablePadding = false;
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals /** Hides any overflow within the page content */
// eslint-disable-next-line @angular-eslint/prefer-signals readonly hideOverflow = input(false, { transform: booleanAttribute });
protected scrolled = signal(false);
protected readonly scrolled = signal(false);
isScrolled = this.scrolled.asReadonly(); isScrolled = this.scrolled.asReadonly();
/** Accessible loading label for the spinner. Defaults to "loading" */ /** Accessible loading label for the spinner. Defaults to "loading" */
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals readonly loadingText = input<string | undefined>(this.i18nService.t("loading"));
// eslint-disable-next-line @angular-eslint/prefer-signals
@Input() loadingText?: string = this.i18nService.t("loading");
handleScroll(event: Event) { handleScroll(event: Event) {
this.scrolled.set((event.currentTarget as HTMLElement).scrollTop !== 0); this.scrolled.set((event.currentTarget as HTMLElement).scrollTop !== 0);

View File

@@ -0,0 +1,15 @@
<section aria-hidden="true">
<div class="tw-mt-1.5 tw-flex tw-flex-col tw-gap-4">
<bit-skeleton-text class="tw-w-[8.625rem] tw-max-w-full tw-mb-2.5"></bit-skeleton-text>
@for (num of numberOfItems; track $index) {
<bit-skeleton-group class="tw-mx-2">
<bit-skeleton class="tw-size-6" slot="start"></bit-skeleton>
<div class="tw-flex tw-flex-col tw-gap-1">
<bit-skeleton class="tw-w-40 tw-h-2.5 tw-max-w-full"></bit-skeleton>
<bit-skeleton class="tw-w-24 tw-h-2.5 tw-max-w-full"></bit-skeleton>
</div>
</bit-skeleton-group>
<hr class="tw-h-[1px] -tw-mr-3 tw-bg-secondary-100 tw-border-none" />
}
</div>
</section>

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component } from "@angular/core";
import {
SkeletonComponent,
SkeletonGroupComponent,
SkeletonTextComponent,
} from "@bitwarden/components";
@Component({
selector: "vault-loading-skeleton",
templateUrl: "./vault-loading-skeleton.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [SkeletonGroupComponent, SkeletonComponent, SkeletonTextComponent],
})
export class VaultLoadingSkeletonComponent {
protected readonly numberOfItems: null[] = new Array(15).fill(null);
}