Skip to content

Commit

Permalink
feat(cdk-scrollable): add methods to normalize scrolling in RTL
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba committed Aug 9, 2018
1 parent ea10d94 commit cd90ac9
Show file tree
Hide file tree
Showing 4 changed files with 473 additions and 20 deletions.
43 changes: 43 additions & 0 deletions src/cdk/platform/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/** Cached result of whether the user's browser supports passive event listeners. */
let supportsPassiveEvents: boolean;
let rtlScrollAxisType: 'normal' | 'negated' | 'inverted';

/**
* Checks whether the user's browser supports passive event listeners.
Expand Down Expand Up @@ -89,3 +90,45 @@ export function getSupportedInputTypes(): Set<string> {

return supportedInputTypes;
}

/**
* Checks the type of RTL scroll axis used by this browser. The possible values are
* - normal: scrollLeft is 0 when scrolled all the way left and (scrollWidth - clientWidth) when
* scrolled all the way right.
* - negated: scrollLeft is -(scrollWidth - clientWidth) when scrolled all the way left and 0 when
* scrolled all the way right.
* - inverted: scrollLeft is (scrollWidth - clientWidth) when scrolled all the way left and 0 when
* scrolled all the way right.
*/
export function getRtlScrollAxisType(): 'normal' | 'negated' | 'inverted' {
// We can't check unless we're on the browser. Just assume 'normal' if we're not.
if (typeof document !== 'object' || !document) {
return 'normal';
}

if (!rtlScrollAxisType) {
const viewport = document.createElement('div');
viewport.dir = 'rtl';
viewport.style.height = '1px';
viewport.style.width = '1px';
viewport.style.overflow = 'auto';
viewport.style.visibility = 'hidden';
viewport.style.pointerEvents = 'none';
viewport.style.position = 'absolute';

const content = document.createElement('div');
content.style.width = '2px';
content.style.height = '1px';

viewport.appendChild(content);
document.body.appendChild(viewport);

rtlScrollAxisType = 'normal';
if (viewport.scrollLeft == 0) {
viewport.scrollLeft = 1;
rtlScrollAxisType = viewport.scrollLeft == 0 ? 'negated' : 'inverted';
}
document.body.removeChild(viewport);
}
return rtlScrollAxisType;
}
291 changes: 291 additions & 0 deletions src/cdk/scrolling/scrollable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import {Direction} from '@angular/cdk/bidi';
import {CdkScrollable, ScrollingModule} from '@angular/cdk/scrolling';
import {Component, ElementRef, Input, ViewChild} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';

function checkIntersecting(r1: {top: number, left: number, bottom: number, right: number},
r2: {top: number, left: number, bottom: number, right: number},
expected = true) {
const actual =
r1.left < r2.right && r1.right > r2.left && r1.top < r2.bottom && r1.bottom > r2.top;
if (expected) {
expect(actual)
.toBe(expected, `${JSON.stringify(r1)} should intersect with ${JSON.stringify(r2)}`);
} else {
expect(actual)
.toBe(expected, `${JSON.stringify(r1)} should not intersect with ${JSON.stringify(r2)}`);
}
}

fdescribe('CdkScrollable', () => {
let fixture: ComponentFixture<ScrollableViewport>;
let testComponent: ScrollableViewport;

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ScrollingModule],
declarations: [ScrollableViewport],
}).compileComponents();

fixture = TestBed.createComponent(ScrollableViewport);
testComponent = fixture.componentInstance;
}));

describe('in LTR context', () => {
let maxOffset = 0;

beforeEach(() => {
fixture.detectChanges();
maxOffset = testComponent.viewport.nativeElement.scrollHeight -
testComponent.viewport.nativeElement.clientHeight;
});

it('should initially be scrolled to top-left', () => {
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});

it('should scrollTo top-left', () => {
testComponent.scrollable.scrollTo({top: 0, left: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});

it('should scrollTo bottom-right', () => {
testComponent.scrollable.scrollTo({bottom: 0, right: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), true);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(0);
});

it('should scroll to top-end', () => {
testComponent.scrollable.scrollTo({top: 0, end: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(0);
});

it('should scroll to bottom-start', () => {
testComponent.scrollable.scrollTo({bottom: 0, start: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});
});

describe('in RTL context', () => {
let maxOffset = 0;

beforeEach(() => {
testComponent.dir = 'rtl';
fixture.detectChanges();
maxOffset = testComponent.viewport.nativeElement.scrollHeight -
testComponent.viewport.nativeElement.clientHeight;
});

it('should initially be scrolled to top-right', () => {
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});

it('should scrollTo top-left', () => {
testComponent.scrollable.scrollTo({top: 0, left: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(0);
});

it('should scrollTo bottom-right', () => {
testComponent.scrollable.scrollTo({bottom: 0, right: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});

it('should scroll to top-end', () => {
testComponent.scrollable.scrollTo({top: 0, end: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(0);
});

it('should scroll to bottom-start', () => {
testComponent.scrollable.scrollTo({bottom: 0, start: 0});

checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topStart.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.topEnd.nativeElement.getBoundingClientRect(), false);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomStart.nativeElement.getBoundingClientRect(), true);
checkIntersecting(testComponent.viewport.nativeElement.getBoundingClientRect(),
testComponent.bottomEnd.nativeElement.getBoundingClientRect(), false);

expect(testComponent.scrollable.measureScrollOffset('top')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('bottom')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('left')).toBe(maxOffset);
expect(testComponent.scrollable.measureScrollOffset('right')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('start')).toBe(0);
expect(testComponent.scrollable.measureScrollOffset('end')).toBe(maxOffset);
});
});
});

@Component({
template: `
<div #viewport class="viewport" cdkScrollable [dir]="dir">
<div class="row">
<div #topStart class="cell"></div>
<div #topEnd class="cell"></div>
</div>
<div class="row">
<div #bottomStart class="cell"></div>
<div #bottomEnd class="cell"></div>
</div>
</div>`,
styles: [`
.viewport {
width: 100px;
height: 100px;
overflow: auto;
}
.row {
display: flex;
flex-direction: row;
}
.cell {
flex: none;
width: 100px;
height: 100px;
}
`]
})
class ScrollableViewport {
@Input() dir: Direction;
@ViewChild(CdkScrollable) scrollable: CdkScrollable;
@ViewChild('viewport') viewport: ElementRef<HTMLElement>;
@ViewChild('topStart') topStart: ElementRef<HTMLElement>;
@ViewChild('topEnd') topEnd: ElementRef<HTMLElement>;
@ViewChild('bottomStart') bottomStart: ElementRef<HTMLElement>;
@ViewChild('bottomEnd') bottomEnd: ElementRef<HTMLElement>;
}
Loading

0 comments on commit cd90ac9

Please sign in to comment.