Skip to content

Commit

Permalink
feat: create useTaggedInput hook and add it to textinput
Browse files Browse the repository at this point in the history
  • Loading branch information
saurabhdaware committed Apr 8, 2024
1 parent b3baecd commit dc07a07
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 82 deletions.
111 changes: 111 additions & 0 deletions packages/blade/src/components/Input/BaseInput/useTaggedInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import type { BaseInputProps } from './BaseInput';
import type { BladeElementRefWithValue } from '~utils/types';
import type { FormInputOnKeyDownEvent } from '~components/Form/FormTypes';
import { isReactNative } from '~utils';
import { getTagsGroup } from '~components/Tag/getTagsGroup';

type TaggedInputProps = {
isTaggedInput?: boolean;
tags?: string[];
onTagChange?: ({ tags }: { tags: string[] }) => void;
};

type UseTaggedInputProps = TaggedInputProps &
Pick<BaseInputProps, 'isDisabled' | 'onChange' | 'name' | 'value'> & {
inputRef: React.RefObject<BladeElementRefWithValue | null>;
};

const useTaggedInput = ({
tags,
isDisabled,
onTagChange,
isTaggedInput,
inputRef,
onChange,
name,
value,
}: UseTaggedInputProps): {
activeTagIndex: number;
setActiveTagIndex: (activeTagIndex: number) => void;
getTags: ({ size }: { size: NonNullable<BaseInputProps['size']> }) => React.ReactElement[];
handleTaggedInputKeydown: (e: FormInputOnKeyDownEvent) => void;
} => {
const [activeTagIndex, setActiveTagIndex] = React.useState(-1);

const getNewTagsArray = (indexToRemove: number): string[] => {
if (!tags) {
return [];
}

// Check if the index is valid
if (indexToRemove < 0 || indexToRemove >= tags.length) {
return tags; // Return the original array
}

// Create a new array without the element at the specified index
const newArray = tags.slice(0, indexToRemove).concat(tags.slice(indexToRemove + 1));

return newArray;
};

const getTags = React.useMemo(
() => ({ size }: { size: NonNullable<BaseInputProps['size']> }): React.ReactElement[] => {
return getTagsGroup({
size,
tags: tags ?? [],
activeTagIndex,
isDisabled,
onDismiss: ({ tagIndex }) => {
if (!isReactNative()) {
inputRef.current?.focus();
}
onTagChange?.({ tags: getNewTagsArray(tagIndex) });
},
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeTagIndex, tags, isDisabled],
);

const handleTaggedInputKeydown = (e: FormInputOnKeyDownEvent): void => {
if (!isTaggedInput || !inputRef.current) {
return;
}

const inputElement = inputRef.current;

const currentTags = tags ?? [];
const isControlledValue = Boolean(value);
const inputValue = isControlledValue ? value?.trim() : inputElement.value.trim();
if (e.key === 'Enter') {
e.event.preventDefault(); // we don't want textarea to treat enter as line break in tagged inputs
if (inputValue) {
onTagChange?.({ tags: [...currentTags, inputValue] });
if (isControlledValue) {
onChange?.({ name, value: '' });
} else {
inputElement.value = '';
}

setActiveTagIndex(-1);
}
}

console.log({ activeTagIndex });

if (e.key === 'Backspace' && !inputValue && activeTagIndex < 0) {
onTagChange?.({ tags: currentTags.slice(0, -1) });
}
};

return {
activeTagIndex,
setActiveTagIndex,
getTags,
handleTaggedInputKeydown,
};
};

export type { TaggedInputProps };
export { useTaggedInput };
95 changes: 17 additions & 78 deletions packages/blade/src/components/Input/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React from 'react';
import type { TextInput as TextInputReactNative } from 'react-native';
import type { BaseInputProps } from '../BaseInput';
import { BaseInput } from '../BaseInput';
import type { TaggedInputProps } from '../BaseInput/useTaggedInput';
import { useTaggedInput } from '../BaseInput/useTaggedInput';
import isEmpty from '~utils/lodashButBetter/isEmpty';
import { CloseIcon } from '~components/Icons';
import { IconButton } from '~components/Button/IconButton';
Expand All @@ -13,9 +15,8 @@ import { CharacterCounter } from '~components/Form/CharacterCounter';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { getPlatformType } from '~utils';
import { useMergeRefs } from '~utils/useMergeRefs';
import type { BladeElementRef } from '~utils/types';
import type { BladeElementRef, BladeElementRefWithValue } from '~utils/types';
import { hintMarginTop } from '~components/Form/formTokens';
import { getTagsGroup } from '~components/Tag/getTagsGroup';

type TextAreaCommonProps = Pick<
BaseInputProps,
Expand Down Expand Up @@ -51,11 +52,8 @@ type TextAreaCommonProps = Pick<
* Event handler to handle the onClick event for clear button. Used when `showClearButton` is `true`
*/
onClearButtonClick?: () => void;

isTaggedInput?: boolean;
tags?: string[];
onTagChange?: ({ tags }: { tags: string[] }) => void;
} & StyledPropsBlade;
} & TaggedInputProps &
StyledPropsBlade;

/*
Mandatory accessibilityLabel prop when label is not provided
Expand Down Expand Up @@ -129,8 +127,17 @@ const _TextArea: React.ForwardRefRenderFunction<BladeElementRef, TextAreaProps>
) => {
const inputRef = React.useRef<BladeElementRef>(null);
const mergedRef = useMergeRefs(ref, inputRef);
const [activeTagIndex, setActiveTagIndex] = React.useState(-1);
const [isInputFocussed, setIsInputFocussed] = React.useState(false);
const [isInputFocussed, setIsInputFocussed] = React.useState(autoFocus ?? false);
const { activeTagIndex, setActiveTagIndex, getTags, handleTaggedInputKeydown } = useTaggedInput({
tags,
onTagChange,
isDisabled,
inputRef: inputRef as React.RefObject<BladeElementRefWithValue>,
isTaggedInput,
name,
value,
onChange,
});

const [shouldShowClearButton, setShouldShowClearButton] = React.useState(false);

Expand Down Expand Up @@ -169,49 +176,6 @@ const _TextArea: React.ForwardRefRenderFunction<BladeElementRef, TextAreaProps>
return null;
};

const getNewTagsArray = (indexToRemove: number): string[] => {
if (!tags) {
return [];
}

// Check if the index is valid
if (indexToRemove < 0 || indexToRemove >= tags.length) {
return tags; // Return the original array
}

// Create a new array without the element at the specified index
const newArray = tags.slice(0, indexToRemove).concat(tags.slice(indexToRemove + 1));

return newArray;
};

const getTags = React.useMemo(
() => ({ size }: { size: NonNullable<BaseInputProps['size']> }) => {
return getTagsGroup({
size,
tags: tags ?? [],
activeTagIndex,
isDisabled,
onDismiss: ({ tagIndex }) => {
// if (isTagDismissedRef.current) {
// isTagDismissedRef.current.value = true;
// }

if (!isReactNative(0)) {
inputRef.current?.focus();
}

onTagChange?.({ tags: getNewTagsArray(tagIndex) });

// removeOption(selectedIndices[tagIndex]);
// setChangeCallbackTriggerer(Number(changeCallbackTriggerer) + 1);
},
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeTagIndex, tags, isDisabled],
);

return (
<BaseInput
as="textarea"
Expand Down Expand Up @@ -265,32 +229,7 @@ const _TextArea: React.ForwardRefRenderFunction<BladeElementRef, TextAreaProps>
onBlur?.(e);
}}
onKeyDown={(e) => {
if (!isTaggedInput) {
return;
}

const currentTags = tags ?? [];
const isControlledValue = Boolean(value);
const inputElement = inputRef.current as HTMLTextAreaElement;
const inputValue = isControlledValue ? value?.trim() : inputElement.value.trim();
if (e.key === 'Enter') {
e.event.preventDefault(); // we don't want textarea to treat enter as line break in tagged inputs

if (inputValue) {
onTagChange?.({ tags: [...currentTags, inputValue] });
if (isControlledValue) {
onChange?.({ name, value: '' });
} else {
inputElement.value = '';
}

setActiveTagIndex(-1);
}
}

if (e.key === 'Backspace' && !inputValue && activeTagIndex < 0) {
onTagChange?.({ tags: currentTags.slice(0, -1) });
}
handleTaggedInputKeydown(e);
}}
onSubmit={onSubmit}
trailingFooterSlot={(value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,20 @@ inputRef.parameters = {
},
},
};

export const TextInputWithTags: StoryFn<typeof TextInputComponent> = ({ ...args }) => {
const [tags, setTags] = React.useState<string[]>([]);
return (
<Box display="flex" flexDirection="column">
<TextInputComponent
{...args}
isTaggedInput={true}
tags={tags}
showClearButton={false}
onTagChange={({ tags }) => {
setTags(tags);
}}
/>
</Box>
);
};
40 changes: 36 additions & 4 deletions packages/blade/src/components/Input/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { TextInput as TextInputReactNative } from 'react-native';
import type { BaseInputProps } from '../BaseInput';
import { BaseInput } from '../BaseInput';
import { getKeyboardAndAutocompleteProps } from '../BaseInput/utils';
import type { TaggedInputProps } from '../BaseInput/useTaggedInput';
import { useTaggedInput } from '../BaseInput/useTaggedInput';
import isEmpty from '~utils/lodashButBetter/isEmpty';
import type { IconComponent } from '~components/Icons';
import { CloseIcon } from '~components/Icons';
Expand All @@ -16,7 +18,7 @@ import { Spinner } from '~components/Spinner';
import { assignWithoutSideEffects } from '~utils/assignWithoutSideEffects';
import { getPlatformType } from '~utils';
import { useMergeRefs } from '~utils/useMergeRefs';
import type { BladeElementRef } from '~utils/types';
import type { BladeElementRef, BladeElementRefWithValue } from '~utils/types';
import { hintMarginTop } from '~components/Form/formTokens';

// Users should use PasswordInput for input type password
Expand Down Expand Up @@ -90,7 +92,8 @@ type TextInputCommonProps = Pick<
* @default text
*/
type?: Type;
} & StyledPropsBlade;
} & TaggedInputProps &
StyledPropsBlade;

/*
Mandatory accessibilityLabel prop when label is not provided
Expand Down Expand Up @@ -166,13 +169,27 @@ const _TextInput: React.ForwardRefRenderFunction<BladeElementRef, TextInputProps
size = 'medium',
leadingIcon,
trailingIcon,
isTaggedInput,
tags,
onTagChange,
...styledProps
},
ref,
): ReactElement => {
const textInputRef = React.useRef<BladeElementRef>(null);
const mergedRef = useMergeRefs(ref, textInputRef);
const [shouldShowClearButton, setShouldShowClearButton] = useState(false);
const [isInputFocussed, setIsInputFocussed] = useState(autoFocus ?? false);
const { activeTagIndex, setActiveTagIndex, getTags, handleTaggedInputKeydown } = useTaggedInput({
isTaggedInput,
tags,
onTagChange,
isDisabled,
onChange,
name,
value,
inputRef: textInputRef as React.RefObject<BladeElementRefWithValue>,
});

React.useEffect(() => {
setShouldShowClearButton(Boolean(showClearButton && (defaultValue ?? value)));
Expand Down Expand Up @@ -227,6 +244,12 @@ const _TextInput: React.ForwardRefRenderFunction<BladeElementRef, TextInputProps
value={value}
name={name}
maxCharacters={maxCharacters}
isDropdownTrigger={isTaggedInput}
tags={isTaggedInput ? getTags({ size }) : undefined}
showAllTags={isInputFocussed}
maxTagRows="single"
activeTagIndex={activeTagIndex}
setActiveTagIndex={setActiveTagIndex}
onChange={({ name, value }) => {
if (showClearButton && value?.length) {
// show the clear button when the user starts typing in
Expand All @@ -241,8 +264,17 @@ const _TextInput: React.ForwardRefRenderFunction<BladeElementRef, TextInputProps
onChange?.({ name, value });
}}
onClick={onClick}
onFocus={onFocus}
onBlur={onBlur}
onFocus={(e) => {
setIsInputFocussed(true);
onFocus?.(e);
}}
onBlur={(e) => {
setIsInputFocussed(false);
onBlur?.(e);
}}
onKeyDown={(e) => {
handleTaggedInputKeydown(e);
}}
onSubmit={onSubmit}
isDisabled={isDisabled}
necessityIndicator={necessityIndicator}
Expand Down
3 changes: 3 additions & 0 deletions packages/blade/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type BladeElementRef = Platform.Select<{
native: View;
}>;

type BladeElementRefWithValue = BladeElementRef & { value: string };

type ContainerElementType = Platform.Select<{
web: HTMLDivElement;
native: View;
Expand All @@ -147,6 +149,7 @@ export type {
PickIfExist,
PickCSSByPlatform,
BladeElementRef,
BladeElementRefWithValue,
RemoveUndefinedFromUnion,
ContainerElementType,
};

0 comments on commit dc07a07

Please sign in to comment.