1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 06:43:35 +00:00

[CL-58] Make icon button compatible with bit suffix directive (#4057)

* [CL-58] feat: add support for modyfing button types from directives

* [CL-58] feat: set button type secondary when used as prefix/suffix

* [CL-58] chore: add example using suffix to async actions story

* [CL-58] feat: update story with examples

* [CL-58] feat: allow buttons to have their style unset

* [CL-58] feat: move all styling into prefix/suffix

* [CL-58] fix: static content prefix/suffix

* [CL-58] fix: add missing bitFormButton to bitAction

* [CL-58] fix: disabled opacity not overriding correctly

* [CL-58] feat: change hover color to muted

* [CL-58] feat: replace undefined with unstyled

* [CL-58] fix: focus borders on input and prefix/suffix

* [CL-58] feat: update production code to use icon button correctly

* [CL-58] refactor: move out button type to common place

* [CL-58] fix: buttons not migrated correctly

* [CL-58] feat: use icon button in password toggle

* [CL-58] fix: remove button icon stories

* [SM-358] Migrate password toggles (#4129)

* [CL-58] fix: missing i18n service in story

* [CL-58] fix: missing bitIconButton directive in export comp

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Andreas Coroiu
2022-12-19 23:14:29 +01:00
committed by GitHub
parent 8c8d4b3e3e
commit 32ec5bdba1
23 changed files with 278 additions and 252 deletions

View File

@@ -11,8 +11,10 @@ import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { AsyncActionsModule } from "../async-actions";
import { ButtonModule } from "../button";
import { CheckboxModule } from "../checkbox";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module";
import { RadioButtonModule } from "../radio-button";
import { I18nMockService } from "../utils/i18n-mock.service";
@@ -31,6 +33,8 @@ export default {
FormFieldModule,
InputModule,
ButtonModule,
IconButtonModule,
AsyncActionsModule,
CheckboxModule,
RadioButtonModule,
],
@@ -177,10 +181,13 @@ const ButtonGroupTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldCom
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<input bitInput placeholder="Placeholder" type="password" />
<button bitSuffix bitButton bitIconButton="bwi-eye"></button>
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
<button bitPrefix bitIconButton="bwi-star"></button>
<input bitInput placeholder="Placeholder" />
<button bitSuffix bitIconButton="bwi-eye"></button>
<button bitSuffix bitIconButton="bwi-clone"></button>
<button bitSuffix bitButton>
Apply
</button>
</bit-form-field>
`,
});
@@ -195,9 +202,13 @@ const DisabledButtonInputGroupTemplate: Story<BitFormFieldComponent> = (
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<button bitPrefix bitIconButton="bwi-star" disabled></button>
<input bitInput placeholder="Placeholder" disabled />
<button bitSuffix bitButton bitIconButton="bwi-eye" disabled></button>
<button bitSuffix bitButton bitIconButton="bwi-clone"></button>
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
<button bitSuffix bitButton disabled>
Apply
</button>
</bit-form-field>
`,
});

View File

@@ -3,13 +3,16 @@ import {
Directive,
EventEmitter,
Host,
HostBinding,
HostListener,
Input,
OnChanges,
Output,
} from "@angular/core";
import { ButtonComponent } from "../button";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
import { BitFormFieldComponent } from "./form-field.component";
@@ -17,9 +20,18 @@ import { BitFormFieldComponent } from "./form-field.component";
selector: "[bitPasswordInputToggle]",
})
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
@Input() toggled = false;
/**
* Whether the input is toggled to show the password.
*/
@HostBinding("attr.aria-pressed") @Input() toggled = false;
@Output() toggledChange = new EventEmitter<boolean>();
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
@HostBinding("attr.aria-label") label = this.i18nService.t("toggleVisibility");
/**
* Click handler to toggle the state of the input type.
*/
@HostListener("click") onClick() {
this.toggled = !this.toggled;
this.toggledChange.emit(this.toggled);
@@ -29,7 +41,11 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
this.formField.input?.focus();
}
constructor(@Host() private button: ButtonComponent, private formField: BitFormFieldComponent) {}
constructor(
@Host() private button: BitIconButtonComponent,
private formField: BitFormFieldComponent,
private i18nService: I18nService
) {}
get icon() {
return this.toggled ? "bwi-eye-slash" : "bwi-eye";

View File

@@ -2,8 +2,12 @@ import { Component, DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { ButtonComponent, ButtonModule } from "../button";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
import { InputModule } from "../input/input.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { BitFormFieldControl } from "./form-field-control";
import { BitFormFieldComponent } from "./form-field.component";
@@ -17,7 +21,7 @@ import { BitPasswordInputToggleDirective } from "./password-input-toggle.directi
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
</form>
`,
@@ -26,21 +30,22 @@ class TestFormFieldComponent {}
describe("PasswordInputToggle", () => {
let fixture: ComponentFixture<TestFormFieldComponent>;
let button: ButtonComponent;
let button: BitIconButtonComponent;
let input: BitFormFieldControl;
let toggle: DebugElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormFieldModule, ButtonModule, InputModule],
imports: [FormFieldModule, IconButtonModule, InputModule],
declarations: [TestFormFieldComponent],
providers: [{ provide: I18nService, useValue: new I18nMockService({}) }],
}).compileComponents();
fixture = TestBed.createComponent(TestFormFieldComponent);
fixture.detectChanges();
toggle = fixture.debugElement.query(By.directive(BitPasswordInputToggleDirective));
const buttonEl = fixture.debugElement.query(By.directive(ButtonComponent));
const buttonEl = fixture.debugElement.query(By.directive(BitIconButtonComponent));
button = buttonEl.componentInstance;
const formFieldEl = fixture.debugElement.query(By.directive(BitFormFieldComponent));
const formField: BitFormFieldComponent = formFieldEl.componentInstance;

View File

@@ -1,8 +1,11 @@
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module";
import { I18nMockService } from "../utils/i18n-mock.service";
import { FormFieldModule } from "./form-field.module";
import { BitPasswordInputToggleDirective } from "./password-input-toggle.directive";
@@ -12,7 +15,13 @@ export default {
component: BitPasswordInputToggleDirective,
decorators: [
moduleMetadata({
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, ButtonModule],
imports: [FormsModule, ReactiveFormsModule, FormFieldModule, InputModule, IconButtonModule],
providers: [
{
provide: I18nService,
useValue: new I18nMockService({ toggleVisibility: "Toggle visibility" }),
},
],
}),
],
parameters: {
@@ -40,7 +49,7 @@ const Template: Story<BitPasswordInputToggleDirective> = (
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle></button>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
</form>
`,
@@ -60,7 +69,7 @@ const TemplateBinding: Story<BitPasswordInputToggleDirective> = (
<bit-form-field>
<bit-label>Password</bit-label>
<input bitInput type="password" />
<button type="button" bitButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle [(toggled)]="toggled"></button>
</bit-form-field>
<label class="tw-text-main">

View File

@@ -1,24 +1,51 @@
import { Directive, HostBinding, Input } from "@angular/core";
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
export const PrefixClasses = [
"tw-block",
"tw-px-3",
"tw-py-1.5",
"tw-bg-background-alt",
"tw-border",
"tw-border-solid",
"tw-border-secondary-500",
"tw-text-muted",
"tw-rounded-none",
"disabled:!tw-text-muted",
"disabled:tw-border-secondary-500",
];
export const PrefixButtonClasses = [
"hover:tw-bg-text-muted",
"hover:tw-text-contrast",
"disabled:tw-opacity-100",
"disabled:tw-bg-secondary-100",
"disabled:hover:tw-bg-secondary-100",
"disabled:hover:tw-text-muted",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-border-primary-700",
"focus-visible:tw-ring-1",
"focus-visible:tw-ring-inset",
"focus-visible:tw-ring-primary-700",
"focus-visible:tw-z-10",
];
export const PrefixStaticContentClasses = ["tw-block", "tw-px-3", "tw-py-1.5"];
@Directive({
selector: "[bitPrefix]",
})
export class BitPrefixDirective {
export class BitPrefixDirective implements OnInit {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat(["tw-border-r-0", "first:tw-rounded-l"]);
return PrefixClasses.concat([
"tw-border-r-0",
"first:tw-rounded-l",
"focus-visible:tw-border-r",
"focus-visible:tw-mr-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
}
}

View File

@@ -1,12 +1,26 @@
import { Directive, HostBinding, Input } from "@angular/core";
import { Directive, HostBinding, Input, Optional } from "@angular/core";
import { PrefixClasses } from "./prefix.directive";
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
@Directive({
selector: "[bitSuffix]",
})
export class BitSuffixDirective {
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
@HostBinding("class") @Input() get classList() {
return PrefixClasses.concat(["tw-border-l-0", "last:tw-rounded-r"]);
return PrefixClasses.concat([
"tw-border-l-0",
"last:tw-rounded-r",
"focus-visible:tw-border-l",
"focus-visible:tw-ml-[-1px]",
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
}
ngOnInit(): void {
this.buttonComponent?.setButtonType("unstyled");
}
}