Skip to content

Commit bb2392f

Browse files
crisbetommalerba
authored andcommitted
feat(select): add floatingPlaceholder option (#2571)
Adds the `floatingPlaceholder` option that can be used to disable the floating placeholders. Fixes #2569. Fixes #2963.
1 parent 6595ad8 commit bb2392f

File tree

5 files changed

+122
-11
lines changed

5 files changed

+122
-11
lines changed

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
<md-card>
2020
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="isRequired" [disabled]="isDisabled"
21-
#drinkControl="ngModel">
21+
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
2222
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
2323
{{ drink.viewValue }}
2424
</md-option>
@@ -27,6 +27,15 @@
2727
<p> Touched: {{ drinkControl.touched }} </p>
2828
<p> Dirty: {{ drinkControl.dirty }} </p>
2929
<p> Status: {{ drinkControl.control?.status }} </p>
30+
<p>
31+
<label for="floating-placeholder">Floating placeholder:</label>
32+
<select [(ngModel)]="floatPlaceholder" id="floating-placeholder">
33+
<option value="auto">Auto</option>
34+
<option value="always">Always</option>
35+
<option value="never">Never</option>
36+
</select>
37+
</p>
38+
3039
<button md-button (click)="currentDrink='water-2'">SET VALUE</button>
3140
<button md-button (click)="isRequired=!isRequired">TOGGLE REQUIRED</button>
3241
<button md-button (click)="isDisabled=!isDisabled">TOGGLE DISABLED</button>

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

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class SelectDemo {
1414
showSelect = false;
1515
currentDrink: string;
1616
latestChangeEvent: MdSelectChange;
17+
floatPlaceholder: string = 'auto';
1718
foodControl = new FormControl('pizza-1');
1819

1920
foods = [

src/lib/select/select.html

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
<div class="mat-select-trigger" cdk-overlay-origin (click)="toggle()" #origin="cdkOverlayOrigin" #trigger>
2-
<span class="mat-select-placeholder" [class.mat-floating-placeholder]="this.selected"
3-
[@transformPlaceholder]="_placeholderState" [style.width.px]="_selectedValueWidth"> {{ placeholder }} </span>
2+
<span
3+
class="mat-select-placeholder"
4+
[class.mat-floating-placeholder]="selected"
5+
[@transformPlaceholder]="_getPlaceholderAnimationState()"
6+
[style.visibility]="_getPlaceholderVisibility()"
7+
[style.width.px]="_selectedValueWidth"> {{ placeholder }} </span>
48
<span class="mat-select-value" *ngIf="selected">
59
<span class="mat-select-value-text">{{ selected?.viewValue }}</span>
610
</span>
11+
712
<span class="mat-select-arrow"></span>
813
<span class="mat-select-underline"></span>
914
</div>

src/lib/select/select.spec.ts

+65-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@angular/core';
1212
import {MdSelectModule} from './index';
1313
import {OverlayContainer} from '../core/overlay/overlay-container';
14-
import {MdSelect} from './select';
14+
import {MdSelect, MdSelectFloatPlaceholderType} from './select';
1515
import {MdOption} from '../core/option/option';
1616
import {Dir} from '../core/rtl/dir';
1717
import {
@@ -35,6 +35,7 @@ describe('MdSelect', () => {
3535
SelectWithChangeEvent,
3636
CustomSelectAccessor,
3737
CompWithCustomSelect,
38+
FloatPlaceholderSelect,
3839
SelectWithErrorSibling,
3940
ThrowsErrorOnInit,
4041
BasicSelectOnPush
@@ -590,20 +591,20 @@ describe('MdSelect', () => {
590591
});
591592

592593
it('should float the placeholder when the panel is open and unselected', () => {
593-
expect(fixture.componentInstance.select._placeholderState)
594+
expect(fixture.componentInstance.select._getPlaceholderAnimationState())
594595
.toEqual('', 'Expected placeholder to initially have a normal position.');
595596

596597
trigger.click();
597598
fixture.detectChanges();
598-
expect(fixture.componentInstance.select._placeholderState)
599+
expect(fixture.componentInstance.select._getPlaceholderAnimationState())
599600
.toEqual('floating-ltr', 'Expected placeholder to animate up to floating position.');
600601

601602
const backdrop =
602603
overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
603604
backdrop.click();
604605
fixture.detectChanges();
605606

606-
expect(fixture.componentInstance.select._placeholderState)
607+
expect(fixture.componentInstance.select._getPlaceholderAnimationState())
607608
.toEqual('', 'Expected placeholder to animate back down to normal position.');
608609
});
609610

@@ -616,7 +617,7 @@ describe('MdSelect', () => {
616617

617618
expect(placeholderEl.classList)
618619
.toContain('mat-floating-placeholder', 'Expected placeholder to display as floating.');
619-
expect(fixture.componentInstance.select._placeholderState)
620+
expect(fixture.componentInstance.select._getPlaceholderAnimationState())
620621
.toEqual('', 'Expected animation state to be empty to avoid animation.');
621622
});
622623

@@ -625,7 +626,8 @@ describe('MdSelect', () => {
625626

626627
trigger.click();
627628
fixture.detectChanges();
628-
expect(fixture.componentInstance.select._placeholderState).toEqual('floating-rtl');
629+
expect(fixture.componentInstance.select._getPlaceholderAnimationState())
630+
.toEqual('floating-rtl');
629631
});
630632

631633

@@ -1285,6 +1287,39 @@ describe('MdSelect', () => {
12851287
});
12861288
});
12871289

1290+
describe('floatPlaceholder option', () => {
1291+
let fixture: ComponentFixture<FloatPlaceholderSelect>;
1292+
1293+
beforeEach(() => {
1294+
fixture = TestBed.createComponent(FloatPlaceholderSelect);
1295+
});
1296+
1297+
it('should be able to disable the floating placeholder', () => {
1298+
let placeholder = fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
1299+
1300+
fixture.componentInstance.floatPlaceholder = 'never';
1301+
fixture.detectChanges();
1302+
1303+
expect(placeholder.style.visibility).toBe('visible');
1304+
expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBeFalsy();
1305+
1306+
fixture.componentInstance.control.setValue('pizza-1');
1307+
fixture.detectChanges();
1308+
1309+
expect(placeholder.style.visibility).toBe('hidden');
1310+
expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBeFalsy();
1311+
});
1312+
1313+
it('should be able to always float the placeholder', () => {
1314+
expect(fixture.componentInstance.control.value).toBeFalsy();
1315+
1316+
fixture.componentInstance.floatPlaceholder = 'always';
1317+
fixture.detectChanges();
1318+
1319+
expect(fixture.componentInstance.select._getPlaceholderAnimationState()).toBe('floating-ltr');
1320+
});
1321+
});
1322+
12881323
describe('with OnPush change detection', () => {
12891324
let fixture: ComponentFixture<BasicSelectOnPush>;
12901325
let trigger: HTMLElement;
@@ -1309,6 +1344,7 @@ describe('MdSelect', () => {
13091344
});
13101345
});
13111346

1347+
13121348
@Component({
13131349
selector: 'basic-select',
13141350
template: `
@@ -1529,6 +1565,29 @@ class BasicSelectOnPush {
15291565
@ViewChildren(MdOption) options: QueryList<MdOption>;
15301566
}
15311567

1568+
@Component({
1569+
selector: 'floating-placeholder-select',
1570+
template: `
1571+
<md-select placeholder="Food I want to eat right now" [formControl]="control"
1572+
[floatPlaceholder]="floatPlaceholder">
1573+
<md-option *ngFor="let food of foods" [value]="food.value">
1574+
{{ food.viewValue }}
1575+
</md-option>
1576+
</md-select>
1577+
`,
1578+
})
1579+
class FloatPlaceholderSelect {
1580+
floatPlaceholder: MdSelectFloatPlaceholderType = 'auto';
1581+
control = new FormControl();
1582+
foods: any[] = [
1583+
{ value: 'steak-0', viewValue: 'Steak' },
1584+
{ value: 'pizza-1', viewValue: 'Pizza' },
1585+
{ value: 'tacos-2', viewValue: 'Tacos'}
1586+
];
1587+
1588+
@ViewChild(MdSelect) select: MdSelect;
1589+
}
1590+
15321591

15331592
/**
15341593
* TODO: Move this to core testing utility until Angular has event faking

src/lib/select/select.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export class MdSelectChange {
7373
constructor(public source: MdSelect, public value: any) { }
7474
}
7575

76+
/** Allowed values for the floatPlaceholder option. */
77+
export type MdSelectFloatPlaceholderType = 'always' | 'never' | 'auto';
78+
7679
@Component({
7780
moduleId: module.id,
7881
selector: 'md-select, mat-select',
@@ -128,7 +131,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
128131
private _placeholder: string;
129132

130133
/** The animation state of the placeholder. */
131-
_placeholderState = '';
134+
private _placeholderState = '';
132135

133136
/**
134137
* The width of the trigger. Must be saved to set the min width of the overlay panel
@@ -226,6 +229,14 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
226229
get required() { return this._required; }
227230
set required(value: any) { this._required = coerceBooleanProperty(value); }
228231

232+
/** Whether to float the placeholder text. */
233+
@Input()
234+
get floatPlaceholder(): MdSelectFloatPlaceholderType { return this._floatPlaceholder; }
235+
set floatPlaceholder(value: MdSelectFloatPlaceholderType) {
236+
this._floatPlaceholder = value || 'auto';
237+
}
238+
private _floatPlaceholder: MdSelectFloatPlaceholderType = 'auto';
239+
229240
/** Event emitted when the select has been opened. */
230241
@Output() onOpen: EventEmitter<void> = new EventEmitter<void>();
231242

@@ -280,7 +291,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
280291
return;
281292
}
282293
this._calculateOverlayPosition();
283-
this._placeholderState = this._isRtl() ? 'floating-rtl' : 'floating-ltr';
294+
this._placeholderState = this._floatPlaceholderState();
284295
this._panelOpen = true;
285296
}
286297

@@ -588,6 +599,28 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
588599
return clampValue(0, optimalScrollPosition, maxScroll);
589600
}
590601

602+
/**
603+
* Figures out the appropriate animation state for the placeholder.
604+
*/
605+
_getPlaceholderAnimationState(): string {
606+
if (this.floatPlaceholder === 'never') {
607+
return '';
608+
}
609+
610+
if (this.floatPlaceholder === 'always') {
611+
return this._floatPlaceholderState();
612+
}
613+
614+
return this._placeholderState;
615+
}
616+
617+
/**
618+
* Determines the CSS `visibility` of the placeholder element.
619+
*/
620+
_getPlaceholderVisibility(): 'visible'|'hidden' {
621+
return (this.floatPlaceholder !== 'never' || !this.selected) ? 'visible' : 'hidden';
622+
}
623+
591624
/**
592625
* Calculates the y-offset of the select's overlay panel in relation to the
593626
* top start corner of the trigger. It has to be adjusted in order for the
@@ -699,6 +732,10 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
699732
return `50% ${originY}px 0px`;
700733
}
701734

735+
/** Figures out the floating placeholder state value. */
736+
private _floatPlaceholderState(): string {
737+
return this._isRtl() ? 'floating-rtl' : 'floating-ltr';
738+
}
702739
}
703740

704741
/** Clamps a value n between min and max values. */

0 commit comments

Comments
 (0)