ソースを参照

Update tagsinput implementation

Fix styles and behavior.
pull/1/head
コミット
4b8d2afbd1
6個のファイルの変更192行の追加82行の削除
  1. +3
    -3
      packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx
  2. +79
    -47
      packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx
  3. +73
    -13
      packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/style.module.css
  4. +9
    -5
      packages/web-kitchensink-reactnext/src/pages/categories/option/index.tsx
  5. +4
    -4
      packages/web-kitchensink-reactnext/src/styles/globals.css
  6. +24
    -10
      packages/web-kitchensink-reactnext/src/utils/event.ts

+ 3
- 3
packages/web-kitchensink-reactnext/src/categories/blob/react/components/FileSelectBox/index.tsx ファイルの表示

@@ -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);
});
}



+ 79
- 47
packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/index.tsx ファイルの表示

@@ -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)}
/>
)}
{


+ 73
- 13
packages/web-kitchensink-reactnext/src/categories/option/react/components/TagInput/style.module.css ファイルの表示

@@ -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));
}

+ 9
- 5
packages/web-kitchensink-reactnext/src/pages/categories/option/index.tsx ファイルの表示

@@ -1460,14 +1460,11 @@ const OptionPage: NextPage<Props> = ({
<div>
<Option.TagInput
size="small"
label="change"
label="MultilineTextInput"
hint="Type anything here&hellip;"
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&hellip;"
indicator="A"
block
enhanced
separator={['\n', ',']}
onBlur={(e) => {
console.log(e.currentTarget);
}}
onChange={(e) => {
console.log(e.currentTarget.value);
}}
/>
</div>
<div>


+ 4
- 4
packages/web-kitchensink-reactnext/src/styles/globals.css ファイルの表示

@@ -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;
}



+ 24
- 10
packages/web-kitchensink-reactnext/src/utils/event.ts ファイルの表示

@@ -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);
}

読み込み中…
キャンセル
保存