From 4656ab4e11aabab7ca2c64cb24123ad76f735da1 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 14 Aug 2017 10:24:17 -0700 Subject: [PATCH 01/27] strip down select DOM and implement MdFormFieldControl --- src/demo-app/select/select-demo.html | 70 ++++++++------ src/demo-app/select/select-demo.ts | 2 +- src/lib/input/input.ts | 2 +- src/lib/select/_select-theme.scss | 18 ---- src/lib/select/public_api.ts | 2 +- src/lib/select/select-animations.ts | 19 ---- src/lib/select/select.html | 24 ++--- src/lib/select/select.scss | 50 ---------- src/lib/select/select.spec.ts | 2 +- src/lib/select/select.ts | 132 +++++++++------------------ 10 files changed, 96 insertions(+), 225 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index 8fad21eb8a7c..cced5c9eb049 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -6,13 +6,15 @@ ngModel - - None - - {{ drink.viewValue }} - - + + + None + + {{ drink.viewValue }} + + +

Value: {{ currentDrink }}

Touched: {{ drinkControl.touched }}

Dirty: {{ drinkControl.dirty }}

@@ -42,12 +44,14 @@ Multiple selection - - - {{ creature.viewValue }} - - + + + + {{ creature.viewValue }} + + +

Value: {{ currentPokemon }}

Touched: {{ pokemonControl.touched }}

Dirty: {{ pokemonControl.dirty }}

@@ -68,12 +72,14 @@ Without Angular forms - - None - - {{ creature.viewValue }} - - + + + None + + {{ creature.viewValue }} + + +

Value: {{ currentDigimon }}

@@ -85,14 +91,16 @@ Option groups - - - - {{ creature.viewValue }} - - - + + + + + {{ creature.viewValue }} + + + +
@@ -149,9 +157,11 @@ Change event - - {{ creature.viewValue }} - + + + {{ creature.viewValue }} + +

Change event value: {{ latestChangeEvent?.value }}

diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts index 48490643dbdf..3c3baf3c3de0 100644 --- a/src/demo-app/select/select-demo.ts +++ b/src/demo-app/select/select-demo.ts @@ -21,7 +21,7 @@ export class SelectDemo { currentPokemonFromGroup: string; currentDigimon: string; latestChangeEvent: MdSelectChange; - floatPlaceholder: string = 'auto'; + floatPlaceholder: string = 'auto'; // add to form-field foodControl = new FormControl('pizza-1'); topHeightCtrl = new FormControl(0); drinksTheme = 'primary'; diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index 6f02784aee05..5ca13eb27d3a 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -55,7 +55,7 @@ let nextUniqueId = 0; 'class': 'mat-input-element mat-form-field-autofill-control', // Native input properties that are overwritten by Angular inputs need to be synced with // the native input element. Otherwise property bindings for those don't work. - '[id]': 'id', + '[attr.id]': 'id', '[placeholder]': 'placeholder', '[disabled]': 'disabled', '[required]': 'required', diff --git a/src/lib/select/_select-theme.scss b/src/lib/select/_select-theme.scss index de7432ff3c30..18fc5817ef82 100644 --- a/src/lib/select/_select-theme.scss +++ b/src/lib/select/_select-theme.scss @@ -9,10 +9,6 @@ .mat-select-trigger, .mat-select-arrow { color: $color; } - - .mat-select-underline { - background-color: $color; - } } @mixin mat-select-theme($theme) { @@ -22,22 +18,12 @@ $accent: map-get($theme, accent); $warn: map-get($theme, warn); $is-dark-theme: map-get($theme, is-dark); - $underline-color: mat-color($foreground, divider, if($is-dark-theme, 0.7, 0.42)); .mat-select-trigger, .mat-select-arrow { color: mat-color($foreground, secondary-text); } - .mat-select-underline { - background-color: $underline-color; - } - - [aria-disabled='true'] .mat-select-underline { - // Since this is a dotted line, we need to make it slightly darker to get it to stand out. - @include mat-control-disabled-underline($underline-color); - } - .mat-select-disabled .mat-select-value, .mat-select-arrow, .mat-select-trigger { @@ -66,10 +52,6 @@ &.mat-accent { @include _mat-select-inner-content-theme($accent); } - - &.mat-select-required .mat-select-placeholder::after { - color: mat-color($warn); - } } .mat-select:focus:not(.mat-select-disabled).mat-warn, .mat-select-invalid { diff --git a/src/lib/select/public_api.ts b/src/lib/select/public_api.ts index ff3282c457e2..01ccc2f3e727 100644 --- a/src/lib/select/public_api.ts +++ b/src/lib/select/public_api.ts @@ -8,5 +8,5 @@ export * from './select-module'; export * from './select'; -export {fadeInContent, transformPanel, transformPlaceholder} from './select-animations'; +export * from './select-animations'; export * from './mat-exports'; diff --git a/src/lib/select/select-animations.ts b/src/lib/select/select-animations.ts index e92230508043..2813519212ef 100644 --- a/src/lib/select/select-animations.ts +++ b/src/lib/select/select-animations.ts @@ -22,25 +22,6 @@ import { * The values below match the implementation of the AngularJS Material md-select animation. */ -/** - * This animation shrinks the placeholder text to 75% of its normal size and translates - * it to either the top left corner (ltr) or top right corner (rtl) of the trigger, - * depending on the text direction of the application. - */ -export const transformPlaceholder: AnimationTriggerMetadata = trigger('transformPlaceholder', [ - state('floating-ltr', style({ - top: '-22px', - left: '-2px', - transform: 'scale(0.75)' - })), - state('floating-rtl', style({ - top: '-22px', - left: '2px', - transform: 'scale(0.75)' - })), - transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')) -]); - /** * This animation transforms the select's overlay panel on and off the page. * diff --git a/src/lib/select/select.html b/src/lib/select/select.html index e3c5823eea05..7b64cae7d7cc 100644 --- a/src/lib/select/select.html +++ b/src/lib/select/select.html @@ -1,26 +1,18 @@ { spyOn(fixture.componentInstance.customAccessor, 'writeValue'); fixture.detectChanges(); - expect(fixture.componentInstance.customAccessor.select._control) + expect(fixture.componentInstance.customAccessor.select.ngControl) .toBe(null, 'Expected md-select NOT to inherit control from parent value accessor.'); expect(fixture.componentInstance.customAccessor.writeValue).toHaveBeenCalled(); }); diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 8601e3066dcb..bc66c521c7bb 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -49,19 +49,18 @@ import {ControlValueAccessor, FormGroupDirective, NgControl, NgForm} from '@angu import { CanColor, CanDisable, - FloatPlaceholderType, HasTabIndex, - MD_PLACEHOLDER_GLOBAL_OPTIONS, MdOptgroup, MdOption, MdOptionSelectionChange, mixinColor, mixinDisabled, mixinTabIndex, - PlaceholderOptions, } from '@angular/material/core'; +import {MdFormFieldControl} from '@angular/material/form-field'; import {Observable} from 'rxjs/Observable'; import {merge} from 'rxjs/observable/merge'; +import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; import {fadeInContent, transformPanel, transformPlaceholder} from './select-animations'; import { @@ -70,6 +69,9 @@ import { getMdSelectNonFunctionValueError, } from './select-errors'; + +let nextUniqueId = 0; + /** * The following style constants are necessary to save here in order * to properly calculate the alignment of the selected option over @@ -182,30 +184,31 @@ export class MdSelectTrigger {} changeDetection: ChangeDetectionStrategy.OnPush, host: { 'role': 'listbox', + '[attr.id]': 'id', '[attr.tabindex]': 'tabIndex', '[attr.aria-label]': '_ariaLabel', '[attr.aria-labelledby]': 'ariaLabelledby', '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', - '[attr.aria-invalid]': '_isErrorState()', + '[attr.aria-invalid]': 'errorState', '[attr.aria-owns]': '_optionIds', '[attr.aria-multiselectable]': 'multiple', '[class.mat-select-disabled]': 'disabled', - '[class.mat-select-invalid]': '_isErrorState()', + '[class.mat-select-invalid]': 'errorState', '[class.mat-select-required]': 'required', 'class': 'mat-select', '(keydown)': '_handleClosedKeydown($event)', '(blur)': '_onBlur()', }, animations: [ - transformPlaceholder, transformPanel, fadeInContent ], + providers: [{provide: MdFormFieldControl, useExisting: MdSelect}], exportAs: 'mdSelect, matSelect', }) export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, OnDestroy, OnInit, - ControlValueAccessor, CanColor, CanDisable, HasTabIndex { + ControlValueAccessor, CanColor, CanDisable, HasTabIndex, MdFormFieldControl { /** Whether or not the overlay panel is open. */ private _panelOpen = false; @@ -233,15 +236,12 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Comparison function to specify which option is displayed. Defaults to object equality. */ private _compareWith = (o1: any, o2: any) => o1 === o2; + /** Unique id for this input. */ + private _uid = `mat-select-${nextUniqueId++}`; + /** Deals with the selection logic. */ _selectionModel: SelectionModel; - /** The animation state of the placeholder. */ - private _placeholderState = ''; - - /** Deals with configuring placeholder options */ - private _placeholderOptions: PlaceholderOptions; - /** * The width of the trigger. Must be saved to set the min width of the overlay panel * and the width of the selected value. @@ -251,12 +251,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Manages keyboard events for options in the panel. */ _keyManager: FocusKeyManager; - /** - * The width of the selected option's value. Must be set programmatically - * to ensure its overflow is clipped, as it's absolutely positioned. - */ - _selectedValueWidth: number; - /** View -> model callback called when value changes */ _onChange: (value: any) => void = () => {}; @@ -303,6 +297,19 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On }, ]; + /** + * Stream that emits whenever the state of the select changes such that the wrapping `MdFormField` + * needs to run change detection. + * TODO(mmalerba): Call emit at appropriate times. + */ + stateChanges = new Subject(); + + /** Whether the select is focused. TODO(mmalerba): Implement for real. */ + focused = false; + + /** TODO(mmalerba): Implement for real. */ + setDescribedByIds(x: string[]) {return x} + /** Trigger that opens the select. */ @ViewChild('trigger') trigger: ElementRef; @@ -365,14 +372,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On } } - /** Whether to float the placeholder text. */ - @Input() - get floatPlaceholder(): FloatPlaceholderType { return this._floatPlaceholder; } - set floatPlaceholder(value: FloatPlaceholderType) { - this._floatPlaceholder = value || this._placeholderOptions.float || 'auto'; - } - private _floatPlaceholder: FloatPlaceholderType; - /** Value of the select control. */ @Input() get value() { return this._value; } @@ -397,6 +396,12 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Input that can be used to specify the `aria-labelledby` attribute. */ @Input('aria-labelledby') ariaLabelledby: string = ''; + /** Unique id of the element. */ + @Input() + get id() { return this._id; } + set id(value: string) { this._id = value || this._uid; } + private _id: string; + /** Combined stream of all of the child options' change events. */ get optionSelectionChanges(): Observable { return merge(...this.options.map(option => option.onSelectionChange)); @@ -427,20 +432,20 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On @Optional() private _dir: Directionality, @Optional() private _parentForm: NgForm, @Optional() private _parentFormGroup: FormGroupDirective, - @Self() @Optional() public _control: NgControl, + @Self() @Optional() public ngControl: NgControl, @Attribute('tabindex') tabIndex: string, - @Optional() @Inject(MD_PLACEHOLDER_GLOBAL_OPTIONS) placeholderOptions: PlaceholderOptions, @Inject(MD_SELECT_SCROLL_STRATEGY) private _scrollStrategyFactory) { super(renderer, elementRef); - if (this._control) { - this._control.valueAccessor = this; + if (this.ngControl) { + this.ngControl.valueAccessor = this; } this.tabIndex = parseInt(tabIndex) || 0; - this._placeholderOptions = placeholderOptions ? placeholderOptions : {}; - this.floatPlaceholder = this._placeholderOptions.float || 'auto'; + + // Force setter to be called in case id was not specified. + this.id = this.id; } ngOnInit() { @@ -478,7 +483,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On } this._calculateOverlayPosition(); - this._placeholderState = this._floatPlaceholderState(); this._panelOpen = true; this._changeDetectorRef.markForCheck(); } @@ -487,11 +491,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On close(): void { if (this._panelOpen) { this._panelOpen = false; - - if (this._selectionModel.isEmpty()) { - this._placeholderState = ''; - } - this._changeDetectorRef.markForCheck(); this.focus(); } @@ -656,14 +655,14 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On } /** Whether the select has a value. */ - _hasValue(): boolean { - return this._selectionModel && this._selectionModel.hasValue(); + get empty(): boolean { + return this._selectionModel && this._selectionModel.isEmpty(); } /** Whether the select is in an error state. */ - _isErrorState(): boolean { - const isInvalid = this._control && this._control.invalid; - const isTouched = this._control && this._control.touched; + get errorState(): boolean { + const isInvalid = this.ngControl && this.ngControl.invalid; + const isTouched = this.ngControl && this.ngControl.touched; const isSubmitted = (this._parentFormGroup && this._parentFormGroup.submitted) || (this._parentForm && this._parentForm.submitted); @@ -715,12 +714,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On } } - this._setValueWidth(); - - if (this._selectionModel.isEmpty()) { - this._placeholderState = ''; - } - this._changeDetectorRef.markForCheck(); } @@ -788,7 +781,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On this._optionSubscription = filter.call(this.optionSelectionChanges, event => event.isUserInput).subscribe(event => { this._onSelect(event.source); - this._setValueWidth(); if (!this.multiple) { this.close(); @@ -880,15 +872,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On this.options.forEach(option => option.disableRipple = this.disableRipple); } } - /** - * Must set the width of the selected option's value programmatically - * because it is absolutely positioned and otherwise will not clip - * overflow. The selection arrow is 9px wide, add 4px of padding = 13 - */ - private _setValueWidth() { - this._selectedValueWidth = this._triggerWidth - 13; - this._changeDetectorRef.markForCheck(); - } /** * Focuses the selected item. If no option is selected, it will focus @@ -923,7 +906,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On // The farthest the panel can be scrolled before it hits the bottom const maxScroll = scrollContainerHeight - panelHeight; - if (this._hasValue()) { + if (!this.empty) { let selectedOptionOffset = this._getOptionIndex(this._selectionModel.selected[0])!; selectedOptionOffset += MdOption.countGroupLabelsBeforeOption(selectedOptionOffset, @@ -968,28 +951,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On return clampValue(0, optimalScrollPosition, maxScroll); } - /** - * Figures out the appropriate animation state for the placeholder. - */ - _getPlaceholderAnimationState(): string { - if (this.floatPlaceholder === 'never') { - return ''; - } - - if (this.floatPlaceholder === 'always') { - return this._floatPlaceholderState(); - } - - return this._placeholderState; - } - - /** - * Determines the CSS `opacity` of the placeholder element. - */ - _getPlaceholderOpacity(): string { - return (this.floatPlaceholder !== 'never' || this._selectionModel.isEmpty()) ? '1' : '0'; - } - /** Returns the aria-label of the select component. */ get _ariaLabel(): string | null { // If an ariaLabelledby value has been set, the select should not overwrite the @@ -1154,11 +1115,6 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On return `50% ${originY}px 0px`; } - /** Figures out the floating placeholder state value. */ - private _floatPlaceholderState(): string { - return this._isRtl() ? 'floating-rtl' : 'floating-ltr'; - } - /** Handles the user pressing the arrow keys on a closed select. */ private _handleArrowKey(event: KeyboardEvent): void { if (this._multiple) { From 80b65d5b1744b29c6b992c0ed8060a7c425e36b7 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 14 Aug 2017 14:40:03 -0700 Subject: [PATCH 02/27] tweak select styles to fit form-field --- src/lib/form-field/form-field.scss | 1 + src/lib/select/_select-theme.scss | 7 +++---- src/lib/select/select.scss | 32 ++++++++---------------------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index 449f60ecb7db..8a685fc8bd96 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -59,6 +59,7 @@ $mat-form-field-underline-height: 1px !default; display: block; position: relative; flex: auto; + min-width: 0; } // Pseudo-class for Chrome and Safari auto-fill to move the placeholder to the floating position. diff --git a/src/lib/select/_select-theme.scss b/src/lib/select/_select-theme.scss index 18fc5817ef82..684abe0a3e06 100644 --- a/src/lib/select/_select-theme.scss +++ b/src/lib/select/_select-theme.scss @@ -60,15 +60,14 @@ } @mixin mat-select-typography($config) { - $trigger-font-size: mat-font-size($config, subheading-2); + // The unit-less line-height from the font config. + $line-height: mat-line-height($config, input); .mat-select { - // Reserve enough space for the floating placeholder. - padding-top: $trigger-font-size; font-family: mat-font-family($config); } .mat-select-trigger { - font-size: $trigger-font-size; + height: $line-height * 1em; } } diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index 2b7e00e0a860..b70841bd9e0d 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -4,7 +4,6 @@ @import '../core/style/vendor-prefixes'; @import '../../cdk/a11y/a11y'; -$mat-select-trigger-height: 30px !default; $mat-select-trigger-min-width: 112px !default; $mat-select-arrow-size: 5px !default; $mat-select-arrow-margin: 4px !default; @@ -13,12 +12,17 @@ $mat-select-panel-max-height: 256px !default; .mat-select { display: inline-block; outline: none; + + // Inside of a form field we want it to expand to take the available space. + .mat-form-field & { + display: block; + width: 100%; + } } .mat-select-trigger { display: flex; align-items: center; - height: $mat-select-trigger-height; min-width: $mat-select-trigger-min-width; cursor: pointer; position: relative; @@ -31,34 +35,14 @@ $mat-select-panel-max-height: 256px !default; } .mat-select-value { - position: absolute; - max-width: calc(100% - #{($mat-select-arrow-size + $mat-select-arrow-margin) * 2}); - flex-grow: 1; - - // Firefox and some versions of IE incorrectly keep absolutely - // positioned children of flex containers in the flex flow when calculating - // position. This has been fixed for Firefox 52, slated for early 2017. - // Bug report: https://bugzilla.mozilla.org/show_bug.cgi?id=874718 - // - // In the meantime, we must adjust the position to fit the top, left, and bottom edge of the - // containing trigger element. In doing so, we can use align-items: center to allow the text to - // correctly position itself in the middle of the container. - top: 0; - left: 0; - bottom: 0; - display: flex; + flex: auto; align-items: center; - - [dir='rtl'] & { - left: auto; - right: 0; - } + min-width: 0; } .mat-select-value-text { @include mat-truncate-line(); - line-height: $mat-select-trigger-height; } .mat-select-arrow { From 8ff539ff71da3d13e71b93df521ebdf1b178e62e Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Mon, 14 Aug 2017 17:03:32 -0700 Subject: [PATCH 03/27] make dropdown adjust to form field's font size (still 1px rounding erorr in some cases) --- src/lib/select/select.html | 4 +- src/lib/select/select.scss | 19 ++++++++ src/lib/select/select.ts | 94 +++++++++++++++++++------------------- 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/lib/select/select.html b/src/lib/select/select.html index 7b64cae7d7cc..0b67f5ab1b13 100644 --- a/src/lib/select/select.html +++ b/src/lib/select/select.html @@ -6,6 +6,7 @@ #origin="cdkOverlayOrigin" #trigger> + {{ triggerValue }} @@ -36,7 +37,8 @@ (@transformPanel.done)="_onPanelDone()" (keydown)="_handlePanelKeydown($event)" [style.transformOrigin]="_transformOrigin" - [class.mat-select-panel-done-animating]="_panelDoneAnimating"> + [class.mat-select-panel-done-animating]="_panelDoneAnimating" + [style.font-size.px]="_triggerFontSize">
diff --git a/src/lib/select/select.scss b/src/lib/select/select.scss index b70841bd9e0d..93e9fcf05929 100644 --- a/src/lib/select/select.scss +++ b/src/lib/select/select.scss @@ -8,6 +8,7 @@ $mat-select-trigger-min-width: 112px !default; $mat-select-arrow-size: 5px !default; $mat-select-arrow-margin: 4px !default; $mat-select-panel-max-height: 256px !default; +$mat-select-item-height: 3em !default; .mat-select { display: inline-block; @@ -65,3 +66,21 @@ $mat-select-panel-max-height: 256px !default; outline: solid 1px; } } + +.mat-select-measure-font-size { + position: absolute; + display: block; + height: 1em; + visibility: hidden; + pointer-events: none; +} + +// Override optgroup and option to scale based on font-size of the trigger. +.mat-select-panel { + .mat-optgroup-label, + .mat-option { + font-size: inherit; + line-height: $mat-select-item-height; + height: $mat-select-item-height; + } +} diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index bc66c521c7bb..8a0d5dfdac00 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -78,31 +78,18 @@ let nextUniqueId = 0; * the trigger element. */ -/** The fixed height of every option element (option, group header etc.). */ -export const SELECT_ITEM_HEIGHT = 48; - /** The max height of the select's overlay panel */ export const SELECT_PANEL_MAX_HEIGHT = 256; -/** The max number of options visible at once in the select panel. */ -export const SELECT_MAX_OPTIONS_DISPLAYED = - Math.floor(SELECT_PANEL_MAX_HEIGHT / SELECT_ITEM_HEIGHT); - -/** The fixed height of the select's trigger element. */ -export const SELECT_TRIGGER_HEIGHT = 30; - -/** - * Must adjust for the difference in height between the option and the trigger, - * so the text will align on the y axis. - */ -export const SELECT_OPTION_HEIGHT_ADJUSTMENT = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2; - /** The panel's padding on the x-axis */ export const SELECT_PANEL_PADDING_X = 16; /** The panel's x axis padding if it is indented (e.g. there is an option group). */ export const SELECT_PANEL_INDENT_PADDING_X = SELECT_PANEL_PADDING_X * 2; +/** The height of the select items in `em` units. */ +export const SELECT_ITEM_HEIGHT_EM = 3; + /** * Distance between the panel edge and the option text in * multi-selection mode. @@ -239,6 +226,12 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Unique id for this input. */ private _uid = `mat-select-${nextUniqueId++}`; + /** The cached height of the trigger element. */ + private _triggerHeight: number; + + /** The cached font-size of the trigger element. */ + _triggerFontSize = 0; + /** Deals with the selection logic. */ _selectionModel: SelectionModel; @@ -313,6 +306,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Trigger that opens the select. */ @ViewChild('trigger') trigger: ElementRef; + /** Element used to measure the font-size of the trigger element. */ + @ViewChild('measureFontSize') _measureFontSizeEl: ElementRef; + /** Overlay pane containing the options. */ @ViewChild(ConnectedOverlayDirective) overlayDir: ConnectedOverlayDirective; @@ -684,7 +680,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On // Defer setting the value in order to avoid the "Expression // has changed after it was checked" errors from Angular. Promise.resolve().then(() => { - this._setSelectionByValue(this._control ? this._control.value : this._value); + this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value); }); } @@ -899,34 +895,30 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Calculates the scroll position and x- and y-offsets of the overlay panel. */ private _calculateOverlayPosition(): void { + this._triggerHeight = this.trigger.nativeElement.getBoundingClientRect().height; + this._triggerFontSize = this._measureFontSizeEl.nativeElement.getBoundingClientRect().height; + + const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; + const items = this._getItemCount(); - const panelHeight = Math.min(items * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT); - const scrollContainerHeight = items * SELECT_ITEM_HEIGHT; + const panelHeight = Math.min(items * itemHeight, SELECT_PANEL_MAX_HEIGHT); + const scrollContainerHeight = items * itemHeight; // The farthest the panel can be scrolled before it hits the bottom const maxScroll = scrollContainerHeight - panelHeight; - if (!this.empty) { - let selectedOptionOffset = this._getOptionIndex(this._selectionModel.selected[0])!; + // If no value is selected we open the popup to the first item. + let selectedOptionOffset = + this.empty ? 0 : this._getOptionIndex(this._selectionModel.selected[0])!; - selectedOptionOffset += MdOption.countGroupLabelsBeforeOption(selectedOptionOffset, - this.options, this.optionGroups); + selectedOptionOffset += MdOption.countGroupLabelsBeforeOption(selectedOptionOffset, + this.options, this.optionGroups); - // We must maintain a scroll buffer so the selected option will be scrolled to the - // center of the overlay panel rather than the top. - const scrollBuffer = panelHeight / 2; - this._scrollTop = this._calculateOverlayScroll(selectedOptionOffset, scrollBuffer, maxScroll); - this._offsetY = this._calculateOverlayOffsetY(selectedOptionOffset, scrollBuffer, maxScroll); - } else { - // If no option is selected, the panel centers on the first option. In this case, - // we must only adjust for the height difference between the option element - // and the trigger element, then multiply it by -1 to ensure the panel moves - // in the correct direction up the page. - let groupLabels = MdOption.countGroupLabelsBeforeOption(0, this.options, this.optionGroups); - - this._offsetY = (SELECT_ITEM_HEIGHT - SELECT_TRIGGER_HEIGHT) / 2 * -1 - - (groupLabels * SELECT_ITEM_HEIGHT); - } + // We must maintain a scroll buffer so the selected option will be scrolled to the + // center of the overlay panel rather than the top. + const scrollBuffer = panelHeight / 2; + this._scrollTop = this._calculateOverlayScroll(selectedOptionOffset, scrollBuffer, maxScroll); + this._offsetY = this._calculateOverlayOffsetY(selectedOptionOffset, scrollBuffer, maxScroll); this._checkOverlayWithinViewport(maxScroll); } @@ -940,8 +932,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On */ _calculateOverlayScroll(selectedIndex: number, scrollBuffer: number, maxScroll: number): number { - const optionOffsetFromScrollTop = SELECT_ITEM_HEIGHT * selectedIndex; - const halfOptionHeight = SELECT_ITEM_HEIGHT / 2; + const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; + const optionOffsetFromScrollTop = itemHeight * selectedIndex; + const halfOptionHeight = itemHeight / 2; // Starts at the optionOffsetFromScrollTop, which scrolls the option to the top of the // scroll container, then subtracts the scroll buffer to scroll the option down to @@ -1011,12 +1004,15 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On */ private _calculateOverlayOffsetY(selectedIndex: number, scrollBuffer: number, maxScroll: number): number { + const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; + const optionHeightAdjustment = (itemHeight - this._triggerHeight) / 2; + const maxOptionsDisplayed = Math.floor(SELECT_PANEL_MAX_HEIGHT / itemHeight); let optionOffsetFromPanelTop: number; if (this._scrollTop === 0) { - optionOffsetFromPanelTop = selectedIndex * SELECT_ITEM_HEIGHT; + optionOffsetFromPanelTop = selectedIndex * itemHeight; } else if (this._scrollTop === maxScroll) { - const firstDisplayedIndex = this._getItemCount() - SELECT_MAX_OPTIONS_DISPLAYED; + const firstDisplayedIndex = this._getItemCount() - maxOptionsDisplayed; const selectedDisplayIndex = selectedIndex - firstDisplayedIndex; // Because the panel height is longer than the height of the options alone, @@ -1024,18 +1020,18 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On // scrolled to the very bottom, this padding is at the top of the panel and // must be added to the offset. optionOffsetFromPanelTop = - selectedDisplayIndex * SELECT_ITEM_HEIGHT + SELECT_PANEL_PADDING_Y; + selectedDisplayIndex * itemHeight + SELECT_PANEL_PADDING_Y; } else { // If the option was scrolled to the middle of the panel using a scroll buffer, // its offset will be the scroll buffer minus the half height that was added to // center it. - optionOffsetFromPanelTop = scrollBuffer - SELECT_ITEM_HEIGHT / 2; + optionOffsetFromPanelTop = scrollBuffer - itemHeight / 2; } // The final offset is the option's offset from the top, adjusted for the height // difference, multiplied by -1 to ensure that the overlay moves in the correct // direction up the page. - return optionOffsetFromPanelTop * -1 - SELECT_OPTION_HEIGHT_ADJUSTMENT; + return optionOffsetFromPanelTop * -1 - optionHeightAdjustment; } /** @@ -1045,6 +1041,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On * sets the offset back to 0 to allow the fallback position to take over. */ private _checkOverlayWithinViewport(maxScroll: number): void { + const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; const viewportRect = this._viewportRuler.getViewportRect(); const triggerRect = this._getTriggerRect(); @@ -1054,7 +1051,7 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On const panelHeightTop = Math.abs(this._offsetY); const totalPanelHeight = - Math.min(this._getItemCount() * SELECT_ITEM_HEIGHT, SELECT_PANEL_MAX_HEIGHT); + Math.min(this._getItemCount() * itemHeight, SELECT_PANEL_MAX_HEIGHT); const panelHeightBottom = totalPanelHeight - panelHeightTop - triggerRect.height; if (panelHeightBottom > bottomSpaceAvailable) { @@ -1110,8 +1107,9 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On /** Sets the transform origin point based on the selected option. */ private _getOriginBasedOnOption(): string { - const originY = - Math.abs(this._offsetY) - SELECT_OPTION_HEIGHT_ADJUSTMENT + SELECT_ITEM_HEIGHT / 2; + const itemHeight = this._triggerFontSize * SELECT_ITEM_HEIGHT_EM; + const optionHeightAdjustment = (itemHeight - this._triggerHeight) / 2; + const originY = Math.abs(this._offsetY) - optionHeightAdjustment + itemHeight / 2; return `50% ${originY}px 0px`; } From 9ef3503efc9867c13e90c88cb92d59b2e766e8b0 Mon Sep 17 00:00:00 2001 From: Miles Malerba Date: Tue, 15 Aug 2017 11:52:43 -0700 Subject: [PATCH 04/27] fix 1px jitter and baseline alignment --- src/demo-app/baseline/baseline-demo.html | 18 ++++++++++++------ src/lib/select/select.html | 20 ++++++++++---------- src/lib/select/select.scss | 20 +++++++++++--------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/demo-app/baseline/baseline-demo.html b/src/demo-app/baseline/baseline-demo.html index fa30ca0d8aaf..cdcfa261fd12 100644 --- a/src/demo-app/baseline/baseline-demo.html +++ b/src/demo-app/baseline/baseline-demo.html @@ -14,9 +14,12 @@ | Text 5 | - - Option - + + + Option + This option is really really really long + + | Text 6 | @@ -42,9 +45,12 @@

| Text 5 | - - Option - + + + Option + This option is really really really long + + | Text 6 | diff --git a/src/lib/select/select.html b/src/lib/select/select.html index 0b67f5ab1b13..2fd609b40102 100644 --- a/src/lib/select/select.html +++ b/src/lib/select/select.html @@ -1,19 +1,19 @@ -