Skip to content

Commit

Permalink
feat: NO-JIRA new item button (#138)
Browse files Browse the repository at this point in the history
* feat: NO-JIRA new item button

* feat: NO-JIRA review changes

* chore: NO-JIRA test
  • Loading branch information
golas-m authored May 8, 2024
1 parent 2735137 commit 4cfd24a
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 1 deletion.
54 changes: 54 additions & 0 deletions src/components/NewItemButton/NewItemButton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/react';

import { NewItemButton } from './NewItemButton';

import { NewItemButtonDocs } from '@/docs-components/NewItemButtonDocs';
import { TetDocs } from '@/docs-components/TetDocs';

const meta = {
title: 'NewItemButton',
component: NewItemButton,
tags: ['autodocs'],
argTypes: {},
args: {
state: 'normal',
text: 'text',
},
parameters: {
docs: {
description: {
component:
'A dedicated button for creating new items, such as files, events, or tasks, typically placed in a prominent location and distinguished by an icon or label.',
},
page: () => (
<TetDocs docs="https://docs.tetrisly.com/components/in-progress/newitembutton">
<NewItemButtonDocs />
</TetDocs>
),
},
},
} satisfies Meta<typeof NewItemButton>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
state: 'normal',
text: 'New category',
},
};

export const Alert: Story = {
args: {
state: 'alert',
text: 'Alert!',
},
};

export const Disabled: Story = {
args: {
state: 'disabled',
text: 'Disabled',
},
};
69 changes: 69 additions & 0 deletions src/components/NewItemButton/NewItemButton.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { NewItemButtonState } from './NewItemButtonState.type';

import type { BaseProps } from '@/types/BaseProps';

export type NewItemButtonConfig = {
state?: Partial<Record<NewItemButtonState, BaseProps>>;
innerElements?: {
text?: BaseProps;
};
} & BaseProps;

export const defaultConfig = {
display: 'inline-flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '$space-component-gap-small',
minH: '120px',
minWidth: '120px',
h: '100%',
w: '100%',
borderWidth: '$border-width-small',
borderStyle: '$border-style-dashed',
borderRadius: '$border-radius-large',
padding: '$space-component-padding-xLarge',
text: '$typo-body-medium',
textAlign: 'center',
whiteSpace: 'nowrap',
color: '$color-action-neutral-normal',
backgroundColor: '$color-interaction-background-formField',
transition: true,
transitionDuration: 200,
outline: {
focus: 'solid',
},
outlineColor: {
focus: '$color-interaction-focus-default',
},
outlineWidth: {
focus: '$border-width-focus',
},
outlineOffset: 1,
state: {
normal: {
borderColor: {
_: '$color-border-neutral-subtle',
hover: '$color-interaction-border-hover',
},
},
alert: {
borderColor: '$color-interaction-border-alert',
},
disabled: {
borderColor: '$color-border-neutral-subtle',
opacity: '$opacity-disabled',
pointerEvents: 'none',
},
},
innerElements: {
text: {
text: '$typo-body-medium',
color: '$color-content-primary',
},
},
} as const satisfies NewItemButtonConfig;

export const newItemButtonStyles = {
defaultConfig,
};
53 changes: 53 additions & 0 deletions src/components/NewItemButton/NewItemButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { vi } from 'vitest';

import { NewItemButton } from './NewItemButton';
import { NewItemButtonState } from './NewItemButtonState.type';
import { render, screen, fireEvent } from '../../tests/render';

describe('NewItemButton', () => {
const states: NewItemButtonState[] = ['normal', 'alert', 'disabled'];

it('should render the NewItemButton ', () => {
render(<NewItemButton />);
const button = screen.getByTestId('new-item-button');
expect(button).toBeInTheDocument();
});

it('should be disabled if disabled state is passed', () => {
render(<NewItemButton state="disabled" />);
const button = screen.getByTestId('new-item-button');
expect(button).toBeDisabled();
expect(button).toHaveStyle('pointer-events: none');
expect(button).toHaveStyle('opacity: 0.5');
});

states.forEach((state) => {
describe(`State: ${state}`, () => {
it('should render the NewItemButton', () => {
render(<NewItemButton state={state} />);
const button = screen.getByTestId('new-item-button');
expect(button).toBeInTheDocument();
});

it('should render correct text', () => {
render(<NewItemButton state={state} text="Hello there!" />);
const button = screen.getByTestId('new-item-button');
expect(button).toHaveTextContent('Hello there!');
});

it('should handle onClick properly when clicked', () => {
const onClickMock = vi.fn();
render(<NewItemButton state={state} onClick={onClickMock} />);

const button = screen.getByTestId('new-item-button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
if (state !== 'disabled') {
expect(onClickMock).toHaveBeenCalled();
} else {
expect(onClickMock).not.toHaveBeenCalled();
}
});
});
});
});
28 changes: 28 additions & 0 deletions src/components/NewItemButton/NewItemButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Icon } from '@virtuslab/tetrisly-icons';
import { useMemo, type FC } from 'react';

import { NewItemButtonProps } from './NewItemButtons.props';
import { stylesBuilder } from './stylesBuilder';

import { tet } from '@/tetrisly';

export const NewItemButton: FC<NewItemButtonProps> = ({
state = 'normal',
text,
custom,
...rest
}) => {
const styles = useMemo(() => stylesBuilder(state, custom), [custom, state]);

return (
<tet.button
{...styles.container}
{...rest}
data-testid="new-item-button"
disabled={state === 'disabled'}
>
<Icon name="20-plus" />
{!!text && <tet.span {...styles.text}>{text}</tet.span>}
</tet.button>
);
};
1 change: 1 addition & 0 deletions src/components/NewItemButton/NewItemButtonState.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type NewItemButtonState = 'normal' | 'alert' | 'disabled';
10 changes: 10 additions & 0 deletions src/components/NewItemButton/NewItemButtons.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ButtonHTMLAttributes } from 'react';

import { NewItemButtonConfig } from './NewItemButton.styles';
import { NewItemButtonState } from './NewItemButtonState.type';

export type NewItemButtonProps = {
state?: NewItemButtonState | undefined;
text?: string;
custom?: NewItemButtonConfig;
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'disabled' | 'color'>;
3 changes: 3 additions & 0 deletions src/components/NewItemButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { NewItemButton } from './NewItemButton';
export type { NewItemButtonProps } from './NewItemButtons.props';
export { newItemButtonStyles } from './NewItemButton.styles';
35 changes: 35 additions & 0 deletions src/components/NewItemButton/stylesBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NewItemButtonConfig, defaultConfig } from './NewItemButton.styles';
import { NewItemButtonState } from './NewItemButtonState.type';

import { mergeConfigWithCustom } from '@/services';
import { BaseProps } from '@/types/BaseProps';

type NewItemButtonStyleBuilder = {
container: BaseProps;
text: BaseProps;
};

export const stylesBuilder = (
state: NewItemButtonState,
custom?: NewItemButtonConfig,
): NewItemButtonStyleBuilder => {
const {
innerElements,
state: containerState,
...container
} = mergeConfigWithCustom({
defaultConfig,
custom,
});

const { text } = innerElements;
const containerStyles = containerState[state];

return {
container: {
...container,
...containerStyles,
},
text,
};
};
42 changes: 42 additions & 0 deletions src/docs-components/NewItemButtonDocs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SectionHeader } from './common/SectionHeader';
import { States } from './common/States';

import { NewItemButton } from '@/components/NewItemButton';
import { tet } from '@/tetrisly';

const states = ['normal', 'alert', 'disabled'] as const;

export const NewItemButtonDocs = () => (
<tet.section py="$dimension-500">
<SectionHeader
px="$dimension-1000"
py="$dimension-500"
variant="H1"
as="h2"
>
State
</SectionHeader>
<tet.div px="$dimension-1000" pb="$dimension-500">
<States
states={['normal', 'alert', 'disabled']}
flexBasis="130px"
gap="$dimension-300"
/>
<tet.div
display="flex"
flexDirection="row"
gap="$dimension-300"
pt="$dimension-300"
flexBasis="130px"
flexShrink="0"
flexGrow="1"
>
{states.map((state) => (
<tet.div flexBasis="130px" flexGrow="1" flexShrink="0">
<NewItemButton state={state} text="Text" />
</tet.div>
))}
</tet.div>
</tet.div>
</tet.section>
);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './components/InlineMessage';
export * from './components/InlineSearchInput';
export * from './components/Label';
export * from './components/Loader';
export * from './components/NewItemButton';
export * from './components/Popover';
export * from './components/RadioButton';
export * from './components/RadioButtonGroup';
Expand Down
2 changes: 1 addition & 1 deletion src/theme/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,7 @@ const fixedTokens = {
borderStyles: {
'$border-style-none': 'none',
'$border-style-solid': 'solid',
'$border-style-dashed': 'dashed solid',
'$border-style-dashed': 'dashed',
},
fonts: { '$font-family-primary': 'Inter' },
fontSizes: {
Expand Down

0 comments on commit 4cfd24a

Please sign in to comment.