pass the select through

This commit is contained in:
Nate Kelley 2025-09-29 11:45:37 -06:00
parent 8469471a45
commit 5055530bdf
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 118 additions and 92 deletions

View File

@ -1,6 +1,7 @@
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useMemoizedFn } from '@/hooks/useMemoizedFn'; import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import type { MentionInputRef } from '../MentionInput';
import type { BusterInputProps, BusterOnSelectParams } from './BusterInput.types'; import type { BusterInputProps, BusterOnSelectParams } from './BusterInput.types';
import { BusterInputContainer } from './BusterInputContainer'; import { BusterInputContainer } from './BusterInputContainer';
import { BusterInputEmpty } from './BusterInputEmpty'; import { BusterInputEmpty } from './BusterInputEmpty';
@ -41,6 +42,7 @@ export const BusterInput = ({
const commandListNavigatedRef = useRef(false); const commandListNavigatedRef = useRef(false);
const commandRef = useRef<HTMLDivElement>(null); const commandRef = useRef<HTMLDivElement>(null);
const mentionsInputRef = useRef<MentionInputRef>(null);
const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0; const showSuggestionList = !hasClickedSelect && suggestionItems.length > 0;
@ -53,7 +55,7 @@ export const BusterInput = ({
}, []); }, []);
const onSelectItem = useMemoizedFn(({ onClick, ...params }: BusterOnSelectParams) => { const onSelectItem = useMemoizedFn(({ onClick, ...params }: BusterOnSelectParams) => {
const { addValueToInput, value, loading, inputValue, label, disabled } = params; const { addValueToInput, loading, inputValue, label, disabled } = params;
if (disabled) { if (disabled) {
console.warn('Item is disabled', params); console.warn('Item is disabled', params);
return; return;
@ -66,8 +68,11 @@ export const BusterInput = ({
console.warn('Item is loading', params); console.warn('Item is loading', params);
return; return;
} }
console.log('onSelectItem', addValueToInput, params); if (addValueToInput) {
if (addValueToInput) setValue(inputValue ?? String(label)); const stringValue = inputValue ?? String(label);
mentionsInputRef.current?.editor?.commands.setContent(stringValue);
setValue(stringValue);
}
onClick?.(); onClick?.();
if (closeSuggestionOnSelect) setHasClickedSelect(true); if (closeSuggestionOnSelect) setHasClickedSelect(true);
onSuggestionItemClick?.(params); onSuggestionItemClick?.(params);
@ -128,6 +133,7 @@ export const BusterInput = ({
variant={variant} variant={variant}
> >
<BusterMentionsInput <BusterMentionsInput
ref={mentionsInputRef}
defaultValue={defaultValue} defaultValue={defaultValue}
readOnly={readOnly} readOnly={readOnly}
autoFocus={autoFocus} autoFocus={autoFocus}

View File

@ -1,7 +1,7 @@
/** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */ /** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import React from 'react'; import React, { forwardRef } from 'react';
import { MentionInput, type MentionInputProps } from '../MentionInput'; import { MentionInput, type MentionInputProps, type MentionInputRef } from '../MentionInput';
import type { BusterInputProps } from './BusterInput.types'; import type { BusterInputProps } from './BusterInput.types';
export type BusterMentionsInputProps = Pick< export type BusterMentionsInputProps = Pick<
@ -23,24 +23,21 @@ export type BusterMentionsInputProps = Pick<
commandListNavigatedRef?: React.RefObject<boolean>; commandListNavigatedRef?: React.RefObject<boolean>;
}; };
export const BusterMentionsInput = ({ export const BusterMentionsInput = forwardRef<MentionInputRef, BusterMentionsInputProps>(
value: valueProp, ({ value: valueProp, placeholder, defaultValue, mentions, value, ...props }, ref) => {
placeholder, return (
defaultValue, <React.Fragment>
mentions, <MentionInput ref={ref} mentions={mentions} defaultValue={value} {...props} />
value, <Command.Input
...props value={value}
}: BusterMentionsInputProps) => { autoFocus={false}
return ( // className="sr-only hidden h-0 border-0 p-0"
<React.Fragment> className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none"
<MentionInput mentions={mentions} defaultValue={value} {...props} /> aria-hidden="true"
<Command.Input />
value={value} </React.Fragment>
autoFocus={false} );
// className="sr-only hidden h-0 border-0 p-0" }
className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none" );
aria-hidden="true"
/> BusterMentionsInput.displayName = 'BusterMentionsInput';
</React.Fragment>
);
};

View File

@ -3,77 +3,96 @@ import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph'; import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text'; import Text from '@tiptap/extension-text';
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'; import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
import { useMemo } from 'react'; import { forwardRef, useImperativeHandle, useMemo } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { MentionExtension } from './MentionExtension'; import { MentionExtension } from './MentionExtension';
import type { MentionInputProps, MentionSuggestionExtension } from './MentionInput.types'; import type {
MentionInputProps,
MentionInputRef,
MentionSuggestionExtension,
} from './MentionInput.types';
import { SubmitOnEnter } from './SubmitEnterExtension'; import { SubmitOnEnter } from './SubmitEnterExtension';
import { onUpdateTransformer } from './update-transformers'; import { onUpdateTransformer } from './update-transformers';
export const MentionInput = ({ export const MentionInput = forwardRef<MentionInputRef, MentionInputProps>(
mentions, (
onChange, {
defaultValue = '', mentions,
onFocus, onChange,
onBlur, defaultValue = '',
autoFocus, onFocus,
style, onBlur,
className, autoFocus,
readOnly, style,
disabled, className,
onPressEnter, readOnly,
commandListNavigatedRef, disabled,
}: MentionInputProps) => { onPressEnter,
const mentionsByTrigger = useMemo(() => { commandListNavigatedRef,
return mentions.reduce(
(acc, mention) => {
if (mention.char) {
acc[mention.char] = mention;
}
return acc;
},
{} as Record<string, MentionSuggestionExtension>
);
}, [mentions]);
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
MentionExtension(mentions),
SubmitOnEnter({
mentionsByTrigger,
onPressEnter,
commandListNavigatedRef,
}),
],
content: defaultValue,
autofocus: autoFocus,
editable: !disabled && !readOnly,
editorProps: {
attributes: {
class: cn('p-1 border rounded outline-0', className),
},
}, },
onUpdate: ({ editor }) => { ref
const { transformedValue, transformedJson, editorText } = onUpdateTransformer({ ) => {
const mentionsByTrigger = useMemo(() => {
return mentions.reduce(
(acc, mention) => {
if (mention.char) {
acc[mention.char] = mention;
}
return acc;
},
{} as Record<string, MentionSuggestionExtension>
);
}, [mentions]);
const editor = useEditor({
extensions: [
Document,
Paragraph,
Text,
MentionExtension(mentions),
SubmitOnEnter({
mentionsByTrigger,
onPressEnter,
commandListNavigatedRef,
}),
],
content: defaultValue,
autofocus: autoFocus,
editable: !disabled && !readOnly,
editorProps: {
attributes: {
class: cn('p-1 border rounded outline-0', className),
},
},
onUpdate: ({ editor }) => {
const { transformedValue, transformedJson, editorText } = onUpdateTransformer({
editor,
mentionsByTrigger,
});
onChange?.(transformedValue, transformedJson, editorText);
},
onFocus: onFocus,
onBlur: onBlur,
});
useImperativeHandle(
ref,
() => ({
editor, editor,
mentionsByTrigger, }),
}); [editor]
onChange?.(transformedValue, transformedJson, editorText); );
},
onFocus: onFocus,
onBlur: onBlur,
});
const providerValue = useMemo(() => ({ editor }), [editor]); const providerValue = useMemo(() => ({ editor }), [editor]);
return ( return (
<ClientOnly> <ClientOnly>
<EditorContext.Provider value={providerValue}> <EditorContext.Provider value={providerValue}>
<EditorContent className="outline-0" editor={editor} style={style} /> <EditorContent className="outline-0" editor={editor} style={style} />
</EditorContext.Provider> </EditorContext.Provider>
</ClientOnly> </ClientOnly>
); );
}; }
);
MentionInput.displayName = 'MentionInput';

View File

@ -1,5 +1,5 @@
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention'; import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
import type { EditorEvents } from '@tiptap/react'; import type { Editor, EditorEvents } from '@tiptap/react';
import type { MentionPillAttributes } from './MentionPill'; import type { MentionPillAttributes } from './MentionPill';
export type MentionOnSelectParams<T = unknown> = { export type MentionOnSelectParams<T = unknown> = {
@ -102,6 +102,10 @@ export type MentionInputProps = {
commandListNavigatedRef?: React.RefObject<boolean>; commandListNavigatedRef?: React.RefObject<boolean>;
}; };
export type MentionInputRef = {
editor: Editor | null;
};
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Storage { interface Storage {
mention: { mention: {