mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-08 19:18:02 +01:00
[CL-220] item components (#8870)
This commit is contained in:
parent
c70a5aa024
commit
e7416384dc
@ -6,9 +6,11 @@ import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/an
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import {
|
import {
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
|
BadgeModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
I18nMockService,
|
I18nMockService,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
|
ItemModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
import { PopupFooterComponent } from "./popup-footer.component";
|
import { PopupFooterComponent } from "./popup-footer.component";
|
||||||
@ -30,23 +32,34 @@ class ExtensionContainerComponent {}
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "vault-placeholder",
|
selector: "vault-placeholder",
|
||||||
template: `
|
template: `
|
||||||
<div class="tw-mb-8 tw-text-main">vault item</div>
|
<bit-item-group aria-label="Mock Vault Items">
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<bit-item *ngFor="let item of data; index as i">
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<button bit-item-content>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
{{ i }} of {{ data.length - 1 }}
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<span slot="secondary">Bar</span>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
</button>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<ng-container slot="end">
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<bit-item-action>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
<div class="tw-my-8 tw-text-main">vault item</div>
|
</bit-item-action>
|
||||||
<div class="tw-my-8 tw-text-main">vault item last item</div>
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone" aria-label="Copy item"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="More options"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
`,
|
`,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule],
|
||||||
})
|
})
|
||||||
class VaultComponent {}
|
class VaultComponent {
|
||||||
|
protected data = Array.from(Array(20).keys());
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "generator-placeholder",
|
selector: "generator-placeholder",
|
||||||
|
33
libs/components/src/a11y/a11y-cell.directive.ts
Normal file
33
libs/components/src/a11y/a11y-cell.directive.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core";
|
||||||
|
|
||||||
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "bitA11yCell",
|
||||||
|
standalone: true,
|
||||||
|
providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }],
|
||||||
|
})
|
||||||
|
export class A11yCellDirective implements FocusableElement {
|
||||||
|
@HostBinding("attr.role")
|
||||||
|
role: "gridcell" | null;
|
||||||
|
|
||||||
|
@ContentChild(FocusableElement)
|
||||||
|
private focusableChild: FocusableElement;
|
||||||
|
|
||||||
|
getFocusTarget() {
|
||||||
|
let focusTarget: HTMLElement;
|
||||||
|
if (this.focusableChild) {
|
||||||
|
focusTarget = this.focusableChild.getFocusTarget();
|
||||||
|
} else {
|
||||||
|
focusTarget = this.elementRef.nativeElement.querySelector("button, a");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!focusTarget) {
|
||||||
|
return this.elementRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return focusTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef<HTMLElement>) {}
|
||||||
|
}
|
145
libs/components/src/a11y/a11y-grid.directive.ts
Normal file
145
libs/components/src/a11y/a11y-grid.directive.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ContentChildren,
|
||||||
|
Directive,
|
||||||
|
HostBinding,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
QueryList,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import type { A11yCellDirective } from "./a11y-cell.directive";
|
||||||
|
import { A11yRowDirective } from "./a11y-row.directive";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "bitA11yGrid",
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class A11yGridDirective implements AfterViewInit {
|
||||||
|
@HostBinding("attr.role")
|
||||||
|
role = "grid";
|
||||||
|
|
||||||
|
@ContentChildren(A11yRowDirective)
|
||||||
|
rows: QueryList<A11yRowDirective>;
|
||||||
|
|
||||||
|
/** The number of pages to navigate on `PageUp` and `PageDown` */
|
||||||
|
@Input() pageSize = 5;
|
||||||
|
|
||||||
|
private grid: A11yCellDirective[][];
|
||||||
|
|
||||||
|
/** The row that currently has focus */
|
||||||
|
private activeRow = 0;
|
||||||
|
|
||||||
|
/** The cell that currently has focus */
|
||||||
|
private activeCol = 0;
|
||||||
|
|
||||||
|
@HostListener("keydown", ["$event"])
|
||||||
|
onKeyDown(event: KeyboardEvent) {
|
||||||
|
switch (event.code) {
|
||||||
|
case "ArrowUp":
|
||||||
|
this.updateCellFocusByDelta(-1, 0);
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
this.updateCellFocusByDelta(0, 1);
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
this.updateCellFocusByDelta(1, 0);
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
this.updateCellFocusByDelta(0, -1);
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
this.updateCellFocusByDelta(-this.activeRow, -this.activeCol);
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length);
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
this.updateCellFocusByDelta(-this.pageSize, 0);
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
this.updateCellFocusByDelta(this.pageSize, 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prevent default scrolling behavior */
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.initializeGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeGrid(): void {
|
||||||
|
try {
|
||||||
|
this.grid = this.rows.map((listItem) => {
|
||||||
|
listItem.role = "row";
|
||||||
|
return [...listItem.cells];
|
||||||
|
});
|
||||||
|
this.grid.flat().forEach((cell) => {
|
||||||
|
cell.role = "gridcell";
|
||||||
|
cell.getFocusTarget().tabIndex = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getActiveCellContent().tabIndex = 0;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Unable to initialize grid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the focusable content of the active cell */
|
||||||
|
private getActiveCellContent(): HTMLElement {
|
||||||
|
return this.grid[this.activeRow][this.activeCol].getFocusTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move focus via a delta against the currently active gridcell */
|
||||||
|
private updateCellFocusByDelta(rowDelta: number, colDelta: number) {
|
||||||
|
const prevActive = this.getActiveCellContent();
|
||||||
|
|
||||||
|
this.activeCol += colDelta;
|
||||||
|
this.activeRow += rowDelta;
|
||||||
|
|
||||||
|
// Row upper bound
|
||||||
|
if (this.activeRow >= this.grid.length) {
|
||||||
|
this.activeRow = this.grid.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row lower bound
|
||||||
|
if (this.activeRow < 0) {
|
||||||
|
this.activeRow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column upper bound
|
||||||
|
if (this.activeCol >= this.grid[this.activeRow].length) {
|
||||||
|
if (this.activeRow < this.grid.length - 1) {
|
||||||
|
// Wrap to next row on right arrow
|
||||||
|
this.activeCol = 0;
|
||||||
|
this.activeRow += 1;
|
||||||
|
} else {
|
||||||
|
this.activeCol = this.grid[this.activeRow].length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Column lower bound
|
||||||
|
if (this.activeCol < 0) {
|
||||||
|
if (this.activeRow > 0) {
|
||||||
|
// Wrap to prev row on left arrow
|
||||||
|
this.activeRow -= 1;
|
||||||
|
this.activeCol = this.grid[this.activeRow].length - 1;
|
||||||
|
} else {
|
||||||
|
this.activeCol = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextActive = this.getActiveCellContent();
|
||||||
|
nextActive.tabIndex = 0;
|
||||||
|
nextActive.focus();
|
||||||
|
|
||||||
|
if (nextActive !== prevActive) {
|
||||||
|
prevActive.tabIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
libs/components/src/a11y/a11y-row.directive.ts
Normal file
31
libs/components/src/a11y/a11y-row.directive.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ContentChildren,
|
||||||
|
Directive,
|
||||||
|
HostBinding,
|
||||||
|
QueryList,
|
||||||
|
ViewChildren,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import { A11yCellDirective } from "./a11y-cell.directive";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "bitA11yRow",
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class A11yRowDirective implements AfterViewInit {
|
||||||
|
@HostBinding("attr.role")
|
||||||
|
role: "row" | null;
|
||||||
|
|
||||||
|
cells: A11yCellDirective[];
|
||||||
|
|
||||||
|
@ViewChildren(A11yCellDirective)
|
||||||
|
private viewCells: QueryList<A11yCellDirective>;
|
||||||
|
|
||||||
|
@ContentChildren(A11yCellDirective)
|
||||||
|
private contentCells: QueryList<A11yCellDirective>;
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.cells = [...this.viewCells, ...this.contentCells];
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
|
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
|
||||||
|
|
||||||
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
|
|
||||||
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
|
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
|
||||||
|
|
||||||
const styles: Record<BadgeVariant, string[]> = {
|
const styles: Record<BadgeVariant, string[]> = {
|
||||||
@ -22,8 +24,9 @@ const hoverStyles: Record<BadgeVariant, string[]> = {
|
|||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
|
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
|
||||||
|
providers: [{ provide: FocusableElement, useExisting: BadgeDirective }],
|
||||||
})
|
})
|
||||||
export class BadgeDirective {
|
export class BadgeDirective implements FocusableElement {
|
||||||
@HostBinding("class") get classList() {
|
@HostBinding("class") get classList() {
|
||||||
return [
|
return [
|
||||||
"tw-inline-block",
|
"tw-inline-block",
|
||||||
@ -62,6 +65,10 @@ export class BadgeDirective {
|
|||||||
*/
|
*/
|
||||||
@Input() truncate = true;
|
@Input() truncate = true;
|
||||||
|
|
||||||
|
getFocusTarget() {
|
||||||
|
return this.el.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
private hasHoverEffects = false;
|
private hasHoverEffects = false;
|
||||||
|
|
||||||
constructor(private el: ElementRef<HTMLElement>) {
|
constructor(private el: ElementRef<HTMLElement>) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, HostBinding, Input } from "@angular/core";
|
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
|
||||||
|
|
||||||
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction";
|
||||||
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
|
|
||||||
export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light";
|
export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light";
|
||||||
|
|
||||||
@ -123,9 +124,12 @@ const sizes: Record<IconButtonSize, string[]> = {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "button[bitIconButton]:not(button[bitButton])",
|
selector: "button[bitIconButton]:not(button[bitButton])",
|
||||||
templateUrl: "icon-button.component.html",
|
templateUrl: "icon-button.component.html",
|
||||||
providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }],
|
providers: [
|
||||||
|
{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent },
|
||||||
|
{ provide: FocusableElement, useExisting: BitIconButtonComponent },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement {
|
||||||
@Input("bitIconButton") icon: string;
|
@Input("bitIconButton") icon: string;
|
||||||
|
|
||||||
@Input() buttonType: IconButtonType;
|
@Input() buttonType: IconButtonType;
|
||||||
@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
|||||||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||||
this.buttonType = value;
|
this.buttonType = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFocusTarget() {
|
||||||
|
return this.elementRef.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private elementRef: ElementRef) {}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ export * from "./form-field";
|
|||||||
export * from "./icon-button";
|
export * from "./icon-button";
|
||||||
export * from "./icon";
|
export * from "./icon";
|
||||||
export * from "./input";
|
export * from "./input";
|
||||||
|
export * from "./item";
|
||||||
export * from "./layout";
|
export * from "./layout";
|
||||||
export * from "./link";
|
export * from "./link";
|
||||||
export * from "./menu";
|
export * from "./menu";
|
||||||
|
@ -3,12 +3,7 @@ import { take } from "rxjs/operators";
|
|||||||
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
/**
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
* Interface for implementing focusable components. Used by the AutofocusDirective.
|
|
||||||
*/
|
|
||||||
export abstract class FocusableElement {
|
|
||||||
focus: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to focus an element.
|
* Directive to focus an element.
|
||||||
@ -46,7 +41,7 @@ export class AutofocusDirective {
|
|||||||
|
|
||||||
private focus() {
|
private focus() {
|
||||||
if (this.focusableElement) {
|
if (this.focusableElement) {
|
||||||
this.focusableElement.focus();
|
this.focusableElement.getFocusTarget().focus();
|
||||||
} else {
|
} else {
|
||||||
this.el.nativeElement.focus();
|
this.el.nativeElement.focus();
|
||||||
}
|
}
|
||||||
|
1
libs/components/src/item/index.ts
Normal file
1
libs/components/src/item/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./item.module";
|
12
libs/components/src/item/item-action.component.ts
Normal file
12
libs/components/src/item/item-action.component.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
|
||||||
|
import { A11yCellDirective } from "../a11y/a11y-cell.directive";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-item-action",
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
template: `<ng-content></ng-content>`,
|
||||||
|
providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }],
|
||||||
|
})
|
||||||
|
export class ItemActionComponent extends A11yCellDirective {}
|
16
libs/components/src/item/item-content.component.html
Normal file
16
libs/components/src/item/item-content.component.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div class="tw-flex tw-gap-2 tw-items-center">
|
||||||
|
<ng-content select="[slot=start]"></ng-content>
|
||||||
|
|
||||||
|
<div class="tw-flex tw-flex-col tw-items-start tw-text-start tw-w-full [&_p]:tw-mb-0">
|
||||||
|
<div class="tw-text-main tw-text-base">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-muted tw-text-sm">
|
||||||
|
<ng-content select="[slot=secondary]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-flex tw-gap-2 tw-items-center">
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
</div>
|
15
libs/components/src/item/item-content.component.ts
Normal file
15
libs/components/src/item/item-content.component.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-item-content, [bit-item-content]",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule],
|
||||||
|
templateUrl: `item-content.component.html`,
|
||||||
|
host: {
|
||||||
|
class:
|
||||||
|
"fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between",
|
||||||
|
},
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ItemContentComponent {}
|
13
libs/components/src/item/item-group.component.ts
Normal file
13
libs/components/src/item/item-group.component.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-item-group",
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
template: `<ng-content></ng-content>`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
host: {
|
||||||
|
class: "tw-block",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class ItemGroupComponent {}
|
21
libs/components/src/item/item.component.html
Normal file
21
libs/components/src/item/item.component.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!-- TODO: Colors will be finalized in the extension refresh feature branch -->
|
||||||
|
<div
|
||||||
|
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-bg-primary-300/20 tw-text-main tw-border-solid tw-border-b tw-border-0 tw-rounded-lg tw-mb-1.5"
|
||||||
|
[ngClass]="
|
||||||
|
focusVisibleWithin()
|
||||||
|
? 'tw-z-10 tw-rounded tw-outline-none tw-ring tw-ring-primary-600 tw-border-transparent'
|
||||||
|
: 'tw-border-b-secondary-300 [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-border-b-transparent'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<bit-item-action class="item-main-content tw-block tw-w-full">
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</bit-item-action>
|
||||||
|
|
||||||
|
<div
|
||||||
|
#endSlot
|
||||||
|
class="tw-p-2 tw-flex tw-gap-1 tw-items-center"
|
||||||
|
[hidden]="endSlot.childElementCount === 0"
|
||||||
|
>
|
||||||
|
<ng-content select="[slot=end]"></ng-content>
|
||||||
|
</div>
|
||||||
|
</div>
|
29
libs/components/src/item/item.component.ts
Normal file
29
libs/components/src/item/item.component.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core";
|
||||||
|
|
||||||
|
import { A11yRowDirective } from "../a11y/a11y-row.directive";
|
||||||
|
|
||||||
|
import { ItemActionComponent } from "./item-action.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-item",
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ItemActionComponent],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: "item.component.html",
|
||||||
|
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
|
||||||
|
})
|
||||||
|
export class ItemComponent extends A11yRowDirective {
|
||||||
|
/**
|
||||||
|
* We have `:focus-within` and `:focus-visible` but no `:focus-visible-within`
|
||||||
|
*/
|
||||||
|
protected focusVisibleWithin = signal(false);
|
||||||
|
@HostListener("focusin", ["$event.target"])
|
||||||
|
onFocusIn(target: HTMLElement) {
|
||||||
|
this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible"));
|
||||||
|
}
|
||||||
|
@HostListener("focusout")
|
||||||
|
onFocusOut() {
|
||||||
|
this.focusVisibleWithin.set(false);
|
||||||
|
}
|
||||||
|
}
|
141
libs/components/src/item/item.mdx
Normal file
141
libs/components/src/item/item.mdx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs";
|
||||||
|
|
||||||
|
import * as stories from "./item.stories";
|
||||||
|
|
||||||
|
<Meta of={stories} />
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ItemModule } from "@bitwarden/components";
|
||||||
|
```
|
||||||
|
|
||||||
|
# Item
|
||||||
|
|
||||||
|
`<bit-item>` is a horizontal card that contains one or more interactive actions.
|
||||||
|
|
||||||
|
It is a generic container that can be used for either standalone content, an alternative to tables,
|
||||||
|
or to list nav links.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.Default} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Primary Content
|
||||||
|
|
||||||
|
The primary content of an item is supplied by `bit-item-content`.
|
||||||
|
|
||||||
|
### Content Types
|
||||||
|
|
||||||
|
The content can be a button, anchor, or static container.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content routerLink="..."> Hi, I am a link. </a>
|
||||||
|
</bit-item>
|
||||||
|
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content (click)="...">And I am a button.</button>
|
||||||
|
</bit-item>
|
||||||
|
|
||||||
|
<bit-item>
|
||||||
|
<bit-item-content> I'm just static :( </bit-item-content>
|
||||||
|
</bit-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.ContentTypes} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
### Content Slots
|
||||||
|
|
||||||
|
`bit-item-content` contains the following slots to help position the content:
|
||||||
|
|
||||||
|
| Slot | Description |
|
||||||
|
| ------------------ | --------------------------------------------------- |
|
||||||
|
| default | primary text or arbitrary content; fan favorite |
|
||||||
|
| `slot="secondary"` | supporting text; under the default slot |
|
||||||
|
| `slot="start"` | commonly an icon or avatar; before the default slot |
|
||||||
|
| `slot="end"` | commonly an icon; after the default slot |
|
||||||
|
|
||||||
|
- Note: There is also an `end` slot within `bit-item` itself. Place
|
||||||
|
[interactive secondary actions](#secondary-actions) there, and place non-interactive content (such
|
||||||
|
as icons) in `bit-item-content`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content type="button">
|
||||||
|
<bit-avatar slot="start" text="Foo"></bit-avatar>
|
||||||
|
foo@bitwarden.com
|
||||||
|
<ng-container slot="secondary">
|
||||||
|
<div>Bitwarden.com</div>
|
||||||
|
<div><em>locked</em></div>
|
||||||
|
</ng-container>
|
||||||
|
<i slot="end" class="bwi bwi-lock" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.ContentSlots} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
## Secondary Actions
|
||||||
|
|
||||||
|
Secondary interactive actions can be placed in the item through the `"end"` slot, outside of
|
||||||
|
`bit-item-content`.
|
||||||
|
|
||||||
|
Each action must be wrapped by `<bit-item-action>`.
|
||||||
|
|
||||||
|
Actions are commonly icon buttons or badge buttons.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>...</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone" aria-label="Copy"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v" aria-label="Options"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Item Groups
|
||||||
|
|
||||||
|
Groups of items can be associated by wrapping them in the `<bit-item-group>`.
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.MultipleActionList} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.SingleActionList} />
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
### A11y
|
||||||
|
|
||||||
|
Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport.
|
||||||
|
|
||||||
|
Item groups utilize arrow-based keyboard navigation
|
||||||
|
([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)).
|
||||||
|
|
||||||
|
Use `aria-label` or `aria-labelledby` to give groups an accessible name.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<bit-item-group aria-label="My Items">
|
||||||
|
<bit-item>...</bit-item>
|
||||||
|
<bit-item>...</bit-item>
|
||||||
|
<bit-item>...</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Virtual Scrolling
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story of={stories.VirtualScrolling} />
|
||||||
|
</Canvas>
|
12
libs/components/src/item/item.module.ts
Normal file
12
libs/components/src/item/item.module.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ItemActionComponent } from "./item-action.component";
|
||||||
|
import { ItemContentComponent } from "./item-content.component";
|
||||||
|
import { ItemGroupComponent } from "./item-group.component";
|
||||||
|
import { ItemComponent } from "./item.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent],
|
||||||
|
exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent],
|
||||||
|
})
|
||||||
|
export class ItemModule {}
|
326
libs/components/src/item/item.stories.ts
Normal file
326
libs/components/src/item/item.stories.ts
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { A11yGridDirective } from "../a11y/a11y-grid.directive";
|
||||||
|
import { AvatarModule } from "../avatar";
|
||||||
|
import { BadgeModule } from "../badge";
|
||||||
|
import { IconButtonModule } from "../icon-button";
|
||||||
|
import { TypographyModule } from "../typography";
|
||||||
|
|
||||||
|
import { ItemActionComponent } from "./item-action.component";
|
||||||
|
import { ItemContentComponent } from "./item-content.component";
|
||||||
|
import { ItemGroupComponent } from "./item-group.component";
|
||||||
|
import { ItemComponent } from "./item.component";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Item",
|
||||||
|
component: ItemComponent,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
ItemGroupComponent,
|
||||||
|
AvatarModule,
|
||||||
|
IconButtonModule,
|
||||||
|
BadgeModule,
|
||||||
|
TypographyModule,
|
||||||
|
ItemActionComponent,
|
||||||
|
ItemContentComponent,
|
||||||
|
A11yGridDirective,
|
||||||
|
ScrollingModule,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
|
||||||
|
],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<ItemGroupComponent>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContentSlots: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content type="button">
|
||||||
|
<bit-avatar
|
||||||
|
slot="start"
|
||||||
|
[text]="'Foo'"
|
||||||
|
></bit-avatar>
|
||||||
|
foo@bitwarden.com
|
||||||
|
<ng-container slot="secondary">
|
||||||
|
<div>Bitwarden.com</div>
|
||||||
|
<div><em>locked</em></div>
|
||||||
|
</ng-container>
|
||||||
|
<i slot="end" class="bwi bwi-lock" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContentTypes: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Hi, I am a link.
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content href="#">
|
||||||
|
And I am a button.
|
||||||
|
</button>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<bit-item-content>
|
||||||
|
I'm just static :(
|
||||||
|
</bit-item-content>
|
||||||
|
</bit-item>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextOverflow: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<div class="tw-text-main tw-mb-4">TODO: Fix truncation</div>
|
||||||
|
<bit-item>
|
||||||
|
<bit-item-content>
|
||||||
|
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
|
||||||
|
</bit-item-content>
|
||||||
|
</bit-item>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultipleActionList: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-item-group aria-label="Multiple Action List">
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
Foo
|
||||||
|
<span slot="secondary">Bar</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SingleActionList: Story = {
|
||||||
|
render: (args) => ({
|
||||||
|
props: args,
|
||||||
|
template: /*html*/ `
|
||||||
|
<bit-item-group aria-label="Single Action List">
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
<bit-item>
|
||||||
|
<a bit-item-content href="#">
|
||||||
|
Foobar
|
||||||
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VirtualScrolling: Story = {
|
||||||
|
render: (_args) => ({
|
||||||
|
props: {
|
||||||
|
data: Array.from(Array(100000).keys()),
|
||||||
|
},
|
||||||
|
template: /*html*/ `
|
||||||
|
<cdk-virtual-scroll-viewport [itemSize]="46" class="tw-h-[500px]">
|
||||||
|
<bit-item-group aria-label="Single Action List">
|
||||||
|
<bit-item *cdkVirtualFor="let item of data">
|
||||||
|
<button bit-item-content>
|
||||||
|
<i slot="start" class="bwi bwi-globe tw-text-3xl tw-text-muted" aria-hidden="true"></i>
|
||||||
|
{{ item }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ng-container slot="end">
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitBadge variant="primary">Auto-fill</button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-clone"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
<bit-item-action>
|
||||||
|
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
|
||||||
|
</bit-item-action>
|
||||||
|
</ng-container>
|
||||||
|
</bit-item>
|
||||||
|
</bit-item-group>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||||
|
|
||||||
import { FocusableElement } from "../input/autofocus.directive";
|
import { FocusableElement } from "../shared/focusable-element";
|
||||||
|
|
||||||
let nextId = 0;
|
let nextId = 0;
|
||||||
|
|
||||||
@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||||||
@Input() disabled: boolean;
|
@Input() disabled: boolean;
|
||||||
@Input() placeholder: string;
|
@Input() placeholder: string;
|
||||||
|
|
||||||
focus() {
|
getFocusTarget() {
|
||||||
this.input.nativeElement.focus();
|
return this.input.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(searchText: string) {
|
onChange(searchText: string) {
|
||||||
|
8
libs/components/src/shared/focusable-element.ts
Normal file
8
libs/components/src/shared/focusable-element.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Interface for implementing focusable components.
|
||||||
|
*
|
||||||
|
* Used by the `AutofocusDirective` and `A11yGridDirective`.
|
||||||
|
*/
|
||||||
|
export abstract class FocusableElement {
|
||||||
|
getFocusTarget: () => HTMLElement;
|
||||||
|
}
|
@ -49,6 +49,6 @@ $card-icons-base: "../../src/billing/images/cards/";
|
|||||||
@import "multi-select/scss/bw.theme.scss";
|
@import "multi-select/scss/bw.theme.scss";
|
||||||
|
|
||||||
// Workaround for https://bitwarden.atlassian.net/browse/CL-110
|
// Workaround for https://bitwarden.atlassian.net/browse/CL-110
|
||||||
#storybook-docs pre.prismjs {
|
.sbdocs-preview pre.prismjs {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user