@@ -7,7 +7,7 @@ import {ImageFilePreview} from '../ImageFilePreview'; | |||
import {VideoFilePreview} from '../VideoFilePreview'; | |||
import {TextFilePreview} from '../TextFilePreview'; | |||
import {AudioMiniFilePreview} from '../AudioMiniFilePreview'; | |||
import {delegateTriggerChangeEvent} from '@/utils/event'; | |||
import {delegateTriggerEvent} from '@/utils/event'; | |||
import clsx from 'clsx'; | |||
import {useEnhanced} from '@modal-soft/react-utils'; | |||
import {FilePreviewComponent} from '@/categories/blob/react/common'; | |||
@@ -95,7 +95,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS | |||
setFileList(undefined); | |||
setLastUpdated(Date.now()); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(current); | |||
delegateTriggerEvent('change', current); | |||
}); | |||
}; | |||
@@ -121,7 +121,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS | |||
setFileList(current.files = files); | |||
setLastUpdated(Date.now()); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(current); | |||
delegateTriggerEvent('change', current); | |||
}); | |||
} | |||
@@ -4,9 +4,11 @@ import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||
import clsx from 'clsx'; | |||
import {useEnhanced} from '@/packages/react-utils'; | |||
import styles from './style.module.css'; | |||
import {delegateTriggerChangeEvent} from '@/utils/event'; | |||
import {delegateTriggerEvent} from '@/utils/event'; | |||
type TagInputDerivedElement = HTMLInputElement; | |||
type TagInputSeparator = ',' | '\n'; | |||
export type TagInputDerivedElement = HTMLTextAreaElement; | |||
export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedElement>, 'size' | 'type' | 'style' | 'label' | 'list'> { | |||
/** | |||
@@ -46,7 +48,7 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme | |||
*/ | |||
hiddenLabel?: boolean, | |||
enhanced?: boolean, | |||
separator?: string, | |||
separator?: TagInputSeparator[], | |||
} | |||
/** | |||
@@ -68,9 +70,9 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
hiddenLabel = false, | |||
className, | |||
enhanced: enhancedProp = false, | |||
separator = ',', | |||
onChange, | |||
separator = ['\n'], | |||
defaultValue, | |||
disabled, | |||
...etcProps | |||
}: TagInputProps, | |||
forwardedRef, | |||
@@ -84,7 +86,9 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
return []; | |||
} | |||
if (typeof defaultValue === 'string') { | |||
return defaultValue.split(separator); | |||
return separator.reduce((theDefaultValues, separator) => { | |||
return theDefaultValues.join(separator).split(separator); | |||
}, [defaultValue]); | |||
} | |||
if (typeof defaultValue === 'number') { | |||
return [defaultValue.toString()]; | |||
@@ -100,75 +104,100 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
if (!input) { | |||
return; | |||
} | |||
input.value = tags.join(separator); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(input); | |||
delegateTriggerEvent('change', input, tags.map((t => t.trim())).join('\n')); | |||
}); | |||
}; | |||
const inputClassName = clsx( | |||
'bg-negative rounded-inherit w-full block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
{ | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
{ | |||
'pt-4': variant === 'alternate', | |||
}, | |||
{ | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
{ | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
); | |||
const handleBlur = () => { | |||
if (!(typeof ref === 'object' && ref)) { | |||
return; | |||
} | |||
const { current: input } = ref; | |||
if (!input) { | |||
return; | |||
} | |||
setTimeout(() => { | |||
delegateTriggerEvent('blur', input); | |||
}); | |||
} | |||
return ( | |||
<div | |||
className={clsx( | |||
styles['tag-input'], | |||
size === 'small' && styles['tag-input-small'], | |||
size === 'medium' && styles['tag-input-medium'], | |||
size === 'large' && styles['tag-input-large'], | |||
'relative rounded ring-secondary/50 group', | |||
'focus-within:ring-4', | |||
{ | |||
'block': block, | |||
'inline-block align-middle': !block, | |||
}, | |||
{ | |||
enhanced && { | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
styles['tag-input'], | |||
size === 'small' && styles['tag-input-small'], | |||
size === 'medium' && styles['tag-input-medium'], | |||
size === 'large' && styles['tag-input-large'], | |||
indicator && styles['tag-input-indicator'], | |||
variant === 'default' && styles['tag-input-default'], | |||
variant === 'alternate' && styles['tag-input-alternate'], | |||
className, | |||
)} | |||
> | |||
<input | |||
<textarea | |||
{...etcProps} | |||
disabled={disabled} | |||
ref={ref} | |||
readOnly={enhanced} | |||
aria-labelledby={labelId} | |||
type={type} | |||
data-testid="input" | |||
onChange={onChange} | |||
defaultValue={defaultValue} | |||
style={{ | |||
height: enhanced ? undefined : 0, | |||
}} | |||
className={clsx( | |||
inputClassName, | |||
'bg-negative rounded-inherit peer block', | |||
'focus:outline-0', | |||
'disabled:opacity-50 disabled:cursor-not-allowed', | |||
{ | |||
'resize': !block, | |||
'resize-y': block, | |||
}, | |||
{ | |||
'text-xxs': size === 'small', | |||
'text-xs': size === 'medium', | |||
}, | |||
!enhanced && { | |||
'pl-4': variant === 'default', | |||
'pl-1.5': variant === 'alternate', | |||
}, | |||
!enhanced && { | |||
'pt-4': variant === 'alternate' && size === 'small', | |||
'pt-5': variant === 'alternate' && size === 'medium', | |||
'pt-8': variant === 'alternate' && size === 'large', | |||
}, | |||
!enhanced && { | |||
'py-2.5': variant === 'default' && size === 'small', | |||
'py-3': variant === 'default' && size === 'medium', | |||
'py-5': variant === 'default' && size === 'large', | |||
}, | |||
!enhanced && { | |||
'pr-4': variant === 'default' && !indicator, | |||
'pr-1.5': variant === 'alternate' && !indicator, | |||
}, | |||
!enhanced && { | |||
'pr-10': indicator && size === 'small', | |||
'pr-12': indicator && size === 'medium', | |||
'pr-16': indicator && size === 'large', | |||
}, | |||
!enhanced && { | |||
'h-10': size === 'small', | |||
'h-12': size === 'medium', | |||
'h-16': size === 'large', | |||
'min-h-10': size === 'small', | |||
'min-h-12': size === 'medium', | |||
'min-h-16': size === 'large', | |||
}, | |||
!enhanced && 'peer', | |||
!enhanced && 'w-full', | |||
enhanced && 'sr-only', | |||
)} | |||
/> | |||
@@ -177,9 +206,12 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||
value={tags} | |||
classNames={{ | |||
input: 'peer bg-transparent', | |||
tag: 'text-xs p-1', | |||
tag: 'text-xs p-2', | |||
}} | |||
disabled={disabled} | |||
onChange={handleTagsInputChange} | |||
onBlur={handleBlur} | |||
separators={separator.map((separator) => separator === '\n' ? 'Enter' : separator)} | |||
/> | |||
)} | |||
{ | |||
@@ -1,36 +1,96 @@ | |||
.tag-input input + div { | |||
padding: 0.625rem; | |||
.tag-input textarea + div > input { | |||
flex: auto; | |||
} | |||
.tag-input-small input + div { | |||
padding: 0.5rem; | |||
min-height: 2.5rem; | |||
.tag-input-small textarea + div > input { | |||
font-size: 0.625rem; | |||
} | |||
.tag-input-small input + div > input { | |||
.tag-input-medium textarea + div > input { | |||
font-size: 0.75rem; | |||
} | |||
.tag-input-small input + div { | |||
.tag-input-default textarea + div { | |||
padding-left: 1rem; | |||
padding-right: 1rem; | |||
} | |||
.tag-input-alternate textarea + div { | |||
padding-left: 0.375rem; | |||
padding-right: 0.375rem; | |||
} | |||
.tag-input-small.tag-input-default textarea + div { | |||
padding-top: 0.625rem; | |||
padding-bottom: 0.875rem; | |||
} | |||
.tag-input-medium.tag-input-default textarea + div { | |||
padding-top: 0.75rem; | |||
padding-bottom: 1rem; | |||
} | |||
.tag-input-large.tag-input-default textarea + div { | |||
padding-top: 1rem; | |||
padding-bottom: 1.25rem; | |||
} | |||
.tag-input-small.tag-input-alternate textarea + div { | |||
padding-top: 1.375rem; | |||
} | |||
.tag-input-medium.tag-input-alternate textarea + div { | |||
padding-top: 1.5rem; | |||
} | |||
.tag-input-large.tag-input-alternate textarea + div { | |||
padding-top: 1.75rem; | |||
} | |||
.tag-input-small textarea + div { | |||
gap: 0.25rem; | |||
min-height: 2.5rem; | |||
} | |||
.tag-input-small.tag-input-indicator textarea + div { | |||
padding-right: 2.5rem; | |||
} | |||
.tag-input-medium input + div { | |||
.tag-input-medium textarea + div { | |||
gap: 0.375rem; | |||
min-height: 3rem; | |||
} | |||
.tag-input-large input + div { | |||
.tag-input-medium.tag-input-indicator textarea + div { | |||
padding-right: 3rem; | |||
} | |||
.tag-input-large textarea + div { | |||
gap: 0.375rem; | |||
min-height: 4rem; | |||
} | |||
.tag-input input + div > span { | |||
padding: 0.25rem; | |||
.tag-input-large.tag-input-indicator textarea + div { | |||
padding-right: 4rem; | |||
} | |||
.tag-input textarea + div > span { | |||
padding: 0.125rem; | |||
border-radius: 0.25rem; | |||
line-height: 1; | |||
background-color: rgb(var(--color-positive) / 25%); | |||
} | |||
.tag-input input + div > input { | |||
.tag-input textarea + div > span span { | |||
pointer-events: none; | |||
} | |||
.tag-input textarea + div > span button { | |||
color: rgb(var(--color-primary)); | |||
padding: 0; | |||
width: 1rem; | |||
margin-left: 0.25rem; | |||
} | |||
.tag-input textarea + div > span button:hover { | |||
color: rgb(var(--color-primary)); | |||
} |
@@ -1460,14 +1460,11 @@ const OptionPage: NextPage<Props> = ({ | |||
<div> | |||
<Option.TagInput | |||
size="small" | |||
label="change" | |||
label="MultilineTextInput" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
enhanced | |||
onChange={(e) => { | |||
console.log(e.currentTarget, e.currentTarget.value, typeof e.currentTarget.value); | |||
}} | |||
/> | |||
</div> | |||
<div> | |||
@@ -1514,11 +1511,18 @@ const OptionPage: NextPage<Props> = ({ | |||
<Option.TagInput | |||
border | |||
size="large" | |||
label="MultilineTextInput" | |||
label="change" | |||
hint="Type anything here…" | |||
indicator="A" | |||
block | |||
enhanced | |||
separator={['\n', ',']} | |||
onBlur={(e) => { | |||
console.log(e.currentTarget); | |||
}} | |||
onChange={(e) => { | |||
console.log(e.currentTarget.value); | |||
}} | |||
/> | |||
</div> | |||
<div> | |||
@@ -97,13 +97,13 @@ | |||
} | |||
:root .rti--container { | |||
--rti-bg: rgb(var(--color-negative)); | |||
--rti-border: #ccc; | |||
--rti-bg: transparent; | |||
--rti-border: transparent; | |||
--rti-main: transparent; | |||
--rti-radius: 0; | |||
--rti-s: 0.5rem; | |||
--rti-tag: rgb(var(--color-positive) / 25%); | |||
--rti-tag-remove: #e53e3e; | |||
--rti-tag: transparent; | |||
--rti-tag-remove: transparent; | |||
--rti-tag-padding: 0 0; | |||
} | |||
@@ -1,4 +1,4 @@ | |||
export const delegateTriggerChangeEvent = <T extends HTMLElement>(target: T, value?: unknown) => { | |||
export const delegateTriggerEvent = <T extends HTMLElement>(eventName: string, target: T, value?: unknown) => { | |||
if (typeof window === 'undefined') { | |||
return; | |||
} | |||
@@ -15,15 +15,29 @@ export const delegateTriggerChangeEvent = <T extends HTMLElement>(target: T, val | |||
return; | |||
} | |||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(elementCtor.prototype, 'value')?.set; | |||
if (nativeInputValueSetter) { | |||
if ( | |||
(target.tagName === 'INPUT' && (target as unknown as HTMLInputElement).type !== 'file') | |||
|| target.tagName !== 'INPUT' | |||
) { | |||
nativeInputValueSetter.call(target, value); | |||
if (eventName === 'change') { | |||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(elementCtor.prototype, 'value')?.set; | |||
if (nativeInputValueSetter) { | |||
if ( | |||
(target.tagName === 'INPUT' && (target as unknown as HTMLInputElement).type !== 'file') | |||
|| target.tagName !== 'INPUT' | |||
) { | |||
nativeInputValueSetter.call(target, value); | |||
} | |||
const simulatedEvent = new Event(eventName, {bubbles: true}); | |||
target.dispatchEvent(simulatedEvent); | |||
} | |||
const simulatedEvent = new Event('change', { bubbles: true }); | |||
target.dispatchEvent(simulatedEvent); | |||
return; | |||
} | |||
if (eventName === 'blur') { | |||
target.focus({ | |||
preventScroll: true, | |||
}); | |||
setTimeout(() => { | |||
target.blur(); | |||
}); | |||
} | |||
const simulatedEvent = new Event(eventName, {bubbles: true}); | |||
target.dispatchEvent(simulatedEvent); | |||
} |