Skip to content

Commit

Permalink
fix(components/ag-grid): option to show horizontal scrollbar at top w…
Browse files Browse the repository at this point in the history
…hen using trackpad (#552)

* fix(components/ag-grid): option to show horizontal scrollbar at top when using trackpad

* Test coverage

* Move to wrapper so data manager is not required

* Add css rules for .ag-body-horizontal-scroll.ag-scrollbar-invisible
  • Loading branch information
johnhwhite committed Sep 27, 2022
1 parent 5e9afad commit 2f75827
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe(`ag-grid-storybook data manager`, () => {
});

it(`should render ag-grid with data manager, ${label} layout`, () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('#ready')
.should('exist')
.end()
Expand All @@ -30,13 +31,16 @@ describe(`ag-grid-storybook data manager`, () => {
.should('have.length.gt', 14)
.should('have.descendants', '.sky-switch-control')
.end()
// Necessary to wait for the grid to render.
.wait(1000)
.get('#root')
.screenshot(
`datamanagercomponent-datamanager--data-manager-${domLayout}-${theme}`,
{
clip: { x: 0, y: 0, width: 1300, height: 600 },
}
)
.get('#root')
.percySnapshot(
`datamanagercomponent-datamanager--data-manager-${domLayout}-${theme}`,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,16 @@ it('should move the horizontal scroll based on enableTopScroll check', async ()
fixture.componentInstance.agGrid.gridReady.emit();
fixture.detectChanges();
await fixture.whenStable();
const scrollElement = fixture.nativeElement.querySelectorAll(
'.ag-body-horizontal-scroll'
);
expect(scrollElement.length).toEqual(1);
const gridComponents: string[] = Array.from(
fixture.nativeElement.querySelector('.ag-root')?.children || []
).map((el: HTMLElement) => el.classList[0]);
// Expect the scrollbar below the header.
expect(gridComponents).toEqual([
'ag-header',
'ag-body-horizontal-scroll',
'ag-floating-top',
'ag-body-viewport',
'ag-floating-bottom',
'ag-overlay',
]);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SkyAppTestUtility, expect } from '@skyux-sdk/testing';

import { AgGridAngular } from 'ag-grid-angular';
import { Column, ColumnApi, DetailGridInfo, GridApi } from 'ag-grid-community';
import {
Column,
ColumnApi,
DetailGridInfo,
FirstDataRenderedEvent,
GridApi,
GridReadyEvent,
RowDataChangedEvent,
} from 'ag-grid-community';
import { Subject } from 'rxjs';

import { SkyAgGridAdapterService } from './ag-grid-adapter.service';
import { SkyAgGridWrapperComponent } from './ag-grid-wrapper.component';
import { SkyAgGridModule } from './ag-grid.module';
import {
EnableTopScroll,
SkyAgGridFixtureComponent,
} from './fixtures/ag-grid.component.fixture';

describe('SkyAgGridWrapperComponent', () => {
let gridAdapterService: SkyAgGridAdapterService;
Expand All @@ -17,6 +30,9 @@ describe('SkyAgGridWrapperComponent', () => {
const agGrid: AgGridAngular = {
api: new GridApi(),
columnApi: new ColumnApi(),
gridReady: new Subject<GridReadyEvent>(),
rowDataChanged: new Subject<RowDataChangedEvent>(),
firstDataRendered: new Subject<FirstDataRenderedEvent>(),
} as AgGridAngular;

beforeEach(() => {
Expand Down Expand Up @@ -255,3 +271,90 @@ describe('SkyAgGridWrapperComponent', () => {
});
});
});

describe('SkyAgGridWrapperComponent via fixture', () => {
let gridWrapperFixture: ComponentFixture<SkyAgGridFixtureComponent>;
let gridWrapperNativeElement: HTMLElement;
const getChildrenClassNames = () =>
Array.from(
gridWrapperNativeElement.querySelector('.ag-root')?.children || []
).map((el: HTMLElement) => el.classList[0]);

it('should move the horizontal scroll based on enableTopScroll check, static data', async () => {
TestBed.configureTestingModule({
imports: [SkyAgGridModule],
providers: [
{
provide: EnableTopScroll,
useValue: true,
},
],
});
gridWrapperFixture = TestBed.createComponent(SkyAgGridFixtureComponent);
gridWrapperNativeElement = gridWrapperFixture.nativeElement;

gridWrapperFixture.detectChanges();
await gridWrapperFixture.whenStable();

// Expect the scrollbar at the bottom.
expect(getChildrenClassNames()).toEqual([
'ag-header',
'ag-body-horizontal-scroll',
'ag-floating-top',
'ag-body-viewport',
'ag-floating-bottom',
'ag-overlay',
]);
});

it('should move the horizontal scroll based on enableTopScroll check, async loading', async () => {
TestBed.configureTestingModule({
imports: [SkyAgGridModule],
});
gridWrapperFixture = TestBed.createComponent(SkyAgGridFixtureComponent);
gridWrapperNativeElement = gridWrapperFixture.nativeElement;

gridWrapperFixture.detectChanges();
await gridWrapperFixture.whenStable();

// Expect the scrollbar at the bottom.
expect(getChildrenClassNames()).toEqual([
'ag-header',
'ag-floating-top',
'ag-body-viewport',
'ag-floating-bottom',
'ag-body-horizontal-scroll',
'ag-overlay',
]);

gridWrapperFixture.componentInstance.gridOptions.context = {
enableTopScroll: true,
};
gridWrapperFixture.componentInstance.agGrid.gridReady.emit();

// Expect the scrollbar below the header.
expect(getChildrenClassNames()).toEqual([
'ag-header',
'ag-body-horizontal-scroll',
'ag-floating-top',
'ag-body-viewport',
'ag-floating-bottom',
'ag-overlay',
]);

gridWrapperFixture.componentInstance.gridOptions.context = {
enableTopScroll: false,
};
gridWrapperFixture.componentInstance.agGrid.rowDataChanged.emit();

// Expect the scrollbar at the bottom.
expect(getChildrenClassNames()).toEqual([
'ag-header',
'ag-floating-top',
'ag-body-viewport',
'ag-floating-bottom',
'ag-body-horizontal-scroll',
'ag-overlay',
]);
});
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { DOCUMENT } from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
Inject,
OnDestroy,
} from '@angular/core';

import { AgGridAngular } from 'ag-grid-angular';
import { DetailGridInfo } from 'ag-grid-community';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { SkyAgGridAdapterService } from './ag-grid-adapter.service';

Expand All @@ -19,7 +24,7 @@ let idIndex = 0;
templateUrl: './ag-grid-wrapper.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkyAgGridWrapperComponent implements AfterContentInit {
export class SkyAgGridWrapperComponent implements AfterContentInit, OnDestroy {
@ContentChild(AgGridAngular, {
static: true,
})
Expand All @@ -40,10 +45,13 @@ export class SkyAgGridWrapperComponent implements AfterContentInit {

private _viewkeeperClasses: string[] = [];

#ngUnsubscribe = new Subject<void>();

constructor(
private adapterService: SkyAgGridAdapterService,
private changeDetector: ChangeDetectorRef,
private elementRef: ElementRef
private elementRef: ElementRef,
@Inject(DOCUMENT) private document: Document
) {
idIndex++;
this.afterAnchorId = 'sky-ag-grid-nav-anchor-after-' + idIndex;
Expand All @@ -62,6 +70,24 @@ export class SkyAgGridWrapperComponent implements AfterContentInit {
this.viewkeeperClasses.push('.ag-header');
}
}
this.agGrid.gridReady.pipe(takeUntil(this.#ngUnsubscribe)).subscribe(() => {
this.#moveHorizontalScroll();
});
this.agGrid.firstDataRendered
.pipe(takeUntil(this.#ngUnsubscribe))
.subscribe(() => {
this.#moveHorizontalScroll();
});
this.agGrid.rowDataChanged
.pipe(takeUntil(this.#ngUnsubscribe))
.subscribe(() => {
this.#moveHorizontalScroll();
});
}

public ngOnDestroy(): void {
this.#ngUnsubscribe.next();
this.#ngUnsubscribe.complete();
}

/**
Expand Down Expand Up @@ -127,4 +153,42 @@ export class SkyAgGridWrapperComponent implements AfterContentInit {
return false;
}
}

#moveHorizontalScroll() {
if (this.agGrid && this.agGrid.api) {
const toTop = !!this.agGrid.gridOptions.context?.enableTopScroll;
const root: HTMLElement =
this.elementRef.nativeElement.querySelector('.ag-root');
const header: HTMLDivElement | null = root.querySelector('.ag-header');
const floatingBottom: HTMLDivElement | null = root.querySelector(
'.ag-floating-bottom'
);
const scrollbar: HTMLDivElement | null = root.querySelector(
'.ag-body-horizontal-scroll'
);
if (header && floatingBottom && scrollbar) {
if (
scrollbar.style.height !==
scrollbar.style.getPropertyValue(
'--sky-ag-body-horizontal-scroll-width'
)
) {
scrollbar.style.setProperty(
'--sky-ag-body-horizontal-scroll-width',
scrollbar.style.height
);
}
const isTop = !!root.children[1].matches('.ag-body-horizontal-scroll');
if (toTop && !isTop) {
const fragment = this.document.createDocumentFragment();
fragment.appendChild(scrollbar);
header.after(fragment);
} else if (!toTop && isTop) {
const fragment = this.document.createDocumentFragment();
fragment.appendChild(scrollbar);
floatingBottom.after(fragment);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<sky-ag-grid-wrapper>
<ag-grid-angular
#agGrid
class="sky-ag-grid-editable"
[gridOptions]="gridOptions"
[rowData]="gridData"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import {
Component,
Inject,
InjectionToken,
OnInit,
Optional,
ViewChild,
ViewEncapsulation,
} from '@angular/core';

import { AgGridAngular } from 'ag-grid-angular';
import { GridOptions } from 'ag-grid-community';

import { SkyAgGridService } from '../ag-grid.service';
import { SkyCellType } from '../types/cell-type';

import { SKY_AG_GRID_DATA, SKY_AG_GRID_LOOKUP } from './ag-grid-data.fixture';

export const EnableTopScroll = new InjectionToken('EnableTopScroll');

@Component({
selector: 'sky-ag-grid-component-fixture',
templateUrl: './ag-grid.component.fixture.html',
encapsulation: ViewEncapsulation.None,
})
export class SkyAgGridFixtureComponent implements OnInit {
@ViewChild('agGrid', { static: true })
public agGrid: AgGridAngular;

public gridData = SKY_AG_GRID_DATA;
public columnDefs = [
{
Expand Down Expand Up @@ -130,9 +144,17 @@ export class SkyAgGridFixtureComponent implements OnInit {
public gridOptions: GridOptions = {
columnDefs: this.columnDefs,
suppressColumnVirtualisation: true,
context: {
enableTopScroll: this.enableTopScroll,
},
};

constructor(private gridService: SkyAgGridService) {}
constructor(
private gridService: SkyAgGridService,
@Optional()
@Inject(EnableTopScroll)
public enableTopScroll: boolean | undefined
) {}

public ngOnInit(): void {
this.gridOptions = this.gridService.getEditableGridOptions({
Expand Down
33 changes: 14 additions & 19 deletions libs/components/ag-grid/src/lib/styles/ag-grid-styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,23 @@ $sky-cell-wrap-text-line-height-modern: $sky-theme-modern-font-paragraph-line-he
}

.sky-ag-grid.sky-ag-grid-top-scrollbar .ag-root {
> .ag-header {
order: 1;
}

> .ag-body-horizontal-scroll {
order: 2;
}

> .ag-floating-top {
order: 3;
}

> .ag-body-viewport {
order: 4;
}
.ag-center-cols-viewport {
/* Prevent a double scrollbar. */
scrollbar-width: none;

> .ag-floating-bottom {
order: 5;
&::-webkit-scrollbar {
display: none;
}
}
}

> .ag-overlay {
order: 6;
.ag-body-horizontal-scroll.ag-scrollbar-invisible {
/* Value is overridden in SkyAgGridWrapperComponent to match the calculated value AG Grid uses. */
--sky-ag-body-horizontal-scroll-width: 15px;
position: relative;
z-index: 1;
bottom: revert;
margin-bottom: calc(var(--sky-ag-body-horizontal-scroll-width) * -1);
}
}

Expand Down

0 comments on commit 2f75827

Please sign in to comment.