Browse Source

Fix tagsInput event handling

Properly handle events for focus, blur, and change.
master
TheoryOfNekomata 1 year ago
parent
commit
ad8e62c499
5 changed files with 113 additions and 21 deletions
  1. +13
    -0
      categories/multichoice/react/src/components/TagInput/TagInput.css
  2. +78
    -3
      categories/multichoice/react/src/components/TagInput/index.tsx
  3. +5
    -12
      packages/react-utils/src/event.ts
  4. +16
    -5
      showcases/web-kitchensink-reactnext/src/pages/categories/option/index.tsx
  5. +1
    -1
      showcases/web-kitchensink-reactnext/src/pages/categories/presentation/index.tsx

+ 13
- 0
categories/multichoice/react/src/components/TagInput/TagInput.css View File

@@ -80,6 +80,10 @@
background-color: rgb(var(--color-positive) / 25%); background-color: rgb(var(--color-positive) / 25%);
} }


.tesseract-design-tag-input textarea + div > span:focus-within {
background-color: rgb(var(--color-secondary) / 25%);
}

.tesseract-design-tag-input textarea + div > span span { .tesseract-design-tag-input textarea + div > span span {
pointer-events: none; pointer-events: none;
} }
@@ -91,6 +95,15 @@
margin-left: 0.25rem; margin-left: 0.25rem;
} }


.tesseract-design-tag-input textarea + div > span button:focus {
outline: none;
color: rgb(var(--color-secondary));
}

.tesseract-design-tag-input textarea + div > span button:focus:-moz-focusring {
display: none;
}

.tesseract-design-tag-input textarea + div > span button:hover { .tesseract-design-tag-input textarea + div > span button:hover {
color: rgb(var(--color-primary)); color: rgb(var(--color-primary));
} }

+ 78
- 3
categories/multichoice/react/src/components/TagInput/index.tsx View File

@@ -55,6 +55,7 @@ export interface TagInputProps extends Omit<React.HTMLProps<TagInputDerivedEleme
* Separators for splitting the input value into multiple tags. * Separators for splitting the input value into multiple tags.
*/ */
separator?: TagInputSeparator[], separator?: TagInputSeparator[],
editOnRemove?: boolean,
} }


/** /**
@@ -78,6 +79,10 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
defaultValue, defaultValue,
disabled, disabled,
id: idProp, id: idProp,
onFocus,
onBlur,
editOnRemove = false,
placeholder,
...etcProps ...etcProps
}, },
forwardedRef, forwardedRef,
@@ -119,7 +124,19 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
}); });
}; };


const handleBlur = () => {
const handleFocus: React.FocusEventHandler<HTMLTextAreaElement> = (e) => {
if (!clientSide) {
onFocus?.(e);
}
};

const handleBlur: React.FocusEventHandler<HTMLTextAreaElement> = (e) => {
if (!clientSide) {
onBlur?.(e);
}
};

const handleRemoveTag = () => {
if (!(typeof ref === 'object' && ref)) { if (!(typeof ref === 'object' && ref)) {
return; return;
} }
@@ -128,10 +145,59 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
return; return;
} }
setTimeout(() => { setTimeout(() => {
delegateTriggerEvent('blur', input);
const sibling = input.nextElementSibling as HTMLDivElement;
const tagsInput = sibling.children[sibling.children.length - 1] as HTMLInputElement;
tagsInput.focus();
});
};

const handleInputBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
onBlur?.({
...e,
target: input,
currentTarget: input,
}); });
}; };


const handleFocusCapture: React.FocusEventHandler<HTMLDivElement> = (e) => {
const { currentTarget } = e;
if (!clientSide) {
return;
}
const { activeElement } = window.document;
if (!activeElement) {
return;
}
const tagInputWrapper = currentTarget.children[1] as HTMLDivElement;
const tagInput = (
tagInputWrapper.children[tagInputWrapper.children.length - 1] as HTMLInputElement
);
if (activeElement !== tagInput) {
return;
}
if (!(typeof ref === 'object' && ref)) {
return;
}
const { current: input } = ref;
if (!input) {
return;
}
if (activeElement.tagName === 'INPUT') {
onFocus?.({
...e,
target: input,
currentTarget: input,
});
}
};

return ( return (
<div <div
className={clsx( className={clsx(
@@ -155,18 +221,23 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
variant === 'alternate' && 'tag-input-alternate', variant === 'alternate' && 'tag-input-alternate',
className, className,
)} )}
onFocusCapture={handleFocusCapture}
> >
<textarea <textarea
{...etcProps} {...etcProps}
placeholder={placeholder}
disabled={disabled} disabled={disabled}
ref={ref} ref={ref}
id={id} id={id}
aria-labelledby={labelId} aria-labelledby={labelId}
data-testid="input" data-testid="input"
defaultValue={defaultValue} defaultValue={defaultValue}
onFocus={handleFocus}
onBlur={handleBlur}
style={{ style={{
height: clientSide ? undefined : 0, height: clientSide ? undefined : 0,
}} }}
tabIndex={clientSide ? -1 : undefined}
className={clsx( className={clsx(
'bg-negative rounded-inherit peer block', 'bg-negative rounded-inherit peer block',
'focus:outline-0', 'focus:outline-0',
@@ -219,9 +290,12 @@ export const TagInput = React.forwardRef<TagInputDerivedElement, TagInputProps>(
input: 'peer bg-transparent', input: 'peer bg-transparent',
tag: 'text-xs p-2 select-none', tag: 'text-xs p-2 select-none',
}} }}
isEditOnRemove={editOnRemove}
placeHolder={placeholder}
disabled={disabled} disabled={disabled}
onBlur={handleInputBlur}
onChange={handleTagsInputChange} onChange={handleTagsInputChange}
onBlur={handleBlur}
onRemoved={handleRemoveTag}
separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])} separators={separator.map((s) => TAG_INPUT_SEPARATOR_MAP[s])}
/> />
)} )}
@@ -322,4 +396,5 @@ TagInput.defaultProps = {
block: false, block: false,
hiddenLabel: false, hiddenLabel: false,
enhanced: false, enhanced: false,
editOnRemove: false,
}; };

+ 5
- 12
packages/react-utils/src/event.ts View File

@@ -1,4 +1,8 @@
export const delegateTriggerEvent = <T extends HTMLElement>(eventName: string, 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;
} }
@@ -29,15 +33,4 @@ export const delegateTriggerEvent = <T extends HTMLElement>(eventName: string, t
} }
return; return;
} }

if (eventName === 'blur') {
target.focus({
preventScroll: true,
});
setTimeout(() => {
target.blur();
});
}
const simulatedEvent = new Event(eventName, {bubbles: true});
target.dispatchEvent(simulatedEvent);
}; };

+ 16
- 5
showcases/web-kitchensink-reactnext/src/pages/categories/option/index.tsx View File

@@ -1493,11 +1493,19 @@ const OptionPage: NextPage<Props> = ({
<div> <div>
<MultiChoice.TagInput <MultiChoice.TagInput
size="large" size="large"
label="MultilineTextInput"
label="not enhanced"
hint="Type anything here&hellip;" hint="Type anything here&hellip;"
indicator="A" indicator="A"
block block
enhanced
onFocus={(e) => {
console.log('focus', e.currentTarget);
}}
onBlur={(e) => {
console.log('blur', e.currentTarget);
}}
onChange={(e) => {
console.log('change', e.currentTarget.value);
}}
/> />
</div> </div>
<div> <div>
@@ -1509,12 +1517,15 @@ const OptionPage: NextPage<Props> = ({
indicator="A" indicator="A"
block block
enhanced enhanced
separator={['\n', ',']}
separator={['newline', 'comma']}
onFocus={(e) => {
console.log('focus', e.currentTarget);
}}
onBlur={(e) => { onBlur={(e) => {
console.log(e.currentTarget);
console.log('blur', e.currentTarget);
}} }}
onChange={(e) => { onChange={(e) => {
console.log(e.currentTarget.value);
console.log('change', e.currentTarget.value);
}} }}
/> />
</div> </div>


+ 1
- 1
showcases/web-kitchensink-reactnext/src/pages/categories/presentation/index.tsx View File

@@ -1,5 +1,5 @@
import { NextPage } from 'next'; import { NextPage } from 'next';
import { DefaultLayout } from 'src/components/DefaultLayout';
import { DefaultLayout } from '@/components/DefaultLayout';


const PresentationPage: NextPage = () => { const PresentationPage: NextPage = () => {
return ( return (


Loading…
Cancel
Save