mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[CL-696] un-revert "various drawer improvements" + bug fix (#14887)
* Revert "Revert "[CL-622][CL-562][CL-621][CL-632] various drawer improvements …"
This reverts commit 4b32d1f9dd.
* fix virtual scroll: add .cdk-virtual-scrollable to scroll viewport target
* remove references to main el
* use directives instead of querySelector (#14950)
* remove references to main el
* wip
* banish querySelector to the shadow realm
* revert apps/ files
* Add virtual scrolling docs
Co-authored-by: Vicki League <vleague@bitwarden.com>
* add jsdoc
* run eslint
* fix skip links bug
* Update libs/components/src/layout/layout.component.ts
Co-authored-by: Vicki League <vleague@bitwarden.com>
* update tab handler
* only run on tab
* fix lint
* fix virtual scroll issue due to Angular 19 upgrade (#15193)
thanks Vicki
---------
Co-authored-by: Vicki League <vleague@bitwarden.com>
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * from "./layout.component";
|
||||
export * from "./scroll-layout.directive";
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
<div
|
||||
class="tw-fixed tw-z-50 tw-w-full tw-flex tw-justify-center tw-opacity-0 focus-within:tw-opacity-100 tw-pointer-events-none focus-within:tw-pointer-events-auto"
|
||||
>
|
||||
<nav class="tw-bg-background-alt3 tw-rounded-md tw-rounded-t-none tw-py-2 tw-text-alt2">
|
||||
<a
|
||||
bitLink
|
||||
class="tw-mx-6 focus-visible:before:!tw-ring-0"
|
||||
[fragment]="mainContentId"
|
||||
[routerLink]="[]"
|
||||
(click)="focusMainContent()"
|
||||
linkType="light"
|
||||
>{{ "skipToContent" | i18n }}</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
@let mainContentId = "main-content";
|
||||
<div class="tw-flex tw-w-full">
|
||||
<ng-content select="bit-side-nav, [slot=side-nav]"></ng-content>
|
||||
<main
|
||||
[id]="mainContentId"
|
||||
tabindex="-1"
|
||||
class="tw-overflow-auto tw-min-w-0 tw-flex-1 tw-bg-background tw-p-6 md:tw-ms-0 tw-ms-16"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
<div class="tw-flex tw-w-full" cdkTrapFocus>
|
||||
<div
|
||||
class="tw-fixed tw-z-50 tw-w-full tw-flex tw-justify-center tw-opacity-0 focus-within:tw-opacity-100 tw-pointer-events-none focus-within:tw-pointer-events-auto"
|
||||
>
|
||||
<nav class="tw-bg-background-alt3 tw-rounded-md tw-rounded-t-none tw-py-2 tw-text-alt2">
|
||||
<a
|
||||
#skipLink
|
||||
bitLink
|
||||
class="tw-mx-6 focus-visible:before:!tw-ring-0"
|
||||
[fragment]="mainContentId"
|
||||
[routerLink]="[]"
|
||||
(click)="focusMainContent()"
|
||||
linkType="light"
|
||||
>{{ "skipToContent" | i18n }}</a
|
||||
>
|
||||
</nav>
|
||||
</div>
|
||||
<ng-content select="bit-side-nav, [slot=side-nav]"></ng-content>
|
||||
<main
|
||||
#main
|
||||
[id]="mainContentId"
|
||||
tabindex="-1"
|
||||
bitScrollLayoutHost
|
||||
class="tw-overflow-auto tw-max-h-screen tw-min-w-0 tw-flex-1 tw-bg-background tw-p-6 md:tw-ms-0 tw-ms-16"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
|
||||
<!-- overlay backdrop for side-nav -->
|
||||
@if (
|
||||
{
|
||||
open: sideNavService.open$ | async,
|
||||
};
|
||||
as data
|
||||
) {
|
||||
<div
|
||||
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
|
||||
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
|
||||
>
|
||||
@if (data.open) {
|
||||
<div (click)="sideNavService.toggle()" class="tw-pointer-events-auto tw-size-full"></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
|
||||
<!-- overlay backdrop for side-nav -->
|
||||
@if (
|
||||
{
|
||||
open: sideNavService.open$ | async,
|
||||
};
|
||||
as data
|
||||
) {
|
||||
<div
|
||||
class="tw-pointer-events-none tw-fixed tw-inset-0 tw-z-10 tw-bg-black tw-bg-opacity-0 motion-safe:tw-transition-colors md:tw-hidden"
|
||||
[ngClass]="[data.open ? 'tw-bg-opacity-30 md:tw-bg-opacity-0' : 'tw-bg-opacity-0']"
|
||||
>
|
||||
@if (data.open) {
|
||||
<div
|
||||
(click)="sideNavService.toggle()"
|
||||
class="tw-pointer-events-auto tw-size-full"
|
||||
></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
<div class="tw-absolute tw-z-50 tw-left-0 md:tw-sticky tw-top-0 tw-h-screen md:tw-w-auto">
|
||||
<ng-template [cdkPortalOutlet]="drawerPortal()"></ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,61 @@
|
||||
import { A11yModule, CdkTrapFocus } from "@angular/cdk/a11y";
|
||||
import { PortalModule } from "@angular/cdk/portal";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Component, ElementRef, inject, viewChild } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { DrawerHostDirective } from "../drawer/drawer-host.directive";
|
||||
import { DrawerService } from "../drawer/drawer.service";
|
||||
import { LinkModule } from "../link";
|
||||
import { SideNavService } from "../navigation/side-nav.service";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
import { ScrollLayoutHostDirective } from "./scroll-layout.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-layout",
|
||||
templateUrl: "layout.component.html",
|
||||
imports: [CommonModule, SharedModule, LinkModule, RouterModule, PortalModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
LinkModule,
|
||||
RouterModule,
|
||||
PortalModule,
|
||||
A11yModule,
|
||||
CdkTrapFocus,
|
||||
ScrollLayoutHostDirective,
|
||||
],
|
||||
host: {
|
||||
"(document:keydown.tab)": "handleKeydown($event)",
|
||||
},
|
||||
hostDirectives: [DrawerHostDirective],
|
||||
})
|
||||
export class LayoutComponent {
|
||||
protected mainContentId = "main-content";
|
||||
|
||||
protected sideNavService = inject(SideNavService);
|
||||
protected drawerPortal = inject(DrawerHostDirective).portal;
|
||||
protected drawerPortal = inject(DrawerService).portal;
|
||||
|
||||
focusMainContent() {
|
||||
document.getElementById(this.mainContentId)?.focus();
|
||||
private mainContent = viewChild.required<ElementRef<HTMLElement>>("main");
|
||||
protected focusMainContent() {
|
||||
this.mainContent().nativeElement.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Angular CDK's focus trap utility is silly and will not respect focus order.
|
||||
* This is a workaround to explicitly focus the skip link when tab is first pressed, if no other item already has focus.
|
||||
*
|
||||
* @see https://github.com/angular/components/issues/10247#issuecomment-384060265
|
||||
**/
|
||||
private skipLink = viewChild.required<ElementRef<HTMLElement>>("skipLink");
|
||||
handleKeydown(ev: KeyboardEvent) {
|
||||
if (isNothingFocused()) {
|
||||
ev.preventDefault();
|
||||
this.skipLink().nativeElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isNothingFocused = (): boolean => {
|
||||
return [document.documentElement, document.body, null].includes(
|
||||
document.activeElement as HTMLElement,
|
||||
);
|
||||
};
|
||||
|
||||
98
libs/components/src/layout/scroll-layout.directive.ts
Normal file
98
libs/components/src/layout/scroll-layout.directive.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { CdkVirtualScrollable, VIRTUAL_SCROLLABLE } from "@angular/cdk/scrolling";
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
Injectable,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
import { toObservable } from "@angular/core/rxjs-interop";
|
||||
import { filter, fromEvent, Observable, switchMap } from "rxjs";
|
||||
|
||||
/**
|
||||
* A service is needed because we can't inject a directive defined in the template of a parent component. The parent's template is initialized after projected content.
|
||||
**/
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class ScrollLayoutService {
|
||||
scrollableRef = signal<ElementRef<HTMLElement> | null>(null);
|
||||
scrollableRef$ = toObservable(this.scrollableRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the primary scrollable area of a layout component.
|
||||
*
|
||||
* Stores the element reference in a global service so it can be referenced by `ScrollLayoutDirective` even when it isn't a direct child of this directive.
|
||||
**/
|
||||
@Directive({
|
||||
selector: "[bitScrollLayoutHost]",
|
||||
standalone: true,
|
||||
host: {
|
||||
class: "cdk-virtual-scrollable",
|
||||
},
|
||||
})
|
||||
export class ScrollLayoutHostDirective implements OnDestroy {
|
||||
private ref = inject(ElementRef);
|
||||
private service = inject(ScrollLayoutService);
|
||||
|
||||
constructor() {
|
||||
this.service.scrollableRef.set(this.ref as ElementRef<HTMLElement>);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.service.scrollableRef.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll viewport to the element marked with `ScrollLayoutHostDirective`.
|
||||
*
|
||||
* `ScrollLayoutHostDirective` is set on the primary scrollable area of a layout component (`bit-layout`, `popup-page`, etc).
|
||||
*
|
||||
* @see "Virtual Scrolling" in Storybook.
|
||||
*/
|
||||
@Directive({
|
||||
selector: "[bitScrollLayout]",
|
||||
standalone: true,
|
||||
providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: ScrollLayoutDirective }],
|
||||
})
|
||||
export class ScrollLayoutDirective extends CdkVirtualScrollable implements OnInit {
|
||||
private service = inject(ScrollLayoutService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
effect(() => {
|
||||
const scrollableRef = this.service.scrollableRef();
|
||||
if (!scrollableRef) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("ScrollLayoutDirective can't find scroll host");
|
||||
return;
|
||||
}
|
||||
|
||||
this.elementRef = scrollableRef;
|
||||
});
|
||||
}
|
||||
|
||||
override elementScrolled(): Observable<Event> {
|
||||
return this.service.scrollableRef$.pipe(
|
||||
filter((ref) => ref !== null),
|
||||
switchMap((ref) => fromEvent(ref.nativeElement, "scroll")),
|
||||
);
|
||||
}
|
||||
|
||||
override getElementRef(): ElementRef<HTMLElement> {
|
||||
return this.service.scrollableRef()!;
|
||||
}
|
||||
|
||||
override measureBoundingClientRectWithScrollOffset(
|
||||
from: "left" | "top" | "right" | "bottom",
|
||||
): number {
|
||||
return (
|
||||
this.service.scrollableRef()!.nativeElement.getBoundingClientRect()[from] -
|
||||
this.measureScrollOffset(from)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user