Skip to content

Commit d09aa89

Browse files
crisbetoandrewseguin
authored andcommitted
feat(select): add md-optgroup component (#4432)
* feat(select): add md-optgroup component Adds the `md-optgroup` component, which can be used to group options inside of `md-select`, in a similar way to the native `optgroup`. Fixes #3182. * fix: address feedback * fix: address feedback
1 parent d0d79fd commit d09aa89

File tree

15 files changed

+443
-45
lines changed

15 files changed

+443
-45
lines changed

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
<div style="height: 1000px">This div is for testing scrolled selects.</div>
1+
Space above cards: <input type="number" [formControl]="topHeightCtrl">
22
<button md-button (click)="showSelect=!showSelect">SHOW SELECT</button>
3+
<div [style.height.px]="topHeightCtrl.value"></div>
4+
35
<div class="demo-select">
46
<md-card>
57
<md-card-subtitle>ngModel</md-card-subtitle>
@@ -63,6 +65,21 @@
6365
</md-card-content>
6466
</md-card>
6567

68+
<md-card>
69+
<md-card-subtitle>Option groups</md-card-subtitle>
70+
71+
<md-card-content>
72+
<md-select placeholder="Pokemon" [(ngModel)]="currentPokemonFromGroup">
73+
<md-optgroup *ngFor="let group of pokemonGroups" [label]="group.name"
74+
[disabled]="group.disabled">
75+
<md-option *ngFor="let creature of group.pokemon" [value]="creature.value">
76+
{{ creature.viewValue }}
77+
</md-option>
78+
</md-optgroup>
79+
</md-select>
80+
</md-card-content>
81+
</md-card>
82+
6683
<div *ngIf="showSelect">
6784
<md-card>
6885
<md-card-subtitle>formControl</md-card-subtitle>

src/demo-app/select/select-demo.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
margin: 24px;
88
}
99

10-
}
10+
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ export class SelectDemo {
1616
showSelect = false;
1717
currentDrink: string;
1818
currentPokemon: string[];
19+
currentPokemonFromGroup: string;
1920
latestChangeEvent: MdSelectChange;
2021
floatPlaceholder: string = 'auto';
2122
foodControl = new FormControl('pizza-1');
23+
topHeightCtrl = new FormControl(0);
2224
drinksTheme = 'primary';
2325
pokemonTheme = 'primary';
2426

@@ -57,6 +59,41 @@ export class SelectDemo {
5759
{value: 'warn', name: 'Warn' }
5860
];
5961

62+
pokemonGroups = [
63+
{
64+
name: 'Grass',
65+
pokemon: [
66+
{ value: 'bulbasaur-0', viewValue: 'Bulbasaur' },
67+
{ value: 'oddish-1', viewValue: 'Oddish' },
68+
{ value: 'bellsprout-2', viewValue: 'Bellsprout' }
69+
]
70+
},
71+
{
72+
name: 'Water',
73+
pokemon: [
74+
{ value: 'squirtle-3', viewValue: 'Squirtle' },
75+
{ value: 'psyduck-4', viewValue: 'Psyduck' },
76+
{ value: 'horsea-5', viewValue: 'Horsea' }
77+
]
78+
},
79+
{
80+
name: 'Fire',
81+
disabled: true,
82+
pokemon: [
83+
{ value: 'charmander-6', viewValue: 'Charmander' },
84+
{ value: 'vulpix-7', viewValue: 'Vulpix' },
85+
{ value: 'flareon-8', viewValue: 'Flareon' }
86+
]
87+
},
88+
{
89+
name: 'Psychic',
90+
pokemon: [
91+
{ value: 'mew-9', viewValue: 'Mew' },
92+
{ value: 'mewtwo-10', viewValue: 'Mewtwo' },
93+
]
94+
}
95+
];
96+
6097
toggleDisabled() {
6198
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
6299
}

src/lib/core/_core.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
@import 'ripple/ripple';
66
@import 'option/option';
77
@import 'option/option-theme';
8+
@import 'option/optgroup';
9+
@import 'option/optgroup-theme';
810
@import 'selection/pseudo-checkbox/pseudo-checkbox-theme';
911
@import 'typography/all-typography';
1012

@@ -22,6 +24,7 @@
2224
@include angular-material-typography();
2325
@include mat-ripple();
2426
@include mat-option();
27+
@include mat-optgroup();
2528
@include cdk-a11y();
2629
@include cdk-overlay();
2730
}
@@ -30,6 +33,7 @@
3033
@mixin mat-core-theme($theme) {
3134
@include mat-ripple-theme($theme);
3235
@include mat-option-theme($theme);
36+
@include mat-optgroup-theme($theme);
3337
@include mat-pseudo-checkbox-theme($theme);
3438

3539
// Wrapper element that provides the theme background when the

src/lib/core/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {NgModule} from '@angular/core';
22
import {MdLineModule} from './line/line';
33
import {RtlModule} from './rtl/dir';
44
import {ObserveContentModule} from './observe-content/observe-content';
5-
import {MdOptionModule} from './option/option';
5+
import {MdOptionModule} from './option/index';
66
import {PortalModule} from './portal/portal-directives';
77
import {OverlayModule} from './overlay/overlay-directives';
88
import {A11yModule} from './a11y/index';
@@ -16,7 +16,7 @@ export {Dir, LayoutDirection, RtlModule} from './rtl/dir';
1616
// Mutation Observer
1717
export {ObserveContentModule, ObserveContent} from './observe-content/observe-content';
1818

19-
export {MdOptionModule, MdOption, MdOptionSelectionChange} from './option/option';
19+
export * from './option/index';
2020

2121
// Portals
2222
export {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import '../theming/palette';
2+
@import '../theming/theming';
3+
4+
@mixin mat-optgroup-theme($theme) {
5+
$foreground: map-get($theme, foreground);
6+
7+
.mat-optgroup-label {
8+
color: mat-color($foreground, secondary-text);
9+
}
10+
11+
.mat-optgroup-disabled .mat-optgroup-label {
12+
color: mat-color($foreground, hint-text);
13+
}
14+
}

src/lib/core/option/_optgroup.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@import '../style/menu-common';
2+
@import '../style/vendor-prefixes';
3+
4+
@mixin mat-optgroup() {
5+
.mat-optgroup-label {
6+
@include mat-menu-item-base();
7+
@include user-select(none);
8+
cursor: default;
9+
10+
// TODO(crisbeto): should use the typography functions once #4375 is in.
11+
font-weight: bold;
12+
font-size: 14px;
13+
}
14+
}

src/lib/core/option/_option.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@
1717
@include user-select(none);
1818
cursor: default;
1919
}
20+
21+
.mat-optgroup &:not(.mat-option-multiple) {
22+
padding-left: $mat-menu-side-padding * 2;
23+
24+
[dir='rtl'] & {
25+
padding-left: $mat-menu-side-padding;
26+
padding-right: $mat-menu-side-padding * 2;
27+
}
28+
}
2029
}
2130

2231
.mat-option-ripple {
@@ -31,7 +40,7 @@
3140
// Pointer events can be safely disabled because the ripple trigger element is the host element.
3241
pointer-events: none;
3342

34-
// In high contrast mode this completely covers the text.
43+
// Prevents the ripple from completely covering the option in high contrast mode.
3544
@include cdk-high-contrast {
3645
opacity: 0.5;
3746
}

src/lib/core/option/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {NgModule} from '@angular/core';
2+
import {CommonModule} from '@angular/common';
3+
import {MdRippleModule} from '../ripple/index';
4+
import {MdSelectionModule} from '../selection/index';
5+
import {MdOption} from './option';
6+
import {MdOptgroup} from './optgroup';
7+
8+
9+
@NgModule({
10+
imports: [MdRippleModule, CommonModule, MdSelectionModule],
11+
exports: [MdOption, MdOptgroup],
12+
declarations: [MdOption, MdOptgroup]
13+
})
14+
export class MdOptionModule {}
15+
16+
17+
export * from './option';
18+
export * from './optgroup';

src/lib/core/option/optgroup.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<label class="mat-optgroup-label" [id]="_labelId">{{ label }}</label>
2+
<ng-content select="md-option, mat-option"></ng-content>

0 commit comments

Comments
 (0)