Skip to content

Commit 4c37b61

Browse files
committed
feat(select): add md-select-header directive
Adds a `md-select-header` component, which is a fixed header above the select's options. It allows for the user to project an input to be used for filtering long lists of options. **Note:** This component only handles the positioning, styling and exposes the panel id for a11y. The functionality is up to the user to handle. Fixes #2812.
1 parent e3b2486 commit 4c37b61

14 files changed

+253
-28
lines changed

src/demo-app/select/select-demo.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,20 @@
5252
</md-card>
5353
</div>
5454

55+
<md-card *ngIf="showSelect">
56+
<md-select placeholder="Drink" [(ngModel)]="currentDrink" #selectWitHeader="mdSelect">
57+
<md-select-header>
58+
<input type="search" [(ngModel)]="searchTerm" role="combobox" [attr.aria-owns]="selectWitHeader.panelId"
59+
(ngModelChange)="filterDrinks()" placeholder="Search for a drink"/>
60+
</md-select-header>
61+
62+
<md-option *ngFor="let drink of filteredDrinks" [value]="drink.value" [disabled]="drink.disabled">
63+
{{ drink.viewValue }}
64+
</md-option>
65+
66+
</md-select>
67+
</md-card>
68+
69+
5570
</div>
5671
<div style="height: 500px">This div is for testing scrolled selects.</div>

src/demo-app/select/select-demo.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class SelectDemo {
1313
isDisabled = false;
1414
showSelect = false;
1515
currentDrink: string;
16+
searchTerm: string;
1617
latestChangeEvent: MdSelectChange;
1718
floatPlaceholder: string = 'auto';
1819
foodControl = new FormControl('pizza-1');
@@ -35,6 +36,8 @@ export class SelectDemo {
3536
{value: 'milk-8', viewValue: 'Milk'},
3637
];
3738

39+
filteredDrinks = this.drinks.slice();
40+
3841
pokemon = [
3942
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
4043
{value: 'charizard-1', viewValue: 'Charizard'},
@@ -44,4 +47,10 @@ export class SelectDemo {
4447
toggleDisabled() {
4548
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
4649
}
50+
51+
filterDrinks() {
52+
this.filteredDrinks = this.searchTerm ? this.drinks.filter(item => {
53+
return item.viewValue.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
54+
}) : this.drinks.slice();
55+
}
4756
}

src/examples/example-module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {SelectOverviewExample} from './select-overview/select-overview-example';
6969
import {ChipsOverviewExample} from './chips-overview/chips-overview-example';
7070
import {ChipsStackedExample} from './chips-stacked/chips-stacked-example';
7171
import {SelectFormExample} from './select-form/select-form-example';
72+
import {SelectHeaderExample} from './select-header/select-header-example';
7273

7374

7475
export interface LiveExample {
@@ -142,6 +143,7 @@ export const EXAMPLE_COMPONENTS = {
142143
'radio-overview': {title: 'Basic radios', component: RadioOverviewExample},
143144
'select-overview': {title: 'Basic select', component: SelectOverviewExample},
144145
'select-form': {title: 'Select in a form', component: SelectFormExample},
146+
'select-header': {title: 'Select header', component: SelectHeaderExample},
145147
'sidenav-fab': {title: 'Sidenav with a FAB', component: SidenavFabExample},
146148
'sidenav-overview': {title: 'Basic sidenav', component: SidenavOverviewExample},
147149
'slider-configurable': {title: 'Configurable slider', component: SliderConfigurableExample},
@@ -204,6 +206,7 @@ export const EXAMPLE_LIST = [
204206
SidenavFabExample,
205207
SelectOverviewExample,
206208
SelectFormExample,
209+
SelectHeaderExample,
207210
SidenavOverviewExample,
208211
SliderConfigurableExample,
209212
SliderOverviewExample,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/** No CSS for this example */
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<md-select placeholder="Favorite food" [(ngModel)]="currentDrink" name="food" #select="mdSelect">
2+
<md-select-header>
3+
<input type="search" [(ngModel)]="searchString" role="combobox" [attr.aria-owns]="select.panelId"
4+
(ngModelChange)="filterFoods()" placeholder="Search for food"/>
5+
</md-select-header>
6+
7+
<md-option *ngFor="let food of foods" [value]="food.value">
8+
{{food.viewValue}}
9+
</md-option>
10+
</md-select>
11+
12+
<p> Selected value: {{selectedValue}} </p>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Component} from '@angular/core';
2+
3+
4+
@Component({
5+
selector: 'select-form-example',
6+
templateUrl: './select-form-example.html',
7+
})
8+
export class SelectHeaderExample {
9+
selectedValue: string;
10+
searchString: string;
11+
12+
initialFoods = [
13+
{ value: 'steak-0', viewValue: 'Steak' },
14+
{ value: 'pizza-1', viewValue: 'Pizza' },
15+
{ value: 'tacos-2', viewValue: 'Tacos' },
16+
{ value: 'sandwich-3', viewValue: 'Sandwich' },
17+
{ value: 'chips-4', viewValue: 'Chips' },
18+
{ value: 'eggs-5', viewValue: 'Eggs' },
19+
{ value: 'pasta-6', viewValue: 'Pasta' },
20+
{ value: 'sushi-7', viewValue: 'Sushi' },
21+
];
22+
23+
foods = this.initialFoods.slice();
24+
25+
filterFoods() {
26+
this.foods = this.searchString ? this.initialFoods.filter(item => {
27+
return item.viewValue.toLowerCase().indexOf(this.searchString.toLowerCase()) > -1;
28+
}) : this.initialFoods.slice();
29+
}
30+
}

src/lib/core/style/_menu-common.scss

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ $mat-menu-side-padding: 16px !default;
1414

1515
@mixin mat-menu-base() {
1616
@include mat-elevation(8);
17+
@include mat-menu-scrollable();
18+
1719
min-width: $mat-menu-overlay-min-width;
1820
max-width: $mat-menu-overlay-max-width;
19-
20-
overflow: auto;
21-
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile
2221
}
2322

2423
@mixin mat-menu-item-base() {
@@ -87,3 +86,8 @@ $mat-menu-side-padding: 16px !default;
8786
}
8887
}
8988
}
89+
90+
@mixin mat-menu-scrollable() {
91+
overflow: auto;
92+
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile
93+
}

src/lib/select/_select-theme.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,8 @@
5454
color: mat-color($foreground, hint-text);
5555
}
5656
}
57+
58+
.mat-select-header {
59+
color: mat-color($foreground, divider);
60+
}
5761
}

src/lib/select/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {NgModule, ModuleWithProviders} from '@angular/core';
22
import {CommonModule} from '@angular/common';
33
import {MdSelect} from './select';
4+
import {MdSelectHeader} from './select-header';
45
import {MdOptionModule} from '../core/option/option';
56
import {
67
CompatibilityModule,
@@ -12,8 +13,8 @@ export {fadeInContent, transformPanel, transformPlaceholder} from './select-anim
1213

1314
@NgModule({
1415
imports: [CommonModule, OverlayModule, MdOptionModule, CompatibilityModule],
15-
exports: [MdSelect, MdOptionModule, CompatibilityModule],
16-
declarations: [MdSelect],
16+
exports: [MdSelect, MdSelectHeader, MdOptionModule, CompatibilityModule],
17+
declarations: [MdSelect, MdSelectHeader],
1718
})
1819
export class MdSelectModule {
1920
/** @deprecated */

src/lib/select/select-header.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {Directive} from '@angular/core';
2+
3+
4+
/**
5+
* Fixed header that will be rendered above a select's options.
6+
*/
7+
@Directive({
8+
selector: 'md-select-header, mat-select-header',
9+
host: {
10+
'class': 'mat-select-header',
11+
}
12+
})
13+
export class MdSelectHeader { }

src/lib/select/select.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919
<div class="mat-select-panel" [@transformPanel]="'showing'" (@transformPanel.done)="_onPanelDone()"
2020
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin"
2121
[class.mat-select-panel-done-animating]="_panelDoneAnimating">
22-
<div class="mat-select-content" [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
22+
23+
<div [@fadeInContent]="'showing'">
24+
<ng-content select="md-select-header, mat-select-header"></ng-content>
25+
</div>
26+
27+
<div class="mat-select-content" [attr.id]="panelId" [@fadeInContent]="'showing'"
28+
(@fadeInContent.done)="_onFadeInDone()">
2329
<ng-content></ng-content>
2430
</div>
2531
</div>

src/lib/select/select.scss

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,32 @@ $mat-select-trigger-font-size: 16px !default;
119119
margin: 0 $mat-select-arrow-margin;
120120
}
121121

122-
.mat-select-panel {
123-
@include mat-menu-base();
124-
padding-top: 0;
125-
padding-bottom: 0;
122+
.mat-select-content {
123+
@include mat-menu-scrollable();
126124
max-height: $mat-select-panel-max-height;
127125

128126
@include cdk-high-contrast {
129127
outline: solid 1px;
130128
}
131129
}
132130

131+
.mat-select-panel {
132+
@include mat-menu-base();
133+
border: none;
134+
}
135+
136+
.mat-select-header {
137+
@include mat-menu-item-base();
138+
border-bottom: solid 1px;
139+
box-sizing: border-box;
140+
141+
input {
142+
display: block;
143+
width: 100%;
144+
height: 100%;
145+
border: none;
146+
outline: none;
147+
padding: 0;
148+
background: transparent;
149+
}
150+
}

0 commit comments

Comments
 (0)