Skip to content

Commit

Permalink
Merge pull request #1614 from ag-grid/AG-10486/contextmenu_typesafetl…
Browse files Browse the repository at this point in the history
…y_and_docs

AG-10486 Contextmenu callback type-safety and docs
  • Loading branch information
alantreadway committed May 30, 2024
2 parents b1e89fa + b297274 commit 95471fd
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AgContextMenuOptions } from '../../options/chart/contextMenuOptions';
import { Listeners } from '../../util/listeners';
import type { CategoryLegendDatum } from '../legendDatum';
import type { SeriesNodeDatum } from '../series/seriesTypes';
Expand All @@ -19,25 +20,31 @@ type ContextEventProperties<K extends ContextType = ContextType> = {
sourceEvent: Event;
};

// Extract the TEvent types from the AgContextMenuOptions contract:
type ContextMenuActionEventMap = {
all: Parameters<NonNullable<AgContextMenuOptions['extraActions']>[number]['action']>[0];
legend: Parameters<NonNullable<AgContextMenuOptions['extraLegendItemActions']>[number]['action']>[0];
series: Parameters<NonNullable<AgContextMenuOptions['extraNodeActions']>[number]['action']>[0];
};

export type ContextType = keyof ContextTypeMap;
export type ContextMenuEvent<K extends ContextType = ContextType> = ContextEventProperties<K> & ConsumableEvent;

export type ContextMenuAction = {
export type ContextMenuCallback<K extends ContextType> = {
all: (params: ContextMenuActionEventMap['all']) => void;
series: (params: ContextMenuActionEventMap['series']) => void;
legend: (params: ContextMenuActionEventMap['legend']) => void;
}[K];

export type ContextMenuAction<K extends ContextType> = {
id?: string;
label: string;
type: ContextType;
action: (params: ContextMenuActionParams) => void;
};

export type ContextMenuActionParams = {
datum?: any;
itemId?: string;
seriesId?: string;
event: MouseEvent;
type: K;
action: ContextMenuCallback<K>;
};

export class ContextMenuRegistry {
private readonly defaultActions: Array<ContextMenuAction> = [];
private readonly defaultActions: Array<ContextMenuAction<ContextType>> = [];
private readonly disabledActions: Set<string> = new Set();
private readonly hiddenActions: Set<string> = new Set();
private readonly listeners: Listeners<'', (e: ContextMenuEvent) => void> = new Listeners();
Expand Down Expand Up @@ -70,6 +77,14 @@ export class ContextMenuRegistry {
return event.type === type;
}

public static checkCallback<T extends ContextType>(
desiredType: T,
type: ContextType,
_callback: ContextMenuCallback<ContextType>
): _callback is ContextMenuCallback<T> {
return desiredType === type;
}

public dispatchContext<T extends ContextType>(
type: T,
pointerEvent: PointerInteractionEvent<'contextmenu'>,
Expand All @@ -87,13 +102,13 @@ export class ContextMenuRegistry {
return this.listeners.addListener('', handler);
}

public filterActions(type: ContextType): ContextMenuAction[] {
public filterActions(type: ContextType): ContextMenuAction<ContextType>[] {
return this.defaultActions.filter((action) => {
return action.id && !this.hiddenActions.has(action.id) && ['all', type].includes(action.type);
});
}

public registerDefaultAction(action: ContextMenuAction) {
public registerDefaultAction<T extends ContextType>(action: ContextMenuAction<T>) {
if (action.id && this.defaultActions.find(({ id }) => id === action.id)) {
return;
}
Expand Down
11 changes: 7 additions & 4 deletions packages/ag-charts-community/src/chart/legend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ModuleContext } from '../module/moduleContext';
import type {
AgChartLegendClickEvent,
AgChartLegendContextMenuEvent,
AgChartLegendDoubleClickEvent,
AgChartLegendLabelFormatterParams,
AgChartLegendListeners,
Expand Down Expand Up @@ -243,13 +244,13 @@ export class Legend extends BaseProperties {
id: ID_LEGEND_VISIBILITY,
type: 'legend',
label: 'Toggle Visibility',
action: (params: { datum?: CategoryLegendDatum }) => this.contextToggleVisibility(params.datum),
action: (params) => this.contextToggleVisibility(params),
});
ctx.contextMenuRegistry.registerDefaultAction({
id: ID_LEGEND_OTHER_SERIES,
type: 'legend',
label: 'Toggle Other Series',
action: (params: { datum?: CategoryLegendDatum }) => this.contextToggleOtherSeries(params.datum),
action: (params) => this.contextToggleOtherSeries(params),
});

const { Default, Animation, ContextMenu } = InteractionState;
Expand Down Expand Up @@ -872,11 +873,13 @@ export class Legend extends BaseProperties {
return actualBBox;
}

private contextToggleVisibility(datum: CategoryLegendDatum | undefined) {
private contextToggleVisibility(params: AgChartLegendContextMenuEvent) {
const datum = this.data.find((v) => v.itemId === params.itemId);
this.doClick(datum);
}

private contextToggleOtherSeries(datum: CategoryLegendDatum | undefined) {
private contextToggleOtherSeries(params: AgChartLegendContextMenuEvent) {
const datum = this.data.find((v) => v.itemId === params.itemId);
this.doDoubleClick(datum);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/ag-charts-community/src/chart/series/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export interface INodeEventConstructor<
TSeries extends Series<TDatum, any>,
TEvent extends string = SeriesNodeEventTypes,
> {
new (type: TEvent, event: Event, { datum }: TDatum, series: TSeries): INodeEvent<TEvent>;
new <T extends TEvent>(type: T, event: Event, { datum }: TDatum, series: TSeries): INodeEvent<T>;
}

export class SeriesNodeEvent<TDatum extends SeriesNodeDatum, TEvent extends string = SeriesNodeEventTypes>
Expand Down Expand Up @@ -708,7 +708,7 @@ export abstract class Series<
this.fireEvent(new this.NodeEvent('nodeDoubleClick', event, datum, this));
}

createNodeContextMenuActionEvent(event: Event, datum: TDatum): INodeEvent {
createNodeContextMenuActionEvent(event: Event, datum: TDatum): INodeEvent<'nodeContextMenuAction'> {
return new this.NodeEvent('nodeContextMenuAction', event, datum, this);
}

Expand Down
8 changes: 7 additions & 1 deletion packages/ag-charts-community/src/chart/series/seriesTypes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AgContextMenuOptions } from '../../options/chart/contextMenuOptions';
import type { BBox } from '../../scene/bbox';
import type { Group } from '../../scene/group';
import type { Point, SizedPoint } from '../../scene/point';
Expand All @@ -10,6 +11,11 @@ interface ChartAxisLike {
id: string;
}

// Ensure that the created contextmenu event matches the API option contract:
type NodeContextMenuActionEvent = Parameters<
NonNullable<AgContextMenuOptions['extraNodeActions']>[number]['action']
>[0];

export interface ISeries<TDatum, TProps> {
id: string;
axes: Record<ChartAxisDirection, ChartAxisLike | undefined>;
Expand All @@ -19,7 +25,7 @@ export interface ISeries<TDatum, TProps> {
update(opts: { seriesRect?: BBox }): Promise<void>;
fireNodeClickEvent(event: Event, datum: TDatum): void;
fireNodeDoubleClickEvent(event: Event, datum: TDatum): void;
createNodeContextMenuActionEvent(event: Event, datum: TDatum): any;
createNodeContextMenuActionEvent(event: Event, datum: TDatum): NodeContextMenuActionEvent;
getLegendData<T extends ChartLegendType>(legendType: T): ChartLegendDatum<T>[];
getLegendData(legendType: ChartLegendType): ChartLegendDatum<ChartLegendType>[];
getTooltipHtml(seriesDatum: any): TooltipContent;
Expand Down
1 change: 1 addition & 0 deletions packages/ag-charts-community/src/options/agChartOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './chart/axisOptions';
export * from './chart/crosshairOptions';
export * from './chart/chartOptions';
export * from './chart/chartBuilderOptions';
export * from './chart/contextMenuOptions';
export * from './chart/crossLineOptions';
export * from './chart/dropShadowOptions';
export * from './chart/errorBarOptions';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { AgNodeContextMenuActionEvent } from './eventOptions';
import type { AgChartContextMenuEvent, AgNodeContextMenuActionEvent } from './eventOptions';
import type { AgChartLegendContextMenuEvent } from './legendOptions';

export interface AgContextMenuOptions {
/** Whether to show the context menu. */
enabled?: boolean;
/** Custom actions displayed in the context menu when right-clicking anywhere on the chart. */
extraActions?: AgContextMenuAction<AgNodeContextMenuActionEvent>[];
extraActions?: AgContextMenuAction<AgChartContextMenuEvent>[];
/** Custom actions displayed in the context menu when right-clicking on a series node. */
extraNodeActions?: AgContextMenuAction<AgNodeContextMenuActionEvent>[];
/** Custom actions displayed in the context menu when right-clicking on a legend item. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export interface AgNodeDoubleClickEvent<TDatum> extends AgNodeBaseClickEvent<'no
/** Event type. */ type: 'nodeDoubleClick';
}

export interface AgNodeContextMenuActionEvent<TDatum = any>
extends AgNodeBaseClickEvent<'nodeContextMenuAction', TDatum> {
/** Event type. */ type: 'nodeContextMenuAction';
}

export interface AgChartClickEvent extends AgChartEvent<'click'> {
/** Event type. */ type: 'click';
}
Expand All @@ -54,8 +59,8 @@ export interface AgChartDoubleClickEvent extends AgChartEvent<'doubleClick'> {
/** Event type. */ type: 'doubleClick';
}

export interface AgNodeContextMenuActionEvent<TDatum = any> extends AgNodeBaseClickEvent<'contextMenuAction', TDatum> {
/** Event type. */ type: 'contextMenuAction';
export interface AgChartContextMenuEvent extends AgChartEvent<'contextMenuEvent'> {
/** Event type. */ type: 'contextMenuEvent';
}

export interface AgBaseChartListeners<TDatum> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AgChartLegendContextMenuEvent, _Scene } from 'ag-charts-community';
import type { AgContextMenuOptions, _Scene } from 'ag-charts-community';
import { _ModuleSupport, _Util } from 'ag-charts-community';

import {
Expand All @@ -8,16 +8,15 @@ import {
} from './contextMenuStyles';

type ContextMenuGroups = {
default: Array<ContextMenuItem>;
extra: Array<ContextMenuItem>;
extraNode: Array<ContextMenuItem>;
extraLegendItem: Array<ContextMenuItem>;
default: Array<ContextMenuAction>;
extra: Array<ContextMenuAction<'all'>>;
extraNode: Array<ContextMenuAction<'series'>>;
extraLegendItem: Array<ContextMenuAction<'legend'>>;
};
type ContextType = _ModuleSupport.ContextType;
type ContextMenuEvent = _ModuleSupport.ContextMenuEvent;
type ContextMenuAction = _ModuleSupport.ContextMenuAction;
type ContextMenuActionParams = _ModuleSupport.ContextMenuActionParams;
type ContextMenuItem = 'download' | ContextMenuAction;
type ContextMenuAction<T extends ContextType = ContextType> = _ModuleSupport.ContextMenuAction<T>;
type ContextMenuCallback<T extends ContextType> = _ModuleSupport.ContextMenuCallback<T>;

const { BOOLEAN, Validate, createElement, getWindow, ContextMenuRegistry } = _ModuleSupport;

Expand All @@ -33,17 +32,17 @@ export class ContextMenu extends _ModuleSupport.BaseModuleInstance implements _M
/**
* Extra menu actions with a label and callback.
*/
public extraActions: Array<ContextMenuAction> = [];
public extraActions: NonNullable<AgContextMenuOptions['extraActions']> = [];

/**
* Extra menu actions that only appear when clicking on a node.
*/
public extraNodeActions: Array<ContextMenuAction> = [];
public extraNodeActions: NonNullable<AgContextMenuOptions['extraNodeActions']> = [];

/**
* Extra menu actions that only appear when clicking on a legend item
*/
public extraLegendItemActions: Array<ContextMenuAction> = [];
public extraLegendItemActions: NonNullable<AgContextMenuOptions['extraLegendItemActions']> = [];

// Module context
private readonly scene: _Scene.Scene;
Expand Down Expand Up @@ -137,20 +136,26 @@ export class ContextMenu extends _ModuleSupport.BaseModuleInstance implements _M
this.pickedNode = undefined;
this.pickedLegendItem = undefined;

if (this.extraActions.length > 0) {
this.groups.extra = [...this.extraActions];
}
this.groups.extra = this.extraActions.map(({ label, action }) => {
return { type: 'all', label, action };
});

if (ContextMenuRegistry.check('series', event)) {
this.pickedNode = event.context.pickedNode;
if (this.extraNodeActions.length > 0 && this.pickedNode) {
this.groups.extraNode = [...this.extraNodeActions];
if (this.pickedNode) {
this.groups.extraNode = this.extraNodeActions.map(({ label, action }) => {
return { type: 'series', label, action };
});
}
}

if (ContextMenuRegistry.check('legend', event)) {
this.pickedLegendItem = event.context.legendItem;
this.groups.extraLegendItem = [...this.extraLegendItemActions];
if (this.pickedLegendItem) {
this.groups.extraLegendItem = this.extraLegendItemActions.map(({ label, action }) => {
return { type: 'legend', label, action };
});
}
}

const { default: def, extra, extraNode, extraLegendItem } = this.groups;
Expand Down Expand Up @@ -205,21 +210,13 @@ export class ContextMenu extends _ModuleSupport.BaseModuleInstance implements _M
}

if (this.pickedLegendItem) {
const extraLegendItem = this.groups.extraLegendItem
.filter((value): value is ContextMenuAction => typeof value !== 'string')
.map((contextMenuItem: ContextMenuAction) => {
return {
...contextMenuItem,
region: 'legend' as const,
};
});
this.appendMenuGroup(menuElement, extraLegendItem);
this.appendMenuGroup(menuElement, this.groups.extraLegendItem);
}

return menuElement;
}

public appendMenuGroup(menuElement: HTMLElement, group: ContextMenuItem[], divider = true) {
public appendMenuGroup(menuElement: HTMLElement, group: ContextMenuAction[], divider = true) {
if (group.length === 0) return;
if (divider) menuElement.appendChild(this.createDividerElement());
group.forEach((i) => {
Expand All @@ -228,7 +225,7 @@ export class ContextMenu extends _ModuleSupport.BaseModuleInstance implements _M
});
}

public renderItem(item: ContextMenuItem): HTMLElement | void {
public renderItem(item: ContextMenuAction): HTMLElement | void {
if (item && typeof item === 'object' && item.constructor === Object) {
return this.createActionElement(item);
}
Expand All @@ -248,38 +245,38 @@ export class ContextMenu extends _ModuleSupport.BaseModuleInstance implements _M
return this.createButtonElement(type, label, action);
}

private createButtonOnClick(type: ContextType, callback: (params: ContextMenuActionParams) => void): () => void {
if (type === 'legend') {
private createButtonOnClick<T extends ContextType>(type: T, callback: ContextMenuCallback<T>) {
if (ContextMenuRegistry.checkCallback('legend', type, callback)) {
return () => {
if (this.pickedLegendItem) {
const { seriesId, itemId, enabled } = this.pickedLegendItem;
const event: AgChartLegendContextMenuEvent & ContextMenuActionParams = {
type: 'contextmenu',
seriesId,
itemId,
enabled,
event: this.showEvent!,
};
callback({ type: 'contextmenu', seriesId, itemId, enabled });
this.hide();
}
};
} else if (ContextMenuRegistry.checkCallback('series', type, callback)) {
return () => {
const { pickedNode, showEvent } = this;
const event = pickedNode?.series.createNodeContextMenuActionEvent(showEvent!, pickedNode);

if (event) {
callback(event);
} else {
_Util.Logger.error('series node not found');
}
this.hide();
};
}
return () => {
const event = this.pickedNode?.series.createNodeContextMenuActionEvent(this.showEvent!, this.pickedNode);
if (event) {
callback(event);
} else {
callback({ event: this.showEvent! });
}

callback({ type: 'contextMenuEvent', event: this.showEvent! });
this.hide();
};
}

private createButtonElement(
type: ContextType,
private createButtonElement<T extends ContextType>(
type: T,
label: string,
callback: (params: ContextMenuActionParams) => void
callback: ContextMenuCallback<T>
): HTMLElement {
const el = createElement('button');
el.classList.add(`${DEFAULT_CONTEXT_MENU_CLASS}__item`);
Expand Down
Loading

0 comments on commit 95471fd

Please sign in to comment.