|
- import * as React from 'react';
- import * as TextControlBase from '@tesseract-design/web-base-textcontrol';
- import * as BadgeBase from '@tesseract-design/web-base-badge';
-
- export type TagInputProps = Omit<React.HTMLProps<HTMLInputElement>, 'size' | 'style'> & {
- /**
- * Short textual description indicating the nature of the component's value.
- */
- label?: React.ReactNode,
- /**
- * Short textual description as guidelines for valid input values.
- */
- hint?: React.ReactNode,
- /**
- * Size of the component.
- */
- size?: TextControlBase.TextControlSize,
- /**
- * Additional description, usually graphical, indicating the nature of the component's value.
- */
- indicator?: React.ReactNode,
- /**
- * Should the component display a border?
- */
- border?: boolean,
- /**
- * Should the component occupy the whole width of its parent?
- */
- block?: boolean,
- /**
- * Style of the component.
- */
- style?: TextControlBase.TextControlStyle,
- /**
- * Is the label hidden?
- */
- hiddenLabel?: boolean,
- enhanced?: boolean,
- separator?: string,
- }
-
- export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(((
- {
- label = '',
- hint = '',
- indicator = null,
- size = TextControlBase.TextControlSize.MEDIUM,
- border = false,
- block = false,
- style = TextControlBase.TextControlStyle.DEFAULT,
- hiddenLabel = false,
- onInput,
- enhanced = false,
- defaultValue = '',
- separator = ',',
- onFocus,
- onBlur,
- onSelect,
- className: _className,
- placeholder: _placeholder,
- as: _as,
- ...etcProps
- }: TagInputProps,
- forwardedRef,
- ) => {
- const [hydrated, setHydrated] = React.useState(false);
- const [focused, setFocused] = React.useState(false);
- const [selectionStart, setSelectionStart] = React.useState(0);
- const [selectionEnd, setSelectionEnd] = React.useState(0);
- const [viewValue, setViewValue] = React.useState<string[]>(() => {
- const theDefaultValue = !Array.isArray(defaultValue) ? [defaultValue.toString(), ''] : [...defaultValue, ''];
- return theDefaultValue.filter(v => v.length > 0);
- });
- const defaultRef = React.useRef<HTMLInputElement>(null);
- const effectiveRef = forwardedRef ?? defaultRef;
-
- const styleArgs = React.useMemo<TextControlBase.TextControlBaseArgs>(() => ({
- block,
- border,
- size,
- indicator: Boolean(indicator),
- style,
- resizable: true,
- predefinedValues: false,
- }), [block, border, size, indicator, style]);
-
- const renderEnhanced = React.useMemo(() => enhanced && hydrated, [enhanced, hydrated]);
-
- const handleInput: React.FormEventHandler<HTMLInputElement> = (e) => {
- const target = e.target as HTMLInputElement;
- setViewValue(target.value.split(separator))
-
- if (onInput) {
- onInput(e)
- }
- }
-
- const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
- setFocused(true);
- if (onFocus) {
- onFocus(e);
- }
- }
-
- const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
- setFocused(false);
- if (onBlur) {
- onBlur(e);
- }
- }
-
- const handleSelect: React.ReactEventHandler<HTMLInputElement> = (e) => {
- const target = e.target as HTMLInputElement;
- const newSelectionStart = target.selectionStart ?? 0;
- const newSelectionEnd = target.selectionEnd ?? 0;
- const newDirection = Math.sign(newSelectionStart - selectionStart) > 0 ? 'forward' : 'backward'
- setSelectionStart(newSelectionStart);
- setSelectionEnd(newSelectionEnd);
-
- console.log(newDirection);
-
- const lastSeparatorIndex = target.value.lastIndexOf(separator);
- const separatorStartRaw = newSelectionStart > lastSeparatorIndex ? newSelectionStart : target.value.slice(0, newSelectionEnd).lastIndexOf(separator) + separator?.length;
- const separatorEndRaw = newSelectionEnd > lastSeparatorIndex ? newSelectionEnd : target.value.slice(0, newSelectionEnd).length + target.value.slice(newSelectionEnd).indexOf(separator);
-
- let separatorStart = 0;
- let separatorEnd;
- if (lastSeparatorIndex > -1) {
- separatorStart = separatorStartRaw > -1 ? separatorStartRaw : 0;
- }
- separatorEnd = separatorEndRaw;
- if (newSelectionStart <= target.value.lastIndexOf(separator)) {
- if (newSelectionStart === newSelectionEnd && newSelectionStart === separatorStart && newDirection === 'backward') {
- target.selectionStart = newSelectionStart - separator?.length;
- target.selectionEnd = newSelectionStart - separator?.length;
- target.selectionDirection = newDirection;
- } else if (newSelectionStart === newSelectionEnd && newSelectionEnd === separatorEnd && newDirection === 'forward') {
- target.selectionStart = newSelectionEnd + separator?.length;
- target.selectionEnd = newSelectionEnd + separator?.length;
- target.selectionDirection = newDirection;
- } else {
- target.selectionStart = separatorStart;
- target.selectionEnd = separatorEnd;
- target.selectionDirection = 'backward';
- }
- }
- if (onSelect) {
- onSelect(e);
- }
- }
-
- const focusOnInput: React.MouseEventHandler<HTMLDivElement> = (e) => {
- e.preventDefault();
- if (typeof effectiveRef === 'function') {
- return;
- }
- if (effectiveRef !== null && effectiveRef.current) {
- effectiveRef.current.focus();
- }
- }
-
- const tags = React.useMemo(() => viewValue.slice(0, -1), [viewValue])
- const inputText = React.useMemo(() => viewValue.slice(-1)[0] ?? '', [viewValue])
-
- React.useEffect(() => {
- setHydrated(true)
- }, []);
-
- return (
- <div
- className={TextControlBase.Root(styleArgs)}
- >
- <input
- {...etcProps}
- className={TextControlBase.Input(styleArgs)}
- ref={effectiveRef}
- aria-label={label}
- style={{
- height: TextControlBase.MIN_HEIGHTS[size],
- // position: renderEnhanced ? 'absolute' : undefined,
- // left: renderEnhanced ? -999999 : undefined,
- }}
- data-testid="input"
- onInput={handleInput}
- onFocus={handleFocus}
- onBlur={handleBlur}
- onSelect={handleSelect}
- />
- <div
- className={TextControlBase.Input(styleArgs)}
- onClick={focusOnInput}
- style={{
- cursor: 'text',
- }}
- >
- <div
- style={{
- margin: '-0.125rem',
- }}
- >
- {tags.map(v => (
- <div
- style={{
- padding: '0.125rem',
- display: 'inline-block',
- }}
- key={v}
- >
- <button
- className={BadgeBase.Root({ rounded: false })}
- style={{
- border: 0,
- font: 'inherit',
- lineHeight: 0,
- paddingTop: 0,
- paddingBottom: 0,
- color: 'inherit',
- backgroundColor: 'transparent',
- }}
- >
- <div
- className={BadgeBase.Content()}
- >
- {v}
- {' '}
- ×
- </div>
- </button>
- </div>
- ))}
- {
- inputText.lastIndexOf(separator) < 0
- && (
- <>
- {
- inputText.slice(0, selectionStart - tags.join(separator).length - separator?.length)
- }
- <div
- style={{
- display: 'inline-block',
- verticalAlign: 'middle',
- backgroundColor: focused ? 'Highlight' : undefined,
- color: focused ? 'HighlightText' : undefined,
- height: '1.25em',
- minWidth: 1,
- }}
- >
- {
- inputText.slice(selectionStart - tags.join(separator).length - separator?.length, selectionEnd - tags.join(separator).length - separator?.length)
- }
- </div>
- {
- inputText.slice(selectionEnd - tags.join(separator).length - separator?.length)
- }
- </>
- )
- }
- {
- inputText.lastIndexOf(separator) >= 0
- && (
- <>
- {
- inputText.slice(0, selectionStart - tags.join(separator).length)
- }
- <div
- style={{
- display: 'inline-block',
- verticalAlign: 'middle',
- backgroundColor: focused ? 'Highlight' : undefined,
- color: focused ? 'HighlightText' : undefined,
- height: '1.25em',
- minWidth: 1,
- }}
- >
- {
- inputText.slice(selectionStart - tags.join(separator).length - 1, selectionEnd - tags.join(separator).length - 1)
- }
- </div>
- {
- inputText.slice(selectionEnd - tags.join(separator).length - 1)
- }
- </>
- )
- }
- </div>
- </div>
- {
- border && (
- <span
- data-testid="border"
- />
- )
- }
- {
- label && !hiddenLabel && (
- <div
- data-testid="label"
- className={TextControlBase.LabelWrapper(styleArgs)}
- >
- {label}
- </div>
- )
- }
- {hint && (
- <div
- className={TextControlBase.HintWrapper(styleArgs)}
- data-testid="hint"
- >
- <div
- className={TextControlBase.Hint()}
- >
- {hint}
- </div>
- </div>
- )}
- {indicator && (
- <div
- className={TextControlBase.IndicatorWrapper(styleArgs)}
- >
- {indicator}
- </div>
- )}
- </div>
- );
- }));
-
- TagInput.displayName = 'TagInput';
|