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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { createMentionSuggestionExtension } from './createMentionSuggestionOption';
import { MentionInput } from './MentionInput';
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[] = [
{
value: 'Arthur Read',
@ -138,7 +97,7 @@ const arthurCharacters: MentionInputTriggerItem[] = [
),
}));
export const looneyTunesSuggestions = createMentionSuggestionExtension({
const looneyTunesSuggestions = createMentionSuggestionExtension({
trigger: '@',
items: looneyTunesCharacters,
popoverContent: (props) => {
@ -150,10 +109,52 @@ export const looneyTunesSuggestions = createMentionSuggestionExtension({
},
},
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({
trigger: '#',
items: theSimpsonsCharacters,
@ -301,6 +302,7 @@ export const Default: Story = {
arthurSuggestions,
spongebobSuggestions,
],
onChange: fn(),
},
parameters: {
docs: {

View File

@ -3,18 +3,19 @@ import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import {
type Editor,
EditorContent,
EditorContext,
type EditorEvents,
type NodeType,
type TextType,
useEditor,
} from '@tiptap/react';
import { useMemo } from 'react';
import { cn } from '@/lib/utils';
import { MentionExtension } from './MentionExtension';
import type {
MentionArrayItem,
MentionOnChangeParams,
MentionInputProps,
MentionSuggestionExtension,
} from './MentionInput.types';
import type { MentionPillAttributes } from './MentionPill';
@ -25,13 +26,12 @@ export const MentionInput = ({
defaultValue = '',
onFocus,
onBlur,
}: {
mentions: MentionSuggestionExtension[];
onChange?: MentionOnChangeParams;
onFocus?: (v: EditorEvents['focus']) => void;
onBlur?: (v: EditorEvents['blur']) => void;
defaultValue?: string;
}) => {
autoFocus,
style,
className,
readOnly,
disabled,
}: MentionInputProps) => {
const mentionsByTrigger = useMemo(() => {
return mentions.reduce(
(acc, mention) => {
@ -47,43 +47,18 @@ export const MentionInput = ({
const editor = useEditor({
extensions: [Document, Paragraph, Text, MentionExtension(mentions)],
content: defaultValue,
autofocus: true,
autofocus: autoFocus,
editable: !disabled && !readOnly,
editorProps: {
attributes: {
class: 'p-1',
class: cn('p-1 border rounded outline-0', className),
},
},
onUpdate: ({ editor }) => {
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;
}, '');
const { transformedValue, transformedJson, editorText } = onUpdateTransformer({
editor,
mentionsByTrigger,
});
onChange?.(transformedValue, transformedJson, editorText);
},
onFocus: onFocus,
@ -95,8 +70,53 @@ export const MentionInput = ({
return (
<ClientOnly>
<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>
</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 { EditorEvents } from '@tiptap/react';
import type { MentionPillAttributes } from './MentionPill';
export type MentionOnSelectParams<T = unknown> = {
@ -86,6 +87,19 @@ export type MentionOnChangeParams = (
rawValue: string
) => 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' {
interface Storage {
mention: {