-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: TET-866 add tag input component (#140)
* feat: TET-866 add TagInput component * feat: TET-866 fix deafult story * feat: TET-866 resolve problem with props type * feat: TET-866 review changes * feat: TET-866 change import * feat: TET-866 review changes * feat: TET-866 add TODO
- Loading branch information
1 parent
4b00cf6
commit 2070c34
Showing
11 changed files
with
509 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { InputHTMLAttributes } from 'react'; | ||
|
||
import type { TagInputConfig } from './TagInput.styles'; | ||
import { HelperTextProps } from '../HelperText'; | ||
import { LabelProps } from '../Label'; | ||
|
||
import { BasicInputState } from '@/types'; | ||
|
||
export type TagInputProps = { | ||
custom?: TagInputConfig; | ||
state?: BasicInputState; | ||
isValidationError?: boolean; | ||
label?: Pick<LabelProps, 'label' | 'action' | 'optional'>; | ||
helperText?: Pick<HelperTextProps, 'text' | 'intent' | 'hasBeforeIcon'>; | ||
} & Omit< | ||
InputHTMLAttributes<HTMLInputElement>, | ||
'checked' | 'disabled' | 'color' | 'type' | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { TagInput } from './TagInput'; | ||
import { Tag } from '../Tag/Tag'; | ||
|
||
import { TagInputDocs } from '@/docs-components/TagInputDocs'; | ||
import { TetDocs } from '@/docs-components/TetDocs'; | ||
|
||
const meta = { | ||
title: 'Tag Input', | ||
component: TagInput, | ||
tags: ['autodocs'], | ||
args: { | ||
helperText: { text: 'Helper text' }, | ||
label: { label: 'label' }, | ||
isValidationError: false, | ||
children: ['Tag 1', 'Tag 2'].map((tagLabel) => ( | ||
<Tag label="elo" key={tagLabel} /> | ||
)), | ||
}, | ||
argTypes: { | ||
isValidationError: { | ||
options: [true, false], | ||
defaultValue: false, | ||
control: { type: 'radio' }, | ||
}, | ||
}, | ||
|
||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
'A text input field designed for entering multiple tags or keywords, allowing users to create, edit, or remove tags as needed. Tag inputs often provide suggestions or autocomplete functionality to enhance the user experience.', | ||
}, | ||
page: () => ( | ||
<TetDocs docs="https://docs.tetrisly.com/components/list/taginput"> | ||
<TagInputDocs /> | ||
</TetDocs> | ||
), | ||
}, | ||
}, | ||
} satisfies Meta<typeof TagInput>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
helperText: { text: 'Helper text' }, | ||
label: { label: 'label' }, | ||
isValidationError: false, | ||
children: ['Tag1', 'Tag2'].map((tagLabel) => ( | ||
<Tag label={tagLabel} key={tagLabel} onCloseClick={() => null} /> | ||
)), | ||
}, | ||
}; | ||
|
||
export const ValidationErrorNo: Story = { | ||
args: { | ||
helperText: { text: 'Helper text' }, | ||
label: { label: 'label' }, | ||
isValidationError: false, | ||
children: ['Tag1', 'Tag2'].map((tagLabel) => ( | ||
<Tag label={tagLabel} key={tagLabel} onCloseClick={() => null} /> | ||
)), | ||
}, | ||
}; | ||
|
||
export const ValidationErrorYes: Story = { | ||
args: { | ||
helperText: { text: 'Helper text' }, | ||
label: { label: 'label' }, | ||
isValidationError: true, | ||
children: ['Tag1', 'Tag2'].map((tagLabel) => ( | ||
<Tag label={tagLabel} key={tagLabel} onCloseClick={() => null} /> | ||
)), | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { BaseProps } from '@/types'; | ||
|
||
type InputConteinerConfig = BaseProps & { | ||
isValidationError: BaseProps; | ||
}; | ||
|
||
export type TagInputConfig = BaseProps & { | ||
innerElements?: { | ||
input?: BaseProps; | ||
inputContainer?: InputConteinerConfig; | ||
}; | ||
}; | ||
|
||
export const defaultConfig = { | ||
display: 'flex', | ||
flexDirection: 'column', | ||
gap: '$space-component-gap-small', | ||
w: '320px', | ||
padding: | ||
'$space-component-padding-small $space-component-padding-large $space-component-padding-small $space-component-padding-small', | ||
flexShrink: 0, | ||
innerElements: { | ||
inputContainer: { | ||
w: '100%', | ||
h: '$size-medium', | ||
borderRadius: '$border-radius-large', | ||
border: '1px solid $color-interaction-border-neutral-normal', | ||
display: 'flex', | ||
gap: '$space-component-gap-small', | ||
alignItems: 'center', | ||
bg: '$color-interaction-background-formField', | ||
borderColor: '$color-interaction-border-neutral-normal', | ||
p: '$space-component-padding-small $space-component-padding-large $space-component-padding-small $space-component-padding-small', | ||
flexShrink: '0', | ||
ringInset: true, | ||
ring: '$border-width-small', | ||
ringColor: { | ||
_: '$color-interaction-border-neutral-normal', | ||
hoverWithoutButton: '$color-interaction-border-hover', | ||
alert: '$color-interaction-border-alert', | ||
focusWithin: '$color-interaction-neutral-subtle-normal', | ||
}, | ||
outlineStyle: { | ||
'&:has(input:focus)': '$border-style-solid', | ||
}, | ||
outlineColor: { | ||
focusWithin: '$color-interaction-focus-default', | ||
}, | ||
outlineWidth: { | ||
'&:has(input:focus)': '$border-width-focus', | ||
}, | ||
outlineOffset: { | ||
_: '1px', | ||
alert: '$border-width-focus', | ||
}, | ||
transition: 'true', | ||
transitionDuration: '50', // TODO: unify transitionDuration | ||
cursor: 'text', | ||
overflow: 'hidden', | ||
isValidationError: { | ||
ringColor: '$color-interaction-border-alert', | ||
}, | ||
}, | ||
input: { | ||
h: '100%', | ||
w: '100%', | ||
p: '0', | ||
flex: '1 1 auto', | ||
outline: 'none', | ||
text: '$typo-body-medium', | ||
color: { | ||
placeholder: '$color-content-tertiary', | ||
}, | ||
backgroundColor: 'transparent', | ||
opacity: { | ||
placeholder: 1, | ||
}, | ||
}, | ||
}, | ||
} satisfies TagInputConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { vi } from 'vitest'; | ||
|
||
import { TagInput } from './TagInput'; | ||
import { render } from '../../tests/render'; | ||
|
||
import { customPropTester } from '@/tests/customPropTester'; | ||
|
||
const handleEventMock = vi.fn(); | ||
|
||
const getTagInput = (jsx: JSX.Element) => { | ||
const { getByTestId } = render(jsx); | ||
|
||
return { | ||
tagInput: getByTestId('tag-input'), | ||
input: getByTestId('tag-input-input'), | ||
inputContainer: getByTestId('tag-input-input-container'), | ||
}; | ||
}; | ||
|
||
describe('TagInput', () => { | ||
beforeEach(() => { | ||
handleEventMock.mockClear(); | ||
}); | ||
|
||
customPropTester(<TagInput />, { | ||
containerId: 'tag-input', | ||
}); | ||
|
||
it('should render the tag input', () => { | ||
const { tagInput } = getTagInput(<TagInput />); | ||
expect(tagInput).toBeInTheDocument(); | ||
}); | ||
|
||
it('should propagate custom props', () => { | ||
const { tagInput } = getTagInput( | ||
<TagInput | ||
custom={{ backgroundColor: '$color-background-negative-subtle' }} | ||
/>, | ||
); | ||
|
||
expect(tagInput).toHaveStyle('background-color: rgb(254, 245, 245)'); | ||
}); | ||
|
||
it('should render correct colors for validation error', () => { | ||
const { inputContainer } = getTagInput(<TagInput isValidationError />); | ||
expect(inputContainer).toHaveStyle('--x-ring-color: #f26464'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { Props } from '@xstyled/styled-components'; | ||
import { Children, forwardRef, isValidElement, PropsWithChildren } from 'react'; | ||
|
||
import { TagInputProps } from './TagInput.props'; | ||
import { useTagInput } from './useTagInput'; | ||
import { HelperText } from '../HelperText'; | ||
import { Label } from '../Label'; | ||
import { Tag } from '../Tag/Tag'; | ||
|
||
import { tet } from '@/tetrisly'; | ||
import { MarginProps } from '@/types/MarginProps'; | ||
|
||
export const TagInput = forwardRef< | ||
HTMLInputElement, | ||
PropsWithChildren<TagInputProps & MarginProps> | ||
>( | ||
( | ||
{ | ||
children, | ||
label, | ||
custom, | ||
helperText, | ||
value, | ||
defaultValue, | ||
state, | ||
isValidationError, | ||
onChange, | ||
...restProps | ||
}, | ||
inputRef, | ||
) => { | ||
const isAlert = state === 'alert'; | ||
const shouldRenderAlertIcon = | ||
(!!isValidationError || isAlert) && !!helperText; | ||
|
||
const { | ||
containerRef, | ||
handleContainerClick, | ||
styles, | ||
containerProps, | ||
innerValue, | ||
handleOnChange, | ||
tagInputProps, | ||
} = useTagInput({ | ||
custom, | ||
isValidationError, | ||
state, | ||
defaultValue, | ||
...restProps, | ||
}); | ||
|
||
Children.forEach(children, (child) => { | ||
if (isValidElement(child) && child?.type !== TagInputBase.Item) { | ||
console.error( | ||
'You should use only TagInput.Item as a child of a TagInput component.', | ||
); | ||
} | ||
}); | ||
|
||
return ( | ||
<tet.div | ||
ref={containerRef} | ||
onClick={handleContainerClick} | ||
{...styles.container} | ||
data-testid="tag-input" | ||
data-state={state} | ||
{...containerProps} | ||
> | ||
{label && <Label {...label} />} | ||
|
||
<tet.div | ||
data-testid="tag-input-input-container" | ||
{...styles.inputContainer} | ||
> | ||
{children} | ||
<tet.input | ||
{...styles.input} | ||
data-testid="tag-input-input" | ||
type="text" | ||
value={value || innerValue} | ||
onChange={handleOnChange} | ||
disabled={state === 'disabled'} | ||
ref={inputRef} | ||
{...tagInputProps} | ||
/> | ||
</tet.div> | ||
{helperText && ( | ||
<HelperText | ||
{...helperText} | ||
intent={shouldRenderAlertIcon ? 'alert' : 'none'} | ||
hasBeforeIcon={shouldRenderAlertIcon} | ||
/> | ||
)} | ||
</tet.div> | ||
); | ||
}, | ||
); | ||
|
||
const TagInputBase = TagInput as Props; | ||
TagInputBase.Item = Tag; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { TagInput } from './TagInput'; | ||
export type { TagInputProps } from './TagInput.props'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { TagInputConfig, defaultConfig } from './TagInput.styles'; | ||
|
||
import { mergeConfigWithCustom } from '@/services'; | ||
import { BaseProps } from '@/types/BaseProps'; | ||
|
||
type StylesBuilderParams = { | ||
container: BaseProps; | ||
input: BaseProps; | ||
inputContainer: BaseProps; | ||
}; | ||
|
||
export const stylesBuilder = ( | ||
custom?: TagInputConfig, | ||
isAlertOrError?: boolean, | ||
): StylesBuilderParams => { | ||
const { | ||
innerElements: { input, inputContainer }, | ||
...container | ||
} = mergeConfigWithCustom({ | ||
defaultConfig, | ||
custom, | ||
}); | ||
|
||
const { | ||
isValidationError: inputContainerValidationErrorStyle, | ||
...inputContainerRest | ||
} = inputContainer; | ||
|
||
const inputContainerStyles = | ||
(isAlertOrError && { | ||
...inputContainerRest, | ||
...inputContainerValidationErrorStyle, | ||
}) || | ||
inputContainerRest; | ||
|
||
return { | ||
container, | ||
input, | ||
inputContainer: inputContainerStyles, | ||
}; | ||
}; |
Oops, something went wrong.