@@ -65,7 +65,6 @@ | |||||
"@modal-sh/react-utils": "workspace:*", | "@modal-sh/react-utils": "workspace:*", | ||||
"@tesseract-design/web-base": "workspace:*", | "@tesseract-design/web-base": "workspace:*", | ||||
"clsx": "^1.2.1", | "clsx": "^1.2.1", | ||||
"react-tag-input-component": "^2.0.2", | |||||
"tailwindcss": "3.3.2" | "tailwindcss": "3.3.2" | ||||
}, | }, | ||||
"types": "./dist/types/index.d.ts", | "types": "./dist/types/index.d.ts", | ||||
@@ -1,5 +1,5 @@ | |||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { TagsInput } from 'react-tag-input-component'; | |||||
import { TagsInput } from './internal'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; | import { useClientSide, useFallbackId, useProxyInput } from '@modal-sh/react-utils'; | ||||
import { TextControl } from '@tesseract-design/web-base'; | import { TextControl } from '@tesseract-design/web-base'; | ||||
@@ -95,6 +95,9 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||||
export const tagInputPlugin = plugin(({ addComponents }) => { | export const tagInputPlugin = plugin(({ addComponents }) => { | ||||
addComponents({ | addComponents({ | ||||
'.tag-input': { | '.tag-input': { | ||||
'&[data-size] label + * + div': { | |||||
'background': 'red', | |||||
}, | |||||
'& label + * + div > input': { | '& label + * + div > input': { | ||||
'flex': 'auto', | 'flex': 'auto', | ||||
}, | }, | ||||
@@ -462,10 +465,6 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
{clientSide && ( | {clientSide && ( | ||||
<TagsInput | <TagsInput | ||||
value={tags} | value={tags} | ||||
classNames={{ | |||||
input: 'peer bg-transparent font-inherit', | |||||
tag: 'text-xs p-2 select-none font-inherit', | |||||
}} | |||||
isEditOnRemove={editOnRemove} | isEditOnRemove={editOnRemove} | ||||
placeHolder={placeholder} | placeHolder={placeholder} | ||||
disabled={disabled} | disabled={disabled} | ||||
@@ -0,0 +1,148 @@ | |||||
import * as React from "react"; | |||||
const useDidUpdateEffect = (fn: () => void, inputs: unknown[]) => { | |||||
const didMountRef = React.useRef(false); | |||||
React.useEffect(() => { | |||||
if (didMountRef.current) fn(); | |||||
else didMountRef.current = true; | |||||
}, inputs); | |||||
} | |||||
interface TagProps { | |||||
text: string; | |||||
remove: any; | |||||
disabled?: boolean; | |||||
className?: string; | |||||
} | |||||
export const Tag = ({ text, remove, disabled }: TagProps) => { | |||||
const handleOnRemove: React.MouseEventHandler<HTMLButtonElement> = (e) => { | |||||
e.stopPropagation(); | |||||
remove(text); | |||||
}; | |||||
return ( | |||||
<span className="text-xs p-2 select-none font-inherit"> | |||||
<span>{text}</span> | |||||
{!disabled && ( | |||||
<button | |||||
type="button" | |||||
onClick={handleOnRemove} | |||||
aria-label={`remove ${text}`} | |||||
> | |||||
✕ | |||||
</button> | |||||
)} | |||||
</span> | |||||
); | |||||
} | |||||
export interface TagsInputProps { | |||||
name?: string; | |||||
placeHolder?: string; | |||||
value?: string[]; | |||||
onChange?: (tags: string[]) => void; | |||||
onBlur?: any; | |||||
separators?: string[]; | |||||
disableBackspaceRemove?: boolean; | |||||
onExisting?: (tag: string) => void; | |||||
onRemoved?: (tag: string) => void; | |||||
disabled?: boolean; | |||||
isEditOnRemove?: boolean; | |||||
beforeAddValidate?: (tag: string, existingTags: string[]) => boolean; | |||||
onKeyUp?: (e: React.KeyboardEvent<HTMLInputElement>) => void; | |||||
classNames?: { | |||||
input?: string; | |||||
tag?: string; | |||||
}; | |||||
} | |||||
const defaultSeparators = ["Enter"]; | |||||
export const TagsInput = ({ | |||||
name, | |||||
placeHolder, | |||||
value, | |||||
onChange, | |||||
onBlur, | |||||
separators, | |||||
disableBackspaceRemove, | |||||
onExisting, | |||||
onRemoved, | |||||
disabled, | |||||
isEditOnRemove, | |||||
beforeAddValidate, | |||||
onKeyUp, | |||||
classNames, | |||||
}: TagsInputProps) => { | |||||
const [tags, setTags] = React.useState<any>(value || []); | |||||
useDidUpdateEffect(() => { | |||||
onChange && onChange(tags); | |||||
}, [tags]); | |||||
useDidUpdateEffect(() => { | |||||
if (JSON.stringify(value) !== JSON.stringify(tags)) { | |||||
setTags(value); | |||||
} | |||||
}, [value]); | |||||
const handleOnKeyUp: React.KeyboardEventHandler<HTMLInputElement> = (e) => { | |||||
e.stopPropagation(); | |||||
const text = e.currentTarget.value; | |||||
if ( | |||||
!text && | |||||
!disableBackspaceRemove && | |||||
tags.length && | |||||
e.key === "Backspace" | |||||
) { | |||||
e.currentTarget.value = isEditOnRemove ? `${tags.at(-1)} ` : ""; | |||||
setTags([...tags.slice(0, -1)]); | |||||
} | |||||
if (text && (separators || defaultSeparators).includes(e.key)) { | |||||
e.preventDefault(); | |||||
if (beforeAddValidate && !beforeAddValidate(text, tags)) return; | |||||
if (tags.includes(text)) { | |||||
onExisting && onExisting(text); | |||||
return; | |||||
} | |||||
setTags([...tags, text]); | |||||
e.currentTarget.value = ""; | |||||
} | |||||
}; | |||||
const onTagRemove = (text: string) => { | |||||
setTags(tags.filter((tag: string) => tag !== text)); | |||||
onRemoved && onRemoved(text); | |||||
}; | |||||
return ( | |||||
<div aria-labelledby={name} className="rti--container"> | |||||
{tags.map((tag: string) => ( | |||||
<Tag | |||||
key={tag} | |||||
className={classNames?.tag} | |||||
text={tag} | |||||
remove={onTagRemove} | |||||
disabled={disabled} | |||||
/> | |||||
))} | |||||
<input | |||||
className="peer bg-transparent font-inherit focus:outline-0" | |||||
type="text" | |||||
name={name} | |||||
placeholder={placeHolder} | |||||
onKeyDown={handleOnKeyUp} | |||||
onBlur={onBlur} | |||||
disabled={disabled} | |||||
onKeyUp={onKeyUp} | |||||
/> | |||||
</div> | |||||
); | |||||
}; |
@@ -516,9 +516,6 @@ importers: | |||||
clsx: | clsx: | ||||
specifier: ^1.2.1 | specifier: ^1.2.1 | ||||
version: 1.2.1 | version: 1.2.1 | ||||
react-tag-input-component: | |||||
specifier: ^2.0.2 | |||||
version: 2.0.2(react-dom@18.2.0)(react@18.2.0) | |||||
tailwindcss: | tailwindcss: | ||||
specifier: 3.3.2 | specifier: 3.3.2 | ||||
version: 3.3.2 | version: 3.3.2 | ||||
@@ -7814,16 +7811,6 @@ packages: | |||||
react-is: 18.2.0 | react-is: 18.2.0 | ||||
dev: true | dev: true | ||||
/react-tag-input-component@2.0.2(react-dom@18.2.0)(react@18.2.0): | |||||
resolution: {integrity: sha512-dydI9luVwwv9vrjE5u1TTnkcOVkOVL6mhFti8r6hLi78V2F2EKWQOLptURz79UYbDHLSk6tnbvGl8FE+sMpADg==} | |||||
peerDependencies: | |||||
react: ^16 || ^17 || ^18 | |||||
react-dom: ^16 || ^17 || ^18 | |||||
dependencies: | |||||
react: 18.2.0 | |||||
react-dom: 18.2.0(react@18.2.0) | |||||
dev: false | |||||
/react-test-renderer@18.2.0(react@18.2.0): | /react-test-renderer@18.2.0(react@18.2.0): | ||||
resolution: {integrity: sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==} | resolution: {integrity: sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==} | ||||
peerDependencies: | peerDependencies: | ||||