-
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.
Tag Component with updated tokens (#89)
* feat: TET-363 tag * feat: TET-363 tag v2 * feat: tokens update TET-363 --------- Co-authored-by: Marta Kozina <[email protected]>
- Loading branch information
1 parent
0d80296
commit ed5046b
Showing
9 changed files
with
459 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { SyntheticEvent } from 'react'; | ||
|
||
import { TagConfig } from '@/components/Tag/Tag.styles.ts'; | ||
import { TextInputProps } from '@/components/TextInput'; | ||
|
||
export type TagProps = { | ||
label: string; | ||
state?: 'selected' | 'disabled'; | ||
beforeComponent?: TextInputProps.InnerComponents.Avatar; | ||
onClick?: (e: SyntheticEvent<HTMLSpanElement>) => void; | ||
onCloseClick?: (e: SyntheticEvent<HTMLButtonElement>) => void; | ||
custom?: TagConfig; | ||
}; |
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,53 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { Tag } from './Tag'; | ||
|
||
import { TagDocs } from '@/docs-components/TagDocs.tsx'; | ||
import { TetDocs } from '@/docs-components/TetDocs'; | ||
|
||
const meta = { | ||
title: 'Tag', | ||
component: Tag, | ||
tags: ['autodocs'], | ||
parameters: { | ||
docs: { | ||
description: { | ||
component: | ||
'A compact, visually distinct element used to label, categorize, or organize content. Tags can help users quickly identify and filter items by attributes such as keywords, topics, or statuses.', | ||
}, | ||
page: () => ( | ||
<TetDocs docs="https://docs.tetrisly.com/components/in-progress/tag"> | ||
<TagDocs /> | ||
</TetDocs> | ||
), | ||
}, | ||
}, | ||
args: { | ||
label: 'Tag', | ||
onClick: () => null, | ||
}, | ||
} satisfies Meta<typeof Tag>; | ||
|
||
export default meta; | ||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = {}; | ||
|
||
export const BeforeAvatarComponent: Story = { | ||
args: { | ||
beforeComponent: { | ||
type: 'Avatar', | ||
props: { | ||
initials: 'A', | ||
emphasis: 'high', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
export const WithRemoveButton: Story = { | ||
args: { | ||
state: undefined, | ||
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,69 @@ | ||
import type { BaseProps } from '@/types/BaseProps'; | ||
|
||
export type TagConfig = { | ||
hasOnClick?: BaseProps; | ||
innerElements?: { | ||
label: BaseProps; | ||
closeButton?: BaseProps; | ||
beforeComponent?: { | ||
avatar?: BaseProps; | ||
}; | ||
}; | ||
} & BaseProps; | ||
|
||
const backgroundColor = { | ||
hover: '$color-interaction-neutral-subtle-hover', | ||
active: '$color-interaction-neutral-subtle-active', | ||
focus: '$color-interaction-neutral-subtle-normal', | ||
}; | ||
|
||
export const defaultConfig = { | ||
display: 'inline-flex', | ||
h: '$size-xSmall', | ||
alignItems: 'center', | ||
borderRadius: '$border-radius-medium', | ||
backgroundColor: '$color-interaction-neutral-subtle-normal', | ||
opacity: { | ||
disabled: '$opacity-disabled', | ||
}, | ||
cursor: 'default', | ||
outlineColor: { | ||
focus: '$color-interaction-focus-default', | ||
}, | ||
transitionDuration: 50, | ||
color: '$color-content-primary', | ||
hasOnClick: { | ||
backgroundColor: { | ||
_: '$color-interaction-neutral-subtle-normal', | ||
disabled: '$color-interaction-neutral-subtle-normal', | ||
selected: { | ||
_: '$color-interaction-neutral-subtle-selected', | ||
...backgroundColor, | ||
}, | ||
...backgroundColor, | ||
}, | ||
cursor: { | ||
_: 'pointer', | ||
disabled: 'default', | ||
}, | ||
}, | ||
innerElements: { | ||
label: { | ||
mx: '$space-component-padding-small', | ||
text: '$typo-body-medium', | ||
}, | ||
closeButton: { | ||
mr: '$space-component-padding-xSmall', | ||
h: '$size-2xSmall', | ||
w: '$size-2xSmall', | ||
opacity: { | ||
disabled: '$opacity-100', | ||
}, | ||
}, | ||
beforeComponent: { | ||
avatar: { | ||
ml: '$space-component-padding-2xSmall', | ||
}, | ||
}, | ||
}, | ||
} satisfies TagConfig; |
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,112 @@ | ||
import { vi } from 'vitest'; | ||
|
||
import { render, fireEvent } from '../../tests/render'; | ||
|
||
import { Tag } from '@/components/Tag/Tag.tsx'; | ||
import { customPropTester } from '@/tests/customPropTester'; | ||
|
||
const getTag = (jsx: JSX.Element) => { | ||
const { getByTestId, queryByTestId } = render(jsx); | ||
|
||
return { | ||
tag: getByTestId('tag'), | ||
label: getByTestId('tag-label'), | ||
avatar: queryByTestId('tag-avatar'), | ||
closeButton: queryByTestId('tag-iconButton'), | ||
}; | ||
}; | ||
|
||
describe('Tag', () => { | ||
const handleEventMock = vi.fn(); | ||
|
||
customPropTester(<Tag label="Label" />, { | ||
containerId: 'tag', | ||
}); | ||
|
||
beforeEach(() => { | ||
handleEventMock.mockReset(); | ||
}); | ||
|
||
it('should render the tag', () => { | ||
const { tag } = getTag(<Tag label="label" />); | ||
expect(tag).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render the correct label', () => { | ||
const { tag } = getTag(<Tag label="label" />); | ||
expect(tag).toHaveTextContent('label'); | ||
}); | ||
|
||
it('should render beforeComponent', () => { | ||
const { avatar } = getTag( | ||
<Tag | ||
label="label" | ||
beforeComponent={{ type: 'Avatar', props: { initials: 'A' } }} | ||
/>, | ||
); | ||
expect(avatar).toBeInTheDocument(); | ||
}); | ||
|
||
it('should render closeButton', () => { | ||
const { closeButton } = getTag( | ||
<Tag label="label" onCloseClick={handleEventMock} />, | ||
); | ||
expect(closeButton).toBeInTheDocument(); | ||
}); | ||
|
||
it('should emit onClick', () => { | ||
const { tag } = getTag(<Tag label="label" onClick={handleEventMock} />); | ||
fireEvent.click(tag); | ||
expect(handleEventMock).toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not emit onCloseClick', () => { | ||
const { closeButton } = getTag( | ||
<Tag label="label" onCloseClick={handleEventMock} state="disabled" />, | ||
); | ||
fireEvent.click(closeButton as Element); | ||
expect(handleEventMock).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should render disabled closeButton', () => { | ||
const { closeButton } = getTag( | ||
<Tag label="label" state="disabled" onCloseClick={handleEventMock} />, | ||
); | ||
expect(closeButton).toBeDisabled(); | ||
}); | ||
|
||
it('should render the correct color (disabled)', () => { | ||
const { tag } = getTag(<Tag label="label" state="disabled" />); | ||
expect(tag).toHaveStyle('background-color: rgb(240, 243, 245);'); | ||
}); | ||
|
||
it('should render the right cursor (with onClick)', () => { | ||
const { tag } = getTag(<Tag label="label" onClick={handleEventMock} />); | ||
expect(tag).toHaveStyle('cursor: pointer'); | ||
}); | ||
|
||
it('should render the right cursor (without onClick)', () => { | ||
const { tag } = getTag(<Tag label="label" />); | ||
expect(tag).toHaveStyle('cursor: default'); | ||
}); | ||
|
||
it('should render the right cursor (with state disabled)', () => { | ||
const { tag } = getTag(<Tag label="label" state="disabled" />); | ||
expect(tag).toHaveStyle('cursor: default'); | ||
}); | ||
|
||
it('should not emit onClick', () => { | ||
const onCloseCLick = vi.fn(); | ||
const onClick = vi.fn(); | ||
|
||
const { closeButton } = getTag( | ||
<Tag label="label" onCloseClick={onCloseCLick} onClick={onClick} />, | ||
); | ||
|
||
if (closeButton) { | ||
fireEvent.click(closeButton); | ||
} | ||
expect(onCloseCLick).toBeCalledTimes(1); | ||
expect(onClick).not.toHaveBeenCalled(); | ||
}); | ||
}); |
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,103 @@ | ||
import { | ||
FC, | ||
KeyboardEventHandler, | ||
MouseEventHandler, | ||
useCallback, | ||
useMemo, | ||
useRef, | ||
} from 'react'; | ||
|
||
import { stylesBuilder } from './stylesBuilder'; | ||
import { TagProps } from './Tag.props'; | ||
import { Avatar } from '../Avatar'; | ||
import { IconButton } from '../IconButton'; | ||
|
||
import { tet } from '@/tetrisly'; | ||
import { MarginProps } from '@/types'; | ||
|
||
const KEYBOARD_KEYS = { | ||
Enter: 'Enter', | ||
Space: ' ', | ||
}; | ||
|
||
export const Tag: FC<TagProps & MarginProps> = ({ | ||
label, | ||
state, | ||
beforeComponent, | ||
onClick, | ||
onCloseClick, | ||
custom, | ||
...restProps | ||
}) => { | ||
const hasCloseButton = !!onCloseClick; | ||
const hasOnClick = !!onClick; | ||
const styles = useMemo( | ||
() => stylesBuilder(custom, hasOnClick), | ||
[custom, hasOnClick], | ||
); | ||
|
||
const containerRef = useRef<HTMLSpanElement | null>(null); | ||
const handleOnKeyDown: KeyboardEventHandler<HTMLSpanElement> = useCallback( | ||
(e) => { | ||
if ( | ||
e.target === containerRef.current && | ||
(e.key === KEYBOARD_KEYS.Enter || e.key === KEYBOARD_KEYS.Space) | ||
) { | ||
onClick?.(e); | ||
} | ||
}, | ||
[containerRef, onClick], | ||
); | ||
|
||
const handleOnCloseClick: MouseEventHandler<HTMLButtonElement> = useCallback( | ||
(e) => { | ||
onCloseClick?.(e); | ||
e.stopPropagation(); | ||
}, | ||
[onCloseClick], | ||
); | ||
|
||
return ( | ||
<tet.span | ||
tabIndex={0} | ||
ref={containerRef} | ||
onClick={onClick} | ||
onKeyDown={handleOnKeyDown} | ||
{...styles.container} | ||
data-state={state} | ||
data-testid="tag" | ||
{...restProps} | ||
> | ||
{!!beforeComponent && ( | ||
<Avatar | ||
shape="square" | ||
size="2xSmall" | ||
{...beforeComponent.props} | ||
{...styles.avatar} | ||
data-testid="tag-avatar" | ||
/> | ||
)} | ||
<tet.p | ||
{...styles.label} | ||
mr={ | ||
hasCloseButton | ||
? '$space-component-padding-xSmall' | ||
: '$space-component-padding-small' | ||
} | ||
data-testid="tag-label" | ||
> | ||
{label} | ||
</tet.p> | ||
{hasCloseButton && ( | ||
<IconButton | ||
icon="20-close" | ||
variant="bare" | ||
onClick={handleOnCloseClick} | ||
state={state} | ||
{...styles.closeButton} | ||
data-testid="tag-iconButton" | ||
/> | ||
)} | ||
</tet.span> | ||
); | ||
}; |
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 { Tag } from './Tag'; | ||
export type { TagProps } from './Tag.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 @@ | ||
export { stylesBuilder } from './stylesBuilder'; |
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,36 @@ | ||
import type { TagProps } from '../Tag.props'; | ||
import { defaultConfig } from '../Tag.styles'; | ||
|
||
import { mergeConfigWithCustom } from '@/services'; | ||
import type { BaseProps } from '@/types/BaseProps'; | ||
|
||
type TagStylesBuilder = { | ||
container: BaseProps; | ||
label: BaseProps; | ||
avatar: BaseProps; | ||
closeButton: BaseProps; | ||
}; | ||
export const stylesBuilder = ( | ||
custom: TagProps['custom'], | ||
hasOnClick?: boolean, | ||
): TagStylesBuilder => { | ||
const { | ||
hasOnClick: hasOnClickStyles, | ||
innerElements: { | ||
label, | ||
closeButton, | ||
beforeComponent: { avatar }, | ||
}, | ||
...container | ||
} = mergeConfigWithCustom({ defaultConfig, custom }); | ||
|
||
return { | ||
container: { | ||
...container, | ||
...(hasOnClick && hasOnClickStyles), | ||
}, | ||
label, | ||
avatar, | ||
closeButton, | ||
}; | ||
}; |
Oops, something went wrong.