Skip to content

Commit

Permalink
feat: TET-848 add button group (#160)
Browse files Browse the repository at this point in the history
* feat: TET-848 add ButtonGroup component

* feat: TET-848 add tests and fix a bug

* feat: TET-848 add export

* feat: TET-848  resolve build problems
  • Loading branch information
karolinaszarek authored Sep 2, 2024
1 parent 9236719 commit d48dd7f
Show file tree
Hide file tree
Showing 9 changed files with 298 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/components/ButtonGroup/ButtonGroup.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HTMLAttributes } from 'react';

import { ButtonGroupConfig } from './ButtonGroup.styles';

export type ButtonGroupSize = 'medium' | 'small';
export type ButtonGroupProps = {
size?: ButtonGroupSize;
custom?: ButtonGroupConfig;
} & Omit<HTMLAttributes<HTMLSpanElement>, 'color'>;
47 changes: 47 additions & 0 deletions src/components/ButtonGroup/ButtonGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/react';

import { ButtonGroup } from './ButtonGroup';
import { Button } from '../Button/Button';

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

const meta = {
title: 'ButtonGroup',
component: ButtonGroup,
tags: ['autodocs'],
argTypes: {
size: {
options: ['small', 'medium'],
defaultValue: 'medium',
control: { type: 'radio' },
},
},
parameters: {
docs: {
description: {
component:
'A set of related buttons that are visually and functionally grouped. Button Group acts as a cohesive unit, providing users with clear options for actions or navigation.',
},
page: () => (
<TetDocs docs="https://docs.tetrisly.com/components/list/buttongroup">
<ButtonGroupDocs />
</TetDocs>
),
},
},
} satisfies Meta<typeof ButtonGroup>;

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

export const Default: Story = {
args: {
children: [
<Button label="button" />,
<Button label="button" />,
<Button label="button" />,
<Button label="button" />,
],
},
};
50 changes: 50 additions & 0 deletions src/components/ButtonGroup/ButtonGroup.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { BaseProps } from '@/types';

export type ButtonGroupConfig = {
button: {
size: {
medium: BaseProps;
small: BaseProps;
};
} & BaseProps;
} & BaseProps;

export const defaultConfig = {
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
button: {
color: {
_: '$color-action-neutral-normal',
hover: '$color-action-neutral-hover',
},
backgroundColor: {
_: '$color-action-inverted-normal',
active: '$color-action-ghost-active',
hover: '$color-action-ghost-hover',
selected: '$color-action-ghost-selected',
},
ringColor: '$color-action-outline-normal',
size: {
// TODO think if it can be done by passing size prop to a button component
medium: {
h: '$size-medium',
px: '$space-component-padding-large',
},
small: {
h: '$size-small',
px: '$space-component-padding-medium',
},
},
borderRadius: {
first: `$border-radius-large 0px 0px $border-radius-large`,
last: `0px $border-radius-large $border-radius-large 0px`,
},
transition: true,
transitionDuration: 200,
},
} as const satisfies ButtonGroupConfig;

export const buttonGroupStyles = {
defaultConfig,
};
41 changes: 41 additions & 0 deletions src/components/ButtonGroup/ButtonGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render } from '../../tests/render';

import { ButtonGroup } from '.';

import { customPropTester } from '@/tests/customPropTester';

// TODO

const getButtonGroup = (jsx: JSX.Element) => {
const { getByTestId } = render(jsx);
return {
buttonGroup: getByTestId('button-group'),
};
};
describe('ButtonGroup', () => {
it('should render the ButtonGroup ', () => {
const { buttonGroup } = getButtonGroup(<ButtonGroup />);
expect(buttonGroup).toBeInTheDocument();
});

it('should render correct number of children', () => {
const { buttonGroup } = getButtonGroup(
<ButtonGroup>
<ButtonGroup.Item label="label" />
<ButtonGroup.Item label="label" />
<ButtonGroup.Item label="label" />
<ButtonGroup.Item label="label" />
<ButtonGroup.Item label="label" />
</ButtonGroup>,
);

expect(buttonGroup?.children.length).toEqual(5);
});

customPropTester(<ButtonGroup />, {
containerId: 'button-group',
props: {
options: ['small', 'medium'],
},
});
});
58 changes: 58 additions & 0 deletions src/components/ButtonGroup/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { MarginProps } from '@xstyled/styled-components';
import {
Children,
cloneElement,
isValidElement,
PropsWithChildren,
useMemo,
type FC,
} from 'react';

import { ButtonGroupProps } from './ButtonGroup.props';
import { stylesBuilder } from './stylesBuilder';
import { Button, ButtonProps } from '../Button';

import { tet } from '@/tetrisly';

type Props = FC<PropsWithChildren<ButtonGroupProps & MarginProps>> & {
Item: FC<ButtonProps & MarginProps>;
};

export const ButtonGroup: Props = ({
size = 'medium',
children,
custom,
...rest
}) => {
const styles = useMemo(
() =>
stylesBuilder({
size,
custom,
}),
[custom, size],
);

Children.forEach(children, (child) => {
if (isValidElement(child) && child?.type !== ButtonGroup.Item) {
console.error(
'You should use only ButtonGroup.Item as a child of a CheckboxGroup component.',
);
}
});

const childrenWithProps = Children.map(children, (child) => {
if (isValidElement(child)) {
return cloneElement(child, { ...styles.button });
}
return child;
});

return (
<tet.span data-testid="button-group" {...styles.container} {...rest}>
{childrenWithProps}
</tet.span>
);
};

ButtonGroup.Item = Button;
3 changes: 3 additions & 0 deletions src/components/ButtonGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ButtonGroup } from './ButtonGroup';
export type { ButtonGroupProps } from './ButtonGroup.props';
export { buttonGroupStyles } from './ButtonGroup.styles';
31 changes: 31 additions & 0 deletions src/components/ButtonGroup/stylesBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ButtonGroupSize } from './ButtonGroup.props';
import { ButtonGroupConfig, defaultConfig } from './ButtonGroup.styles';

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

type ButtonGroupStyleBuilder = {
container: BaseProps;
button: BaseProps;
};

type ButtonGroupStyleBuilderInput = {
size: ButtonGroupSize;
custom?: ButtonGroupConfig;
};

export const stylesBuilder = ({
size,
custom,
}: ButtonGroupStyleBuilderInput): ButtonGroupStyleBuilder => {
const { button, ...container } = mergeConfigWithCustom({
defaultConfig,
custom,
});
const buttonStyles = { ...button, ...button.size[size] };

return {
container,
button: buttonStyles,
};
};
58 changes: 58 additions & 0 deletions src/docs-components/ButtonGroupDocs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { FC } from 'react';
import { v4 as uuidv4 } from 'uuid';

import { SectionHeader } from './common/SectionHeader';

import { Button } from '@/components/Button';
import { ButtonGroup } from '@/components/ButtonGroup';
import { tet } from '@/tetrisly';

const items = [4, 3, 2] as const;
const sizes = ['medium', 'small'] as const;

export const ButtonGroupDocs: FC = () => (
<>
{items.map((item) => (
<tet.section
key={item}
display="flex"
pt="$dimension-500"
pb="$dimension-500"
flexDirection="column"
>
<SectionHeader variant="H1" as="h1" px="$dimension-1000">
{item} items
</SectionHeader>
{sizes.map((size) => (
<tet.div key={size} display="flex" flexDirection="column">
<SectionHeader
variant="H2"
as="h3"
px="$dimension-1000"
py="$dimension-300"
borderBottom="$color-neutral-strong"
>
Size: {size === 'medium' ? 'Medium (Default)' : 'Small'}
</SectionHeader>
<tet.div
px="$dimension-1000"
pb="$dimension-500"
mt="$dimension-400"
mb="$dimension-400"
key={size}
>
<ButtonGroup>
{[...Array(item)].map(() => {
const id = uuidv4();
return (
<Button key={`button-item-${id}`} label="Button label" />
);
})}
</ButtonGroup>
</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 @@ -3,6 +3,7 @@ export * from './components/Avatar';
export * from './components/Badge';
export * from './components/BooleanPill';
export * from './components/Button';
export * from './components/ButtonGroup';
export * from './components/Checkbox';
export * from './components/CheckboxGroup';
export * from './components/CornerDialog';
Expand Down

0 comments on commit d48dd7f

Please sign in to comment.