mirror of https://github.com/buster-so/buster.git
invite people modal
This commit is contained in:
parent
dd8e1c98cd
commit
1892f812f0
|
@ -51,10 +51,14 @@ export const InvitePeopleModal: React.FC<{
|
|||
tags={emails}
|
||||
onChangeText={setInputText}
|
||||
onTagAdd={(v) => {
|
||||
if (validate(v)) {
|
||||
setEmails([...emails, v]);
|
||||
} else {
|
||||
openErrorMessage(`Invalid email - ${v}`);
|
||||
const arrayedTags = Array.isArray(v) ? v : [v];
|
||||
const hadMultipleTags = arrayedTags.length > 1;
|
||||
const validTags = arrayedTags.filter((tag) => validate(tag));
|
||||
|
||||
setEmails([...emails, ...validTags]);
|
||||
|
||||
if (validTags.length !== arrayedTags.length) {
|
||||
openErrorMessage(hadMultipleTags ? 'List contained invalid emails' : 'Invalid email');
|
||||
}
|
||||
}}
|
||||
onTagRemove={(index) => {
|
||||
|
|
|
@ -26,6 +26,13 @@ const meta: Meta<typeof InputTagInput> = {
|
|||
},
|
||||
maxTags: {
|
||||
control: 'number'
|
||||
},
|
||||
delimiter: {
|
||||
control: 'text',
|
||||
description: 'Character used to separate tags (default is comma)',
|
||||
table: {
|
||||
defaultValue: { summary: ',' }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -37,8 +44,13 @@ type Story = StoryObj<typeof InputTagInput>;
|
|||
const InteractiveTagInput = (args: React.ComponentProps<typeof InputTagInput>) => {
|
||||
const [tags, setTags] = useState<string[]>(args.tags || []);
|
||||
|
||||
const handleTagAdd = (tag: string) => {
|
||||
setTags([...tags, tag]);
|
||||
const handleTagAdd = (tag: string | string[]) => {
|
||||
console.log('tag', tag);
|
||||
if (Array.isArray(tag)) {
|
||||
setTags([...tags, ...tag]);
|
||||
} else {
|
||||
setTags([...tags, tag]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagRemove = (index: number) => {
|
||||
|
@ -112,3 +124,39 @@ export const Empty: Story = {
|
|||
tags: []
|
||||
}
|
||||
};
|
||||
|
||||
export const CustomDelimiter: Story = {
|
||||
render: (args) => <InteractiveTagInput {...args} />,
|
||||
args: {
|
||||
placeholder: 'Type or paste "tag1;tag2;tag3"...',
|
||||
delimiter: ';',
|
||||
tags: []
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Use a custom delimiter (semicolon in this example) to separate tags. You can:\n' +
|
||||
'1. Type text and press the delimiter to create a tag\n' +
|
||||
'2. Paste a delimited list (e.g., "tag1;tag2;tag3")\n' +
|
||||
'3. Type or paste text with delimiters for automatic tag creation'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const SpaceDelimiter: Story = {
|
||||
render: (args) => <InteractiveTagInput {...args} />,
|
||||
args: {
|
||||
placeholder: 'Type or paste space-separated tags...',
|
||||
delimiter: ' ',
|
||||
tags: []
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Using space as a delimiter. Perfect for handling space-separated lists of tags.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,13 +9,14 @@ import { InputTag } from './InputTag';
|
|||
|
||||
export interface TagInputProps extends VariantProps<typeof inputVariants> {
|
||||
tags: string[];
|
||||
onTagAdd?: (tag: string) => void;
|
||||
onTagAdd?: (tag: string | string[]) => void;
|
||||
onTagRemove?: (index: number) => void;
|
||||
onChangeText?: (text: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
maxTags?: number;
|
||||
className?: string;
|
||||
delimiter?: string;
|
||||
}
|
||||
|
||||
const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
||||
|
@ -31,6 +32,7 @@ const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|||
placeholder,
|
||||
disabled = false,
|
||||
maxTags,
|
||||
delimiter = ',',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
|
@ -39,29 +41,42 @@ const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const addTag = useMemoizedFn((value: string) => {
|
||||
const newTag = value.trim();
|
||||
if (newTag !== '' && !tags.includes(newTag)) {
|
||||
if (maxTags && tags.length >= maxTags) return;
|
||||
onTagAdd?.(newTag);
|
||||
setInputValue('');
|
||||
// Scroll to the end after adding a new tag
|
||||
const addMultipleTags = useMemoizedFn((value: string) => {
|
||||
const newTags = value
|
||||
.split(delimiter)
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag !== '' && !tags.includes(tag));
|
||||
|
||||
if (maxTags) {
|
||||
const availableSlots = maxTags - tags.length;
|
||||
const validTags = newTags.slice(0, availableSlots);
|
||||
onTagAdd?.(validTags);
|
||||
} else {
|
||||
onTagAdd?.(newTags);
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollLeft = scrollRef.current.scrollWidth;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleBlur = useMemoizedFn(() => {
|
||||
if (inputValue.trim() !== '') {
|
||||
addTag(inputValue);
|
||||
addMultipleTags(inputValue);
|
||||
}
|
||||
});
|
||||
|
||||
const handleKeyDown = useMemoizedFn((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if ((e.key === 'Tab' || e.key === 'Enter' || e.key === ',') && inputValue.trim() !== '') {
|
||||
if (
|
||||
(e.key === 'Tab' || e.key === 'Enter' || e.key === delimiter) &&
|
||||
inputValue.trim() !== ''
|
||||
) {
|
||||
e.preventDefault();
|
||||
addTag(inputValue);
|
||||
addMultipleTags(inputValue);
|
||||
} else if (e.key === 'Backspace' && inputValue === '' && tags.length > 0 && !disabled) {
|
||||
onTagRemove?.(tags.length - 1);
|
||||
}
|
||||
|
@ -69,12 +84,21 @@ const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|||
|
||||
const handleInputChange = useMemoizedFn((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value.endsWith(',')) {
|
||||
addTag(value.slice(0, -1));
|
||||
if (value.includes(delimiter)) {
|
||||
addMultipleTags(value);
|
||||
} else {
|
||||
setInputValue(value);
|
||||
onChangeText?.(value);
|
||||
}
|
||||
onChangeText?.(value);
|
||||
});
|
||||
|
||||
const handlePaste = useMemoizedFn((e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pastedText = e.clipboardData.getData('text');
|
||||
if (pastedText.includes(delimiter)) {
|
||||
e.preventDefault();
|
||||
addMultipleTags(pastedText);
|
||||
}
|
||||
// If no delimiter is found, let the default paste behavior handle it
|
||||
});
|
||||
|
||||
// Focus the container when clicked
|
||||
|
@ -124,6 +148,7 @@ const InputTagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
className="placeholder:text-gray-light min-w-[120px] flex-1 bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={tags.length === 0 ? placeholder : undefined}
|
||||
disabled={isDisabledInput}
|
||||
|
|
Loading…
Reference in New Issue