@opengov/mention-component
v0.1.3
Published
React component library for user mentions with typeahead search, permission validation, and notification integration
Maintainers
Readme
@opengov/mention-component
Shared React component library for user mention functionality across OpenGov products. Provides a rich text input with @-mention typeahead, user hover cards, permission warnings, and mention chips.
Installation
bun add @opengov/mention-componentPeer Dependencies
| Package | Version |
| ---------------------------- | ------- |
| @mui/material | ^6.0.0 |
| @opengov/capital-mui-theme | * |
| react | ^19.0.0 |
| react-dom | ^19.0.0 |
Quick Start
Wrap your app (or subtree) with MentionProvider, then drop in MentionInputConfigurable:
import { MentionProvider, MentionInputConfigurable } from '@opengov/mention-component';
function CommentSection() {
return (
<MentionProvider
baseUrl="/api/user-mention-service/v1"
getAccessToken={() => auth0.getAccessTokenSilently()}
>
<MentionInputConfigurable
entityId="entity-uuid"
sourceProduct="cit"
sourceRecordUrn="urn:opengov:cit:permit:12345"
sourceContextType="comment"
sourceContextId="comment-thread-1"
deepLinkUrl="https://app.opengov.com/cit/permits/12345"
placeholder="Add a comment... Type @ to mention someone"
onMentionCreated={(mention) => console.log('Created:', mention)}
/>
</MentionProvider>
);
}That's it for most use cases. The Configurable API handles typeahead, suggestion rendering, permission warnings, and mention creation out of the box.
Components
MentionProvider
Global context provider that supplies the API base URL, auth token getter, and (optionally) the tenant entityId to all mention components. When entityId is set, smart components like <MentionChip /> and <MentionHoverCard /> can self-fetch user details without any prop drilling.
<MentionProvider
baseUrl="/api/user-mention-service/v1"
getAccessToken={() => auth0.getAccessTokenSilently()}
entityId="entity-uuid"
>
{children}
</MentionProvider>| Prop | Type | Required | Description |
| ----------------- | ----------------------- | -------- | -------------------------------------------------------------------------------------------- |
| baseUrl | string | Yes | Base URL of the user-mention-service API |
| getAccessToken | () => Promise<string> | Yes | Async function that returns a valid bearer token |
| entityId | string | No | Tenant UUID used by smart MentionChip / MentionHoverCard to self-fetch user details |
| debounceMs | number | No | Typeahead debounce (default 150ms) |
| minQueryLength | number | No | Minimum query length (fixed at 3 by AC API) |
| currentUserName | string | No | Name of the current user; used in mention notifications |
MentionInputConfigurable
All-in-one mention input. Covers ~90% of use cases.
<MentionInputConfigurable
entityId="entity-uuid"
sourceProduct="cit"
sourceRecordUrn="urn:opengov:cit:permit:12345"
sourceContextType="comment"
sourceContextId="comment-thread-1"
deepLinkUrl="https://app.opengov.com/cit/permits/12345"
placeholder="Type @ to mention..."
onMentionCreated={(mention) => {
/* ... */
}}
onMentionError={(error) => {
/* ... */
}}
/>| Prop | Type | Required | Description |
| ----------------------- | -------------------------------------- | -------- | ------------------------------------------------------ |
| entityId | string | Yes | Entity UUID for the current tenant |
| sourceProduct | SourceProduct | Yes | Product identifier (see types below) |
| sourceRecordUrn | string | Yes | URN of the record being commented on |
| sourceContextType | SourceContextType | Yes | Context type |
| sourceContextId | string | Yes | Unique ID for the comment thread/context |
| deepLinkUrl | string | Yes | URL included in the notification to the mentioned user |
| placeholder | string | No | Input placeholder text |
| disabled | boolean | No | Disable the input |
| maxMentionsPerComment | number | No | Max mentions per submission (default: 20) |
| sx | SxProps<Theme> | No | MUI sx styling overrides |
| onMentionCreated | (mention: MentionResult) => void | No | Callback fired per successful mention |
| onMentionError | (error: MentionApiErrorData) => void | No | Callback fired on error |
MentionInputComposable
For teams that need fine-grained control over layout and rendering (~10% of use cases). Uses compound component pattern with sub-components.
<MentionInputComposable
entityId="entity-uuid"
sourceProduct="financial"
sourceRecordUrn="urn:opengov:financial:journal:999"
sourceContextType="note"
sourceContextId="note-1"
deepLinkUrl="https://app.opengov.com/financial/journals/999"
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<MentionInputComposable.Editor placeholder="Write a note..." />
<MentionInputComposable.PermissionWarning />
</Box>
</MentionInputComposable>Sub-components:
| Sub-component | Description |
| ------------------------------------------ | ------------------------------------------------------------------- |
| MentionInputComposable.Editor | The TipTap rich text editor. Props: placeholder, disabled, sx |
| MentionInputComposable.Suggestions | Suggestion list (rendered automatically by TipTap) |
| MentionInputComposable.PermissionWarning | Renders the permission warning when a user lacks record access |
MentionChip
Smart inline badge for displaying a mentioned user. Triggers a hover card on hover/focus.
When rendered inside a <MentionProvider entityId="…" />, the chip self-fetches the user's details (display name, role, department, avatar, deactivated status) from GET /:entityId/mentions/users/:userId — so callers only need to supply userId.
// Smart mode — pass only userId; details are fetched automatically
<MentionChip userId="user-uuid" />
// Presentational mode — pass details explicitly; nothing is fetched
<MentionChip
userId="user-uuid"
displayName="Jane Smith"
role="Budget Analyst"
department="Finance"
permissionStatus="allowed"
disableAutoFetch
/>Any prop that's passed in takes precedence over fetched values, so callers that already have a display name (e.g. from a persisted mention record) see it instantly while the rest is filled in after hydration.
| Prop | Type | Required | Description |
| ------------------ | -------------------------- | -------- | ------------------------------------------------------------------------- |
| userId | string | Yes | User UUID |
| displayName | string | No | Display name shown on the chip (falls back to fetched fullName) |
| role | string | No | User's role (falls back to fetched value) |
| department | string | No | User's department (falls back to fetched value) |
| avatar | string \| null | No | Avatar URL (falls back to fetched value) |
| isDeactivated | boolean | No | Show deactivated styling (falls back to fetched disabled flag) |
| permissionStatus | PermissionStatus | No | Shows warning/blocked icon |
| entityId | string | No | Tenant UUID override; falls back to <MentionProvider entityId="…" /> |
| disableAutoFetch | boolean | No | When true, the chip renders only from props and makes no network calls |
| onClick | (userId: string) => void | No | Click handler |
| sx | SxProps<Theme> | No | MUI sx styling overrides |
MentionHoverCard
Smart popover card showing user details. Typically triggered by MentionChip but can be used standalone. Fetches user details lazily when opened (when inside a <MentionProvider />), unless disableAutoFetch is set.
// Standalone smart usage — the card fetches on first open
<MentionHoverCard
userId="user-uuid"
anchorEl={chipRef.current}
open={isOpen}
onClose={() => setIsOpen(false)}
/>Same prop-vs-fetched precedence rules as MentionChip. Accepts the same entityId and disableAutoFetch props.
MentionSuggestionList
Typeahead dropdown for selecting users. Used internally by MentionInputConfigurable, but available for custom implementations.
<MentionSuggestionList
items={users}
isLoading={loading}
error={null}
selectedIndex={0}
onSelect={(user) => handleSelect(user)}
renderItem={(user, isSelected) => <CustomItem user={user} selected={isSelected} />}
/>PermissionWarning
Inline alert shown when a mentioned user may not have access to the record.
<PermissionWarning
userName="Jane Smith"
onConfirm={() => proceedWithMention()}
onCancel={() => cancelMention()}
/>Hooks
useMentionTypeahead
Handles debounced user search with rate limiting.
const { items, isLoading, error, search, clear, isRateLimited } = useMentionTypeahead();
// Trigger search (200ms debounce, 1-char minimum)
search('jan', entityId, sourceRecordUrn);useMentionApi
Handles mention creation requests.
const { createMention } = useMentionApi({
onMentionCreated: (mention) => console.log(mention),
onMentionError: (error) => console.error(error),
});
await createMention({
sourceProduct: 'cit',
sourceRecordUrn: 'urn:opengov:cit:permit:12345',
sourceContextType: 'comment',
sourceContextId: 'thread-1',
mentions: [{ mentionedUserId: 'user-uuid' }],
deepLinkUrl: 'https://app.opengov.com/cit/permits/12345',
});useMentionContext
Access the MentionProvider context (baseUrl, getAccessToken, entityId).
useMentionUser
Resolve a single user's details by ID using the MentionProvider context. Backed by a shared module-level cache (5 min TTL) with in-flight request dedup, so N chips rendering the same user issue at most one network call.
const { user, isLoading, error } = useMentionUser('user-uuid');Returns { user: null, isLoading: false } when no entityId is available (from either the argument or <MentionProvider />).
useMentionInputContext
Access the MentionInput context (mention count, pending warnings, callbacks). Must be used within a MentionInputConfigurable or MentionInputComposable.
Types
type SourceProduct = 'cit' | 'financial' | 'procurement' | 'eam' | 'budget' | 'reporting' | 'oth';
type SourceContextType = 'comment' | 'note' | 'workflow_thread';
type PermissionStatus = 'allowed' | 'warned' | 'blocked';
interface ResolvedUser {
uuid: string;
email: string;
firstName: string;
lastName: string;
fullName: string;
role: { name: string; displayName: string };
department: string | null;
avatar: string | null;
hasRecordAccess?: boolean;
}
interface MentionResult {
mentionId: string;
mentionedUserId: string;
permissionStatus: PermissionStatus;
createdAt: string;
}
interface MentionApiErrorData {
code: string;
message: string;
details?: Record<string, unknown>;
}Storybook
Run Storybook locally to browse and interact with all components:
bun run storybookAccessibility
All components target WCAG 2.1 AA compliance:
- Full keyboard navigation (arrow keys, Enter/Space to select, Escape to dismiss)
- ARIA roles and labels on suggestion list, hover card, and warning alert
- Focus management when opening/closing popovers
- Screen reader announcements for loading, error, and empty states
