Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
feat(empty-state): Progressive enhancements for relayout based on Res…
Browse files Browse the repository at this point in the history
…izeObserver.

Fixes #648
  • Loading branch information
tomheller authored and lukasholzer committed Mar 4, 2020
1 parent 6f230a5 commit 532e275
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 28 deletions.
7 changes: 7 additions & 0 deletions apps/components-e2e/src/app/app.routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export const routes: Routes = [
module => module.DtE2EDrawerModule,
),
},
{
path: 'empty-state',
loadChildren: () =>
import('../components/empty-state/empty-state.module').then(
module => module.DtE2EEmptyStateModule,
),
},
{
path: 'event-chart',
loadChildren: () =>
Expand Down
51 changes: 51 additions & 0 deletions apps/components-e2e/src/components/empty-state/empty-state.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { resetWindowSizeToDefault, waitForAngular } from '../../utils';
import { emptyStateItem, emptyStateHorizontalWrapper } from './empty-state.po';

fixture('Empty state')
.page('http://localhost:4200/empty-state')
.beforeEach(async () => {
await waitForAngular();
await resetWindowSizeToDefault();
});

test('should show three items vertically aligned', async (testController: TestController) => {
await testController
.expect(emptyStateItem.count)
.eql(3)
.expect(emptyStateHorizontalWrapper.exists)
.notOk();
});

test('should show three items horizontally aligned on smaller screens', async (testController: TestController) => {
await testController
.resizeWindow(570, 800)
.expect(emptyStateItem.count)
.eql(3)
.expect(emptyStateHorizontalWrapper.exists)
.ok();
});

test('should show three items vertically aligned on very small screens', async (testController: TestController) => {
await testController
.resizeWindow(250, 800)
.expect(emptyStateItem.count)
.eql(3)
.expect(emptyStateHorizontalWrapper.exists)
.notOk();
});
34 changes: 34 additions & 0 deletions apps/components-e2e/src/components/empty-state/empty-state.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<dt-empty-state>
<dt-empty-state-item>
<!-- Give the empty state item an aria-level to indicate which level of heading it represents within the page -->
<dt-empty-state-item-title aria-level="2">
Optional Heading 1
</dt-empty-state-item-title>

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
</dt-empty-state-item>

<dt-empty-state-item>
<!-- Give the empty state item an aria-level to indicate which level of heading it represents within the page -->
<dt-empty-state-item-title aria-level="2">
Optional Heading 2
</dt-empty-state-item-title>

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
</dt-empty-state-item>

<dt-empty-state-item>
<!-- Give the empty state item an aria-level to indicate which level of heading it represents within the page -->
<dt-empty-state-item-title aria-level="2">
Optional Heading 3
</dt-empty-state-item-title>

Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
</dt-empty-state-item>
</dt-empty-state>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { Route, RouterModule } from '@angular/router';
import { DtEmptyStateModule } from '@dynatrace/barista-components/empty-state';
import { DtE2EEmptyState } from './empty-state';

const routes: Route[] = [{ path: '', component: DtE2EEmptyState }];

@NgModule({
declarations: [DtE2EEmptyState],
imports: [CommonModule, RouterModule.forChild(routes), DtEmptyStateModule],
exports: [],
providers: [],
})
export class DtE2EEmptyStateModule {}
23 changes: 23 additions & 0 deletions apps/components-e2e/src/components/empty-state/empty-state.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Selector } from 'testcafe';

export const emptyStateHorizontalWrapper = Selector(
'.dt-empty-state-items-horizontal',
);

export const emptyStateItem = Selector('.dt-empty-state-item');
23 changes: 23 additions & 0 deletions apps/components-e2e/src/components/empty-state/empty-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Component } from '@angular/core';

@Component({
selector: 'dt-e2e-empty-state',
templateUrl: 'empty-state.html',
})
export class DtE2EEmptyState {}
94 changes: 68 additions & 26 deletions libs/barista-components/empty-state/src/empty-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {
ViewChild,
} from '@angular/core';
import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { takeUntil, startWith } from 'rxjs/operators';

import {
DtViewportResizer,
Expand All @@ -50,6 +50,8 @@ const ITEMS_HORIZONTAL_BREAKPOINT = 540;
/** The min-width from which the empty state items are aligned next to each other. */
const LAYOUT_HORIZONTAL_BREAKPOINT = 760;

declare const window: any;

/**
* An empty state item. An empty state card may contain one or more such items.
*
Expand Down Expand Up @@ -162,6 +164,8 @@ export class DtEmptyState

private readonly _destroy$ = new Subject<void>();

private _containerSizeObserver: any;

/**
* @internal
* Whether empty state items should have a horizontal layout
Expand All @@ -186,6 +190,10 @@ export class DtEmptyState
constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef<HTMLElement>,
/**
* @deprecated Remove the viewportResizer from the constructor as it is no longer needed.
* @breaking-change Remove the viewportResizer in version 8.0.0
*/
private _viewportResizer: DtViewportResizer,
private _platform: Platform,
) {}
Expand All @@ -197,48 +205,82 @@ export class DtEmptyState
}

ngAfterViewInit(): void {
this._viewportResizer
.change()
.pipe(startWith(null), takeUntil(this._destroy$))
.subscribe(() => {
this._updateLayout();
// Check if the browser supports the resizeObserver.
if (this._platform.isBrowser && 'ResizeObserver' in window) {
this._containerSizeObserver = new window.ResizeObserver(entries => {
if (entries && entries[0]) {
// We need to wrap the call to the layout update into an additional
// requestAnimationFrame, because the resize observer would trow a
// javascript error when it is not able to process all entries within
// a single animation frame.
// From the specification:
// > This error means that ResizeObserver was not able to deliver all
// > observations within a single animation frame. It is benign (your
// > site will not break). - Aleksandar Totic
// https://stackoverflow.com/a/50387233
requestAnimationFrame(() => {
this._updateLayoutForSize(entries[0].contentRect.width);
});
}
});
this._containerSizeObserver.observe(this._elementRef.nativeElement);
} else {
this._viewportResizer
.change()
.pipe(startWith(null), takeUntil(this._destroy$))
.subscribe(() => {
this._updateLayout();
});
}
}

ngOnDestroy(): void {
this._destroy$.next();
this._destroy$.complete();
if (this._containerSizeObserver) {
this._containerSizeObserver.disconnect();
}
}

/** @internal Updates the layout according to the width of the container (horizontal or vertical) */
_updateLayout(): void {
if (this._platform.isBrowser) {
const componentWidth = this._elementRef.nativeElement.getBoundingClientRect()
.width;
/** Function that updates the layout based on the passed component width */
private _updateLayoutForSize(componentWidth: number): void {
const itemLayoutHorizontal = componentWidth > ITEMS_HORIZONTAL_BREAKPOINT;
const layoutHorizontal =
this._items?.length > 1 && componentWidth > LAYOUT_HORIZONTAL_BREAKPOINT;

const itemLayoutHorizontal = componentWidth > ITEMS_HORIZONTAL_BREAKPOINT;
const layoutHorizontal =
this._items?.length > 1 &&
componentWidth > LAYOUT_HORIZONTAL_BREAKPOINT;
_toggleCssClass(
layoutHorizontal,
this._elementRef.nativeElement,
'dt-empty-state-layout-horizontal',
);
_toggleCssClass(
itemLayoutHorizontal && !layoutHorizontal,
this._elementRef.nativeElement,
'dt-empty-state-items-horizontal',
);
}

_toggleCssClass(
layoutHorizontal,
this._elementRef.nativeElement,
'dt-empty-state-layout-horizontal',
);
_toggleCssClass(
itemLayoutHorizontal && !layoutHorizontal,
this._elementRef.nativeElement,
'dt-empty-state-items-horizontal',
);
}
/**
* @internal
* Updates the layout according to the width of the container (horizontal or vertical)
* @deprecated will be removed once the viewportResizer is removed from the constructor.
* @breaking-change Remove with version 8.0.0
*/
_updateLayout(): void {
const componentWidth = this._elementRef.nativeElement.getBoundingClientRect()
.width;
this._updateLayoutForSize(componentWidth);
}
}

/**
* Empty state base class that needs to be implemented by every custom
* empty state that is used inside the table. It provides a proxy to the updateLayout
* function of the empty state that will be called by the table.
*
* @deprecated Remove this class, as it is no longer needed to proxy the update
* layout call, when the layout updates are triggered by the resize observer.
* @breaking-change Remove the viewportResizer in version 8.0.0
*/
export class DtCustomEmptyStateBase {
/** @internal Finds the empty state inside the component */
Expand Down
1 change: 0 additions & 1 deletion libs/barista-components/table/src/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ export class DtTable<T> extends _DtTableBase<T> implements OnDestroy {
.pipe(mapTo(this._emptyState.first))
.subscribe(emptyState => {
// Update the layout of the empty state after it was attached
emptyState._updateLayout();
emptyState._visible = true;
});

Expand Down
1 change: 0 additions & 1 deletion typings.d.ts

This file was deleted.

0 comments on commit 532e275

Please sign in to comment.