Skip to content

Commit

Permalink
feat(select): add mat-select-header component
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
crisbeto committed Oct 16, 2017
1 parent 26bbeb2 commit 1c41bfb
Show file tree
Hide file tree
Showing 17 changed files with 303 additions and 37 deletions.
25 changes: 25 additions & 0 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,31 @@
</mat-card-content>
</mat-card>

<mat-card>
<mat-card-subtitle>Select header</mat-card-subtitle>

<mat-card-content>
<mat-form-field>
<mat-select placeholder="Drink" [(ngModel)]="currentDrink" #selectWitHeader="matSelect">
<mat-select-header>
<input
type="search"
role="combobox"
class="mat-select-header-input"
[(ngModel)]="searchTerm"
[attr.aria-owns]="selectWitHeader.panelId"
(ngModelChange)="filterDrinks()"
placeholder="Search for a drink"/>
</mat-select-header>

<mat-option *ngFor="let drink of filteredDrinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-card-content>
</mat-card>

<div *ngIf="showSelect">
<mat-card>
<mat-card-subtitle>formControl</mat-card-subtitle>
Expand Down
9 changes: 9 additions & 0 deletions src/demo-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class SelectDemo {
currentPokemon: string[];
currentPokemonFromGroup: string;
currentDigimon: string;
searchTerm: string;
latestChangeEvent: MatSelectChange;
floatPlaceholder: string = 'auto';
foodControl = new FormControl('pizza-1');
Expand Down Expand Up @@ -47,6 +48,8 @@ export class SelectDemo {
{value: 'milk-8', viewValue: 'Milk'},
];

filteredDrinks = this.drinks.slice();

pokemon = [
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
{value: 'charizard-1', viewValue: 'Charizard'},
Expand Down Expand Up @@ -126,4 +129,10 @@ export class SelectDemo {
compareByReference(o1: any, o2: any) {
return o1 === o2;
}

filterDrinks() {
this.filteredDrinks = this.searchTerm ? this.drinks.filter(item => {
return item.viewValue.toLowerCase().indexOf(this.searchTerm.toLowerCase()) > -1;
}) : this.drinks.slice();
}
}
4 changes: 4 additions & 0 deletions src/lib/core/style/_menu-common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ $mat-menu-icon-margin: 16px !default;

@mixin mat-menu-base($default-elevation) {
@include mat-overridable-elevation($default-elevation);
@include mat-menu-scrollable();
min-width: $mat-menu-overlay-min-width;
max-width: $mat-menu-overlay-max-width;
}

@mixin mat-menu-scrollable() {
overflow: auto;
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile
}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/select/_select-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
}
}

.mat-select-header {
color: mat-color($foreground, divider);
}

.mat-form-field {
&.mat-focused {
&.mat-primary .mat-select-arrow {
Expand Down
1 change: 1 addition & 0 deletions src/lib/select/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
export * from './select-module';
export * from './select';
export * from './select-animations';
export * from './select-header';
7 changes: 3 additions & 4 deletions src/lib/select/select-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ export const transformPanel: AnimationTriggerMetadata = trigger('transformPanel'
* panel has transformed in.
*/
export const fadeInContent: AnimationTriggerMetadata = trigger('fadeInContent', [
state('void', style({opacity: 0})),
state('showing', style({opacity: 1})),
transition('void => showing', [
style({opacity: 0}),
animate('150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')
])
transition('void => showing', animate('150ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')),
transition('showing => void', animate('150ms cubic-bezier(0.55, 0, 0.55, 0.2)'))
]);
3 changes: 3 additions & 0 deletions src/lib/select/select-header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<span cdkTrapFocus>
<ng-content></ng-content>
</span>
33 changes: 33 additions & 0 deletions src/lib/select/select-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component, ViewEncapsulation, ChangeDetectionStrategy, ViewChild} from '@angular/core';
import {FocusTrapDirective} from '@angular/cdk/a11y';

/**
* Fixed header that will be rendered above a select's options.
* Can be used as a bar for filtering out options.
*/
@Component({
moduleId: module.id,
selector: 'mat-select-header',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
preserveWhitespaces: false,
templateUrl: 'select-header.html',
host: {
'class': 'mat-select-header',
}
})
export class MatSelectHeader {
@ViewChild(FocusTrapDirective) _focusTrap: FocusTrapDirective;

_trapFocus() {
this._focusTrap.focusTrap.focusFirstTabbableElementWhenReady();
}
}
14 changes: 12 additions & 2 deletions src/lib/select/select-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MatSelect, MatSelectTrigger, MAT_SELECT_SCROLL_STRATEGY_PROVIDER} from './select';
import {MatSelectHeader} from './select-header';
import {MatCommonModule, MatOptionModule} from '@angular/material/core';
import {OverlayModule} from '@angular/cdk/overlay';
import {MatFormFieldModule} from '@angular/material/form-field';
import {ErrorStateMatcher} from '@angular/material/core';
import {A11yModule} from '@angular/cdk/a11y';


@NgModule({
Expand All @@ -20,9 +22,17 @@ import {ErrorStateMatcher} from '@angular/material/core';
OverlayModule,
MatOptionModule,
MatCommonModule,
A11yModule,
],
exports: [MatFormFieldModule, MatSelect, MatSelectTrigger, MatOptionModule, MatCommonModule],
declarations: [MatSelect, MatSelectTrigger],
exports: [
MatFormFieldModule,
MatSelect,
MatSelectTrigger,
MatSelectHeader,
MatOptionModule,
MatCommonModule,
],
declarations: [MatSelect, MatSelectTrigger, MatSelectHeader],
providers: [MAT_SELECT_SCROLL_STRATEGY_PROVIDER, ErrorStateMatcher]
})
export class MatSelectModule {}
15 changes: 8 additions & 7 deletions src/lib/select/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,21 @@
(detach)="close()">

<div
#panel
class="mat-select-panel {{ _getPanelTheme() }}"
[ngClass]="panelClass"
[@transformPanel]="multiple ? 'showing-multiple' : 'showing'"
(@transformPanel.done)="_onPanelDone()"
[style.transformOrigin]="_transformOrigin"
[class.mat-select-panel-done-animating]="_panelDoneAnimating"
[style.font-size.px]="_triggerFontSize">
[style.font-size.px]="_triggerFontSize"
(keydown)="_handleKeydown($event)">

<div
class="mat-select-content"
[@fadeInContent]="'showing'"
(@fadeInContent.done)="_onFadeInDone()">
<ng-content></ng-content>
<div [@fadeInContent]="'showing'" (@fadeInContent.done)="_onFadeInDone()">
<ng-content select="mat-select-header"></ng-content>

<div #panel class="mat-select-content" [attr.id]="panelId">
<ng-content></ng-content>
</div>
</div>
</div>
</ng-template>
31 changes: 26 additions & 5 deletions src/lib/select/select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,8 @@ $mat-select-placeholder-arrow-space: 2 * ($mat-select-arrow-size + $mat-select-a
margin: 0 $mat-select-arrow-margin;
}

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

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

.mat-select-panel {
@include mat-menu-base(8);
border: none;
}

.mat-select-header {
@include mat-menu-item-base();
border-bottom: solid 1px;
box-sizing: border-box;
}

// Opt-in header input styling.
.mat-select-header-input {
display: block;
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 0;
background: transparent;
}

// Override optgroup and option to scale based on font-size of the trigger.
.mat-select-panel {
.mat-optgroup-label,
.mat-option {
.mat-option,
.mat-select-header {
font-size: inherit;
line-height: $mat-select-item-height;
height: $mat-select-item-height;
Expand Down
Loading

0 comments on commit 1c41bfb

Please sign in to comment.