Skip to content

Commit 66b9c01

Browse files
committed
fix(virtual-scroll): make horizontal scrolling work in RTL
1 parent dcd2282 commit 66b9c01

File tree

9 files changed

+216
-70
lines changed

9 files changed

+216
-70
lines changed

src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
125125
attach(viewport: CdkVirtualScrollViewport) {
126126
this._averager.reset();
127127
this._viewport = viewport;
128-
this._setScrollOffset();
128+
this._renderContentForCurrentOffset();
129129
}
130130

131131
/** Detaches this scroll strategy from the currently attached viewport. */
@@ -143,7 +143,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
143143
/** @docs-private Implemented as part of VirtualScrollStrategy. */
144144
onDataLengthChanged() {
145145
if (this._viewport) {
146-
this._setScrollOffset();
146+
this._renderContentForCurrentOffset();
147147
this._checkRenderedContentSize();
148148
}
149149
}
@@ -238,7 +238,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
238238
// jitteriness if we just jump to the estimated position. Instead we make sure to scroll by
239239
// the same number of pixels as the scroll magnitude.
240240
if (scrollMagnitude >= viewport.getViewportSize()) {
241-
this._setScrollOffset();
241+
this._renderContentForCurrentOffset();
242242
} else {
243243
// The number of new items to render on the side the user is scrolling towards. Rather than
244244
// just filling the underscan space, we actually fill enough to have a buffer size of
@@ -343,18 +343,12 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
343343
}
344344

345345
/**
346-
* Sets the scroll offset and renders the content we estimate should be shown at that point.
347-
* @param scrollOffset The offset to jump to. If not specified the scroll offset will not be
348-
* changed, but the rendered content will be recalculated based on our estimate of what should
349-
* be shown at the current scroll offset.
346+
* Recalculates the rendered content based on our estimate of what should be shown at the current
347+
* scroll offset.
350348
*/
351-
private _setScrollOffset(scrollOffset?: number) {
349+
private _renderContentForCurrentOffset() {
352350
const viewport = this._viewport!;
353-
if (scrollOffset == null) {
354-
scrollOffset = viewport.measureScrollOffset();
355-
} else {
356-
viewport.setScrollOffset(scrollOffset);
357-
}
351+
const scrollOffset = viewport.measureScrollOffset();
358352
this._lastScrollOffset = scrollOffset;
359353
this._removalFailures = 0;
360354

src/cdk/scrolling/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ng_module(
1010
module_name = "@angular/cdk/scrolling",
1111
assets = [":virtual-scroll-viewport.css"] + glob(["**/*.html"]),
1212
deps = [
13+
"//src/cdk/bidi",
1314
"//src/cdk/coercion",
1415
"//src/cdk/collections",
1516
"//src/cdk/platform",

src/cdk/scrolling/scrolling-module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {BidiModule} from '@angular/cdk/bidi';
910
import {PlatformModule} from '@angular/cdk/platform';
1011
import {NgModule} from '@angular/core';
1112
import {CdkFixedSizeVirtualScroll} from './fixed-size-virtual-scroll';
@@ -14,8 +15,9 @@ import {CdkVirtualForOf} from './virtual-for-of';
1415
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
1516

1617
@NgModule({
17-
imports: [PlatformModule],
18+
imports: [BidiModule, PlatformModule],
1819
exports: [
20+
BidiModule,
1921
CdkFixedSizeVirtualScroll,
2022
CdkScrollable,
2123
CdkVirtualForOf,

src/cdk/scrolling/virtual-scroll-viewport.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ cdk-virtual-scroll-viewport {
4141
top: 0;
4242
left: 0;
4343
will-change: contents, transform;
44+
45+
[dir='rtl'] & {
46+
right: 0;
47+
left: auto;
48+
}
4449
}
4550

4651
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {

src/cdk/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 141 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,6 @@ describe('CdkVirtualScrollViewport', () => {
142142
expect(viewport.getOffsetToRenderedContentStart()).toBe(10);
143143
}));
144144

145-
it('should set scroll offset', fakeAsync(() => {
146-
finishInit(fixture);
147-
viewport.setScrollOffset(testComponent.itemSize * 2);
148-
fixture.detectChanges();
149-
flush();
150-
151-
triggerScroll(viewport);
152-
fixture.detectChanges();
153-
flush();
154-
155-
expect(viewport.elementRef.nativeElement.scrollTop).toBe(testComponent.itemSize * 2);
156-
expect(viewport.getRenderedRange()).toEqual({start: 2, end: 6});
157-
}));
158-
159145
it('should scroll to offset', fakeAsync(() => {
160146
finishInit(fixture);
161147
viewport.scrollToOffset(testComponent.itemSize * 2);
@@ -552,6 +538,92 @@ describe('CdkVirtualScrollViewport', () => {
552538
expect(testComponent.virtualForViewContainer.createEmbeddedView).toHaveBeenCalledTimes(5);
553539
}));
554540
});
541+
542+
describe('with RTL direction', () => {
543+
let fixture: ComponentFixture<FixedSizeVirtualScrollWithRtlDirection>;
544+
let testComponent: FixedSizeVirtualScrollWithRtlDirection;
545+
let viewport: CdkVirtualScrollViewport;
546+
let viewportEl: HTMLElement;
547+
let contentWrapperEl: HTMLElement;
548+
549+
beforeEach(() => {
550+
TestBed.configureTestingModule({
551+
imports: [ScrollingModule],
552+
declarations: [FixedSizeVirtualScrollWithRtlDirection],
553+
}).compileComponents();
554+
555+
fixture = TestBed.createComponent(FixedSizeVirtualScrollWithRtlDirection);
556+
testComponent = fixture.componentInstance;
557+
viewport = testComponent.viewport;
558+
viewportEl = viewport.elementRef.nativeElement;
559+
contentWrapperEl =
560+
viewportEl.querySelector('.cdk-virtual-scroll-content-wrapper') as HTMLElement;
561+
});
562+
563+
it('should initially be scrolled all the way right and showing the first item in horizontal' +
564+
' mode', fakeAsync(() => {
565+
testComponent.orientation = 'horizontal';
566+
finishInit(fixture);
567+
568+
expect(viewportEl.scrollLeft).toBe(
569+
testComponent.itemSize * testComponent.items.length - testComponent.viewportSize);
570+
expect(contentWrapperEl.style.transform).toMatch(/translateX\(0(px)?\)/);
571+
expect((contentWrapperEl.children[0] as HTMLElement).innerText).toBe('0 - 0');
572+
}));
573+
574+
it('should scroll through items as user scrolls to the left in horizontal mode',
575+
fakeAsync(() => {
576+
testComponent.orientation = 'horizontal';
577+
finishInit(fixture);
578+
579+
triggerScroll(viewport, 0);
580+
fixture.detectChanges();
581+
flush();
582+
583+
expect(contentWrapperEl.style.transform).toBe('translateX(-300px)');
584+
expect((contentWrapperEl.children[0] as HTMLElement).innerText).toBe('6 - 6');
585+
}));
586+
587+
it('should interpret scrollToOffset amount as an offset from the right in horizontal mode',
588+
fakeAsync(() => {
589+
testComponent.orientation = 'horizontal';
590+
finishInit(fixture);
591+
592+
viewport.scrollToOffset(100);
593+
triggerScroll(viewport);
594+
fixture.detectChanges();
595+
flush();
596+
597+
expect(viewportEl.scrollLeft).toBe(testComponent.itemSize * testComponent.items.length -
598+
testComponent.viewportSize - 100);
599+
}));
600+
601+
it('should scroll to the correct index in horizontal mode', fakeAsync(() => {
602+
testComponent.orientation = 'horizontal';
603+
finishInit(fixture);
604+
605+
viewport.scrollToIndex(2);
606+
triggerScroll(viewport);
607+
fixture.detectChanges();
608+
flush();
609+
610+
expect((contentWrapperEl.children[0] as HTMLElement).innerText).toBe('2 - 2');
611+
}));
612+
613+
it('should emit the scrolled to index in horizontal mode', fakeAsync(() => {
614+
testComponent.orientation = 'horizontal';
615+
finishInit(fixture);
616+
617+
expect(testComponent.scrolledToIndex).toBe(0);
618+
619+
viewport.scrollToIndex(2);
620+
triggerScroll(viewport);
621+
fixture.detectChanges();
622+
flush();
623+
624+
expect(testComponent.scrolledToIndex).toBe(2);
625+
}));
626+
});
555627
});
556628

557629

@@ -564,6 +636,11 @@ function finishInit(fixture: ComponentFixture<any>) {
564636
// On the second cycle we render the items.
565637
fixture.detectChanges();
566638
flush();
639+
640+
// Flush the initial fake scroll event.
641+
animationFrameScheduler.flush();
642+
flush();
643+
fixture.detectChanges();
567644
}
568645

569646
/** Trigger a scroll event on the viewport (optionally setting a new scroll offset). */
@@ -629,3 +706,53 @@ class FixedSizeVirtualScroll {
629706
return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize;
630707
}
631708
}
709+
710+
@Component({
711+
template: `
712+
<cdk-virtual-scroll-viewport dir="rtl"
713+
[itemSize]="itemSize" [bufferSize]="bufferSize" [orientation]="orientation"
714+
[style.height.px]="viewportHeight" [style.width.px]="viewportWidth"
715+
(scrolledIndexChange)="scrolledToIndex = $event">
716+
<div class="item"
717+
*cdkVirtualFor="let item of items; let i = index; trackBy: trackBy; \
718+
templateCacheSize: templateCacheSize"
719+
[style.height.px]="itemSize" [style.width.px]="itemSize">
720+
{{i}} - {{item}}
721+
</div>
722+
</cdk-virtual-scroll-viewport>
723+
`,
724+
styles: [`
725+
.cdk-virtual-scroll-content-wrapper {
726+
display: flex;
727+
flex-direction: column;
728+
}
729+
730+
.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper {
731+
flex-direction: row;
732+
}
733+
`],
734+
encapsulation: ViewEncapsulation.None,
735+
})
736+
class FixedSizeVirtualScrollWithRtlDirection {
737+
@ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;
738+
@ViewChild(CdkVirtualForOf, {read: ViewContainerRef}) virtualForViewContainer: ViewContainerRef;
739+
740+
@Input() orientation = 'vertical';
741+
@Input() viewportSize = 200;
742+
@Input() viewportCrossSize = 100;
743+
@Input() itemSize = 50;
744+
@Input() bufferSize = 0;
745+
@Input() items = Array(10).fill(0).map((_, i) => i);
746+
@Input() trackBy;
747+
@Input() templateCacheSize = 20;
748+
749+
scrolledToIndex = 0;
750+
751+
get viewportWidth() {
752+
return this.orientation == 'horizontal' ? this.viewportSize : this.viewportCrossSize;
753+
}
754+
755+
get viewportHeight() {
756+
return this.orientation == 'horizontal' ? this.viewportCrossSize : this.viewportSize;
757+
}
758+
}

0 commit comments

Comments
 (0)