Skip to content
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ options = {
delayMs: 300,
staggerMs: 40,
maxAnimationDurationMs: 1500,
replayOn: ['reset-rows'],
extraColumnEffect: {
class: 'smart-rotating-gradient',
delayMs: 120,
Expand All @@ -273,6 +274,7 @@ Fields:
- `delayMs` (number): Delay before the sequence starts. Default: `300`.
- `staggerMs` (number): Extra delay applied per row (`rowIndex * staggerMs`). Default: `40`.
- `maxAnimationDurationMs` (number): Extra duration added after stagger starts to keep the animation state active. Default: `5000`.
- `replayOn` (string[]): List of handler event names that trigger the animation again after the initial play. Default: `[]`. Currently only the `reset-rows` event is supported..
- `extraColumnEffect` (object): Optional extra effect options.
- `extraColumnEffect.class` (string): Optional extra CSS class added to targeted cells while animation is active.
- `extraColumnEffect.delayMs` (number): Extra delay applied before the `extraColumnEffect.class` effect starts. Default: `0`.
Expand All @@ -281,7 +283,7 @@ Fields:

Notes:

- The sequence runs once per component lifecycle.
- The sequence plays once on first load, and replays whenever one of the `replayOn` events fires.

## Core Concepts

Expand Down
6 changes: 4 additions & 2 deletions addon/components/hyper-table-v2/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import type { InitialLoadAnimationContext } from '@upfluence/hypertable/components/hyper-table-v2';
import TableHandler from '@upfluence/hypertable/core/handler';
import TableHandler, { ROWS_PER_PAGE } from '@upfluence/hypertable/core/handler';
import { Column, ResolvedRenderingComponent, Row } from '@upfluence/hypertable/core/interfaces';

interface HyperTableV2CellArgs {
Expand Down Expand Up @@ -112,7 +112,9 @@ export default class HyperTableV2Cell extends Component<HyperTableV2CellArgs> {
}

private get shouldApplyInitialLoadAnimationSequence(): boolean {
return this.isInitialLoadAnimationEnabled && !this.loading;
const rowIndex = this.args.rowIndex ?? 0;

return this.isInitialLoadAnimationEnabled && !this.loading && rowIndex < ROWS_PER_PAGE;
}

private get shouldApplyInitialLoadAnimationCustomEffect(): boolean {
Expand Down
33 changes: 31 additions & 2 deletions addon/components/hyper-table-v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import TableHandler from '@upfluence/hypertable/core/handler';
import type { HandlerEvent } from '@upfluence/hypertable/core/handler';
import { Column, Row } from '@upfluence/hypertable/core/interfaces';

export type FeatureSet = {
Expand Down Expand Up @@ -37,6 +38,7 @@ type InitialLoadAnimationConfig = {
maxAnimationDurationMs: number;
extraColumnEffect?: InitialLoadAnimationExtraColumnEffect;
includeSelectionColumnInExtraEffect?: boolean;
replayOn?: Extract<HandlerEvent, 'reset-rows'>[];
};

interface HyperTableV2Args {
Expand Down Expand Up @@ -94,6 +96,7 @@ export default class HyperTableV2 extends Component<HyperTableV2Args> {
});

this.hypertableInstanceID = crypto.randomUUID();
this.registerAnimationReplayListeners(args.handler);
}

get features(): FeatureSet {
Expand Down Expand Up @@ -132,6 +135,10 @@ export default class HyperTableV2 extends Component<HyperTableV2Args> {
}
}

get columnsCountStyle(): ReturnType<typeof htmlSafe> {
return htmlSafe(`--hypertable-responsive-columns-number: ${this.args.handler.columns.length - 1}`);
}

get initialLoadAnimationContext(): InitialLoadAnimationContext | null {
return this.initialLoadAnimation ? { active: this.initialLoadAnimationActive, ...this.initialLoadAnimation } : null;
}
Expand Down Expand Up @@ -245,11 +252,22 @@ export default class HyperTableV2 extends Component<HyperTableV2Args> {
this.initialLoadAnimationTimeout = undefined;
}

this.unregisterAnimationReplayListeners();
this.args.handler.teardown();
}

get columnsCountStyle(): ReturnType<typeof htmlSafe> {
return htmlSafe(`--hypertable-responsive-columns-number: ${this.args.handler.columns.length - 1}`);
private registerAnimationReplayListeners(handler: TableHandler): void {
if (!this.initialLoadAnimation?.replayOn?.length) return;

for (const event of this.initialLoadAnimation.replayOn) {
handler.on(event, this.onAnimationReplay);
}
Comment thread
Miexil marked this conversation as resolved.
}

private unregisterAnimationReplayListeners(): void {
for (const event of this.initialLoadAnimation?.replayOn ?? []) {
this.args.handler.off(event, this.onAnimationReplay);
}
}

private _resetFilters(): void {
Expand All @@ -276,6 +294,17 @@ export default class HyperTableV2 extends Component<HyperTableV2Args> {
this.computeScrollableTable();
}

private onAnimationReplay = (): void => {
if (this.initialLoadAnimationTimeout) {
window.clearTimeout(this.initialLoadAnimationTimeout);
this.initialLoadAnimationTimeout = undefined;
}

this.initialLoadAnimationPlayed = false;
this.activateInitialLoadAnimationIfNeeded();
this.finalizeInitialLoadAnimation();
};

private activateInitialLoadAnimationIfNeeded(): void {
if (this.initialLoadAnimationPlayed || !this.initialLoadAnimation) {
return;
Expand Down
35 changes: 32 additions & 3 deletions addon/core/handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { set } from '@ember/object';
import { addListener, sendEvent } from '@ember/object/events';
import { addListener, removeListener, sendEvent } from '@ember/object/events';
import { scheduleOnce } from '@ember/runloop';
import { isEmpty } from '@ember/utils';
import { tracked } from '@glimmer/tracking';
Expand All @@ -22,7 +22,22 @@ import BaseRenderingResolver from './rendering-resolver';

export type RowMutator = (row: Row) => boolean;

const ROWS_PER_PAGE = 30;
export const HANDLER_EVENTS = [
'columns-loaded',
'row-click',
'apply-filters',
'apply-order',
'reset-columns',
'remove-column',
'remove-row',
'mutate-rows',
'reset-rows'
] as const;

export type HandlerEvent = (typeof HANDLER_EVENTS)[number];
export type LooseAutocomplete<T extends string> = T | (string & {});

export const ROWS_PER_PAGE = 30;

export default class TableHandler {
private _context: unknown;
Expand Down Expand Up @@ -167,12 +182,26 @@ export default class TableHandler {
* @param {Function} handler - A callback function to be called when the subscribed event is triggered.
* @returns {TableHandler}
*/
on(event: string, handler: (...args: any[]) => any): TableHandler {
on(event: LooseAutocomplete<HandlerEvent>, handler: (...args: any[]) => any): TableHandler {
addListener(this, event, handler);

return this;
}

off(event: LooseAutocomplete<HandlerEvent>, handler: (...args: any[]) => any): TableHandler {
try {
removeListener(this, event, handler);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);

if (!message.includes('did not exist on the instance')) {
throw error;
}
}

return this;
}

/**
* Add a column to the table.
*
Expand Down
53 changes: 52 additions & 1 deletion tests/integration/components/hyper-table-v2-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { click, render, findAll, type TestContext } from '@ember/test-helpers';
import { click, render, findAll, waitUntil, type TestContext } from '@ember/test-helpers';

import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
Expand Down Expand Up @@ -263,6 +263,57 @@ module('Integration | Component | hyper-table-v2', function (hooks) {

assert.dom('.hypertable__cell.smart-rotating-gradient').exists({ count: 12 });
});

module('resetRows', function () {
test('it replays the animation when resetRows is called', async function (this: TestContext, assert: Assert) {
Comment thread
Miexil marked this conversation as resolved.
this.options = {
initialLoadAnimation: {
delayMs: 0,
staggerMs: 0,
maxAnimationDurationMs: 50,
replayOn: ['reset-rows']
}
};

await render(hbs`<HyperTableV2 @handler={{this.handler}} @options={{this.options}} />`);
assert.dom('.hypertable__cell--initial-load-sequence').exists({ count: 12 });

await waitUntil(() => !document.querySelector('.hypertable__cell--initial-load-sequence'));
assert.dom('.hypertable__cell--initial-load-sequence').doesNotExist();

await this.handler.resetRows();
await waitUntil(() => document.querySelectorAll('.hypertable__cell--initial-load-sequence').length === 12);
assert.dom('.hypertable__cell--initial-load-sequence').exists({ count: 12 });
});

test('it does not apply the animation when resetRows is called without replayOn', async function (this: TestContext, assert: Assert) {
this.options = {
initialLoadAnimation: {
delayMs: 0,
staggerMs: 0,
maxAnimationDurationMs: 0
}
};

await render(hbs`<HyperTableV2 @handler={{this.handler}} @options={{this.options}} />`);

await waitUntil(() => !document.querySelector('.hypertable__cell--initial-load-sequence'));

await this.handler.resetRows();

assert.dom('.hypertable__cell--initial-load-sequence').doesNotExist();
});

test('it does not apply the animation when resetRows is called without the proper config', async function (this: TestContext, assert: Assert) {
await render(hbs`<HyperTableV2 @handler={{this.handler}} />`);

assert.dom('.hypertable__cell--initial-load-sequence').doesNotExist();

await this.handler.resetRows();

assert.dom('.hypertable__cell--initial-load-sequence').doesNotExist();
});
});
});

module('empty state', function (hooks) {
Expand Down
27 changes: 26 additions & 1 deletion tests/unit/core/handler-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ module('Unit | core/handler', function (hooks) {
});

module('Events', function () {
test('callbacks are called properly when an event is subscribed to', function (this: TestContext, assert: Assert) {
test('Handler#on - callbacks are called properly when an event is subscribed to', function (this: TestContext, assert: Assert) {
const handler = new TableHandler(getContext(), this.tableManager, this.rowsFetcher);
assert.expect(1);
handler.on('row-click', (row: Row) => {
Expand All @@ -539,6 +539,31 @@ module('Unit | core/handler', function (hooks) {

handler.triggerEvent('row-click', handler.rows[0]);
});

test('Handler#off - unsubscribes the callback so it is no longer called', function (this: TestContext, assert: Assert) {
const handler = new TableHandler(getContext(), this.tableManager, this.rowsFetcher);
const callback = sinon.spy();

handler.on('row-click', callback);
handler.off('row-click', callback);
handler.triggerEvent('row-click', handler.rows[0]);

assert.ok(callback.notCalled);
});

test('Handler#off - only removes the targeted callback and leaves other listeners intact', function (this: TestContext, assert: Assert) {
const handler = new TableHandler(getContext(), this.tableManager, this.rowsFetcher);
const removedCallback = sinon.spy();
const remainingCallback = sinon.spy();

handler.on('row-click', removedCallback);
handler.on('row-click', remainingCallback);
handler.off('row-click', removedCallback);
handler.triggerEvent('row-click', handler.rows[0]);

assert.ok(removedCallback.notCalled);
assert.ok(remainingCallback.calledOnce);
});
});

function populateSelectionAndExclusionHandler(handler: TableHandler): void {
Expand Down
Loading