Skip to content

Commit

Permalink
Merge pull request #1601 from ag-grid/AG-10486/context_menu_refactor
Browse files Browse the repository at this point in the history
AG-10486 Context Menu Refactor
  • Loading branch information
alantreadway committed May 23, 2024
2 parents 0242903 + 38fea43 commit 1e71e17
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 105 deletions.
20 changes: 11 additions & 9 deletions packages/ag-charts-community/src/chart/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,18 +351,18 @@ export abstract class Chart extends Observable implements AgChartInstance {
this.subtitle.registerInteraction(moduleContext),
this.footnote.registerInteraction(moduleContext),

ctx.interactionManager.addListener('click', (event) => this.onClick(event)),
ctx.interactionManager.addListener('dblclick', (event) => this.onDoubleClick(event)),
ctx.regionManager.listenAll('click', (event) => this.onClick(event)),
ctx.regionManager.listenAll('dblclick', (event) => this.onDoubleClick(event)),
seriesRegion.addListener('hover', (event) => this.onMouseMove(event)),
seriesRegion.addListener('leave', (event) => this.onLeave(event)),
seriesRegion.addListener('blur', () => this.onBlur()),
seriesRegion.addListener('tab', (event) => this.onTab(event)),
seriesRegion.addListener('nav-vert', (event) => this.onNavVert(event)),
seriesRegion.addListener('nav-hori', (event) => this.onNavHori(event)),
seriesRegion.addListener('submit', (event) => this.onSubmit(event)),
seriesRegion.addListener('contextmenu', (event) => this.onContextMenu(event), All),
ctx.keyNavManager.addListener('browserfocus', (event) => this.onBrowserFocus(event)),
ctx.interactionManager.addListener('page-left', () => this.destroy()),
ctx.interactionManager.addListener('contextmenu', (event) => this.onContextMenu(event), All),
ctx.animationManager.addListener('animation-start', () => this.onAnimationStart()),

ctx.animationManager.addListener('animation-frame', () => {
Expand Down Expand Up @@ -1169,11 +1169,16 @@ export abstract class Chart extends Observable implements AgChartInstance {
// We check InteractionState.Default too just in case we were in ContextMenu and the
// mouse hasn't moved since (see AG-10233).
const { Default, ContextMenu } = InteractionState;

let pickedNode: SeriesNodeDatum | undefined;
if (this.ctx.interactionManager.getState() & (Default | ContextMenu)) {
this.checkSeriesNodeRange(event, () => {
this.checkSeriesNodeRange(event, (_series, datum) => {
this.ctx.highlightManager.updateHighlight(this.id);
pickedNode = datum;
});
}

this.ctx.contextMenuRegistry.dispatchContext('series', event, { pickedNode });
}

protected focus: ChartFocusData = {
Expand Down Expand Up @@ -1357,6 +1362,7 @@ export abstract class Chart extends Observable implements AgChartInstance {
protected onClick(event: PointerInteractionEvent<'click'>) {
if (this.checkSeriesNodeClick(event)) {
this.update(ChartUpdateType.SERIES_UPDATE);
event.consume();
return;
}
this.fireEvent<AgChartClickEvent>({
Expand All @@ -1368,6 +1374,7 @@ export abstract class Chart extends Observable implements AgChartInstance {
protected onDoubleClick(event: PointerInteractionEvent<'dblclick'>) {
if (this.checkSeriesNodeDoubleClick(event)) {
this.update(ChartUpdateType.SERIES_UPDATE);
event.consume();
return;
}
this.fireEvent<AgChartDoubleClickEvent>({
Expand Down Expand Up @@ -1402,11 +1409,6 @@ export abstract class Chart extends Observable implements AgChartInstance {

// Find the node if exactly matched and update the highlight picked node
let pickedNode = this.pickSeriesNode({ x: event.offsetX, y: event.offsetY }, true);
if (pickedNode) {
this.ctx.highlightManager.updatePicked(this.id, pickedNode.datum);
} else {
this.ctx.highlightManager.updatePicked(this.id);
}

// First check if we should trigger the callback based on nearest node
if (datum && nodeClickRange === 'nearest') {
Expand Down
3 changes: 2 additions & 1 deletion packages/ag-charts-community/src/chart/chartContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ export class ChartContext implements ModuleContext {
this.annotationManager = new AnnotationManager(chart.annotationRoot);
this.ariaAnnouncementService = new AriaAnnouncementService(this.scene.canvas.element);
this.chartEventManager = new ChartEventManager();
this.contextMenuRegistry = new ContextMenuRegistry();
this.cursorManager = new CursorManager(this.domManager);
this.highlightManager = new HighlightManager();
this.interactionManager = new InteractionManager(chart.keyboard, this.domManager);
this.keyNavManager = new KeyNavManager(this.interactionManager, this.domManager);
this.focusIndicator = new FocusIndicator(this.domManager);
this.regionManager = new RegionManager(this.interactionManager, this.keyNavManager, this.focusIndicator);
this.contextMenuRegistry = new ContextMenuRegistry(this.regionManager);
this.toolbarManager = new ToolbarManager();
this.gestureDetector = new GestureDetector(this.domManager);
this.layoutService = new LayoutService();
Expand All @@ -111,6 +111,7 @@ export class ChartContext implements ModuleContext {
destroy() {
// chart.ts handles the destruction of the scene and zoomManager.
this.tooltipManager.destroy();
this.contextMenuRegistry.destroy();
this.regionManager.destroy();
this.focusIndicator.destroy();
this.keyNavManager.destroy();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import { Listeners } from '../../util/listeners';
import type { CategoryLegendDatum } from '../legendDatum';
import type { SeriesNodeDatum } from '../series/seriesTypes';
import { type ConsumableEvent, buildConsumable } from './consumableEvent';
import { InteractionState, type PointerInteractionEvent } from './interactionManager';
import type { RegionManager } from './regionManager';

type ContextTypeMap = {
all: {};
legend: { legendItem: CategoryLegendDatum | undefined };
series: { pickedNode: SeriesNodeDatum | undefined };
};

type ContextEventProperties<K extends ContextType = ContextType> = {
type: K;
x: number;
y: number;
context: ContextTypeMap[K];
sourceEvent: Event;
};

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

export type ContextMenuAction = {
id?: string;
label: string;
region: 'all' | 'series' | 'legend';
type: ContextType;
action: (params: ContextMenuActionParams) => void;
};

Expand All @@ -16,10 +40,56 @@ export class ContextMenuRegistry {
private readonly defaultActions: Array<ContextMenuAction> = [];
private readonly disabledActions: Set<string> = new Set();
private readonly hiddenActions: Set<string> = new Set();
private readonly listeners: Listeners<'', (e: ContextMenuEvent) => void> = new Listeners();
private readonly destroyFns: (() => void)[];

public constructor(regionManager: RegionManager) {
const { Default, ContextMenu } = InteractionState;
this.destroyFns = [regionManager.listenAll('contextmenu', (e) => this.onContextMenu(e), Default | ContextMenu)];
}

public destroy() {
this.destroyFns.forEach((d) => d());
}

private onContextMenu(event: PointerInteractionEvent<'contextmenu'>) {
const type = ContextMenuRegistry.toContextType(event.region);
if (type === 'all') {
this.dispatchContext('all', event, {});
}
}

private static toContextType(region: string | undefined): ContextType {
if (region === 'legend' || region === 'series') {
return region;
}
return 'all';
}

public static check<T extends ContextType>(type: T, event: ContextMenuEvent): event is ContextMenuEvent<T> {
return event.type === type;
}

public dispatchContext<T extends ContextType>(
type: T,
pointerEvent: PointerInteractionEvent<'contextmenu'>,
context: ContextTypeMap[T]
) {
const { pageX: x, pageY: y, sourceEvent } = pointerEvent;
this.listeners.dispatch('', this.buildConsumable({ type, x, y, context, sourceEvent }));
}

private buildConsumable<T extends ContextType>(nonconsumble: ContextEventProperties<T>): ContextMenuEvent<T> {
return buildConsumable(nonconsumble);
}

public addListener(handler: (event: ContextMenuEvent) => void) {
return this.listeners.addListener('', handler);
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { StateTracker } from '../../util/stateTracker';
import { BaseManager } from '../baseManager';
import type { CategoryLegendDatum } from '../legendDatum';
import type { SeriesNodeDatum } from '../series/seriesTypes';

export interface HighlightNodeDatum extends SeriesNodeDatum {
Expand All @@ -24,12 +23,7 @@ export interface HighlightChangeEvent {
*/
export class HighlightManager extends BaseManager<'highlight-change', HighlightChangeEvent> {
private readonly highlightStates = new StateTracker<HighlightNodeDatum>();
private readonly pickedStates = new StateTracker<SeriesNodeDatum>();
private readonly legendItemStates = new StateTracker<CategoryLegendDatum>();

private activeHighlight?: HighlightNodeDatum;
private activePicked?: SeriesNodeDatum;
private activeLegendItem?: CategoryLegendDatum;

public updateHighlight(callerId: string, highlightedDatum?: HighlightNodeDatum) {
const { activeHighlight: previousHighlight } = this;
Expand All @@ -48,24 +42,6 @@ export class HighlightManager extends BaseManager<'highlight-change', HighlightC
return this.activeHighlight;
}

public updatePicked(callerId: string, clickableDatum?: SeriesNodeDatum) {
this.pickedStates.set(callerId, clickableDatum);
this.activePicked = this.pickedStates.stateValue();
}

public getActivePicked(): SeriesNodeDatum | undefined {
return this.activePicked;
}

public updateLegendItem(callerId: string, clickableLegendItem?: CategoryLegendDatum) {
this.legendItemStates.set(callerId, clickableLegendItem);
this.activeLegendItem = this.legendItemStates.stateValue();
}

public getActiveLegendItem(): CategoryLegendDatum | undefined {
return this.activeLegendItem;
}

private isEqual(a?: SeriesNodeDatum, b?: SeriesNodeDatum) {
return a === b || (a?.series === b?.series && a?.itemId === b?.itemId && a?.datum === b?.datum);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ export class RegionManager {

private dispatch(region: Region | undefined, event: RegionEvent) {
event.region = region?.properties.name;
region?.listeners.dispatch(event.type, event);
this.allRegionsListeners.dispatch(event.type, event);
region?.listeners.dispatch(event.type, event);
}

// Process events during a drag action. Returns false if this event should follow the standard
Expand Down
28 changes: 15 additions & 13 deletions packages/ag-charts-community/src/chart/legend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,21 +241,23 @@ export class Legend extends BaseProperties {

ctx.contextMenuRegistry.registerDefaultAction({
id: ID_LEGEND_VISIBILITY,
region: 'legend',
type: 'legend',
label: 'Toggle Visibility',
action: (_params) => this.contextToggleVisibility(),
action: (params: { datum?: CategoryLegendDatum }) => this.contextToggleVisibility(params.datum),
});
ctx.contextMenuRegistry.registerDefaultAction({
id: ID_LEGEND_OTHER_SERIES,
region: 'legend',
type: 'legend',
label: 'Toggle Other Series',
action: (_params) => this.contextToggleOtherSeries(),
action: (params: { datum?: CategoryLegendDatum }) => this.contextToggleOtherSeries(params.datum),
});

const animationState = InteractionState.Default | InteractionState.Animation;
const { Default, Animation, ContextMenu } = InteractionState;
const animationState = Default | Animation;
const contextMenuState = Default | Animation | ContextMenu;
const region = ctx.regionManager.addRegion('legend', this.group);
this.destroyFns.push(
region.addListener('contextmenu', (e) => this.checkContextClick(e), animationState),
region.addListener('contextmenu', (e) => this.checkContextClick(e), contextMenuState),
region.addListener('click', (e) => this.checkLegendClick(e), animationState),
region.addListener('dblclick', (e) => this.checkLegendDoubleClick(e), animationState),
region.addListener('hover', (e) => this.handleLegendMouseMove(e)),
Expand Down Expand Up @@ -870,23 +872,24 @@ export class Legend extends BaseProperties {
return actualBBox;
}

private contextToggleVisibility() {
this.doClick(this.contextMenuDatum);
private contextToggleVisibility(datum: CategoryLegendDatum | undefined) {
this.doClick(datum);
}

private contextToggleOtherSeries() {
this.doDoubleClick(this.contextMenuDatum);
private contextToggleOtherSeries(datum: CategoryLegendDatum | undefined) {
this.doDoubleClick(datum);
}

private checkContextClick(event: PointerInteractionEvent<'contextmenu'>) {
this.contextMenuDatum = this.getDatumForPoint(event.offsetX, event.offsetY);
this.ctx.highlightManager.updateLegendItem(this.id, this.contextMenuDatum);
const legendItem = this.getDatumForPoint(event.offsetX, event.offsetY);

if (this.preventHidingAll && this.contextMenuDatum?.enabled && this.getVisibleItemCount() <= 1) {
this.ctx.contextMenuRegistry.disableAction(ID_LEGEND_VISIBILITY);
} else {
this.ctx.contextMenuRegistry.enableAction(ID_LEGEND_VISIBILITY);
}

this.ctx.contextMenuRegistry.dispatchContext('legend', event, { legendItem });
}

private checkLegendClick(event: PointerInteractionEvent<'click'>) {
Expand Down Expand Up @@ -1067,7 +1070,6 @@ export class Legend extends BaseProperties {
// is in a state when highlighting is possible.
if (this.ctx.interactionManager.getState() === InteractionState.Default) {
this.ctx.highlightManager.updateHighlight(this.id);
this.ctx.highlightManager.updateLegendItem(this.id);
}
}

Expand Down
Loading

0 comments on commit 1e71e17

Please sign in to comment.