Skip to content

feat(select): support fallback positions #1873

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

Merged
merged 1 commit into from
Nov 16, 2016
Merged
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
14 changes: 12 additions & 2 deletions src/lib/select/select-animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,26 @@ export const transformPlaceholder: AnimationEntryMetadata = trigger('transformPl
* When the panel is removed from the DOM, it simply fades out linearly.
*/
export const transformPanel: AnimationEntryMetadata = trigger('transformPanel', [
state('showing-ltr', style({
state('top-ltr', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(-16px, -9px, 0) scaleY(1)`
})),
state('showing-rtl', style({
state('top-rtl', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(16px, -9px, 0) scaleY(1)`
})),
state('bottom-ltr', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(-16px, 8px, 0) scaleY(1)`
})),
state('bottom-rtl', style({
opacity: 1,
width: 'calc(100% + 32px)',
transform: `translate3d(16px, 8px, 0) scaleY(1)`
})),
transition('void => *', [
style({
opacity: 0,
Expand Down
5 changes: 3 additions & 2 deletions src/lib/select/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
</div>

<template connected-overlay [origin]="origin" [open]="panelOpen" hasBackdrop (backdropClick)="close()"
backdropClass="md-overlay-transparent-backdrop" [positions]="_positions" [width]="_getWidth()">
backdropClass="md-overlay-transparent-backdrop" [positions]="_positions" [width]="_getWidth()"
(positionChange)="_updateTransformOrigin($event)">
<div class="md-select-panel" [@transformPanel]="_getPanelState()" (@transformPanel.done)="_onPanelDone()"
(keydown)="_keyManager.onKeydown($event)">
(keydown)="_keyManager.onKeydown($event)" [style.transformOrigin]="_transformOrigin">
<div class="md-select-content" [@fadeInContent]="'showing'">
<ng-content></ng-content>
</div>
Expand Down
94 changes: 91 additions & 3 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {MdSelect} from './select';
import {MdOption} from './option';
import {Dir} from '../core/rtl/dir';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ViewportRuler} from '../core/overlay/position/viewport-ruler';

describe('MdSelect', () => {
let overlayContainerElement: HTMLElement;
Expand All @@ -19,17 +20,33 @@ describe('MdSelect', () => {
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');

// add fixed positioning to match real overlay container styles
overlayContainerElement.style.position = 'fixed';
Copy link
Member

Choose a reason for hiding this comment

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

Add comment explaining why you add these styles

overlayContainerElement.style.top = '0';
overlayContainerElement.style.left = '0';
document.body.appendChild(overlayContainerElement);

// remove body padding to keep consistent cross-browser
document.body.style.padding = '0';
document.body.style.margin = '0';

return {getContainerElement: () => overlayContainerElement};
}},
{provide: Dir, useFactory: () => {
return dir = { value: 'ltr' };
}}
}},
{provide: ViewportRuler, useClass: FakeViewportRuler}
]
});

TestBed.compileComponents();
}));

afterEach(() => {
document.body.removeChild(overlayContainerElement);
});

describe('overlay panel', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;
Expand Down Expand Up @@ -457,19 +474,78 @@ describe('MdSelect', () => {

trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPanelState()).toEqual('showing-ltr');
expect(fixture.componentInstance.select._getPanelState()).toEqual('top-ltr');
});

it('should use the rtl panel state when the dir is rtl', () => {
dir.value = 'rtl';

trigger.click();
fixture.detectChanges();
expect(fixture.componentInstance.select._getPanelState()).toEqual('showing-rtl');
expect(fixture.componentInstance.select._getPanelState()).toEqual('top-rtl');
});

});

describe('positioning', () => {
let fixture: ComponentFixture<BasicSelect>;
let trigger: HTMLElement;

beforeEach(() => {
fixture = TestBed.createComponent(BasicSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.md-select-trigger')).nativeElement;
});

it('should open below the trigger if the panel will fit', () => {
trigger.click();
fixture.detectChanges();

const overlayPane = overlayContainerElement.children[0] as HTMLElement;
const overlayRect = overlayPane.getBoundingClientRect();
const triggerRect = trigger.getBoundingClientRect();

// when the select panel opens below the trigger, the tops of the trigger and the overlay
// should be aligned.
expect(overlayRect.top.toFixed(2))
.toEqual(triggerRect.top.toFixed(2), `Expected panel to open below by default.`);

// animation should match the position
expect(fixture.componentInstance.select._getPanelState())
.toEqual('top-ltr', `Expected panel animation values to match the position.`);
expect(fixture.componentInstance.select._transformOrigin)
.toBe('top', `Expected panel animation to originate at the top.`);
});

it('should open above the trigger if there is not space below for the panel', () => {
// Push trigger to the bottom part of viewport, so it doesn't have space to open
// in its default position below the trigger.
trigger.style.position = 'relative';
trigger.style.top = '650px';

trigger.click();
fixture.detectChanges();

const overlayPane = overlayContainerElement.children[0] as HTMLElement;
const overlayRect = overlayPane.getBoundingClientRect();
const triggerRect = trigger.getBoundingClientRect();

// In "above" position, the bottom edges of the overlay and the origin are aligned.
// To find the overlay top, subtract the panel height from the origin's bottom edge.
const expectedTop = triggerRect.bottom - overlayRect.height;
expect(overlayRect.top.toFixed(2))
.toEqual(expectedTop.toFixed(2),
`Expected panel to open above the trigger if below wouldn't fit.`);

// animation should match the position
expect(fixture.componentInstance.select._getPanelState())
.toEqual('bottom-ltr', `Expected panel animation values to match the position.`);
expect(fixture.componentInstance.select._transformOrigin)
.toBe('bottom', `Expected panel animation to originate at the bottom.`);
});

});

describe('accessibility', () => {
let fixture: ComponentFixture<BasicSelect>;

Expand Down Expand Up @@ -658,3 +734,15 @@ function dispatchEvent(eventName: string, element: HTMLElement): void {
event.initEvent(eventName, true, true);
element.dispatchEvent(event);
}

class FakeViewportRuler {
getViewportRect() {
return {
left: 0, top: 0, width: 1014, height: 686, bottom: 686, right: 1014
};
}

getViewportScrollPosition() {
return {top: 0, left: 0};
}
}
42 changes: 32 additions & 10 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {Subscription} from 'rxjs/Subscription';
import {transformPlaceholder, transformPanel, fadeInContent} from './select-animations';
import {ControlValueAccessor, NgControl} from '@angular/forms';
import {coerceBooleanProperty} from '../core/coersion/boolean-property';
import {ConnectedOverlayPositionChange} from '../core/overlay/position/connected-position';

@Component({
moduleId: module.id,
Expand Down Expand Up @@ -77,16 +78,29 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
/** View -> model callback called when select has been touched */
_onTouched: Function;

/** This position config ensures that the top left corner of the overlay
* is aligned with with the top left of the origin (overlapping the trigger
* completely). In RTL mode, the top right corners are aligned instead.
/** The value of the select panel's transform-origin property. */
_transformOrigin: string = 'top';

/**
* This position config ensures that the top "start" corner of the overlay
* is aligned with with the top "start" of the origin by default (overlapping
* the trigger completely). If the panel cannot fit below the trigger, it
* will fall back to a position above the trigger.
*/
_positions = [{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top'
}];
_positions = [
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'top',
},
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'bottom',
},
];

@ViewChild('trigger') trigger: ElementRef;
@ContentChildren(MdOption) options: QueryList<MdOption>;
Expand Down Expand Up @@ -226,7 +240,7 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr

/** The animation state of the overlay panel. */
_getPanelState(): string {
return this._isRtl() ? 'showing-rtl' : 'showing-ltr';
return this._isRtl() ? `${this._transformOrigin}-rtl` : `${this._transformOrigin}-ltr`;
}

/** Ensures the panel opens if activated by the keyboard. */
Expand Down Expand Up @@ -264,6 +278,14 @@ export class MdSelect implements AfterContentInit, ControlValueAccessor, OnDestr
return this.disabled ? '-1' : '0';
}

/**
* Sets the transform-origin property of the panel to ensure that it
* animates in the correct direction based on its positioning.
*/
_updateTransformOrigin(pos: ConnectedOverlayPositionChange): void {
this._transformOrigin = pos.connectionPair.originY;
}

/** Sets up a key manager to listen to keyboard events on the overlay panel. */
private _initKeyManager() {
this._keyManager = new ListKeyManager(this.options);
Expand Down
2 changes: 1 addition & 1 deletion test/browser-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const configuration: { [name: string]: ConfigurationInfo } = {
'Safari8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'Safari9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}},
'iOS7': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
'iOS8': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
'iOS8': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}},
'iOS9': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}},
'WindowsPhone': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}
};
Expand Down