Skip to content

Commit 118b730

Browse files
committed
feat(select): add mat-select-header component
Adds a `mat-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, some basic focus management and exposes the panel id for a11y. The functionality is up to the consumer to handle. Fixes angular#2812.
1 parent 26bbeb2 commit 118b730

17 files changed

+303
-38
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div [style.height.px]="topHeightCtrl.value"></div>
44

55
<div class="demo-select">
6-
<mat-card>
6+
<mat-card>
77
<mat-card-subtitle>ngModel</mat-card-subtitle>
88

99
<mat-form-field [floatPlaceholder]="floatPlaceholder" [color]="drinksTheme">
@@ -140,6 +140,31 @@
140140
</mat-card-content>
141141
</mat-card>
142142

143+
<mat-card>
144+
<mat-card-subtitle>Select header</mat-card-subtitle>
145+
146+
<mat-card-content>
147+
<mat-form-field>
148+
<mat-select placeholder="Drink" [(ngModel)]="currentDrink" #selectWitHeader="matSelect">
149+
<mat-select-header>
150+
<input
151+
type="search"
152+
role="combobox"
153+
class="mat-select-header-input"
154+
[(ngModel)]="searchTerm"
155+
[attr.aria-owns]="selectWitHeader.panelId"
156+
(ngModelChange)="filterDrinks()"
157+
placeholder="Search for a drink"/>
158+
</mat-select-header>
159+
160+
<mat-option *ngFor="let drink of filteredDrinks" [value]="drink.value" [disabled]="drink.disabled">
161+
{{ drink.viewValue }}
162+
</mat-option>
163+
</mat-select>
164+
</mat-form-field>
165+
</mat-card-content>
166+
</mat-card>
167+
143168
<div *ngIf="showSelect">
144169
<mat-card>
145170
<mat-card-subtitle>formControl</mat-card-subtitle>

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class SelectDemo {
2020
currentPokemon: string[];
2121
currentPokemonFromGroup: string;
2222
currentDigimon: string;
23+
searchTerm: string;
2324
latestChangeEvent: MatSelectChange;
2425
floatPlaceholder: string = 'auto';
2526
foodControl = new FormControl('pizza-1');
@@ -47,6 +48,8 @@ export class SelectDemo {
4748
{value: 'milk-8', viewValue: 'Milk'},
4849
];
4950

51+
filteredDrinks = this.drinks.slice();
52+
5053
pokemon = [
5154
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
5255
{value: 'charizard-1', viewValue: 'Charizard'},
@@ -126,4 +129,10 @@ export class SelectDemo {
126129
compareByReference(o1: any, o2: any) {
127130
return o1 === o2;
128131
}
132+
133+
filterDrinks() {
134+
this.filteredDrinks = this.searchTerm ? this.drinks.filter(item => {
135+
return item.viewValue.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
136+
}) : this.drinks.slice();
137+
}
129138
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ $mat-menu-icon-margin: 16px !default;
1717
@include mat-overridable-elevation($default-elevation);
1818
min-width: $mat-menu-overlay-min-width;
1919
max-width: $mat-menu-overlay-max-width;
20+
}
21+
22+
@mixin mat-menu-scrollable() {
2023
overflow: auto;
2124
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile
2225
}

src/lib/select/_select-theme.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
}
3232
}
3333

34+
.mat-select-header {
35+
color: mat-color($foreground, divider);
36+
}
37+
3438
.mat-form-field {
3539
&.mat-focused {
3640
&.mat-primary .mat-select-arrow {

src/lib/select/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
export * from './select-module';
1010
export * from './select';
1111
export * from './select-animations';
12+
export * from './select-header';

src/lib/select/select-animations.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export const transformPanel: AnimationTriggerMetadata = trigger('transformPanel'
6161
* panel has transformed in.
6262
*/
6363
export const fadeInContent: AnimationTriggerMetadata = trigger('fadeInContent', [
64+
state('void', style({opacity: 0})),
6465
state('showing', style({opacity: 1})),
65-
transition('void => showing', [
66-
style({opacity: 0}),
67-
animate('150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')
68-
])
66+
transition('void => showing', animate('150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
67+
transition('showing => void', animate('150ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
6968
]);

src/lib/select/select-header.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<span cdkTrapFocus>
2+
<ng-content></ng-content>
3+
</span>

src/lib/select/select-header.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component, ViewEncapsulation, ChangeDetectionStrategy, ViewChild} from '@angular/core';
10+
import {FocusTrapDirective} from '@angular/cdk/a11y';
11+
12+
/**
13+
* Fixed header that will be rendered above a select's options.
14+
* Can be used as a bar for filtering out options.
15+
*/
16+
@Component({
17+
moduleId: module.id,
18+
selector: 'mat-select-header',
19+
changeDetection: ChangeDetectionStrategy.OnPush,
20+
encapsulation: ViewEncapsulation.None,
21+
preserveWhitespaces: false,
22+
templateUrl: 'select-header.html',
23+
host: {
24+
'class': 'mat-select-header',
25+
}
26+
})
27+
export class MatSelectHeader {
28+
@ViewChild(FocusTrapDirective) _focusTrap: FocusTrapDirective;
29+
30+
_trapFocus() {
31+
this._focusTrap.focusTrap.focusFirstTabbableElementWhenReady();
32+
}
33+
}

src/lib/select/select-module.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import {NgModule} from '@angular/core';
99
import {CommonModule} from '@angular/common';
1010
import {MatSelect, MatSelectTrigger, MAT_SELECT_SCROLL_STRATEGY_PROVIDER} from './select';
11+
import {MatSelectHeader} from './select-header';
1112
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
1213
import {OverlayModule} from '@angular/cdk/overlay';
1314
import {MatFormFieldModule} from '@angular/material/form-field';
1415
import {ErrorStateMatcher} from '@angular/material/core';
16+
import {A11yModule} from '@angular/cdk/a11y';
1517

1618

1719
@NgModule({
@@ -20,9 +22,17 @@ import {ErrorStateMatcher} from '@angular/material/core';
2022
OverlayModule,
2123
MatOptionModule,
2224
MatCommonModule,
25+
A11yModule,
2326
],
24-
exports: [MatFormFieldModule, MatSelect, MatSelectTrigger, MatOptionModule, MatCommonModule],
25-
declarations: [MatSelect, MatSelectTrigger],
27+
exports: [
28+
MatFormFieldModule,
29+
MatSelect,
30+
MatSelectTrigger,
31+
MatSelectHeader,
32+
MatOptionModule,
33+
MatCommonModule,
34+
],
35+
declarations: [MatSelect, MatSelectTrigger, MatSelectHeader],
2636
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER, ErrorStateMatcher]
2737
})
2838
export class MatSelectModule {}

src/lib/select/select.html

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,21 @@
3434
(detach)="close()">
3535

3636
<div
37-
#panel
3837
class="mat-select-panel {{ _getPanelTheme() }}"
3938
[ngClass]="panelClass"
4039
[@transformPanel]="multiple ? 'showing-multiple' : 'showing'"
4140
(@transformPanel.done)="_onPanelDone()"
4241
[style.transformOrigin]="_transformOrigin"
4342
[class.mat-select-panel-done-animating]="_panelDoneAnimating"
44-
[style.font-size.px]="_triggerFontSize">
43+
[style.font-size.px]="_triggerFontSize"
44+
(keydown)="_handleKeydown($event)">
4545

46-
<div
47-
class="mat-select-content"
48-
[@fadeInContent]="'showing'"
49-
(@fadeInContent.done)="_onFadeInDone()">
50-
<ng-content></ng-content>
46+
<div [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
47+
<ng-content select="mat-select-header"></ng-content>
48+
49+
<div #panel class="mat-select-content" [attr.id]="panelId">
50+
<ng-content></ng-content>
51+
</div>
5152
</div>
5253
</div>
5354
</ng-template>

src/lib/select/select.scss

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,8 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a
5555
margin: 0 $mat-select-arrow-margin;
5656
}
5757

58-
.mat-select-panel {
59-
@include mat-menu-base(8);
60-
padding-top: 0;
61-
padding-bottom: 0;
58+
.mat-select-content {
59+
@include mat-menu-scrollable();
6260
max-height: $mat-select-panel-max-height;
6361
min-width: 100%; // prevents some animation twitching and test inconsistencies in IE11
6462

@@ -67,10 +65,33 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a
6765
}
6866
}
6967

68+
.mat-select-panel {
69+
@include mat-menu-base(8);
70+
border: none;
71+
}
72+
73+
.mat-select-header {
74+
@include mat-menu-item-base();
75+
border-bottom: solid 1px;
76+
box-sizing: border-box;
77+
}
78+
79+
// Opt-in header input styling.
80+
.mat-select-header-input {
81+
display: block;
82+
width: 100%;
83+
height: 100%;
84+
border: none;
85+
outline: none;
86+
padding: 0;
87+
background: transparent;
88+
}
89+
7090
// Override optgroup and option to scale based on font-size of the trigger.
7191
.mat-select-panel {
7292
.mat-optgroup-label,
73-
.mat-option {
93+
.mat-option,
94+
.mat-select-header {
7495
font-size: inherit;
7596
line-height: $mat-select-item-height;
7697
height: $mat-select-item-height;

0 commit comments

Comments
 (0)