Skip to content

Commit

Permalink
feat(select): add search (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
roll314 authored and pimenovoleg committed May 29, 2019
1 parent f69c381 commit dcaf303
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 110 deletions.
14 changes: 12 additions & 2 deletions packages/cdk/a11y/key-manager/list-key-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ export class ListKeyManager<T extends ListKeyManagerOption> {

/**
* Turns on typeahead mode which allows users to set the active item by typing.
* @param searchLetterIndex letter index for incremental search, if is -1 search is disabled
* @param debounceInterval Time to wait after the last keystroke before setting the active item.
*/
withTypeAhead(debounceInterval: number = 200): this {
withTypeAhead(debounceInterval: number = 200, searchLetterIndex: number = 0): this {
if (this._items.length && this._items.some((item) => typeof item.getLabel !== 'function')) {
throw Error('ListKeyManager items in typeahead mode must implement the `getLabel` method.');
}
Expand All @@ -133,6 +134,12 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
filter(() => this._pressedLetters.length > 0),
map(() => this._pressedLetters.join(''))
).subscribe((inputString) => {
if (searchLetterIndex === -1) {
this._pressedLetters = [];

return;
}

const items = this._items.toArray();

// Start at 1 because we want to start searching at the item immediately
Expand All @@ -141,7 +148,10 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
const index = (this._activeItemIndex + i) % items.length;
const item = items[index];

if (!item.disabled && item.getLabel!().toUpperCase().trim().indexOf(inputString) === 0) {
if (
!item.disabled &&
item.getLabel!().toUpperCase().trim().indexOf(inputString) === searchLetterIndex
) {
this.setActiveItem(index);
break;
}
Expand Down
49 changes: 45 additions & 4 deletions packages/mosaic-dev/select/module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, NgModule, ViewEncapsulation } from '@angular/core';
import { Component, NgModule, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
Expand All @@ -7,8 +7,11 @@ import { McButtonModule } from '@ptsecurity/mosaic/button';
import { McFormFieldModule } from '@ptsecurity/mosaic/form-field';
import { McIconModule } from '@ptsecurity/mosaic/icon';
import { McInputModule } from '@ptsecurity/mosaic/input';

import { McSelectModule, McSelectChange } from '@ptsecurity/mosaic/select';
import { merge, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import { OPTIONS } from './options';


@Component({
Expand All @@ -17,17 +20,55 @@ import { McSelectModule, McSelectChange } from '@ptsecurity/mosaic/select';
styleUrls: ['./styles.scss'],
encapsulation: ViewEncapsulation.None
})
export class DemoComponent {
export class DemoComponent implements OnInit {
singleSelected = 'Normal';
multipleSelected = ['Normal', 'Hovered', 'Selected', 'Selected1'];

singleSelectedWithSearch = 'Moscow';
multipleSelectedWithSearch = ['Dzerzhinsk', 'Pskov'];

singleSelectFormControl = new FormControl('', Validators.required);

multiSelectSelectFormControl = new FormControl([], Validators.pattern(/^w/));

searchCtrl: FormControl = new FormControl();
filteredOptions: Observable<string[]>;

multipleSearchCtrl: FormControl = new FormControl();
filteredMultipleOptions: Observable<string[]>;

allOptions = OPTIONS;
optionCounter = 0;

private options: string[] = OPTIONS;

ngOnInit(): void {
this.filteredOptions = merge(
of(OPTIONS),
this.searchCtrl.valueChanges
.pipe(map((value) => this.getFilteredOptions(value)))
);

this.filteredMultipleOptions = merge(
of(OPTIONS),
this.multipleSearchCtrl.valueChanges
.pipe(map((value) => this.getFilteredOptions(value)))
);
}

onSelectionChange($event: McSelectChange) {
// tslint:disable-next-line:no-console
console.log(`onSelectionChange: ${$event.value}`);
}

private getFilteredOptions(value): string[] {
const searchFilter = (value && value.new) ? value.value : value;

return searchFilter
? this.options.filter((option) =>
option.toLowerCase().includes((searchFilter.toLowerCase())))
: this.options;
}
}


Expand Down Expand Up @@ -55,5 +96,5 @@ export class DemoModule {}

platformBrowserDynamic()
.bootstrapModule(DemoModule)
.catch((error) => console.error(error));
.catch((error) => console.error(error)); // tslint:disable-line:no-console

19 changes: 19 additions & 0 deletions packages/mosaic-dev/select/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const OPTIONS =
['Abakan', 'Almetyevsk', 'Anadyr', 'Anapa', 'Arkhangelsk', 'Astrakhan', 'Barnaul', 'Belgorod', 'Beslan',
'Biysk', 'Birobidzhan', 'Blagoveshchensk', 'Bologoye', 'Bryansk', 'Veliky Novgorod',
'Veliky Ustyug', 'Vladivostok', 'Vladikavkaz', 'Vladimir', 'Volgograd', 'Vologda', 'Vorkuta',
'Voronezh', 'Gatchina', 'Gdov', 'Gelendzhik', 'Gorno-Altaysk', 'Grozny', 'Gudermes',
'Gus-Khrustalny', 'Dzerzhinsk', 'Dmitrov', 'Dubna', 'Yeysk', 'Yekaterinburg', 'Yelabuga', 'Yelets',
'Yessentuki', 'Zlatoust', 'Ivanovo', 'Izhevsk', 'Irkutsk', 'Yoshkar-Ola', 'Kazan', 'Kaliningrad',
'Kaluga', 'Kemerovo', 'Kislovodsk', 'Komsomolsk-on-Amur', 'Kotlas', 'Krasnodar', 'Krasnoyarsk', 'Kurgan',
'Kursk', 'Kyzyl', 'Leninogorsk', 'Lensk', 'Lipetsk', 'Luga', 'Lyuban', 'Lyubertsy', 'Magadan', 'Maykop',
'Makhachkala', 'Miass', 'Mineralnye Vody', 'Mirny', 'Moscow', 'Murmansk', 'Murom', 'Mytishchi',
'Naberezhnye Chelny', 'Nadym', 'Nalchik', 'Nazran', 'Naryan-Mar', 'Nakhodka', 'Nizhnevartovsk',
'Nizhnekamsk', 'Nizhny Novgorod', 'Nizhny Tagil', 'Novokuznetsk', 'Novosibirsk', 'Novy Urengoy',
'Norilsk', 'Obninsk', 'Oktyabrsky', 'Omsk', 'Orenburg', 'Orekhovo-Zuyevo', 'Oryol', 'Penza', 'Perm',
'Petrozavodsk', 'Petropavlovsk-Kamchatsky', 'Podolsk', 'Pskov', 'Pyatigorsk', 'Rostov-on-Don', 'Rybinsk',
'Ryazan', 'Salekhard', 'Samara', 'Saint Petersburg', 'Saransk', 'Saratov', 'Severodvinsk', 'Smolensk',
'Sol-Iletsk', 'Sochi', 'Stavropol', 'Surgut', 'Syktyvkar', 'Tambov', 'Tver', 'Tobolsk', 'Tolyatti', 'Tomsk',
'Tuapse', 'Tula', 'Tynda', 'Tyumen', 'Ulan-Ude', 'Ulyanovsk', 'Ufa', 'Khabarovsk', 'Khanty-Mansiysk',
'Chebarkul', 'Cheboksary', 'Chelyabinsk', 'Cherepovets', 'Cherkessk', 'Chistopol', 'Chita', 'Shadrinsk',
'Shatura', 'Shuya', 'Elista', 'Engels', 'Yuzhno-Sakhalinsk', 'Yakutsk', 'Yaroslavl'];
1 change: 0 additions & 1 deletion packages/mosaic-dev/select/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

.dev-container {
width: 300px;
height: 140px;

border: 1px solid red;

Expand Down
44 changes: 43 additions & 1 deletion packages/mosaic-dev/select/template.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<br><br><br><br><br><br><br>
<div style="height: 20px">{{ selected }}</div>
<!--<div style="height: 20px">{{ selected }}</div>-->

<!--<div class="dev-container">-->
<!--<mc-form-field>-->
Expand Down Expand Up @@ -28,6 +28,48 @@
<br>

<div class="dev-container">

{{singleSelectedWithSearch}}

<mc-form-field>
<mc-select [(value)]="singleSelectedWithSearch">
<mc-form-field mcFormFieldWithoutBorders mcSelectSearch>
<i mcPrefix mc-icon="mc-search_16"></i>
<input mcInput [formControl]="searchCtrl" type="text" placeholder="Search" />
<mc-cleaner></mc-cleaner>
</mc-form-field>

<mc-option value="StickedOption">Sticked Option</mc-option>
<mc-option *ngFor="let option of filteredOptions | async" [value]="option">{{ option }}</mc-option>
</mc-select>
</mc-form-field>

<br><br>

<button mc-button (click)="optionCounter = optionCounter + 1;allOptions.push(optionCounter.toString())">add option</button>

<br><br>

{{multipleSelectedWithSearch}}

<mc-form-field>
<mc-select multiple [(value)]="multipleSelectedWithSearch">
<mc-form-field mcFormFieldWithoutBorders mcSelectSearch>
<i mcPrefix mc-icon="mc-search_16"></i>
<input mcInput [formControl]="multipleSearchCtrl" type="text" placeholder="Search" />
<mc-cleaner></mc-cleaner>
</mc-form-field>

<mc-option value="StickedOption">Sticked Option</mc-option>
<mc-option *ngFor="let option of filteredMultipleOptions | async" [value]="option">{{ option }}</mc-option>
</mc-select>
</mc-form-field>
</div>

<br>

<div class="dev-container">

<mc-form-field>
<mc-select [(value)]="singleSelected">
<mc-option value="Disabled" disabled>Disabled</mc-option>
Expand Down
2 changes: 2 additions & 0 deletions packages/mosaic/form-field/form-field-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ export abstract class McFormFieldControl<T> {

/** Handles a click on the control's container. */
abstract onContainerClick(event: MouseEvent): void;

abstract focus(): void;
}
1 change: 1 addition & 0 deletions packages/mosaic/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export class McFormField extends _McFormFieldMixinBase implements

if (this._control && this._control.ngControl) {
this._control.ngControl.reset();
this._control.focus();
}
}

Expand Down
8 changes: 8 additions & 0 deletions packages/mosaic/select/_select-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@

background-color: if($is-dark, map-get($second, 700), map-get($background, background));
}

.mc-select__search-container {
border-bottom: {
width: 1px;
style: solid;
color: mc-color($second);
}
}
}

@mixin mc-select-typography($config) {
Expand Down
Loading

0 comments on commit dcaf303

Please sign in to comment.