mirror of
https://github.com/bitwarden/browser
synced 2026-02-13 06:54:07 +00:00
[CL-233] Update form field styles (#9776)
This commit is contained in:
@@ -20,7 +20,7 @@ input[type="search"]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: -cancel-button;
|
||||
}
|
||||
|
||||
label:not(.form-check-label):not(.btn),
|
||||
label:not(.form-check-label):not(.btn):not(:has(bit-label)),
|
||||
label.bold {
|
||||
font-weight: 600;
|
||||
@include themify($themes) {
|
||||
|
||||
@@ -33,12 +33,10 @@
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitButton
|
||||
bitIconButton="bwi-clone"
|
||||
appA11yTitle="{{ 'copyDnsTxtRecord' | i18n }}"
|
||||
(click)="copyDnsTxt()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-callout
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
/>
|
||||
<button
|
||||
bitSuffix
|
||||
bitButton
|
||||
bitLink
|
||||
[disabled]="!enableTestKeyConnector"
|
||||
type="button"
|
||||
(click)="validateKeyConnectorUrl()"
|
||||
@@ -349,14 +349,12 @@
|
||||
<bit-label>{{ "spMetadataUrl" | i18n }}</bit-label>
|
||||
<input bitInput disabled [value]="spMetadataUrl" />
|
||||
<button
|
||||
bitButton
|
||||
bitIconButton="bwi-external-link"
|
||||
bitSuffix
|
||||
type="button"
|
||||
[appLaunchClick]="spMetadataUrl"
|
||||
[appA11yTitle]="'launch' | i18n"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
|
||||
</button>
|
||||
></button>
|
||||
<button
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
|
||||
@@ -94,8 +94,4 @@ export class ButtonComponent implements ButtonLikeAbstraction {
|
||||
@Input() loading = false;
|
||||
|
||||
@Input() disabled = false;
|
||||
|
||||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||
this.buttonType = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ let nextId = 0;
|
||||
@Directive({
|
||||
selector: "bit-hint",
|
||||
host: {
|
||||
class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1",
|
||||
class: "tw-text-muted tw-font-normal tw-inline-block tw-mt-1 tw-text-xs",
|
||||
},
|
||||
})
|
||||
export class BitHintComponent {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
|
||||
@Directive({
|
||||
selector: "bit-label",
|
||||
})
|
||||
export class BitLabel {}
|
||||
export class BitLabel {
|
||||
constructor(private elementRef: ElementRef<HTMLInputElement>) {}
|
||||
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return ["tw-truncate"];
|
||||
}
|
||||
|
||||
@HostBinding("title") get title() {
|
||||
return this.elementRef.nativeElement.textContent;
|
||||
}
|
||||
|
||||
@HostBinding() @Input() id = `bit-label-${nextId++}`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ let nextId = 0;
|
||||
selector: "bit-error",
|
||||
template: `<i class="bwi bwi-error"></i> {{ displayError }}`,
|
||||
host: {
|
||||
class: "tw-block tw-mt-1 tw-text-danger",
|
||||
class: "tw-block tw-mt-1 tw-text-danger tw-text-xs",
|
||||
"aria-live": "assertive",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main" [attr.for]="input.labelForId">
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||
</label>
|
||||
<div class="tw-flex">
|
||||
<div *ngIf="prefixChildren.length" class="tw-flex">
|
||||
<ng-content select="[bitPrefix]"></ng-content>
|
||||
<div class="tw-w-full tw-relative tw-group/bit-form-field">
|
||||
<div
|
||||
class="tw-gap-1 tw-bg-background tw-rounded-lg tw-flex tw-min-h-11 [&:not(:has(button:enabled)):has(input:read-only)]:tw-bg-secondary-100 [&:not(:has(button:enabled)):has(textarea:read-only)]:tw-bg-secondary-100"
|
||||
>
|
||||
<div
|
||||
#prefixSlot
|
||||
[hidden]="prefixSlot.childElementCount === 0"
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pl-3 tw-py-2"
|
||||
>
|
||||
<ng-content select="[bitPrefix]"></ng-content>
|
||||
</div>
|
||||
<div
|
||||
class="tw-w-full tw-relative tw-py-2 has-[bit-select]:tw-p-0 has-[bit-multi-select]:tw-p-0 has-[input:read-only:not([hidden])]:tw-bg-secondary-100 has-[textarea:read-only:not([hidden])]:tw-bg-secondary-100"
|
||||
[ngClass]="[
|
||||
prefixSlot.childElementCount === 0 ? 'tw-rounded-l-lg tw-pl-3' : '',
|
||||
suffixSlot.childElementCount === 0 ? 'tw-rounded-r-lg tw-pr-3' : ''
|
||||
]"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div
|
||||
#suffixSlot
|
||||
[hidden]="suffixSlot.childElementCount === 0"
|
||||
class="tw-flex tw-items-center tw-gap-1 tw-pr-3 tw-py-2"
|
||||
>
|
||||
<ng-content select="[bitSuffix]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
<div *ngIf="suffixChildren.length" class="tw-flex">
|
||||
<ng-content select="[bitSuffix]"></ng-content>
|
||||
<div class="tw-absolute tw-w-full tw-h-full tw-top-0 tw-pointer-events-none">
|
||||
<div class="tw-w-full tw-h-full tw-flex">
|
||||
<div
|
||||
class="tw-min-w-3 tw-border-r-0 group-focus-within/bit-form-field:tw-border-r-0 !tw-rounded-l-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
<div
|
||||
class="tw-px-1 tw-shrink tw-min-w-0 tw-mt-px tw-border-x-0 tw-border-t-0 group-focus-within/bit-form-field:tw-border-x-0 group-focus-within/bit-form-field:tw-border-t-0 tw-hidden group-has-[bit-label]/bit-form-field:tw-block"
|
||||
[ngClass]="inputBorderClasses"
|
||||
>
|
||||
<label
|
||||
class="tw-flex tw-gap-1 tw-text-sm tw-text-muted -tw-translate-y-2.5 tw-mb-0 tw-max-w-full tw-pointer-events-auto"
|
||||
[attr.for]="input.labelForId"
|
||||
>
|
||||
<ng-content select="bit-label"></ng-content>
|
||||
<span *ngIf="input.required" class="tw-text-[0.625rem]"> ({{ "required" | i18n }})</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="tw-min-w-3 tw-grow tw-border-l-0 group-focus-within/bit-form-field:tw-border-l-0 !tw-rounded-r-lg"
|
||||
[ngClass]="inputBorderClasses"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container [ngSwitch]="input.hasError">
|
||||
|
||||
@@ -3,19 +3,19 @@ import {
|
||||
AfterContentChecked,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
signal,
|
||||
} from "@angular/core";
|
||||
|
||||
import { BitHintComponent } from "../form-control/hint.component";
|
||||
import { BitLabel } from "../form-control/label.directive";
|
||||
import { inputBorderClasses } from "../input/input.directive";
|
||||
|
||||
import { BitErrorComponent } from "./error.component";
|
||||
import { BitFormFieldControl } from "./form-field-control";
|
||||
import { BitPrefixDirective } from "./prefix.directive";
|
||||
import { BitSuffixDirective } from "./suffix.directive";
|
||||
|
||||
@Component({
|
||||
selector: "bit-form-field",
|
||||
@@ -24,12 +24,10 @@ import { BitSuffixDirective } from "./suffix.directive";
|
||||
export class BitFormFieldComponent implements AfterContentChecked {
|
||||
@ContentChild(BitFormFieldControl) input: BitFormFieldControl;
|
||||
@ContentChild(BitHintComponent) hint: BitHintComponent;
|
||||
@ContentChild(BitLabel) label: BitLabel;
|
||||
|
||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||
|
||||
@ContentChildren(BitPrefixDirective) prefixChildren: QueryList<BitPrefixDirective>;
|
||||
@ContentChildren(BitSuffixDirective) suffixChildren: QueryList<BitSuffixDirective>;
|
||||
|
||||
private _disableMargin = false;
|
||||
@Input() set disableMargin(value: boolean | "") {
|
||||
this._disableMargin = coerceBooleanProperty(value);
|
||||
@@ -38,11 +36,50 @@ export class BitFormFieldComponent implements AfterContentChecked {
|
||||
return this._disableMargin;
|
||||
}
|
||||
|
||||
get inputBorderClasses(): string {
|
||||
const shouldFocusBorderAppear = !this.buttonIsFocused();
|
||||
|
||||
const groupClasses = [
|
||||
this.input.hasError
|
||||
? "group-hover/bit-form-field:tw-border-danger-700"
|
||||
: "group-hover/bit-form-field:tw-border-primary-500",
|
||||
"group-focus-within/bit-form-field:tw-outline-none",
|
||||
shouldFocusBorderAppear ? "group-focus-within/bit-form-field:tw-border-2" : "",
|
||||
shouldFocusBorderAppear ? "group-focus-within/bit-form-field:tw-border-primary-500" : "",
|
||||
shouldFocusBorderAppear
|
||||
? "group-focus-within/bit-form-field:group-hover/bit-form-field:tw-border-primary-500"
|
||||
: "",
|
||||
];
|
||||
|
||||
const baseInputBorderClasses = inputBorderClasses(this.input.hasError);
|
||||
|
||||
const borderClasses = baseInputBorderClasses.concat(groupClasses);
|
||||
|
||||
return borderClasses.join(" ");
|
||||
}
|
||||
|
||||
@HostBinding("class")
|
||||
get classList() {
|
||||
return ["tw-block"].concat(this.disableMargin ? [] : ["tw-mb-6"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the currently focused element is a button, then we don't want to show focus on the
|
||||
* input field itself.
|
||||
*
|
||||
* This is necessary because the `tw-group/bit-form-field` wraps the input and any prefix/suffix
|
||||
* buttons
|
||||
*/
|
||||
protected buttonIsFocused = signal(false);
|
||||
@HostListener("focusin", ["$event.target"])
|
||||
onFocusIn(target: HTMLElement) {
|
||||
this.buttonIsFocused.set(target.matches("button"));
|
||||
}
|
||||
@HostListener("focusout")
|
||||
onFocusOut() {
|
||||
this.buttonIsFocused.set(false);
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
if (this.error) {
|
||||
this.input.ariaDescribedBy = this.error.id;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ButtonModule } from "../button";
|
||||
import { CheckboxModule } from "../checkbox";
|
||||
import { IconButtonModule } from "../icon-button";
|
||||
import { InputModule } from "../input/input.module";
|
||||
import { LinkModule } from "../link";
|
||||
import { RadioButtonModule } from "../radio-button";
|
||||
import { SelectModule } from "../select";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
@@ -39,6 +40,7 @@ export default {
|
||||
CheckboxModule,
|
||||
RadioButtonModule,
|
||||
SelectModule,
|
||||
LinkModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
@@ -74,6 +76,7 @@ const defaultFormObj = fb.group({
|
||||
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
|
||||
terms: [false, [Validators.requiredTrue]],
|
||||
updates: ["yes"],
|
||||
file: [""],
|
||||
});
|
||||
|
||||
// Custom error message, `message` is shown as the error message
|
||||
@@ -96,7 +99,7 @@ export const Default: Story = {
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
@@ -108,13 +111,58 @@ export const Default: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const LabelWithIcon: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-field>
|
||||
<bit-label>
|
||||
Label
|
||||
<a href="#">
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
<bit-hint>Optional Hint</bit-hint>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const LongLabel: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj" style="width: 200px">
|
||||
<bit-form-field>
|
||||
<bit-label>
|
||||
Hello I am a very long label with lots of very cool helpful information
|
||||
</bit-label>
|
||||
<input bitInput formControlName="name" />
|
||||
<bit-hint>Optional Hint</bit-hint>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput required placeholder="Placeholder" />
|
||||
@@ -134,7 +182,7 @@ export const Hint: Story = {
|
||||
formObj: formObj,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field [formGroup]="formObj">
|
||||
<bit-label>FormControl</bit-label>
|
||||
<input bitInput formControlName="required" placeholder="Placeholder" />
|
||||
@@ -147,7 +195,7 @@ export const Hint: Story = {
|
||||
export const Disabled: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
@@ -160,7 +208,7 @@ export const Disabled: Story = {
|
||||
export const Readonly: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Input</bit-label>
|
||||
<input bitInput value="Foobar" readonly />
|
||||
@@ -178,7 +226,7 @@ export const Readonly: Story = {
|
||||
export const InputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
@@ -193,13 +241,14 @@ export const InputGroup: Story = {
|
||||
export const ButtonInputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<button bitPrefix bitIconButton="bwi-star"></button>
|
||||
<bit-label>Label</bit-label>
|
||||
<button bitPrefix bitIconButton="bwi-star" aria-label="Favorite"></button>
|
||||
<input bitInput placeholder="Placeholder" />
|
||||
<button bitSuffix bitIconButton="bwi-eye"></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone"></button>
|
||||
<button bitSuffix bitButton>
|
||||
<button bitSuffix bitIconButton="bwi-eye" aria-label="Hide"></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" aria-label="Clone"></button>
|
||||
<button bitSuffix bitLink>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
@@ -211,14 +260,32 @@ export const ButtonInputGroup: Story = {
|
||||
export const DisabledButtonInputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<button bitPrefix bitIconButton="bwi-star" disabled></button>
|
||||
<button bitPrefix bitIconButton="bwi-star" disabled aria-label="Favorite"></button>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
<button bitSuffix bitIconButton="bwi-eye" disabled></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" disabled></button>
|
||||
<button bitSuffix bitButton disabled>
|
||||
<button bitSuffix bitIconButton="bwi-eye" disabled aria-label="Hide"></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" disabled aria-label="Clone"></button>
|
||||
<button bitSuffix bitLink disabled>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const PartiallyDisabledButtonInputGroup: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<input bitInput placeholder="Placeholder" disabled />
|
||||
<button bitSuffix bitIconButton="bwi-eye" aria-label="Hide"></button>
|
||||
<button bitSuffix bitIconButton="bwi-clone" aria-label="Clone"></button>
|
||||
<button bitSuffix bitLink disabled>
|
||||
Apply
|
||||
</button>
|
||||
</bit-form-field>
|
||||
@@ -230,7 +297,7 @@ export const DisabledButtonInputGroup: Story = {
|
||||
export const Select: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<select bitInput>
|
||||
@@ -246,7 +313,7 @@ export const Select: Story = {
|
||||
export const AdvancedSelect: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Label</bit-label>
|
||||
<bit-select>
|
||||
@@ -258,10 +325,40 @@ export const AdvancedSelect: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const FileInput: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: defaultFormObj,
|
||||
submit: submit,
|
||||
...args,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj">
|
||||
<bit-form-field>
|
||||
<bit-label>File</bit-label>
|
||||
<div class="tw-text-main">
|
||||
<button bitButton type="button" buttonType="secondary">
|
||||
Choose File
|
||||
</button>
|
||||
No file chosen
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
hidden
|
||||
/>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Textarea: Story = {
|
||||
render: (args: BitFormFieldComponent) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Textarea</bit-label>
|
||||
<textarea bitInput rows="4"></textarea>
|
||||
|
||||
@@ -66,9 +66,9 @@ export const actionsData = {
|
||||
};
|
||||
|
||||
const fb = new FormBuilder();
|
||||
const formObjFactory = () =>
|
||||
const formObjFactory = (isDisabled = false) =>
|
||||
fb.group({
|
||||
select: [[], [Validators.required]],
|
||||
select: fb.control({ value: [], disabled: isDisabled }, { validators: [Validators.required] }),
|
||||
});
|
||||
|
||||
function submit(formObj: FormGroup) {
|
||||
@@ -85,7 +85,7 @@ export const Loading: Story = {
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
},
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ name }}</bit-label>
|
||||
@@ -100,7 +100,6 @@ export const Loading: Story = {
|
||||
</bit-multi-select>
|
||||
<bit-hint>{{ hint }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
@@ -113,8 +112,33 @@ export const Loading: Story = {
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
...Loading,
|
||||
render: (args) => ({
|
||||
props: {
|
||||
formObj: formObjFactory(true),
|
||||
submit: submit,
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
},
|
||||
template: /*html*/ `
|
||||
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ name }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
formControlName="select"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
<bit-hint>{{ hint }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</form>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
baseItems: [] as any,
|
||||
name: "Disabled",
|
||||
disabled: true,
|
||||
hint: "This is what a disabled multi-select looks like",
|
||||
@@ -269,34 +293,3 @@ export const RemoveSelected: Story = {
|
||||
removeSelectedItems: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Standalone: Story = {
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||
},
|
||||
template: `
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
[baseItems]="baseItems"
|
||||
[removeSelectedItems]="removeSelectedItems"
|
||||
[loading]="loading"
|
||||
[disabled]="disabled"
|
||||
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||
</bit-multi-select>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
baseItems: [
|
||||
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||
],
|
||||
removeSelectedItems: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ type Story = StoryObj<BitPasswordInputToggleDirective>;
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
@@ -58,7 +58,7 @@ export const Default: Story = {
|
||||
export const Binding: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
template: /*html*/ `
|
||||
<form>
|
||||
<bit-form-field>
|
||||
<bit-label>Password</bit-label>
|
||||
|
||||
@@ -1,51 +1,30 @@
|
||||
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
|
||||
import { Directive, HostBinding, Input, Optional } from "@angular/core";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
export const PrefixClasses = [
|
||||
"tw-bg-background-alt",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
"tw-border-secondary-600",
|
||||
"tw-text-muted",
|
||||
"tw-rounded-none",
|
||||
];
|
||||
|
||||
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"];
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
|
||||
@Directive({
|
||||
selector: "[bitPrefix]",
|
||||
})
|
||||
export class BitPrefixDirective implements OnInit {
|
||||
constructor(@Optional() private buttonComponent: ButtonLikeAbstraction) {}
|
||||
|
||||
export class BitPrefixDirective {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
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);
|
||||
return ["tw-text-muted"];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.buttonComponent?.setButtonType("unstyled");
|
||||
@HostBinding("attr.aria-describedby")
|
||||
get ariaDescribedBy() {
|
||||
return this.parentFormField?.label?.id || null;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Optional() private parentFormField: BitFormFieldComponent,
|
||||
@Optional() private iconButtonComponent: BitIconButtonComponent,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.iconButtonComponent) {
|
||||
this.iconButtonComponent.size = "small";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { Directive, HostBinding, Input, Optional } from "@angular/core";
|
||||
|
||||
import { ButtonLikeAbstraction } from "../shared/button-like.abstraction";
|
||||
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
|
||||
|
||||
import { PrefixButtonClasses, PrefixClasses, PrefixStaticContentClasses } from "./prefix.directive";
|
||||
import { BitFormFieldComponent } from "./form-field.component";
|
||||
|
||||
@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",
|
||||
|
||||
"focus-visible:tw-border-l",
|
||||
"focus-visible:tw-ml-[-1px]",
|
||||
]).concat(this.buttonComponent != undefined ? PrefixButtonClasses : PrefixStaticContentClasses);
|
||||
return ["tw-text-muted"];
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.buttonComponent?.setButtonType("unstyled");
|
||||
@HostBinding("attr.aria-describedby")
|
||||
get ariaDescribedBy() {
|
||||
return this.parentFormField?.label?.id || null;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Optional() private parentFormField: BitFormFieldComponent,
|
||||
@Optional() private iconButtonComponent: BitIconButtonComponent,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.iconButtonComponent) {
|
||||
this.iconButtonComponent.size = "small";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +163,6 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableE
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
|
||||
setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") {
|
||||
this.buttonType = value;
|
||||
}
|
||||
|
||||
getFocusTarget() {
|
||||
return this.elementRef.nativeElement;
|
||||
}
|
||||
|
||||
@@ -11,41 +11,43 @@ import {
|
||||
import { NgControl, Validators } from "@angular/forms";
|
||||
|
||||
import { BitFormFieldControl, InputTypes } from "../form-field/form-field-control";
|
||||
import { BitFormFieldComponent } from "../form-field/form-field.component";
|
||||
|
||||
// Increments for each instance of this component
|
||||
let nextId = 0;
|
||||
|
||||
export function inputBorderClasses(error: boolean) {
|
||||
return [
|
||||
"tw-border",
|
||||
"!tw-border-solid",
|
||||
error ? "tw-border-danger-600" : "tw-border-secondary-500",
|
||||
"focus:tw-outline-none",
|
||||
];
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
||||
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
|
||||
})
|
||||
export class BitInputDirective implements BitFormFieldControl {
|
||||
@HostBinding("class") @Input() get classList() {
|
||||
return [
|
||||
const classes = [
|
||||
"tw-block",
|
||||
"tw-w-full",
|
||||
"tw-px-3",
|
||||
"tw-py-1.5",
|
||||
"tw-bg-background-alt",
|
||||
"tw-border",
|
||||
"tw-border-solid",
|
||||
this.hasError ? "tw-border-danger-600" : "tw-border-secondary-600",
|
||||
"tw-h-full",
|
||||
"tw-text-main",
|
||||
"tw-placeholder-text-muted",
|
||||
// Rounded
|
||||
"tw-rounded-none",
|
||||
"first:tw-rounded-l",
|
||||
"last:tw-rounded-r",
|
||||
// Focus
|
||||
"tw-bg-background",
|
||||
"tw-border-none",
|
||||
"focus:tw-outline-none",
|
||||
"focus:tw-border-primary-700",
|
||||
"focus:tw-ring-1",
|
||||
"focus:tw-ring-inset",
|
||||
"focus:tw-ring-primary-700",
|
||||
"focus:tw-z-10",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
"[&:is(input,textarea):read-only]:tw-bg-secondary-100",
|
||||
].filter((s) => s != "");
|
||||
];
|
||||
|
||||
if (this.parentFormField === null) {
|
||||
classes.push(...inputBorderClasses(this.hasError), ...this.standaloneInputClasses);
|
||||
}
|
||||
|
||||
return classes.filter((s) => s != "");
|
||||
}
|
||||
|
||||
@HostBinding() @Input() id = `bit-input-${nextId++}`;
|
||||
@@ -105,6 +107,7 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
@Optional() @Self() private ngControl: NgControl,
|
||||
private ngZone: NgZone,
|
||||
private elementRef: ElementRef<HTMLInputElement>,
|
||||
@Optional() private parentFormField: BitFormFieldComponent,
|
||||
) {}
|
||||
|
||||
focus() {
|
||||
@@ -114,4 +117,23 @@ export class BitInputDirective implements BitFormFieldControl {
|
||||
this.elementRef.nativeElement.focus();
|
||||
});
|
||||
}
|
||||
|
||||
get standaloneInputClasses() {
|
||||
return [
|
||||
"tw-px-3",
|
||||
"tw-py-2",
|
||||
"tw-rounded-lg",
|
||||
// Hover
|
||||
this.hasError ? "hover:tw-border-danger-700" : "hover:tw-border-primary-500",
|
||||
// Focus
|
||||
"focus:hover:tw-border-primary-500",
|
||||
"disabled:tw-bg-secondary-100",
|
||||
"disabled:hover:tw-border-secondary-500",
|
||||
"focus:tw-border-primary-500",
|
||||
"focus:tw-ring-1",
|
||||
"focus:tw-ring-inset",
|
||||
"focus:tw-ring-primary-500",
|
||||
"focus:tw-z-10",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { hasModifierKey } from "@angular/cdk/keycodes";
|
||||
import {
|
||||
Component,
|
||||
@@ -39,7 +40,7 @@ export class MultiSelectComponent implements OnInit, BitFormFieldControl, Contro
|
||||
@Input() removeSelectedItems = false;
|
||||
@Input() placeholder: string;
|
||||
@Input() loading = false;
|
||||
@Input() disabled = false;
|
||||
@Input({ transform: coerceBooleanProperty }) disabled?: boolean;
|
||||
|
||||
// Internal tracking of selected items
|
||||
protected selectedItems: SelectItemView[];
|
||||
|
||||
@@ -9,19 +9,18 @@ $ng-select-highlight: rgb(var(--color-primary-700)) !default;
|
||||
$ng-select-primary-text: rgb(var(--color-text-main)) !default;
|
||||
$ng-select-disabled-text: rgb(var(--color-secondary-100)) !default;
|
||||
$ng-select-border: rgb(var(--color-secondary-600)) !default;
|
||||
$ng-select-border-radius: 4px !default;
|
||||
$ng-select-bg: rgb(var(--color-background-alt)) !default;
|
||||
$ng-select-border-radius: 0.5rem !default;
|
||||
$ng-select-bg: rgb(var(--color-background)) !default;
|
||||
$ng-select-selected: transparent !default;
|
||||
$ng-select-selected-alt: rgb(var(--color-text-main) / 0.06) !default;
|
||||
$ng-select-selected-text: $ng-select-primary-text !default;
|
||||
|
||||
$ng-select-marked: rgb(var(--color-text-main) / 0.12) !default;
|
||||
$ng-select-marked: rgb(var(--color-primary-100)) !default;
|
||||
$ng-select-marked-text: $ng-select-primary-text !default;
|
||||
|
||||
$ng-select-box-shadow: none !default;
|
||||
$ng-select-placeholder: rgb(var(--color-text-muted)) !default;
|
||||
$ng-select-height: 35px !default;
|
||||
$ng-select-value-padding-left: 10px !default;
|
||||
$ng-select-height: 100%;
|
||||
$ng-select-value-padding-left: 1rem !default;
|
||||
$ng-select-value-font-size: 0.9em !default;
|
||||
$ng-select-value-text: $ng-select-primary-text !default;
|
||||
|
||||
@@ -31,7 +30,7 @@ $ng-select-dropdown-optgroup-text: rgb(var(--color-text-muted)) !default;
|
||||
$ng-select-dropdown-optgroup-marked: $ng-select-dropdown-optgroup-text !default;
|
||||
$ng-select-dropdown-option-bg: $ng-select-dropdown-bg !default;
|
||||
$ng-select-dropdown-option-text: $ng-select-primary-text !default;
|
||||
$ng-select-dropdown-option-disabled: rgb(var(--color-text-muted) / 0.6) !default;
|
||||
$ng-select-dropdown-option-disabled: rgb(var(--color-secondary-300)) !default;
|
||||
|
||||
$ng-select-input-text: $ng-select-primary-text !default;
|
||||
|
||||
@@ -41,12 +40,12 @@ $ng-clear-icon-hover: rgb(var(--color-text-main)) !default;
|
||||
$ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
|
||||
.ng-select {
|
||||
height: $ng-select-height;
|
||||
&.ng-select-opened {
|
||||
> .ng-select-container {
|
||||
background: $ng-select-bg;
|
||||
border-color: $ng-select-border;
|
||||
background: transparent;
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
box-shadow: $ng-select-box-shadow;
|
||||
}
|
||||
.ng-arrow {
|
||||
top: -2px;
|
||||
@@ -100,11 +99,11 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
color: $ng-select-primary-text;
|
||||
background-color: $ng-select-bg;
|
||||
border-radius: $ng-select-border-radius;
|
||||
border: 1px solid $ng-select-border;
|
||||
min-height: $ng-select-height;
|
||||
border: none;
|
||||
height: $ng-select-height;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
box-shadow: 0 1px 0 $ng-dropdown-shadow;
|
||||
box-shadow: $ng-select-box-shadow;
|
||||
}
|
||||
.ng-value-container {
|
||||
align-items: center;
|
||||
@@ -154,9 +153,7 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
.ng-select-container {
|
||||
.ng-value-container {
|
||||
padding-top: 5px;
|
||||
padding-left: 7px;
|
||||
@include rtl {
|
||||
padding-right: 7px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.ng-value {
|
||||
@@ -206,20 +203,8 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ng-input {
|
||||
padding: 0 0 3px 3px;
|
||||
@include rtl {
|
||||
padding: 0 3px 3px 0;
|
||||
}
|
||||
}
|
||||
.ng-placeholder {
|
||||
top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 3px;
|
||||
@include rtl {
|
||||
padding-right: 3px;
|
||||
padding-left: 0;
|
||||
}
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +215,8 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
&:hover .ng-clear {
|
||||
color: $ng-clear-icon-hover;
|
||||
}
|
||||
border-radius: $ng-select-border-radius;
|
||||
text-align: center;
|
||||
}
|
||||
.ng-spinner-zone {
|
||||
padding: 5px 5px 0 0;
|
||||
@@ -262,7 +249,8 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
z-index: 2050 !important;
|
||||
background-color: $ng-select-dropdown-bg;
|
||||
border: 1px solid $ng-select-dropdown-border;
|
||||
box-shadow: 0 1px 0 $ng-dropdown-shadow;
|
||||
border-radius: $ng-select-border-radius;
|
||||
box-shadow: $ng-select-box-shadow;
|
||||
left: 0;
|
||||
&.ng-select-top {
|
||||
bottom: 100%;
|
||||
@@ -335,6 +323,8 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
.ng-dropdown-panel-items {
|
||||
border-radius: $ng-select-border-radius;
|
||||
background: $ng-select-bg;
|
||||
.ng-optgroup {
|
||||
user-select: none;
|
||||
padding: 8px 10px;
|
||||
@@ -357,10 +347,7 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||
.ng-option {
|
||||
background-color: $ng-select-dropdown-option-bg;
|
||||
color: $ng-select-dropdown-option-text;
|
||||
padding: 8px 10px;
|
||||
&.ng-option-selected {
|
||||
background-color: $ng-select-selected-alt;
|
||||
}
|
||||
padding: 0.375rem 0.75rem;
|
||||
&.ng-option-selected.ng-option-marked {
|
||||
background-color: $ng-select-marked;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
type="search"
|
||||
[id]="id"
|
||||
[placeholder]="placeholder ?? ('search' | i18n)"
|
||||
class="tw-rounded-l tw-pl-9"
|
||||
class="tw-pl-9"
|
||||
[ngModel]="searchText"
|
||||
(ngModelChange)="onChange($event)"
|
||||
(blur)="onTouch()"
|
||||
|
||||
@@ -54,7 +54,7 @@ export class SelectComponent<T> implements BitFormFieldControl, ControlValueAcce
|
||||
this.selectedOption = this.findSelectedOption(this.items, this.selectedValue);
|
||||
}
|
||||
|
||||
@HostBinding("class") protected classes = ["tw-block", "tw-w-full"];
|
||||
@HostBinding("class") protected classes = ["tw-block", "tw-w-full", "tw-h-full"];
|
||||
|
||||
@HostBinding()
|
||||
@Input()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { FormFieldModule } from "../form-field";
|
||||
import { MultiSelectComponent } from "../multi-select/multi-select.component";
|
||||
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||
|
||||
@@ -13,7 +14,7 @@ export default {
|
||||
component: SelectComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [SelectModule],
|
||||
imports: [SelectModule, FormFieldModule],
|
||||
providers: [
|
||||
{
|
||||
provide: I18nService,
|
||||
@@ -44,12 +45,17 @@ export const Default: Story = {
|
||||
props: {
|
||||
...args,
|
||||
},
|
||||
template: `<bit-select [disabled]="disabled">
|
||||
<bit-option value="value1" label="Value 1" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value2" label="Value 2" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value3" label="Value 3" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value4" label="Value 4" icon="bwi-collection" disabled></bit-option>
|
||||
</bit-select>`,
|
||||
template: /*html*/ `
|
||||
<bit-form-field>
|
||||
<bit-label>Choose a value</bit-label>
|
||||
<bit-select [disabled]="disabled">
|
||||
<bit-option value="value1" label="Value 1" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value2" label="Value 2" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value3" label="Value 3" icon="bwi-collection"></bit-option>
|
||||
<bit-option value="value4" label="Value 4" icon="bwi-collection" disabled></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
`,
|
||||
}),
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -3,5 +3,4 @@ export type ButtonType = "primary" | "secondary" | "danger" | "unstyled";
|
||||
export abstract class ButtonLikeAbstraction {
|
||||
loading: boolean;
|
||||
disabled: boolean;
|
||||
setButtonType: (value: ButtonType) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user