@@ -7,7 +7,7 @@ import {ImageFilePreview} from '../ImageFilePreview'; | |||||
import {VideoFilePreview} from '../VideoFilePreview'; | import {VideoFilePreview} from '../VideoFilePreview'; | ||||
import {TextFilePreview} from '../TextFilePreview'; | import {TextFilePreview} from '../TextFilePreview'; | ||||
import {AudioMiniFilePreview} from '../AudioMiniFilePreview'; | import {AudioMiniFilePreview} from '../AudioMiniFilePreview'; | ||||
import {delegateTriggerChangeEvent} from '@/utils/event'; | |||||
import {delegateTriggerEvent} from '@/utils/event'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import {useEnhanced} from '@modal-soft/react-utils'; | import {useEnhanced} from '@modal-soft/react-utils'; | ||||
import {FilePreviewComponent} from '@/categories/blob/react/common'; | import {FilePreviewComponent} from '@/categories/blob/react/common'; | ||||
@@ -95,7 +95,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS | |||||
setFileList(undefined); | setFileList(undefined); | ||||
setLastUpdated(Date.now()); | setLastUpdated(Date.now()); | ||||
setTimeout(() => { | setTimeout(() => { | ||||
delegateTriggerChangeEvent(current); | |||||
delegateTriggerEvent('change', current); | |||||
}); | }); | ||||
}; | }; | ||||
@@ -121,7 +121,7 @@ export const FileSelectBox = React.forwardRef<FileSelectBoxDerivedElement, FileS | |||||
setFileList(current.files = files); | setFileList(current.files = files); | ||||
setLastUpdated(Date.now()); | setLastUpdated(Date.now()); | ||||
setTimeout(() => { | setTimeout(() => { | ||||
delegateTriggerChangeEvent(current); | |||||
delegateTriggerEvent('change', current); | |||||
}); | }); | ||||
} | } | ||||
@@ -4,9 +4,11 @@ import * as TextControlBase from '@tesseract-design/web-base-textcontrol'; | |||||
import clsx from 'clsx'; | import clsx from 'clsx'; | ||||
import {useEnhanced} from '@/packages/react-utils'; | import {useEnhanced} from '@/packages/react-utils'; | ||||
import styles from './style.module.css'; | 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'> { | 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, | hiddenLabel?: boolean, | ||||
enhanced?: boolean, | enhanced?: boolean, | ||||
separator?: string, | |||||
separator?: TagInputSeparator[], | |||||
} | } | ||||
/** | /** | ||||
@@ -68,9 +70,9 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
hiddenLabel = false, | hiddenLabel = false, | ||||
className, | className, | ||||
enhanced: enhancedProp = false, | enhanced: enhancedProp = false, | ||||
separator = ',', | |||||
onChange, | |||||
separator = ['\n'], | |||||
defaultValue, | defaultValue, | ||||
disabled, | |||||
...etcProps | ...etcProps | ||||
}: TagInputProps, | }: TagInputProps, | ||||
forwardedRef, | forwardedRef, | ||||
@@ -84,7 +86,9 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
return []; | return []; | ||||
} | } | ||||
if (typeof defaultValue === 'string') { | if (typeof defaultValue === 'string') { | ||||
return defaultValue.split(separator); | |||||
return separator.reduce((theDefaultValues, separator) => { | |||||
return theDefaultValues.join(separator).split(separator); | |||||
}, [defaultValue]); | |||||
} | } | ||||
if (typeof defaultValue === 'number') { | if (typeof defaultValue === 'number') { | ||||
return [defaultValue.toString()]; | return [defaultValue.toString()]; | ||||
@@ -100,75 +104,100 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
if (!input) { | if (!input) { | ||||
return; | return; | ||||
} | } | ||||
input.value = tags.join(separator); | |||||
setTimeout(() => { | 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 ( | return ( | ||||
<div | <div | ||||
className={clsx( | 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', | 'relative rounded ring-secondary/50 group', | ||||
'focus-within:ring-4', | 'focus-within:ring-4', | ||||
{ | { | ||||
'block': block, | 'block': block, | ||||
'inline-block align-middle': !block, | 'inline-block align-middle': !block, | ||||
}, | }, | ||||
{ | |||||
enhanced && { | |||||
'min-h-10': size === 'small', | 'min-h-10': size === 'small', | ||||
'min-h-12': size === 'medium', | 'min-h-12': size === 'medium', | ||||
'min-h-16': size === 'large', | '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, | className, | ||||
)} | )} | ||||
> | > | ||||
<input | |||||
<textarea | |||||
{...etcProps} | {...etcProps} | ||||
disabled={disabled} | |||||
ref={ref} | ref={ref} | ||||
readOnly={enhanced} | |||||
aria-labelledby={labelId} | aria-labelledby={labelId} | ||||
type={type} | |||||
data-testid="input" | data-testid="input" | ||||
onChange={onChange} | |||||
defaultValue={defaultValue} | |||||
style={{ | |||||
height: enhanced ? undefined : 0, | |||||
}} | |||||
className={clsx( | 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 && { | !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 && 'peer', | ||||
!enhanced && 'w-full', | |||||
enhanced && 'sr-only', | enhanced && 'sr-only', | ||||
)} | )} | ||||
/> | /> | ||||
@@ -177,9 +206,12 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>( | |||||
value={tags} | value={tags} | ||||
classNames={{ | classNames={{ | ||||
input: 'peer bg-transparent', | input: 'peer bg-transparent', | ||||
tag: 'text-xs p-1', | |||||
tag: 'text-xs p-2', | |||||
}} | }} | ||||
disabled={disabled} | |||||
onChange={handleTagsInputChange} | 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; | 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; | 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; | gap: 0.375rem; | ||||
min-height: 3rem; | 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; | gap: 0.375rem; | ||||
min-height: 4rem; | 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; | border-radius: 0.25rem; | ||||
line-height: 1; | 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> | <div> | ||||
<Option.TagInput | <Option.TagInput | ||||
size="small" | size="small" | ||||
label="change" | |||||
label="MultilineTextInput" | |||||
hint="Type anything here…" | hint="Type anything here…" | ||||
indicator="A" | indicator="A" | ||||
block | block | ||||
enhanced | enhanced | ||||
onChange={(e) => { | |||||
console.log(e.currentTarget, e.currentTarget.value, typeof e.currentTarget.value); | |||||
}} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
@@ -1514,11 +1511,18 @@ const OptionPage: NextPage<Props> = ({ | |||||
<Option.TagInput | <Option.TagInput | ||||
border | border | ||||
size="large" | size="large" | ||||
label="MultilineTextInput" | |||||
label="change" | |||||
hint="Type anything here…" | hint="Type anything here…" | ||||
indicator="A" | indicator="A" | ||||
block | block | ||||
enhanced | enhanced | ||||
separator={['\n', ',']} | |||||
onBlur={(e) => { | |||||
console.log(e.currentTarget); | |||||
}} | |||||
onChange={(e) => { | |||||
console.log(e.currentTarget.value); | |||||
}} | |||||
/> | /> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
@@ -97,13 +97,13 @@ | |||||
} | } | ||||
:root .rti--container { | :root .rti--container { | ||||
--rti-bg: rgb(var(--color-negative)); | |||||
--rti-border: #ccc; | |||||
--rti-bg: transparent; | |||||
--rti-border: transparent; | |||||
--rti-main: transparent; | --rti-main: transparent; | ||||
--rti-radius: 0; | --rti-radius: 0; | ||||
--rti-s: 0.5rem; | --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; | --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') { | if (typeof window === 'undefined') { | ||||
return; | return; | ||||
} | } | ||||
@@ -15,15 +15,29 @@ export const delegateTriggerChangeEvent = <T extends HTMLElement>(target: T, val | |||||
return; | 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); | |||||
} | } |