pass in read props

This commit is contained in:
Nate Kelley 2025-09-29 10:37:45 -06:00
parent 0c576b62e6
commit d4075311a3
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 140 additions and 103 deletions

View File

@ -22,6 +22,8 @@ export const BusterInput = ({
variant = 'default', variant = 'default',
onChange, onChange,
ariaLabel = 'Buster Input', ariaLabel = 'Buster Input',
readOnly,
autoFocus,
//suggestions //suggestions
suggestionItems, suggestionItems,
closeSuggestionOnSelect = true, closeSuggestionOnSelect = true,
@ -58,6 +60,7 @@ export const BusterInput = ({
console.warn('Item is loading', params); console.warn('Item is loading', params);
return; return;
} }
console.log('onSelectItem', addValueToInput, params);
if (addValueToInput) setValue(inputValue ?? String(label)); if (addValueToInput) setValue(inputValue ?? String(label));
onClick?.(); onClick?.();
if (closeSuggestionOnSelect) setHasClickedSelect(true); if (closeSuggestionOnSelect) setHasClickedSelect(true);
@ -92,13 +95,13 @@ export const BusterInput = ({
variant={variant} variant={variant}
> >
<BusterMentionsInput <BusterMentionsInput
autoFocus
defaultValue={defaultValue} defaultValue={defaultValue}
readOnly readOnly={readOnly}
autoFocus={autoFocus}
placeholder={placeholder} placeholder={placeholder}
mentions={mentions} mentions={mentions}
value={value} value={value}
onChangeInputValue={onChangeInputValue} onChange={onChangeInputValue}
shouldFilter={shouldFilter} shouldFilter={shouldFilter}
filter={filter} filter={filter}
onMentionItemClick={onMentionItemClick} onMentionItemClick={onMentionItemClick}

View File

@ -57,9 +57,11 @@ export type BusterInputProps<T = string> = {
onChange?: (value: string) => void; onChange?: (value: string) => void;
submitting?: boolean; submitting?: boolean;
disabled?: boolean; disabled?: boolean;
readOnly?: boolean;
onSubmit: (value: string) => void; onSubmit: (value: string) => void;
onStop: () => void; onStop: () => void;
variant?: 'default'; variant?: 'default';
autoFocus?: boolean;
sendIcon?: React.ReactNode; sendIcon?: React.ReactNode;
secondaryActions?: React.ReactNode; secondaryActions?: React.ReactNode;
placeholder?: string; placeholder?: string;
@ -67,7 +69,7 @@ export type BusterInputProps<T = string> = {
emptyComponent?: React.ReactNode | string | false; //if false, no empty component will be shown emptyComponent?: React.ReactNode | string | false; //if false, no empty component will be shown
//mentions //mentions
onMentionItemClick?: (params: MentionTriggerItem<T>) => void; onMentionItemClick?: (params: MentionTriggerItem<T>) => void;
mentions?: MentionSuggestionExtension[]; mentions: MentionSuggestionExtension[];
//suggestions //suggestions
suggestionItems: ( suggestionItems: (
| BusterInputDropdownItem<T> | BusterInputDropdownItem<T>

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 from 'react';
import { cn } from '@/lib/classMerge'; import { MentionInput, type MentionInputProps } from '../MentionInput';
import type { BusterInputProps } from './BusterInput.types'; import type { BusterInputProps } from './BusterInput.types';
export type BusterMentionsInputProps = Pick< export type BusterMentionsInputProps = Pick<
@ -14,33 +14,29 @@ export type BusterMentionsInputProps = Pick<
| 'filter' | 'filter'
| 'onMentionItemClick' | 'onMentionItemClick'
> & { > & {
onChangeInputValue: (value: string) => void; onChange: MentionInputProps['onChange'];
} & React.ComponentPropsWithoutRef<typeof Command.Input>; className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
readOnly?: boolean;
};
export const BusterMentionsInput = ({ export const BusterMentionsInput = ({
children,
value: valueProp, value: valueProp,
placeholder, placeholder,
defaultValue, defaultValue,
mentions, mentions,
value, value,
onChangeInputValue,
className,
style,
...props ...props
}: BusterMentionsInputProps) => { }: BusterMentionsInputProps) => {
return ( return (
<React.Fragment> <React.Fragment>
<textarea /> <MentionInput mentions={mentions} defaultValue={value} {...props} />
<Command.Input <Command.Input
value={value} value={value}
{...props}
autoFocus={false} autoFocus={false}
className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none" className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none"
> />
{children}
</Command.Input>
</React.Fragment> </React.Fragment>
); );
}; };

View File

@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { createMentionSuggestionExtension } from './createMentionSuggestionOption'; import { createMentionSuggestionExtension } from './createMentionSuggestionOption';
import { MentionInput } from './MentionInput'; import { MentionInput } from './MentionInput';
import type { MentionInputTriggerItem } from './MentionInput.types'; import type { MentionInputTriggerItem } from './MentionInput.types';
@ -55,48 +56,6 @@ const looneyTunesCharacters: MentionInputTriggerItem[] = [
}, },
]; ];
const theSimpsonsCharacters: MentionInputTriggerItem[] = [
{
value: 'Homer Simpson',
label: 'Homer Simpson',
},
{
value: 'Marge Simpson',
label: 'Marge Simpson',
},
{
value: 'Bart Simpson',
label: 'Bart Simpson',
},
{
value: 'Lisa Simpson',
label: 'Lisa Simpson',
},
{
value: 'Maggie Simpson',
label: 'Maggie Simpson',
},
{
value: 'Ned Flanders',
label: 'Ned Flanders',
},
].map((item) => ({
...item,
label: (
<span className="gap-x-1 space-x-1">
<img
src={faker.image.url({
width: 16,
height: 16,
})}
alt=""
className="w-3 h-3 rounded-full bg-item-active inline-block align-middle"
/>
<span className="inline-block">{item.label}</span>
</span>
),
}));
const arthurCharacters: MentionInputTriggerItem[] = [ const arthurCharacters: MentionInputTriggerItem[] = [
{ {
value: 'Arthur Read', value: 'Arthur Read',
@ -138,7 +97,7 @@ const arthurCharacters: MentionInputTriggerItem[] = [
), ),
})); }));
export const looneyTunesSuggestions = createMentionSuggestionExtension({ const looneyTunesSuggestions = createMentionSuggestionExtension({
trigger: '@', trigger: '@',
items: looneyTunesCharacters, items: looneyTunesCharacters,
popoverContent: (props) => { popoverContent: (props) => {
@ -150,10 +109,52 @@ export const looneyTunesSuggestions = createMentionSuggestionExtension({
}, },
}, },
onChangeTransform: (v) => { onChangeTransform: (v) => {
return `[@${String(v.label)}](${String(v.value)})`; return `We can totally transform this into anything we want. The original value was [@${String(v.label)}](${String(v.value)})`;
}, },
}); });
const theSimpsonsCharacters: MentionInputTriggerItem[] = [
{
value: 'Homer Simpson',
label: 'Homer Simpson',
},
{
value: 'Marge Simpson',
label: 'Marge Simpson',
},
{
value: 'Bart Simpson',
label: 'Bart Simpson',
},
{
value: 'Lisa Simpson',
label: 'Lisa Simpson',
},
{
value: 'Maggie Simpson',
label: 'Maggie Simpson',
},
{
value: 'Ned Flanders',
label: 'Ned Flanders',
},
].map((item) => ({
...item,
label: (
<span className="gap-x-1 space-x-1">
<img
src={faker.image.url({
width: 16,
height: 16,
})}
alt=""
className="w-3 h-3 rounded-full bg-item-active inline-block align-middle"
/>
<span className="inline-block">{item.label}</span>
</span>
),
}));
const theSimpsonsSuggestions = createMentionSuggestionExtension({ const theSimpsonsSuggestions = createMentionSuggestionExtension({
trigger: '#', trigger: '#',
items: theSimpsonsCharacters, items: theSimpsonsCharacters,
@ -301,6 +302,7 @@ export const Default: Story = {
arthurSuggestions, arthurSuggestions,
spongebobSuggestions, spongebobSuggestions,
], ],
onChange: fn(),
}, },
parameters: { parameters: {
docs: { docs: {

View File

@ -3,18 +3,19 @@ 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 { import {
type Editor,
EditorContent, EditorContent,
EditorContext, EditorContext,
type EditorEvents,
type NodeType, type NodeType,
type TextType, type TextType,
useEditor, useEditor,
} from '@tiptap/react'; } from '@tiptap/react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { cn } from '@/lib/utils';
import { MentionExtension } from './MentionExtension'; import { MentionExtension } from './MentionExtension';
import type { import type {
MentionArrayItem, MentionArrayItem,
MentionOnChangeParams, MentionInputProps,
MentionSuggestionExtension, MentionSuggestionExtension,
} from './MentionInput.types'; } from './MentionInput.types';
import type { MentionPillAttributes } from './MentionPill'; import type { MentionPillAttributes } from './MentionPill';
@ -25,13 +26,12 @@ export const MentionInput = ({
defaultValue = '', defaultValue = '',
onFocus, onFocus,
onBlur, onBlur,
}: { autoFocus,
mentions: MentionSuggestionExtension[]; style,
onChange?: MentionOnChangeParams; className,
onFocus?: (v: EditorEvents['focus']) => void; readOnly,
onBlur?: (v: EditorEvents['blur']) => void; disabled,
defaultValue?: string; }: MentionInputProps) => {
}) => {
const mentionsByTrigger = useMemo(() => { const mentionsByTrigger = useMemo(() => {
return mentions.reduce( return mentions.reduce(
(acc, mention) => { (acc, mention) => {
@ -47,43 +47,18 @@ export const MentionInput = ({
const editor = useEditor({ const editor = useEditor({
extensions: [Document, Paragraph, Text, MentionExtension(mentions)], extensions: [Document, Paragraph, Text, MentionExtension(mentions)],
content: defaultValue, content: defaultValue,
autofocus: true, autofocus: autoFocus,
editable: !disabled && !readOnly,
editorProps: { editorProps: {
attributes: { attributes: {
class: 'p-1', class: cn('p-1 border rounded outline-0', className),
}, },
}, },
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
const editorText = editor.getText(); const { transformedValue, transformedJson, editorText } = onUpdateTransformer({
const editorJson = editor.getJSON(); editor,
const transformedJson: MentionArrayItem[] = editorJson.content.reduce<MentionArrayItem[]>( mentionsByTrigger,
(acc, item) => { });
if (item.type === 'paragraph') {
item.content?.forEach((item) => {
if (item.type === 'text') {
const _item = item as TextType;
acc.push({ type: 'text', text: _item.text });
} else if (item.type === 'mention') {
const _item = item as NodeType<'mention', MentionPillAttributes>;
acc.push({ type: 'mention', attrs: _item.attrs });
}
});
}
return acc;
},
[]
);
const transformedValue = transformedJson.reduce((acc, item) => {
if (item.type === 'text') {
return acc + item.text;
}
if (item.type === 'mention') {
const onChangeTransform = mentionsByTrigger[item.attrs.trigger]?.onChangeTransform;
if (onChangeTransform) return acc + onChangeTransform(item.attrs);
return acc + item.attrs.label;
}
return acc;
}, '');
onChange?.(transformedValue, transformedJson, editorText); onChange?.(transformedValue, transformedJson, editorText);
}, },
onFocus: onFocus, onFocus: onFocus,
@ -95,8 +70,53 @@ export const MentionInput = ({
return ( return (
<ClientOnly> <ClientOnly>
<EditorContext.Provider value={providerValue}> <EditorContext.Provider value={providerValue}>
<EditorContent className="rounded p-1 border outline-1 min-w-120" editor={editor} /> <EditorContent className="outline-0" editor={editor} style={style} />
</EditorContext.Provider> </EditorContext.Provider>
</ClientOnly> </ClientOnly>
); );
}; };
const onUpdateTransformer = ({
editor,
mentionsByTrigger,
}: {
editor: Editor;
mentionsByTrigger: Record<string, MentionSuggestionExtension>;
}) => {
const editorText = editor.getText();
const editorJson = editor.getJSON();
const transformedJson: MentionArrayItem[] = editorJson.content.reduce<MentionArrayItem[]>(
(acc, item) => {
if (item.type === 'paragraph') {
item.content?.forEach((item) => {
if (item.type === 'text') {
const _item = item as TextType;
acc.push({ type: 'text', text: _item.text });
} else if (item.type === 'mention') {
const _item = item as NodeType<'mention', MentionPillAttributes>;
acc.push({ type: 'mention', attrs: _item.attrs });
}
});
}
return acc;
},
[]
);
const transformedValue = transformedJson.reduce((acc, item) => {
if (item.type === 'text') {
return acc + item.text;
}
if (item.type === 'mention') {
const onChangeTransform = mentionsByTrigger[item.attrs.trigger]?.onChangeTransform;
if (onChangeTransform) return acc + onChangeTransform(item.attrs);
return acc + item.attrs.label;
}
return acc;
}, '');
return {
transformedValue,
transformedJson,
editorText,
};
};

View File

@ -1,4 +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 { MentionPillAttributes } from './MentionPill'; import type { MentionPillAttributes } from './MentionPill';
export type MentionOnSelectParams<T = unknown> = { export type MentionOnSelectParams<T = unknown> = {
@ -86,6 +87,19 @@ export type MentionOnChangeParams = (
rawValue: string rawValue: string
) => void; ) => void;
export type MentionInputProps = {
mentions: MentionSuggestionExtension[];
onChange: MentionOnChangeParams;
onFocus?: (v: EditorEvents['focus']) => void;
onBlur?: (v: EditorEvents['blur']) => void;
defaultValue?: string;
autoFocus?: boolean;
style?: React.CSSProperties;
className?: string;
readOnly?: boolean;
disabled?: boolean;
};
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Storage { interface Storage {
mention: { mention: {