Merge 10c9b621c9
into c21a58f2fb
This commit is contained in:
commit
7a265066de
|
@ -0,0 +1,23 @@
|
|||
import { AfterContentChecked, ContentChild, Directive, HostBinding } from "@angular/core";
|
||||
|
||||
import { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
@Directive({
|
||||
selector: "bitA11yCell",
|
||||
standalone: true,
|
||||
})
|
||||
export class A11yCellDirective implements AfterContentChecked {
|
||||
@HostBinding("attr.role")
|
||||
role = "gridcell";
|
||||
|
||||
@ContentChild(FocusableElement)
|
||||
focusableChild: FocusableElement;
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
if (!this.focusableChild) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("A11yCellDirective must contain content that provides FocusableElement");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
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 {
|
||||
this.grid = this.rows.map((listItem) => [...listItem.cells]);
|
||||
this.grid.flat().map((cell) => (cell.focusableChild.getFocusTarget().tabIndex = -1));
|
||||
|
||||
this.getActiveCellContent().tabIndex = 0;
|
||||
}
|
||||
|
||||
/** Get the focusable content of the active cell */
|
||||
private getActiveCellContent(): HTMLElement {
|
||||
return this.grid[this.activeRow][this.activeCol].focusableChild.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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
||||
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 { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
|
||||
|
||||
const styles: Record<BadgeVariant, string[]> = {
|
||||
|
@ -22,8 +24,9 @@ const hoverStyles: Record<BadgeVariant, string[]> = {
|
|||
|
||||
@Directive({
|
||||
selector: "span[bitBadge], a[bitBadge], button[bitBadge]",
|
||||
providers: [{ provide: FocusableElement, useExisting: BadgeDirective }],
|
||||
})
|
||||
export class BadgeDirective {
|
||||
export class BadgeDirective implements FocusableElement {
|
||||
@HostBinding("class") get classList() {
|
||||
return [
|
||||
"tw-inline-block",
|
||||
|
@ -62,6 +65,10 @@ export class BadgeDirective {
|
|||
*/
|
||||
@Input() truncate = true;
|
||||
|
||||
getFocusTarget() {
|
||||
return this.el.nativeElement;
|
||||
}
|
||||
|
||||
private hasHoverEffects = false;
|
||||
|
||||
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 { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light";
|
||||
|
||||
|
@ -123,9 +124,12 @@ const sizes: Record<IconButtonSize, string[]> = {
|
|||
@Component({
|
||||
selector: "button[bitIconButton]:not(button[bitButton])",
|
||||
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() buttonType: IconButtonType;
|
||||
|
@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction {
|
|||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||
this.buttonType = value;
|
||||
}
|
||||
|
||||
getFocusTarget() {
|
||||
return this.elementRef.nativeElement;
|
||||
}
|
||||
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,7 @@ import { take } from "rxjs/operators";
|
|||
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
/**
|
||||
* Interface for implementing focusable components. Used by the AutofocusDirective.
|
||||
*/
|
||||
export abstract class FocusableElement {
|
||||
focus: () => void;
|
||||
}
|
||||
import { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
/**
|
||||
* Directive to focus an element.
|
||||
|
@ -46,7 +41,7 @@ export class AutofocusDirective {
|
|||
|
||||
private focus() {
|
||||
if (this.focusableElement) {
|
||||
this.focusableElement.focus();
|
||||
this.focusableElement.getFocusTarget().focus();
|
||||
} else {
|
||||
this.el.nativeElement.focus();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { A11yCellDirective } from "../a11y/a11y-cell.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-item-action",
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `<ng-content></ng-content>`,
|
||||
providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }],
|
||||
})
|
||||
export class ItemActionComponent extends A11yCellDirective {}
|
|
@ -0,0 +1,32 @@
|
|||
<!-- TODO: Colors will be finalized in the extension refresh feature branch -->
|
||||
<div
|
||||
class="tw-box-border tw-overflow-auto tw-flex tw-bg-background [&:hover:not(:has(.end-slot: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 [&:hover:not(:has(.end-slot:hover))]:tw-border-b-transparent'
|
||||
"
|
||||
>
|
||||
<!-- TODO render as anchor -->
|
||||
<bit-item-action class="tw-block tw-w-full">
|
||||
<button
|
||||
bitFocusableElement
|
||||
type="button"
|
||||
class="fvw-target tw-outline-none tw-text-main tw-text-base tw-p-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between"
|
||||
>
|
||||
<div class="tw-flex tw-gap-2 tw-items-center">
|
||||
<i *ngIf="iconStart" class="bwi tw-text-[1.75rem] tw-text-muted" [ngClass]="iconStart"></i>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<i *ngIf="iconEnd" class="bwi tw-text-2xl tw-text-main" [ngClass]="iconEnd"></i>
|
||||
</button>
|
||||
</bit-item-action>
|
||||
|
||||
<div
|
||||
#endSlot
|
||||
class="end-slot tw-p-2 tw-flex tw-gap-2 tw-items-center"
|
||||
[hidden]="endSlot.childElementCount === 0"
|
||||
>
|
||||
<ng-content select="bit-item-action"></ng-content>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
|
||||
import { A11yRowDirective } from "../a11y/a11y-row.directive";
|
||||
import { FocusableElementDirective } from "../shared/focusable-element";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
import { ItemActionComponent } from "./item-action.component";
|
||||
|
||||
@Component({
|
||||
selector: "bit-item",
|
||||
standalone: true,
|
||||
imports: [CommonModule, TypographyModule, ItemActionComponent, FocusableElementDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: "item.component.html",
|
||||
providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }],
|
||||
})
|
||||
export class ItemComponent extends A11yRowDirective {
|
||||
@Input()
|
||||
iconStart: string | null = null;
|
||||
|
||||
@Input()
|
||||
iconEnd: string | null = null;
|
||||
|
||||
@Output()
|
||||
mainContentClicked: EventEmitter<MouseEvent> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { A11yGridDirective } from "../a11y/a11y-grid.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-list",
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `<ng-content></ng-content>`,
|
||||
})
|
||||
export class ListComponent extends A11yGridDirective {}
|
|
@ -0,0 +1,17 @@
|
|||
import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
|
||||
|
||||
import * as stories from "./list.stories";
|
||||
|
||||
<Meta of={stories} />
|
||||
|
||||
# List
|
||||
|
||||
The `BitListComponent` is a container that displays one or more instances of `BitItemComponent`.
|
||||
|
||||
## Icons
|
||||
|
||||
## Primary Action
|
||||
|
||||
## Secondary Actions
|
||||
|
||||
## A11y
|
|
@ -0,0 +1,206 @@
|
|||
import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { AvatarModule } from "../avatar";
|
||||
import { BadgeModule } from "../badge";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { TypographyModule } from "../typography";
|
||||
|
||||
import { ItemActionComponent } from "./item-action.component";
|
||||
import { ItemComponent } from "./item.component";
|
||||
import { ListComponent } from "./list.component";
|
||||
|
||||
export default {
|
||||
title: "Component Library/List",
|
||||
component: ListComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
ItemComponent,
|
||||
AvatarModule,
|
||||
IconButtonModule,
|
||||
BadgeModule,
|
||||
TypographyModule,
|
||||
ItemActionComponent,
|
||||
],
|
||||
}),
|
||||
componentWrapperDecorator((story) => `<div class="tw-bg-background-alt tw-p-2">${story}</div>`),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<ListComponent>;
|
||||
|
||||
export const StandaloneItem: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const CustomContent: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-item iconEnd="bwi-lock">
|
||||
<bit-avatar slot="start" size="small" text="Baz"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col tw-items-start">
|
||||
<span>baz@bitwarden.com</span>
|
||||
<span bitTypography="helper" class="tw-text-muted">bitwarden.com</span>
|
||||
<span bitTypography="helper" class="tw-text-muted"><em>locked</em></span>
|
||||
</div>
|
||||
</bit-item>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SingleActionList: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-list>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
<bit-item iconEnd="bwi-angle-right">
|
||||
Foo
|
||||
</bit-item>
|
||||
</bit-list>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const MultiActionList: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-list>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
<bit-item iconStart="bwi-globe">
|
||||
Bar
|
||||
<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>
|
||||
</bit-item>
|
||||
</bit-list>
|
||||
`,
|
||||
}),
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, ElementRef, Input, ViewChild } from "@angular/core";
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
|
||||
|
||||
import { FocusableElement } from "../input/autofocus.directive";
|
||||
import { FocusableElement } from "../shared/focusable-element";
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
|
@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
|
|||
@Input() disabled: boolean;
|
||||
@Input() placeholder: string;
|
||||
|
||||
focus() {
|
||||
this.input.nativeElement.focus();
|
||||
getFocusTarget() {
|
||||
return this.input.nativeElement;
|
||||
}
|
||||
|
||||
onChange(searchText: string) {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Directive, ElementRef } from "@angular/core";
|
||||
|
||||
/**
|
||||
* Interface for implementing focusable components. Used by the AutofocusDirective.
|
||||
*/
|
||||
export abstract class FocusableElement {
|
||||
getFocusTarget: () => HTMLElement;
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "[bitFocusableElement]",
|
||||
standalone: true,
|
||||
providers: [{ provide: FocusableElement, useExisting: FocusableElementDirective }],
|
||||
})
|
||||
export class FocusableElementDirective implements FocusableElement {
|
||||
constructor(private elementRef: ElementRef) {}
|
||||
|
||||
getFocusTarget() {
|
||||
return this.elementRef.nativeElement;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue