JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 24
  • Score
    100M100P100Q99284F
  • License MIT

Flexible context menu component for Cesium/Resium applications with multiple activation modes

Package Exports

  • resium-entity-context-menu

Readme

Entity Context Menu for React/Cesium

A lightweight, type-safe, and functional context menu system for Resium/Cesium applications. Fully controlled via React Context without global registries or singletons.

โœจ Features

  • ๐ŸŽฏ Context-first Architecture - Everything is declaratively controlled via React Context
  • ๐Ÿ”ง Pure Functional Design - Menu factories as pure functions without side effects
  • ๐ŸŽจ Flexible Configuration - Per-entity overrides, type-based factories
  • โšก Async Ready - Supports asynchronous menu generation with loading states
  • โ™ฟ Fully Accessible - Keyboard navigation, ARIA roles, focus management
  • ๐Ÿ“ฆ TypeScript Support - Type-safe throughout
  • ๐Ÿš€ Zero Dependencies - Only React as peer dependency

๐Ÿ“ฆ Installation

npm install resium-entity-context-menu
# or
yarn add resium-entity-context-menu
# or
pnpm add resium-entity-context-menu

๐Ÿš€ Quick Start

1. Setup Provider

import { EntityContextMenuProvider, EntityContextMenu } from 'resium-entity-context-menu';

function App() {
  // Default factory for all entities
  const defaultFactory = (ctx) => [
    {
      id: 'info',
      label: 'Show Info',
      onClick: () => console.log(ctx),
    },
  ];

  // Type-specific factories
  const factoriesByType = {
    city: (ctx) => [
      {
        id: 'fly',
        label: 'Fly Here',
        onClick: () => flyToCity(ctx.worldPosition),
      },
    ],
  };

  return (
    <EntityContextMenuProvider defaultFactory={defaultFactory} factoriesByType={factoriesByType}>
      <CesiumMap />
      <EntityContextMenu />
    </EntityContextMenuProvider>
  );
}

2. Use in Components

import { useEntityContextMenu } from 'resium-entity-context-menu';

function MyEntity({ entity }) {
  const { showMenu } = useEntityContextMenu();

  const handleRightClick = (e) => {
    e.preventDefault();
    showMenu({
      entityId: entity.id,
      entityType: entity.type,
      position: { x: e.clientX, y: e.clientY },
      entityData: entity,
      clickedAt: new Date().toISOString(),
    });
  };

  return <div onContextMenu={handleRightClick}>{/* Entity content */}</div>;
}

3. Per-Entity Override

Entities can provide their own menu factory:

const berlinEntity = {
  id: 'berlin',
  type: 'city',
  name: 'Berlin',
  // Highest priority!
  menuFactory: (ctx) => [
    {
      id: 'special',
      label: 'Berlin-specific Action',
      onClick: () => openBerlinDetails(),
    },
  ],
};

๐ŸŽฏ Priority System

Menu resolution follows this priority:

  1. entity.menuFactory - Entity-specific menu (highest priority)
  2. factoriesByType[entityType] - Type-based menu
  3. defaultFactory - Default menu (lowest priority)

๐Ÿ“ API Reference

EntityContextMenuProvider

type EntityContextMenuProviderProps = {
  children: React.ReactNode;
  defaultFactory: (ctx: EntityContext) => MenuItem[] | Promise<MenuItem[]>;
  factoriesByType?: Record<string, MenuFactory>;
  onOpen?: (ctx: EntityContext) => void;
  onClose?: () => void;
  closeOnAction?: boolean; // default: true
};

useEntityContextMenu Hook

function useEntityContextMenu(): {
  showMenu: (ctx: EntityContext) => void;
  hideMenu: () => void;
  isVisible: boolean;
  context?: EntityContext;
  menuItems?: MenuItem[];
};
type MenuItem = {
  id: string;
  label: string;
  type?: 'action' | 'submenu' | 'toggle' | 'separator' | 'custom';
  visible?: (ctx: EntityContext) => boolean;
  enabled?: (ctx: EntityContext) => boolean;
  onClick?: (ctx: EntityContext) => void | Promise<void>;
  items?: MenuItem[]; // for submenus
  render?: (ctx: EntityContext) => React.ReactNode; // for custom items
  checked?: boolean; // for toggle items
};

๐Ÿ”ฅ Advanced Features

Asynchronous Menu Generation

const cityFactory = async (ctx) => {
  // Load data from server
  const cityData = await fetchCityData(ctx.entityId);

  return [
    {
      id: 'population',
      label: `Population: ${cityData.population}`,
      onClick: () => showDetails(cityData),
    },
  ];
};

Conditional Visibility & Enabling

const menuItems = [
  {
    id: 'edit',
    label: 'Edit',
    visible: (ctx) => ctx.entityData.editable,
    enabled: (ctx) => !ctx.entityData.locked,
    onClick: (ctx) => editEntity(ctx.entityId),
  },
];
const menuItems = [
  {
    id: 'export',
    label: 'Export',
    type: 'submenu',
    items: [
      { id: 'pdf', label: 'As PDF', onClick: exportPDF },
      { id: 'csv', label: 'As CSV', onClick: exportCSV },
    ],
  },
];

Custom Rendering

const menuItems = [
  {
    id: 'color',
    type: 'custom',
    render: (ctx) => (
      <ColorPicker
        value={ctx.entityData.color}
        onChange={(color) => updateColor(ctx.entityId, color)}
      />
    ),
  },
];

โŒจ๏ธ Keyboard Shortcuts

  • โ†‘/โ†“ - Navigate between menu items
  • โ†’ - Open submenu
  • โ† - Close submenu
  • Enter/Space - Activate menu item
  • Escape - Close menu

๐ŸŽจ Styling

The menu uses basic CSS classes. For custom styling:

<EntityContextMenu className="my-custom-menu" />
.my-custom-menu {
  background: #2a2a2a;
  border: 1px solid #444;
  /* More styles */
}

๐Ÿงช Testing

import { render, screen, fireEvent } from '@testing-library/react';
import { EntityContextMenuProvider, useEntityContextMenu } from 'resium-entity-context-menu';

test('shows menu on showMenu call', () => {
  const TestComponent = () => {
    const { showMenu } = useEntityContextMenu();

    return (
      <button
        onClick={() =>
          showMenu({
            entityId: 'test',
            position: { x: 100, y: 100 },
            clickedAt: new Date().toISOString(),
          })
        }
      >
        Open Menu
      </button>
    );
  };

  render(
    <EntityContextMenuProvider defaultFactory={() => [{ id: 'test', label: 'Test Item' }]}>
      <TestComponent />
      <EntityContextMenu />
    </EntityContextMenuProvider>,
  );

  fireEvent.click(screen.getByText('Open Menu'));
  expect(screen.getByText('Test Item')).toBeInTheDocument();
});

๐Ÿ”ง Configuration for Cesium/Resium

import { Viewer, Entity } from 'resium';
import { useEntityContextMenu } from 'resium-entity-context-menu';

function CesiumEntity({ position, name }) {
  const { showMenu } = useEntityContextMenu();

  const handleClick = (movement, target) => {
    if (!target) return;

    showMenu({
      entityId: target.id.id,
      entityType: 'cesium-entity',
      position: {
        x: movement.position.x,
        y: movement.position.y,
      },
      worldPosition: target.id.position,
      entityData: target.id,
      clickedAt: new Date().toISOString(),
    });
  };

  return <Entity position={position} name={name} onClick={handleClick} />;
}

๐Ÿ“‹ Requirements

  • React 16.8+ (Hooks support)
  • TypeScript 4.0+ (optional but recommended)

๐Ÿค Contributing

Contributions are welcome! Please create an issue or pull request.

๐Ÿ“„ License

MIT

๐Ÿ™ Credits

Built with โค๏ธ for the React/Cesium community