Mentions

Add @mentions functionality to spreadsheet cells

The mentions feature allows users to reference people, tags, or entities within cells using the @ symbol, similar to social media platforms. This is useful for collaborative spreadsheets, task management, and commenting systems.

Overview

Mentions enable:

  • User tagging: Reference team members in cells

  • Entity references: Link to external resources or data

  • Autocomplete: Dropdown suggestions as users type

  • Custom rendering: Display mentions with custom styling

  • Data binding: Connect mentions to your application's data model

Basic Usage

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

function SpreadsheetWithMentions() {
  const getMentions = useCallback(async (query?: string) => {
    // Fetch mentions from your API or database
    return [
      { label: "John Doe", value: "user-123" },
      { label: "Jane Smith", value: "user-456" },
      { label: "Marketing Team", value: "team-789" },
    ];
  }, []);

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

getMentions Function

The getMentions callback is called when users type @ in a cell. It should return a Promise that resolves to an array of mention objects.

Function Signature

type MentionItem = {
  label: string;      // Display text in dropdown
  value: string;      // Unique identifier
  [key: string]: any; // Additional custom properties
};

type GetMentions = (query?: string) => Promise<MentionItem[]>;

Parameters

  • query (optional): The search text entered by the user after @

const getMentions = useCallback(async (query?: string) => {
  // Filter mentions based on query
  const allMentions = [
    { label: "Alice Johnson", value: "user-1", email: "[email protected]" },
    { label: "Bob Wilson", value: "user-2", email: "[email protected]" },
    { label: "Charlie Brown", value: "user-3", email: "[email protected]" },
  ];

  if (!query) {
    return allMentions;
  }

  // Filter by query
  const filtered = allMentions.filter((mention) =>
    mention.label.toLowerCase().includes(query.toLowerCase())
  );

  return filtered;
}, []);

Custom Dropdown Rendering

Customize how mentions appear in the autocomplete dropdown using MentionDropdownItemComponent:

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

function SpreadsheetWithCustomMentions() {
  const MentionItem = ({ mention }) => {
    return (
      <div className="flex items-center gap-2 p-2">
        <img
          src={mention.avatar}
          alt={mention.label}
          className="w-8 h-8 rounded-full"
        />
        <div>
          <div className="font-semibold">{mention.label}</div>
          <div className="text-xs text-gray-500">{mention.email}</div>
        </div>
      </div>
    );
  };

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

MentionDropdownItemComponent Props

type MentionDropdownItemProps = {
  mention: MentionItem;
  isSelected?: boolean;
  onClick?: () => void;
};

Async Data Loading

Fetch mentions from an API:

const getMentions = useCallback(async (query?: string) => {
  try {
    const response = await fetch(
      `/api/mentions?query=${encodeURIComponent(query || '')}`
    );
    const data = await response.json();
    return data.mentions;
  } catch (error) {
    console.error("Failed to fetch mentions:", error);
    return [];
  }
}, []);

Complete Example

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

// Mock user database
const users = [
  {
    label: "Alice Johnson",
    value: "user-1",
    email: "[email protected]",
    department: "Engineering",
    avatar: "/avatars/alice.jpg",
  },
  {
    label: "Bob Smith",
    value: "user-2",
    email: "[email protected]",
    department: "Marketing",
    avatar: "/avatars/bob.jpg",
  },
  {
    label: "Charlie Davis",
    value: "user-3",
    email: "[email protected]",
    department: "Sales",
    avatar: "/avatars/charlie.jpg",
  },
];

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

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

  // Fetch mentions with search
  const getMentions = useCallback(async (query?: string) => {
    // Simulate API delay
    await new Promise((resolve) => setTimeout(resolve, 100));

    if (!query) {
      return users;
    }

    // Filter by name or email
    return users.filter((user) =>
      user.label.toLowerCase().includes(query.toLowerCase()) ||
      user.email.toLowerCase().includes(query.toLowerCase()) ||
      user.department.toLowerCase().includes(query.toLowerCase())
    );
  }, []);

  // Custom mention dropdown item
  const MentionDropdownItem = ({ mention }) => {
    return (
      <div className="flex items-center gap-3 px-3 py-2 hover:bg-gray-100">
        <img
          src={mention.avatar}
          alt={mention.label}
          className="w-10 h-10 rounded-full"
        />
        <div className="flex-1">
          <div className="font-medium text-sm">{mention.label}</div>
          <div className="text-xs text-gray-600">{mention.email}</div>
        </div>
        <span className="text-xs text-gray-500 bg-gray-200 px-2 py-1 rounded">
          {mention.department}
        </span>
      </div>
    );
  };

  return (
    <SpreadsheetProvider>
      <CanvasGrid
        sheetId={activeSheetId}
        activeCell={activeCell}
        selections={selections}
        getCellData={getCellData}
        onChangeActiveCell={onChangeActiveCell}
        onChangeSelections={onChangeSelections}
        onChange={onChange}
        getMentions={getMentions}
        MentionDropdownItemComponent={MentionDropdownItem}
      />
    </SpreadsheetProvider>
  );
}

export default TaskSpreadsheet;

Accessing Mention Data

When a mention is selected, it's stored in the cell data. You can access it through the cell's data structure:

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

// Check if cell contains mentions
if (cellData?.mentions) {
  cellData.mentions.forEach((mention) => {
    console.log("Mentioned user:", mention.label, mention.value);
  });
}

Use Cases

Team Collaboration

// Track task assignments
const getMentions = async () => [
  { label: "@alice", value: "user-1", role: "Developer" },
  { label: "@bob", value: "user-2", role: "Designer" },
];

Project Management

// Reference project resources
const getMentions = async (query) => {
  return [
    { label: "@project-alpha", value: "proj-1", type: "project" },
    { label: "@milestone-q1", value: "mile-1", type: "milestone" },
    { label: "@alice", value: "user-1", type: "user" },
  ];
};

Comments and Notes

// Add comments with user mentions
const getMentions = async () => {
  const teamMembers = await fetchTeamMembers();
  return teamMembers.map((member) => ({
    label: `@${member.username}`,
    value: member.id,
    name: member.fullName,
  }));
};

Styling

Mentions in cells can be styled using custom text format runs. The spreadsheet automatically formats mentioned text when rendered.

Performance Optimization

import { useMemo } from "react";
import debounce from "lodash/debounce";

function SpreadsheetWithMentions() {
  const fetchMentions = async (query?: string) => {
    const response = await fetch(`/api/mentions?q=${query}`);
    return response.json();
  };

  const getMentions = useMemo(
    () => debounce(fetchMentions, 300),
    []
  );

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

Caching

const mentionCache = new Map();

const getMentions = useCallback(async (query?: string) => {
  const cacheKey = query || "all";

  if (mentionCache.has(cacheKey)) {
    return mentionCache.get(cacheKey);
  }

  const mentions = await fetchMentions(query);
  mentionCache.set(cacheKey, mentions);

  return mentions;
}, []);

Best Practices

  1. Limit Results: Return a maximum of 10-20 mentions to keep the dropdown manageable

  2. Fast Response: Keep the getMentions function fast (< 300ms) for good UX

  3. Error Handling: Handle API failures gracefully and return an empty array

  4. Unique Values: Ensure each mention has a unique value property

  5. Clear Labels: Use descriptive labels that help users identify the right mention

  6. Type Safety: Use TypeScript to define your mention structure

Troubleshooting

Mentions Not Appearing

  • Verify getMentions returns an array of objects with label and value properties

  • Check that the function returns a Promise

  • Ensure there are no JavaScript errors in the console

Search Not Working

  • Confirm the query parameter is being used in your filter logic

  • Test the search function independently

  • Check for case sensitivity issues

Slow Performance

  • Implement debouncing for API calls

  • Add caching for frequently accessed results

  • Limit the number of returned results

  • Use pagination for large datasets

Last updated

Was this helpful?