learning_ai_common_plat/packages/rich-text/src/SlashMenuList.tsx
saravanakumardb1 fc8502ac0c feat(rich-text): @bytelyst/rich-text@0.1.0 on Tiptap v3
RichTextEditor (toolbar + slash menu + async mentions, SSR-safe via
immediatelyRender:false) + RichTextViewer (generateHTML, server-renderable) +
standalone Toolbar. Pure filterSlashItems/filterUsers helpers. 12/12 vitest
(incl. live editor mount + bold toggle in happy-dom); tsc clean; published to
Gitea.
2026-05-28 18:20:34 -07:00

87 lines
2.7 KiB
TypeScript

import * as React from 'react';
import type { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
import type { SlashItem } from './slashMenu.js';
export interface SlashMenuListHandle {
onKeyDown: (props: SuggestionKeyDownProps) => boolean;
}
/**
* Keyboard-navigable list rendered inside the slash-menu popup. Exposes an
* imperative `onKeyDown` so the Suggestion plugin can route ↑/↓/Enter here.
*/
export const SlashMenuList = React.forwardRef<SlashMenuListHandle, SuggestionProps<SlashItem>>(
function SlashMenuList(props, ref) {
const { items, command } = props;
const [selected, setSelected] = React.useState(0);
React.useEffect(() => setSelected(0), [items]);
const select = React.useCallback(
(index: number) => {
const item = items[index];
if (item) command(item);
},
[items, command]
);
React.useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
setSelected(s => (s + items.length - 1) % items.length);
return true;
}
if (event.key === 'ArrowDown') {
setSelected(s => (s + 1) % items.length);
return true;
}
if (event.key === 'Enter') {
select(selected);
return true;
}
return false;
},
}));
if (items.length === 0) {
return (
<div
role="listbox"
aria-label="Slash commands"
className="min-w-56 rounded-lg border border-[var(--bl-border,rgba(0,0,0,0.12))] bg-[var(--bl-surface-card,#fff)] p-2 text-sm text-[var(--bl-text-tertiary,#999)] shadow-lg"
>
No matches
</div>
);
}
return (
<div
role="listbox"
aria-label="Slash commands"
className="min-w-56 overflow-hidden rounded-lg border border-[var(--bl-border,rgba(0,0,0,0.12))] bg-[var(--bl-surface-card,#fff)] p-1 shadow-lg"
>
{items.map((item, index) => (
<button
key={item.title}
type="button"
role="option"
aria-selected={index === selected}
onMouseEnter={() => setSelected(index)}
onClick={() => select(index)}
className={
'flex w-full flex-col items-start rounded-md px-2.5 py-1.5 text-left text-sm ' +
(index === selected
? 'bg-[var(--bl-accent-muted,rgba(99,102,241,0.12))]'
: 'bg-transparent')
}
>
<span className="font-medium text-[var(--bl-text-primary,#111)]">{item.title}</span>
<span className="text-xs text-[var(--bl-text-tertiary,#999)]">{item.description}</span>
</button>
))}
</div>
);
}
);