1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02:00

[EC-667] Collection modal add search function (#4291)

* [EC-667] feat: scaffold new select component

* [EC-667] feat: sort of working implementation

* [EC-667] feat: support for using in forms

* [EC-667] feat: add bit-select example to full form

* [EC-667] fix: broken aria label connetion

* [EC-667] fix: web not building

* [EC-667] fix: dropdown getting trapped in dialog

* [EC-667] fix: select not emitting correct value

* [EC-667] feat: add collection icon to options

* [EC-667] feat: add default select placeholder translation

* [EC-667] fix: undefined handling

* [EC-667] fix: value vs options race condition

* [EC-667] feat: remove x and add "no collection" option

* [EC-667] chore: add country list disclaimer

* chore: clean up comments

* [EC-667] chore: cleanup commented import

* [EC-667] fix: input text color not applying to single-select
This commit is contained in:
Andreas Coroiu 2023-02-06 15:54:23 +01:00 committed by GitHub
parent a4aa042fc9
commit 55d9ee22ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 608 additions and 17 deletions

View File

@ -30,15 +30,24 @@
<bit-form-field>
<bit-label>{{ "nestCollectionUnder" | i18n }}</bit-label>
<select bitInput formControlName="parent">
<option [ngValue]="null">-</option>
<option *ngIf="deletedParentName" disabled [ngValue]="deletedParentName">
{{ deletedParentName }} ({{ "deleted" | i18n }})
</option>
<option *ngFor="let collection of nestOptions" [ngValue]="collection.name">
{{ collection.name }}
</option>
</select>
<bit-select bitInput formControlName="parent">
<bit-option [value]="undefined" [label]="'noCollection' | i18n"> </bit-option>
<bit-option
*ngIf="deletedParentName"
disabled
icon="bwi-collection"
[value]="deletedParentName"
label="{{ deletedParentName }} ({{ 'deleted' | i18n }})"
>
</bit-option>
<bit-option
*ngFor="let collection of nestOptions"
icon="bwi-collection"
[value]="collection.name"
[label]="collection.name"
>
</bit-option>
</bit-select>
</bit-form-field>
</bit-tab>
<bit-tab label="{{ 'access' | i18n }}">

View File

@ -62,7 +62,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
protected formGroup = this.formBuilder.group({
name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]],
externalId: "",
parent: null as string | null,
parent: undefined as string | undefined,
access: [[] as AccessItemValue[]],
});
protected PermissionMode = PermissionMode;
@ -121,7 +121,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
}
const { name, parent } = parseName(this.collection);
if (parent !== null && !this.nestOptions.find((c) => c.name === parent)) {
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
this.deletedParentName = parent;
}
@ -135,7 +135,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
} else {
this.nestOptions = collections;
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
this.formGroup.patchValue({ parent: parent?.name ?? null });
this.formGroup.patchValue({ parent: parent?.name ?? undefined });
}
this.loading = false;
@ -237,7 +237,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
function parseName(collection: CollectionView) {
const nameParts = collection.name?.split("/");
const name = nameParts[nameParts.length - 1];
const parent = nameParts.length > 1 ? nameParts.slice(0, -1).join("/") : null;
const parent = nameParts.length > 1 ? nameParts.slice(0, -1).join("/") : undefined;
return { name, parent };
}

View File

@ -1,12 +1,14 @@
import { NgModule } from "@angular/core";
import { SelectModule } from "@bitwarden/components";
import { SharedModule } from "../../../../shared/shared.module";
import { AccessSelectorModule } from "../access-selector";
import { CollectionDialogComponent } from "./collection-dialog.component";
@NgModule({
imports: [SharedModule, AccessSelectorModule],
imports: [SharedModule, AccessSelectorModule, SelectModule],
declarations: [CollectionDialogComponent],
exports: [CollectionDialogComponent],
})

View File

@ -5670,6 +5670,9 @@
},
"customColor": {
"message": "Custom Color"
},
"selectPlaceholder": {
"message": "-- Select --"
},
"multiSelectPlaceholder": {
"message": "-- Type to filter --"
@ -6000,6 +6003,9 @@
"collection": {
"message": "Collection"
},
"noCollection": {
"message": "No collection"
},
"canView": {
"message": "Can view"
},

View File

@ -17,6 +17,7 @@ import { CheckboxModule } from "../checkbox";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module";
import { RadioButtonModule } from "../radio-button";
import { SelectModule } from "../select";
import { I18nMockService } from "../utils/i18n-mock.service";
import { BitFormFieldComponent } from "./form-field.component";
@ -37,12 +38,14 @@ export default {
AsyncActionsModule,
CheckboxModule,
RadioButtonModule,
SelectModule,
],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
selectPlaceholder: "-- Select --",
required: "required",
inputRequired: "Input is required.",
inputEmail: "Input is not an email-address.",
@ -232,6 +235,22 @@ const SelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponen
export const Select = SelectTemplate.bind({});
Select.args = {};
const AdvancedSelectTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `
<bit-form-field>
<bit-label>Label</bit-label>
<bit-select>
<bit-option label="Select"></bit-option>
<bit-option label="Other"></bit-option>
</bit-select>
</bit-form-field>
`,
});
export const AdvancedSelect = AdvancedSelectTemplate.bind({});
AdvancedSelectTemplate.args = {};
const TextareaTemplate: Story<BitFormFieldComponent> = (args: BitFormFieldComponent) => ({
props: args,
template: `

View File

@ -0,0 +1,253 @@
// DISCLAIMER: This is not an official list and should only used
// to provide realistic example data in stories.
export const countries = [
{ value: "US", name: "United States" },
{ value: "CN", name: "China" },
{ value: "FR", name: "France" },
{ value: "DE", name: "Germany" },
{ value: "CA", name: "Canada" },
{ value: "GB", name: "United Kingdom" },
{ value: "AU", name: "Australia" },
{ value: "IN", name: "India" },
{ value: "AF", name: "Afghanistan" },
{ value: "AX", name: "Åland Islands" },
{ value: "AL", name: "Albania" },
{ value: "DZ", name: "Algeria" },
{ value: "AS", name: "American Samoa" },
{ value: "AD", name: "Andorra" },
{ value: "AO", name: "Angola" },
{ value: "AI", name: "Anguilla" },
{ value: "AQ", name: "Antarctica" },
{ value: "AG", name: "Antigua and Barbuda" },
{ value: "AR", name: "Argentina" },
{ value: "AM", name: "Armenia" },
{ value: "AW", name: "Aruba" },
{ value: "AT", name: "Austria" },
{ value: "AZ", name: "Azerbaijan" },
{ value: "BS", name: "Bahamas" },
{ value: "BH", name: "Bahrain" },
{ value: "BD", name: "Bangladesh" },
{ value: "BB", name: "Barbados" },
{ value: "BY", name: "Belarus" },
{ value: "BE", name: "Belgium" },
{ value: "BZ", name: "Belize" },
{ value: "BJ", name: "Benin" },
{ value: "BM", name: "Bermuda" },
{ value: "BT", name: "Bhutan" },
{ value: "BO", name: "Bolivia, Plurinational State of" },
{ value: "BQ", name: "Bonaire, Sint Eustatius and Saba" },
{ value: "BA", name: "Bosnia and Herzegovina" },
{ value: "BW", name: "Botswana" },
{ value: "BV", name: "Bouvet Island" },
{ value: "BR", name: "Brazil" },
{ value: "IO", name: "British Indian Ocean Territory" },
{ value: "BN", name: "Brunei Darussalam" },
{ value: "BG", name: "Bulgaria" },
{ value: "BF", name: "Burkina Faso" },
{ value: "BI", name: "Burundi" },
{ value: "KH", name: "Cambodia" },
{ value: "CM", name: "Cameroon" },
{ value: "CV", name: "Cape Verde" },
{ value: "KY", name: "Cayman Islands" },
{ value: "CF", name: "Central African Republic" },
{ value: "TD", name: "Chad" },
{ value: "CL", name: "Chile" },
{ value: "CX", name: "Christmas Island" },
{ value: "CC", name: "Cocos (Keeling) Islands" },
{ value: "CO", name: "Colombia" },
{ value: "KM", name: "Comoros" },
{ value: "CG", name: "Congo" },
{ value: "CD", name: "Congo, the Democratic Republic of the" },
{ value: "CK", name: "Cook Islands" },
{ value: "CR", name: "Costa Rica" },
{ value: "CI", name: "Côte d'Ivoire" },
{ value: "HR", name: "Croatia" },
{ value: "CU", name: "Cuba" },
{ value: "CW", name: "Curaçao" },
{ value: "CY", name: "Cyprus" },
{ value: "CZ", name: "Czech Republic" },
{ value: "DK", name: "Denmark" },
{ value: "DJ", name: "Djibouti" },
{ value: "DM", name: "Dominica" },
{ value: "DO", name: "Dominican Republic" },
{ value: "EC", name: "Ecuador" },
{ value: "EG", name: "Egypt" },
{ value: "SV", name: "El Salvador" },
{ value: "GQ", name: "Equatorial Guinea" },
{ value: "ER", name: "Eritrea" },
{ value: "EE", name: "Estonia" },
{ value: "ET", name: "Ethiopia" },
{ value: "FK", name: "Falkland Islands (Malvinas)" },
{ value: "FO", name: "Faroe Islands" },
{ value: "FJ", name: "Fiji" },
{ value: "FI", name: "Finland" },
{ value: "GF", name: "French Guiana" },
{ value: "PF", name: "French Polynesia" },
{ value: "TF", name: "French Southern Territories" },
{ value: "GA", name: "Gabon" },
{ value: "GM", name: "Gambia" },
{ value: "GE", name: "Georgia" },
{ value: "GH", name: "Ghana" },
{ value: "GI", name: "Gibraltar" },
{ value: "GR", name: "Greece" },
{ value: "GL", name: "Greenland" },
{ value: "GD", name: "Grenada" },
{ value: "GP", name: "Guadeloupe" },
{ value: "GU", name: "Guam" },
{ value: "GT", name: "Guatemala" },
{ value: "GG", name: "Guernsey" },
{ value: "GN", name: "Guinea" },
{ value: "GW", name: "Guinea-Bissau" },
{ value: "GY", name: "Guyana" },
{ value: "HT", name: "Haiti" },
{ value: "HM", name: "Heard Island and McDonald Islands" },
{ value: "VA", name: "Holy See (Vatican City State)" },
{ value: "HN", name: "Honduras" },
{ value: "HK", name: "Hong Kong" },
{ value: "HU", name: "Hungary" },
{ value: "IS", name: "Iceland" },
{ value: "ID", name: "Indonesia" },
{ value: "IR", name: "Iran, Islamic Republic of" },
{ value: "IQ", name: "Iraq" },
{ value: "IE", name: "Ireland" },
{ value: "IM", name: "Isle of Man" },
{ value: "IL", name: "Israel" },
{ value: "IT", name: "Italy" },
{ value: "JM", name: "Jamaica" },
{ value: "JP", name: "Japan" },
{ value: "JE", name: "Jersey" },
{ value: "JO", name: "Jordan" },
{ value: "KZ", name: "Kazakhstan" },
{ value: "KE", name: "Kenya" },
{ value: "KI", name: "Kiribati" },
{ value: "KP", name: "Korea, Democratic People's Republic of" },
{ value: "KR", name: "Korea, Republic of" },
{ value: "KW", name: "Kuwait" },
{ value: "KG", name: "Kyrgyzstan" },
{ value: "LA", name: "Lao People's Democratic Republic" },
{ value: "LV", name: "Latvia" },
{ value: "LB", name: "Lebanon" },
{ value: "LS", name: "Lesotho" },
{ value: "LR", name: "Liberia" },
{ value: "LY", name: "Libya" },
{ value: "LI", name: "Liechtenstein" },
{ value: "LT", name: "Lithuania" },
{ value: "LU", name: "Luxembourg" },
{ value: "MO", name: "Macao" },
{ value: "MK", name: "Macedonia, the former Yugoslav Republic of" },
{ value: "MG", name: "Madagascar" },
{ value: "MW", name: "Malawi" },
{ value: "MY", name: "Malaysia" },
{ value: "MV", name: "Maldives" },
{ value: "ML", name: "Mali" },
{ value: "MT", name: "Malta" },
{ value: "MH", name: "Marshall Islands" },
{ value: "MQ", name: "Martinique" },
{ value: "MR", name: "Mauritania" },
{ value: "MU", name: "Mauritius" },
{ value: "YT", name: "Mayotte" },
{ value: "MX", name: "Mexico" },
{ value: "FM", name: "Micronesia, Federated States of" },
{ value: "MD", name: "Moldova, Republic of" },
{ value: "MC", name: "Monaco" },
{ value: "MN", name: "Mongolia" },
{ value: "ME", name: "Montenegro" },
{ value: "MS", name: "Montserrat" },
{ value: "MA", name: "Morocco" },
{ value: "MZ", name: "Mozambique" },
{ value: "MM", name: "Myanmar" },
{ value: "NA", name: "Namibia" },
{ value: "NR", name: "Nauru" },
{ value: "NP", name: "Nepal" },
{ value: "NL", name: "Netherlands" },
{ value: "NC", name: "New Caledonia" },
{ value: "NZ", name: "New Zealand" },
{ value: "NI", name: "Nicaragua" },
{ value: "NE", name: "Niger" },
{ value: "NG", name: "Nigeria" },
{ value: "NU", name: "Niue" },
{ value: "NF", name: "Norfolk Island" },
{ value: "MP", name: "Northern Mariana Islands" },
{ value: "NO", name: "Norway" },
{ value: "OM", name: "Oman" },
{ value: "PK", name: "Pakistan" },
{ value: "PW", name: "Palau" },
{ value: "PS", name: "Palestinian Territory, Occupied" },
{ value: "PA", name: "Panama" },
{ value: "PG", name: "Papua New Guinea" },
{ value: "PY", name: "Paraguay" },
{ value: "PE", name: "Peru" },
{ value: "PH", name: "Philippines" },
{ value: "PN", name: "Pitcairn" },
{ value: "PL", name: "Poland" },
{ value: "PT", name: "Portugal" },
{ value: "PR", name: "Puerto Rico" },
{ value: "QA", name: "Qatar" },
{ value: "RE", name: "Réunion" },
{ value: "RO", name: "Romania" },
{ value: "RU", name: "Russian Federation" },
{ value: "RW", name: "Rwanda" },
{ value: "BL", name: "Saint Barthélemy" },
{ value: "SH", name: "Saint Helena, Ascension and Tristan da Cunha" },
{ value: "KN", name: "Saint Kitts and Nevis" },
{ value: "LC", name: "Saint Lucia" },
{ value: "MF", name: "Saint Martin (French part)" },
{ value: "PM", name: "Saint Pierre and Miquelon" },
{ value: "VC", name: "Saint Vincent and the Grenadines" },
{ value: "WS", name: "Samoa" },
{ value: "SM", name: "San Marino" },
{ value: "ST", name: "Sao Tome and Principe" },
{ value: "SA", name: "Saudi Arabia" },
{ value: "SN", name: "Senegal" },
{ value: "RS", name: "Serbia" },
{ value: "SC", name: "Seychelles" },
{ value: "SL", name: "Sierra Leone" },
{ value: "SG", name: "Singapore" },
{ value: "SX", name: "Sint Maarten (Dutch part)" },
{ value: "SK", name: "Slovakia" },
{ value: "SI", name: "Slovenia" },
{ value: "SB", name: "Solomon Islands" },
{ value: "SO", name: "Somalia" },
{ value: "ZA", name: "South Africa" },
{ value: "GS", name: "South Georgia and the South Sandwich Islands" },
{ value: "SS", name: "South Sudan" },
{ value: "ES", name: "Spain" },
{ value: "LK", name: "Sri Lanka" },
{ value: "SD", name: "Sudan" },
{ value: "SR", name: "Suriname" },
{ value: "SJ", name: "Svalbard and Jan Mayen" },
{ value: "SZ", name: "Swaziland" },
{ value: "SE", name: "Sweden" },
{ value: "CH", name: "Switzerland" },
{ value: "SY", name: "Syrian Arab Republic" },
{ value: "TW", name: "Taiwan" },
{ value: "TJ", name: "Tajikistan" },
{ value: "TZ", name: "Tanzania, United Republic of" },
{ value: "TH", name: "Thailand" },
{ value: "TL", name: "Timor-Leste" },
{ value: "TG", name: "Togo" },
{ value: "TK", name: "Tokelau" },
{ value: "TO", name: "Tonga" },
{ value: "TT", name: "Trinidad and Tobago" },
{ value: "TN", name: "Tunisia" },
{ value: "TR", name: "Turkey" },
{ value: "TM", name: "Turkmenistan" },
{ value: "TC", name: "Turks and Caicos Islands" },
{ value: "TV", name: "Tuvalu" },
{ value: "UG", name: "Uganda" },
{ value: "UA", name: "Ukraine" },
{ value: "AE", name: "United Arab Emirates" },
{ value: "UM", name: "United States Minor Outlying Islands" },
{ value: "UY", name: "Uruguay" },
{ value: "UZ", name: "Uzbekistan" },
{ value: "VU", name: "Vanuatu" },
{ value: "VE", name: "Venezuela, Bolivarian Republic of" },
{ value: "VN", name: "Viet Nam" },
{ value: "VG", name: "Virgin Islands, British" },
{ value: "VI", name: "Virgin Islands, U.S." },
{ value: "WF", name: "Wallis and Futuna" },
{ value: "EH", name: "Western Sahara" },
{ value: "YE", name: "Yemen" },
{ value: "ZM", name: "Zambia" },
{ value: "ZW", name: "Zimbabwe" },
];

View File

@ -17,8 +17,11 @@ import { FormControlModule } from "../form-control";
import { FormFieldModule } from "../form-field";
import { InputModule } from "../input/input.module";
import { RadioButtonModule } from "../radio-button";
import { SelectModule } from "../select";
import { I18nMockService } from "../utils/i18n-mock.service";
import { countries } from "./countries";
export default {
title: "Component Library/Form",
decorators: [
@ -32,12 +35,14 @@ export default {
FormControlModule,
CheckboxModule,
RadioButtonModule,
SelectModule,
],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
selectPlaceholder: "-- Select --",
required: "required",
checkboxRequired: "Option is required",
inputRequired: "Input is required.",
@ -60,6 +65,7 @@ const fb = new FormBuilder();
const exampleFormObj = fb.group({
name: ["", [Validators.required]],
email: ["", [Validators.required, Validators.email, forbiddenNameValidator(/bit/i)]],
country: [undefined as string | undefined, [Validators.required]],
terms: [false, [Validators.requiredTrue]],
updates: ["yes"],
});
@ -90,6 +96,13 @@ const FullExampleTemplate: Story = (args) => ({
<input bitInput formControlName="email" />
</bit-form-field>
<bit-form-field>
<bit-label>Country</bit-label>
<bit-select formControlName="country">
<bit-option *ngFor="let country of countries" [value]="country.value" [label]="country.name"></bit-option>
</bit-select>
</bit-form-field>
<bit-form-control>
<bit-label>Agree to terms</bit-label>
<input type="checkbox" bitCheckbox formControlName="terms">
@ -115,3 +128,6 @@ const FullExampleTemplate: Story = (args) => ({
});
export const FullExample = FullExampleTemplate.bind({});
FullExample.args = {
countries,
};

View File

@ -20,5 +20,6 @@ export * from "./radio-button";
export * from "./table";
export * from "./tabs";
export * from "./toggle-group";
export * from "./select";
export * from "./typography";
export * from "./utils/i18n-mock.service";

View File

@ -115,6 +115,12 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
.ng-placeholder {
color: $ng-select-placeholder;
}
.ng-input {
> input {
color: $ng-select-input-text;
}
}
}
}
&.ng-select-single {
@ -204,9 +210,6 @@ $ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
@include rtl {
padding: 0 3px 3px 0;
}
> input {
color: $ng-select-input-text;
}
}
.ng-placeholder {
top: 5px;

View File

@ -0,0 +1,3 @@
export * from "./select.module";
export * from "./select.component";
export * from "./option.component";

View File

@ -0,0 +1,28 @@
import { coerceBooleanProperty } from "@angular/cdk/coercion";
import { Component, Input } from "@angular/core";
import { Option } from "./option";
@Component({
selector: "bit-option",
template: `<ng-template><ng-content></ng-content></ng-template>`,
})
export class OptionComponent<T = unknown> implements Option<T> {
@Input()
icon?: string;
@Input()
value?: T = undefined;
@Input()
label?: string;
private _disabled = false;
@Input()
get disabled() {
return this._disabled;
}
set disabled(value: boolean | "") {
this._disabled = coerceBooleanProperty(value);
}
}

View File

@ -0,0 +1,8 @@
import { TemplateRef } from "@angular/core";
export interface Option<T> {
icon?: string;
value?: T;
label?: string;
content?: TemplateRef<unknown>;
}

View File

@ -0,0 +1,22 @@
<ng-select
[(ngModel)]="selectedOption"
(ngModelChange)="onChange($event)"
[disabled]="disabled"
[placeholder]="placeholder"
[items]="items"
(blur)="onBlur()"
[labelForId]="labelForId"
[clearable]="false"
appendTo="body"
>
<ng-template ng-option-tmp let-item="item">
<div class="tw-flex">
<div class="tw-mr-2 tw-flex-initial">
<i *ngIf="item.icon != null" class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
</div>
<div class="tw-flex-1">
{{ item.label }}
</div>
</div>
</ng-template>
</ng-select>

View File

@ -0,0 +1,149 @@
import {
Component,
ContentChildren,
HostBinding,
Input,
Optional,
QueryList,
Self,
ViewChild,
} from "@angular/core";
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
import { NgSelectComponent } from "@ng-select/ng-select";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { BitFormFieldControl } from "../form-field";
import { Option } from "./option";
import { OptionComponent } from "./option.component";
let nextId = 0;
@Component({
selector: "bit-select",
templateUrl: "select.component.html",
providers: [{ provide: BitFormFieldControl, useExisting: SelectComponent }],
})
export class SelectComponent<T> implements BitFormFieldControl, ControlValueAccessor {
@ViewChild(NgSelectComponent) select: NgSelectComponent;
/** Optional: Options can be provided using an array input or using `bit-option` */
@Input() items: Option<T>[] = [];
@Input() placeholder = this.i18nService.t("selectPlaceholder");
protected selectedValue: T;
protected selectedOption: Option<T>;
protected searchInputId = `bit-select-search-input-${nextId++}`;
private notifyOnChange?: (value: T) => void;
private notifyOnTouched?: () => void;
constructor(private i18nService: I18nService, @Optional() @Self() private ngControl?: NgControl) {
if (ngControl != null) {
ngControl.valueAccessor = this;
}
}
@ContentChildren(OptionComponent)
protected set options(value: QueryList<OptionComponent<T>>) {
this.items = value.toArray();
this.selectedOption = this.findSelectedOption(this.items, this.selectedValue);
}
@HostBinding("class") protected classes = ["tw-block", "tw-w-full"];
@HostBinding()
@Input()
get disabled() {
return this._disabled ?? this.ngControl?.disabled ?? false;
}
set disabled(value: any) {
this._disabled = value != null && value !== false;
}
private _disabled: boolean;
/**Implemented as part of NG_VALUE_ACCESSOR */
writeValue(obj: T): void {
this.selectedValue = obj;
this.selectedOption = this.findSelectedOption(this.items, this.selectedValue);
}
/**Implemented as part of NG_VALUE_ACCESSOR */
registerOnChange(fn: (value: T) => void): void {
this.notifyOnChange = fn;
}
/**Implemented as part of NG_VALUE_ACCESSOR */
registerOnTouched(fn: any): void {
this.notifyOnTouched = fn;
}
/**Implemented as part of NG_VALUE_ACCESSOR */
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
/**Implemented as part of NG_VALUE_ACCESSOR */
protected onChange(option: Option<T> | null) {
if (!this.notifyOnChange) {
return;
}
this.notifyOnChange(option?.value);
}
/**Implemented as part of NG_VALUE_ACCESSOR */
protected onBlur() {
if (!this.notifyOnTouched) {
return;
}
this.notifyOnTouched();
}
/**Implemented as part of BitFormFieldControl */
@HostBinding("attr.aria-describedby")
get ariaDescribedBy() {
return this._ariaDescribedBy;
}
set ariaDescribedBy(value: string) {
this._ariaDescribedBy = value;
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
}
private _ariaDescribedBy: string;
/**Implemented as part of BitFormFieldControl */
get labelForId() {
return this.searchInputId;
}
/**Implemented as part of BitFormFieldControl */
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
/**Implemented as part of BitFormFieldControl */
@HostBinding("attr.required")
@Input()
get required() {
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
}
set required(value: any) {
this._required = value != null && value !== false;
}
private _required: boolean;
/**Implemented as part of BitFormFieldControl */
get hasError() {
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
}
/**Implemented as part of BitFormFieldControl */
get error(): [string, any] {
const key = Object.keys(this.ngControl?.errors)[0];
return [key, this.ngControl?.errors[key]];
}
private findSelectedOption(items: Option<T>[], value: T): Option<T> | undefined {
return items.find((item) => item.value === value);
}
}

View File

@ -0,0 +1,14 @@
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { NgSelectModule } from "@ng-select/ng-select";
import { OptionComponent } from "./option.component";
import { SelectComponent } from "./select.component";
@NgModule({
imports: [CommonModule, NgSelectModule, FormsModule],
declarations: [SelectComponent, OptionComponent],
exports: [SelectComponent, OptionComponent],
})
export class SelectModule {}

View File

@ -0,0 +1,58 @@
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MultiSelectComponent } from "../multi-select/multi-select.component";
import { I18nMockService } from "../utils/i18n-mock.service";
import { SelectComponent } from "./select.component";
import { SelectModule } from "./select.module";
export default {
title: "Component Library/Form/Select",
component: SelectComponent,
decorators: [
moduleMetadata({
imports: [SelectModule],
providers: [
{
provide: I18nService,
useFactory: () => {
return new I18nMockService({
selectPlaceholder: "-- Select --",
});
},
},
],
}),
],
args: {
disabled: false,
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/3tWtMSYoLB0ZLEimLNzYsm/End-user-%26-admin-Vault-Refresh?t=7QEmGA69YTOF8sXU-0",
},
},
} as Meta;
const DefaultTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
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>`,
});
export const Default = DefaultTemplate.bind({});
Default.args = {};
export const Disabled = DefaultTemplate.bind({});
Disabled.args = {
disabled: true,
};