Cell Tooltips and Popovers

Display tooltips and expandable content for spreadsheet cells

Enhance your spreadsheet with custom tooltips and expandable cell content. Display additional information, rich media, or interactive components when users hover over or click on cells.

Overview

The spreadsheet provides two main ways to display additional cell content:

  • Tooltips (getTooltipContent): Show hover tooltips with custom content

  • Expandable Content (getCellExpandContent): Display rich content in a popover when cells are expanded

Cell Tooltips

Display custom tooltips when users hover over cells using the getTooltipContent callback.

Basic Usage

import { SpreadsheetProvider, CanvasGrid } from "@rowsncolumns/spreadsheet";

function SpreadsheetWithTooltips() {
  const getTooltipContent = (
    sheetId: number,
    rowIndex: number,
    columnIndex: number
  ) => {
    // Return undefined for no tooltip
    if (rowIndex === 0 || columnIndex === 0) {
      return undefined;
    }

    // Return JSX for custom tooltip
    return (
      <div className="p-2">
        <strong>Cell:</strong> {cellToAddress({ rowIndex, columnIndex })}
        <br />
        <strong>Sheet:</strong> {sheetId}
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getTooltipContent={getTooltipContent}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}

Function Signature

type GetTooltipContent = (
  sheetId: number,
  rowIndex: number,
  columnIndex: number
) => React.ReactNode | undefined;

Return Values

  • React.ReactNode: Display tooltip with custom content

  • undefined: No tooltip for this cell

Examples

Display Cell Metadata

const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const cellData = getCellData(sheetId, rowIndex, columnIndex);

  if (!cellData) return undefined;

  return (
    <div className="text-sm">
      <div className="font-semibold mb-1">Cell Information</div>
      {cellData.note && (
        <div className="mb-1">
          <strong>Note:</strong> {cellData.note}
        </div>
      )}
      {cellData.userEnteredValue && (
        <div>
          <strong>Formula:</strong> {cellData.userEnteredValue.formulaValue}
        </div>
      )}
    </div>
  );
};

Show Validation Rules

const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const validation = getDataValidation(sheetId, rowIndex, columnIndex);

  if (!validation) return undefined;

  return (
    <div className="p-2 bg-yellow-50 border border-yellow-200 rounded">
      <div className="font-semibold text-yellow-800">Validation Rule</div>
      <div className="text-sm text-yellow-700 mt-1">
        {validation.condition?.type}: {validation.condition?.values?.join(", ")}
      </div>
    </div>
  );
};

Display Error Messages

const getTooltipContent = (sheetId, rowIndex, columnIndex) => {
  const cellData = getCellData(sheetId, rowIndex, columnIndex);

  if (cellData?.effectiveValue?.errorValue) {
    return (
      <div className="p-2 bg-red-50 border border-red-200 rounded">
        <div className="font-semibold text-red-800">Formula Error</div>
        <div className="text-sm text-red-700 mt-1">
          {cellData.effectiveValue.errorValue}
        </div>
      </div>
    );
  }

  return undefined;
};

Expandable Cell Content

Display rich, interactive content when users expand cells using the getCellExpandContent callback.

Basic Usage

function SpreadsheetWithExpandableContent() {
  const getCellExpandContent = () => {
    return (
      <div className="p-4 max-w-md">
        <h3 className="font-bold mb-2">Additional Information</h3>
        <p className="text-sm text-gray-700">
          This is expandable content that appears when the cell is expanded.
          You can include any React component here.
        </p>
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={1}
        getCellExpandContent={getCellExpandContent}
        // ... other props
      />
    </SpreadsheetProvider>
  );
}

Function Signature

type GetCellExpandContent = () => React.ReactNode;

Advanced Examples

Rich Text Editor

const getCellExpandContent = () => {
  return (
    <div className="overflow-auto max-h-96 max-w-2xl p-4">
      <div className="prose">
        <h3>Project Description</h3>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit.
          Detailed information about this cell can be displayed here.
        </p>
        <ul>
          <li>Feature A</li>
          <li>Feature B</li>
          <li>Feature C</li>
        </ul>
      </div>
    </div>
  );
};
const getCellExpandContent = () => {
  const images = [
    "/images/chart1.png",
    "/images/chart2.png",
    "/images/chart3.png",
  ];

  return (
    <div className="p-4 max-w-4xl">
      <h3 className="font-bold mb-4">Related Charts</h3>
      <div className="grid grid-cols-3 gap-4">
        {images.map((src, index) => (
          <img
            key={index}
            src={src}
            alt={`Chart ${index + 1}`}
            className="rounded shadow-lg"
          />
        ))}
      </div>
    </div>
  );
};

Interactive Form

const getCellExpandContent = () => {
  const [notes, setNotes] = useState("");

  return (
    <div className="p-4 max-w-md">
      <h3 className="font-bold mb-2">Add Notes</h3>
      <textarea
        className="w-full border rounded p-2 mb-2"
        rows={4}
        value={notes}
        onChange={(e) => setNotes(e.target.value)}
        placeholder="Enter your notes here..."
      />
      <button
        className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        onClick={() => console.log("Save notes:", notes)}
      >
        Save
      </button>
    </div>
  );
};

Data Visualization

import { Chart } from "react-chartjs-2";

const getCellExpandContent = () => {
  const chartData = {
    labels: ["Jan", "Feb", "Mar", "Apr", "May"],
    datasets: [
      {
        label: "Sales",
        data: [12, 19, 3, 5, 2],
        backgroundColor: "rgba(75, 192, 192, 0.2)",
        borderColor: "rgba(75, 192, 192, 1)",
      },
    ],
  };

  return (
    <div className="p-4 max-w-2xl">
      <h3 className="font-bold mb-4">Sales Trend</h3>
      <Chart type="line" data={chartData} />
    </div>
  );
};

Cell-Specific Content

Customize content based on the cell being expanded:

import { useSpreadsheet } from "@rowsncolumns/spreadsheet";

function SpreadsheetWithDynamicContent() {
  const { activeCell } = useSpreadsheet();

  const getCellExpandContent = useCallback(() => {
    const cellData = getCellData(
      activeSheetId,
      activeCell.rowIndex,
      activeCell.columnIndex
    );

    // Different content based on cell value
    if (cellData?.formattedValue?.includes("Task")) {
      return <TaskDetailsPanel cellData={cellData} />;
    }

    if (cellData?.formattedValue?.includes("User")) {
      return <UserProfilePanel cellData={cellData} />;
    }

    // Default content
    return (
      <div className="p-4">
        <p>No additional information available</p>
      </div>
    );
  }, [activeCell, activeSheetId]);

  return (
    <CanvasGrid
      getCellExpandContent={getCellExpandContent}
      // ... other props
    />
  );
}

Complete Example

import React, { useState, useCallback } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  Sheet,
} from "@rowsncolumns/spreadsheet";
import {
  useSpreadsheetState,
  SheetData,
  CellData,
} from "@rowsncolumns/spreadsheet-state";
import { cellToAddress } from "@rowsncolumns/utils";

function EnhancedSpreadsheet() {
  const [sheets, setSheets] = useState<Sheet[]>([
    { sheetId: 1, rowCount: 100, columnCount: 26, title: "Data" }
  ]);
  const [sheetData, setSheetData] = useState<SheetData<CellData>>({});

  const {
    activeCell,
    activeSheetId,
    selections,
    getCellData,
    onChangeActiveCell,
    onChangeSelections,
  } = useSpreadsheetState({
    sheets,
    sheetData,
    onChangeSheets: setSheets,
    onChangeSheetData: setSheetData,
  });

  // Tooltip handler
  const getTooltipContent = useCallback(
    (sheetId: number, rowIndex: number, columnIndex: number) => {
      // Skip headers
      if (rowIndex === 0 || columnIndex === 0) return undefined;

      const cellData = getCellData(sheetId, rowIndex, columnIndex);

      if (!cellData) return undefined;

      const address = cellToAddress({ rowIndex, columnIndex });

      return (
        <div className="p-3 bg-white shadow-lg rounded-lg border">
          <div className="text-xs text-gray-500 mb-1">{address}</div>
          {cellData.note && (
            <div className="text-sm border-t pt-2 mt-2">
              <strong className="text-gray-700">Note:</strong>
              <div className="text-gray-600 mt-1">{cellData.note}</div>
            </div>
          )}
          {cellData.hyperlink && (
            <div className="text-sm border-t pt-2 mt-2">
              <strong className="text-blue-700">Link:</strong>
              <a
                href={cellData.hyperlink}
                className="text-blue-600 hover:underline ml-1"
              >
                {cellData.hyperlink}
              </a>
            </div>
          )}
        </div>
      );
    },
    [getCellData]
  );

  // Expandable content handler
  const getCellExpandContent = useCallback(() => {
    const cellData = getCellData(
      activeSheetId,
      activeCell.rowIndex,
      activeCell.columnIndex
    );

    return (
      <div className="overflow-auto max-h-96 max-w-2xl p-6 bg-white rounded-lg">
        <h2 className="text-xl font-bold mb-4">Cell Details</h2>

        <div className="space-y-4">
          <div>
            <strong className="text-gray-700">Location:</strong>
            <span className="ml-2">
              {cellToAddress({
                rowIndex: activeCell.rowIndex,
                columnIndex: activeCell.columnIndex,
              })}
            </span>
          </div>

          {cellData?.formattedValue && (
            <div>
              <strong className="text-gray-700">Value:</strong>
              <div className="mt-1 p-3 bg-gray-50 rounded font-mono text-sm">
                {cellData.formattedValue}
              </div>
            </div>
          )}

          {cellData?.userEnteredValue?.formulaValue && (
            <div>
              <strong className="text-gray-700">Formula:</strong>
              <div className="mt-1 p-3 bg-blue-50 rounded font-mono text-sm">
                {cellData.userEnteredValue.formulaValue}
              </div>
            </div>
          )}

          <div className="border-t pt-4">
            <button className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
              Edit Cell
            </button>
          </div>
        </div>
      </div>
    );
  }, [activeCell, activeSheetId, getCellData]);

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        getTooltipContent={getTooltipContent}
        getCellExpandContent={getCellExpandContent}
      />
    </SpreadsheetProvider>
  );
}

export default EnhancedSpreadsheet;

Styling

Tooltip Styling

Tooltips automatically position themselves to avoid going off-screen. Style them using standard CSS classes:

const getTooltipContent = () => (
  <div className="p-3 bg-gray-900 text-white rounded-lg shadow-xl max-w-xs">
    <div className="text-sm">Your tooltip content</div>
  </div>
);

Popover Styling

Expandable content appears in a popover with scrolling support:

const getCellExpandContent = () => (
  <div className="overflow-auto max-h-96 max-w-4xl p-6">
    {/* Scrollable content */}
  </div>
);

Use Cases

Data Annotations

Show additional context or metadata for cells containing important data.

Error Explanations

Display helpful error messages and suggestions when formulas fail.

Rich Content Preview

Preview images, charts, or formatted text without cluttering the grid.

Interactive Dialogs

Provide forms or interactive elements for complex data entry.

Audit Information

Display who last edited a cell and when.

Performance Considerations

  1. Memoize Callbacks: Use useCallback to prevent unnecessary re-renders

  2. Conditional Rendering: Return undefined when no tooltip is needed

  3. Lazy Loading: Load heavy content only when the popover is opened

  4. Limit Complexity: Keep tooltip content simple for smooth hover interactions

Best Practices

  1. Keep Tooltips Concise: Display only essential information in tooltips

  2. Use Popovers for Rich Content: Save complex content for expandable popovers

  3. Avoid Heavy Computations: Don't perform expensive calculations in tooltip callbacks

  4. Accessibility: Ensure tooltips and popovers work with keyboard navigation

  5. Consistent Styling: Match your application's design system

Troubleshooting

Tooltips Not Showing

  • Verify getTooltipContent returns valid JSX or undefined (not null)

  • Check that the function is properly passed to CanvasGrid

  • Ensure there are no JavaScript errors in the console

Popovers Not Opening

  • Confirm getCellExpandContent is defined

  • Check that the cell expansion trigger is working (usually double-click or icon)

  • Verify the content isn't too large or causing layout issues

Performance Issues

  • Use React.memo for complex tooltip components

  • Implement lazy loading for heavy content

  • Consider debouncing tooltip display

Last updated

Was this helpful?