Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): add mat-select-header component #7835

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we always want to focus trap here? What's your reasoning for adding it by default instead of letting the user add it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd imagine that we do, just because the primary use case is having a search field. Also it removes a little bit of boilerplate for the consumer.

<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>
11 changes: 10 additions & 1 deletion src/lib/select/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ on the group.

### Multiple selection

`<mat-select>` defaults to single-selection mode, but can be configured to allow multiple selection
`<mat-select>` defaults to single-selection mode, but can be configured to allow multiple selection
by setting the `multiple` property. This will allow the user to select multiple values at once. When
using the `<mat-select>` in multiple selection mode, its value will be a sorted list of all selected
values rather than a single value.
Expand All @@ -81,6 +81,15 @@ If you want to display a custom trigger label inside a select, you can use the

<!-- example(select-custom-trigger) -->

### Adding a header

You can add an extra header that will stay fixed on top of the select's option as the user scrolls.
The header can be used as a filter bar or as an extra title. Note that the accessibility of the
header content is up to the consumer. For example when using it as a filter bar, the `input` element
should have a `role="combobox"` and an `[attr.aria-owns]="select.panelId"`.

<!-- example(select-header) -->

### Disabling the ripple effect

By default, when a user clicks on a `<mat-option>`, a ripple animation is shown. This can be disabled
Expand Down
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