Skip to content

Commit

Permalink
Merge pull request #33 from JaleelB/feat/29-inline-tags-input
Browse files Browse the repository at this point in the history
Feat/29-inline-tags-input
  • Loading branch information
JaleelB authored May 24, 2024
2 parents a0ae887 + 2a0bdcf commit d1f76e1
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 110 deletions.
4 changes: 2 additions & 2 deletions packages/emblor/src/tag/autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export const Autocomplete: React.FC<AutocompleteProps> = ({
return (
<Command className="border min-w-[400px]">
{children}
<CommandList>
<CommandList className="border-t">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
{autocompleteOptions.map((option) => (
<CommandItem key={option.id}>
<CommandItem key={option.id} className="cursor-pointer">
<div
className="w-full"
onClick={() => {
Expand Down
194 changes: 146 additions & 48 deletions packages/emblor/src/tag/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TagPopover } from './tag-popover';
import { TagList } from './tag-list';
import { tagVariants } from './tag';
import { Autocomplete } from './autocomplete';
import { cn } from '../utils';

export enum Delimiter {
Comma = ',',
Expand Down Expand Up @@ -59,6 +60,7 @@ export interface TagInputProps extends OmittedInputProps, VariantProps<typeof ta
onClearAll?: () => void;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
restrictTagsToAutocompleteOptions?: boolean;
includeTagsInInput?: boolean;
}

const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
Expand Down Expand Up @@ -105,12 +107,12 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref)
usePopoverForTags = false,
inputProps = {},
restrictTagsToAutocompleteOptions,
includeTagsInInput = false,
} = props;

const [inputValue, setInputValue] = React.useState('');
const [tagCount, setTagCount] = React.useState(Math.max(0, tags.length));
const inputRef = React.useRef<HTMLInputElement>(null);
const [error, setError] = React.useState<string | null>(null);

if ((maxTags !== undefined && maxTags < 0) || (props.minTags !== undefined && props.minTags < 0)) {
console.warn('maxTags and minTags cannot be less than 0');
Expand Down Expand Up @@ -202,31 +204,77 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref)

return (
<div
className={`w-full flex gap-3 ${
className={`w-full flex ${!includeTagsInInput ? 'gap-3' : ''} ${
inputFieldPosition === 'bottom' ? 'flex-col' : inputFieldPosition === 'top' ? 'flex-col-reverse' : 'flex-row'
}`}
>
{!usePopoverForTags ? (
<TagList
tags={truncatedTags}
customTagRenderer={customTagRenderer}
variant={variant}
size={size}
shape={shape}
borderStyle={borderStyle}
textCase={textCase}
interaction={interaction}
animation={animation}
textStyle={textStyle}
onTagClick={onTagClick}
draggable={draggable}
onSortEnd={onSortEnd}
onRemoveTag={removeTag}
direction={direction}
/>
{!usePopoverForTags && !enableAutocomplete ? (
!includeTagsInInput ? (
<TagList
tags={truncatedTags}
customTagRenderer={customTagRenderer}
variant={variant}
size={size}
shape={shape}
borderStyle={borderStyle}
textCase={textCase}
interaction={interaction}
animation={animation}
textStyle={textStyle}
onTagClick={onTagClick}
draggable={draggable}
onSortEnd={onSortEnd}
onRemoveTag={removeTag}
direction={direction}
/>
) : (
<div className="w-full">
<div
className={`flex flex-row flex-wrap items-center gap-2 h-fit w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`}
>
<TagList
tags={truncatedTags}
customTagRenderer={customTagRenderer}
variant={variant}
size={size}
shape={shape}
borderStyle={borderStyle}
textCase={textCase}
interaction={interaction}
animation={animation}
textStyle={textStyle}
onTagClick={onTagClick}
draggable={draggable}
onSortEnd={onSortEnd}
onRemoveTag={removeTag}
direction={direction}
includeTagsInInput={includeTagsInInput}
/>
<Input
ref={inputRef}
id={id}
type="text"
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
{...inputProps}
className={cn(
className,
'border-0 h-5 bg-transparent sm:min-w-focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0',
)}
autoComplete={enableAutocomplete ? 'on' : 'off'}
list={enableAutocomplete ? 'autocomplete-options' : undefined}
disabled={maxTags !== undefined && tags.length >= maxTags}
/>
</div>
</div>
)
) : null}
{enableAutocomplete ? (
<div className="w-full max-w-[450px]">
<div className="w-full">
<Autocomplete
tags={tags}
setTags={setTags}
Expand All @@ -236,17 +284,65 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref)
allowDuplicates={allowDuplicates ?? false}
>
{!usePopoverForTags ? (
<CommandInput
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
ref={inputRef}
value={inputValue}
disabled={maxTags !== undefined && tags.length >= maxTags}
onChangeCapture={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
className="w-full"
/>
// <CommandInput
// placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
// ref={inputRef}
// value={inputValue}
// disabled={maxTags !== undefined && tags.length >= maxTags}
// onChangeCapture={handleInputChange}
// onKeyDown={handleKeyDown}
// onFocus={onFocus}
// onBlur={onBlur}
// className="w-full"
// />
!includeTagsInInput ? (
<CommandInput
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
ref={inputRef}
value={inputValue}
disabled={maxTags !== undefined && tags.length >= maxTags}
onChangeCapture={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
className="w-full"
/>
) : (
<div
className={`flex flex-row flex-wrap items-center p-2 gap-2 h-fit w-full bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`}
>
<TagList
tags={truncatedTags}
customTagRenderer={customTagRenderer}
variant={variant}
size={size}
shape={shape}
borderStyle={borderStyle}
textCase={textCase}
interaction={interaction}
animation={animation}
textStyle={textStyle}
onTagClick={onTagClick}
draggable={draggable}
onSortEnd={onSortEnd}
onRemoveTag={removeTag}
direction={direction}
includeTagsInInput={includeTagsInInput}
/>
<CommandInput
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
ref={inputRef}
value={inputValue}
disabled={maxTags !== undefined && tags.length >= maxTags}
onChangeCapture={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
includeTagsInInput={includeTagsInInput}
className="border-0 min-w-[130px] h-5"
/>
</div>
)
) : (
<TagPopover
tags={truncatedTags}
Expand Down Expand Up @@ -283,22 +379,24 @@ const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref)
) : (
<div className="w-full">
{!usePopoverForTags ? (
<Input
ref={inputRef}
id={id}
type="text"
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
{...inputProps}
className={className}
autoComplete={enableAutocomplete ? 'on' : 'off'}
list={enableAutocomplete ? 'autocomplete-options' : undefined}
disabled={maxTags !== undefined && tags.length >= maxTags}
/>
!includeTagsInInput ? (
<Input
ref={inputRef}
id={id}
type="text"
placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={onFocus}
onBlur={onBlur}
{...inputProps}
className={className}
autoComplete={enableAutocomplete ? 'on' : 'off'}
list={enableAutocomplete ? 'autocomplete-options' : undefined}
disabled={maxTags !== undefined && tags.length >= maxTags}
/>
) : null
) : (
<TagPopover
tags={truncatedTags}
Expand Down
94 changes: 64 additions & 30 deletions packages/emblor/src/tag/tag-list.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import { type Tag as TagType } from './tag-input';
import { Tag, TagProps } from './tag';

import SortableList, { SortableItem } from 'react-easy-sort';
import { cn } from '../utils';

Expand All @@ -10,6 +9,8 @@ export type TagListProps = {
customTagRenderer?: (tag: TagType) => React.ReactNode;
direction?: TagProps['direction'];
onSortEnd: (oldIndex: number, newIndex: number) => void;
className?: string;
includeTagsInInput?: boolean;
} & Omit<TagProps, 'tagObj'>;

const DropTarget: React.FC = () => {
Expand All @@ -22,6 +23,8 @@ export const TagList: React.FC<TagListProps> = ({
direction,
draggable,
onSortEnd,
className,
includeTagsInInput,
...tagListProps
}) => {
const [draggedTagId, setDraggedTagId] = React.useState<string | null>(null);
Expand All @@ -35,36 +38,67 @@ export const TagList: React.FC<TagListProps> = ({
};

return (
<div
className={cn('rounded-md max-w-[450px]', {
'flex flex-wrap gap-2': direction === 'row',
'flex flex-col gap-2': direction === 'column',
})}
>
{draggable ? (
<SortableList onSortEnd={onSortEnd} className="flex flex-wrap gap-2 list" dropTarget={<DropTarget />}>
{tags.map((tagObj) => (
<SortableItem key={tagObj.id}>
<div
onMouseDown={() => handleMouseDown(tagObj.id)}
onMouseLeave={handleMouseUp}
className={cn(
{
'border border-solid border-primary rounded-md': draggedTagId === tagObj.id,
},
'transition-all duration-200 ease-in-out',
)}
>
{customTagRenderer ? customTagRenderer(tagObj) : <Tag tagObj={tagObj} {...tagListProps} />}
</div>
</SortableItem>
))}
</SortableList>
<>
{!includeTagsInInput ? (
<div
className={cn('rounded-md w-full', className, {
'flex flex-wrap gap-2': direction === 'row',
'flex flex-col gap-2': direction === 'column',
})}
>
{draggable ? (
<SortableList onSortEnd={onSortEnd} className="flex flex-wrap gap-2 list" dropTarget={<DropTarget />}>
{tags.map((tagObj) => (
<SortableItem key={tagObj.id}>
<div
onMouseDown={() => handleMouseDown(tagObj.id)}
onMouseLeave={handleMouseUp}
className={cn(
{
'border border-solid border-primary rounded-md': draggedTagId === tagObj.id,
},
'transition-all duration-200 ease-in-out',
)}
>
{customTagRenderer ? customTagRenderer(tagObj) : <Tag tagObj={tagObj} {...tagListProps} />}
</div>
</SortableItem>
))}
</SortableList>
) : (
tags.map((tagObj) =>
customTagRenderer ? customTagRenderer(tagObj) : <Tag key={tagObj.id} tagObj={tagObj} {...tagListProps} />,
)
)}
</div>
) : (
tags.map((tagObj) =>
customTagRenderer ? customTagRenderer(tagObj) : <Tag key={tagObj.id} tagObj={tagObj} {...tagListProps} />,
)
<>
{draggable ? (
<SortableList onSortEnd={onSortEnd} className="flex flex-wrap gap-2 list" dropTarget={<DropTarget />}>
{tags.map((tagObj) => (
<SortableItem key={tagObj.id}>
<div
onMouseDown={() => handleMouseDown(tagObj.id)}
onMouseLeave={handleMouseUp}
className={cn(
{
'border border-solid border-primary rounded-md': draggedTagId === tagObj.id,
},
'transition-all duration-200 ease-in-out',
)}
>
{customTagRenderer ? customTagRenderer(tagObj) : <Tag tagObj={tagObj} {...tagListProps} />}
</div>
</SortableItem>
))}
</SortableList>
) : (
tags.map((tagObj) =>
customTagRenderer ? customTagRenderer(tagObj) : <Tag key={tagObj.id} tagObj={tagObj} {...tagListProps} />,
)
)}
</>
)}
</div>
</>
);
};
Loading

0 comments on commit d1f76e1

Please sign in to comment.