Skip to content

Commit

Permalink
#396 ButtonGroup rename SegmentedControl + component improvements (#422)
Browse files Browse the repository at this point in the history
* segmented control

* lading status fixes

* simpler

* component rebuilt

* taking state from button props

* picking props and choosing array for state

* story remade to fit new convention

* away with state props

* selective size options
  • Loading branch information
MichalPaszowski authored Oct 21, 2022
1 parent f51ed56 commit 98184c8
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 158 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
.btn-group {
border: 1px solid var(--border-subtle);
border-radius: 4px;
.segmented-control {
display: inline-flex;
flex-direction: row;
border: 1px solid var(--border-subtle);
border-radius: 4px;
padding: 3px;
vertical-align: middle;

.btn {
border-color: transparent;
color: var(--content-default);

&--active {
background-color: var(--surface-basic-active);
Expand All @@ -17,9 +16,11 @@
&--compact {
height: 24px;
}

&--medium {
height: 28px;
}

&--large {
height: 34px;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,64 @@
import * as React from 'react';
import { render, userEvent, vi } from 'test-utils';
import noop from '../../utils/noop';
import { Button } from '../Button';
import { ButtonGroup } from './ButtonGroup';

import styles from './ButtonGroup.module.scss';

describe('<ButtonGroup> component', () => {
function renderComponent(props = {}, onButtonClick = noop) {
return render(
<ButtonGroup {...props}>
<Button onClick={onButtonClick}>First button</Button>
<Button>Second button</Button>
</ButtonGroup>
);
import { SegmentedControl, SegmentedControlProps } from './SegmentedControl';

import styles from './SegmentedControl.module.scss';

const defaultProps: SegmentedControlProps = {
buttons: [
{ id: 'one', label: 'one' },
{ id: 'two', label: 'two' },
],
};

describe('<SegmentedControl> component', () => {
function renderComponent(props: SegmentedControlProps) {
return render(<SegmentedControl {...props} />);
}

it('should have custom css class', () => {
const className = 'my-custom-class';
const {
container: { firstChild: el },
} = renderComponent({ className });
} = renderComponent({ ...defaultProps, className });

expect(el).toHaveClass(className);
});

it('should allow for controlled version of component by passing "currentIndex" prop', () => {
const { getAllByRole } = renderComponent({ currentIndex: 1 });
const { getAllByRole } = renderComponent({
...defaultProps,
currentId: 'one',
});

const [firstButton, secondButton] = getAllByRole('button');

expect(firstButton).not.toHaveClass(styles['btn--active']);
expect(secondButton).toHaveClass(styles['btn--active']);
expect(firstButton).toHaveClass(styles['btn--active']);
expect(secondButton).not.toHaveClass(styles['btn--active']);
});

it('should not have active button by default in unconrolled version', () => {
const { getAllByRole } = renderComponent();
const { getAllByRole } = renderComponent({ ...defaultProps });

const [firstButton, secondButton] = getAllByRole('button');

expect(firstButton).not.toHaveClass(styles['btn--active']);
expect(secondButton).not.toHaveClass(styles['btn--active']);
});

it('should call "onIndexChange" with index of current selected button on click', () => {
const onIndexChange = vi.fn();
it('should call "onButtonClick" with index of current selected button on click', () => {
const onButtonClick = vi.fn();
const { getAllByRole } = renderComponent({ onIndexChange }, onButtonClick);
const { getAllByRole } = renderComponent({
...defaultProps,
onButtonClick,
initialId: 'two',
});

const [firstButton] = getAllByRole('button');

expect(firstButton).not.toHaveClass(styles['btn--active']);

userEvent.click(firstButton);

expect(onIndexChange).toHaveBeenCalledWith(0, expect.anything());
expect(onButtonClick).toHaveBeenCalled();
expect(firstButton).toHaveClass(styles['btn--active']);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from 'react';
import { ComponentMeta, Story } from '@storybook/react';

import { SegmentedControl, SegmentedControlProps } from './SegmentedControl';

const buttonSizes = ['compact', 'medium', 'large'];

export default {
title: 'Components/Segmented Control',
component: SegmentedControl,
argTypes: {
size: {
options: buttonSizes,
control: {
type: 'select',
labels: buttonSizes,
},
},
},
} as ComponentMeta<typeof SegmentedControl>;

export const Default: Story<SegmentedControlProps> = (
args: SegmentedControlProps
) => <SegmentedControl {...args} />;

Default.storyName = 'Controlled';
Default.args = {
buttons: [
{ id: 'one', label: 'one', loading: true, disabled: true },
{ id: 'two', label: 'two', disabled: true },
{ id: 'three', label: 'three' },
{ id: 'fourth', label: 'fourth' },
],
initialId: 'fourth',
};

export const Uncontrolled: Story<SegmentedControlProps> = (
args: SegmentedControlProps
) => <SegmentedControl {...args} />;

Uncontrolled.storyName = 'Uncontrolled With Initial Selection';
Uncontrolled.args = {
buttons: [
{ id: 'one', label: 'one' },
{ id: 'two', label: 'two', disabled: true },
{ id: 'three', label: 'three' },
{ id: 'fourth', label: 'fourth' },
],
currentId: 'one',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import cx from 'clsx';

import { Button, ButtonProps } from '../Button';

import styles from './SegmentedControl.module.scss';

import noop from '../../utils/noop';

const baseClass = 'segmented-control';

export type ButtonSize = 'compact' | 'medium' | 'large';

type ButtonElement = {
id: string;
label: string;
} & Pick<ButtonProps, 'disabled' | 'loading'>;

export interface SegmentedControlProps
extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
buttons: ButtonElement[];
fullWidth?: boolean;
size?: ButtonSize;
initialId?: string;
currentId?: string;
onButtonClick?: (id: string, event: React.MouseEvent<HTMLElement>) => void;
}

export const SegmentedControl: React.FC<SegmentedControlProps> = ({
size = 'medium',
buttons,
className,
initialId,
currentId,
fullWidth = false,
onButtonClick = noop,
}) => {
const mergedClassName = cx(styles[baseClass], className);
const [currentStateId, setCurrentStateId] = React.useState(() => initialId);

const isControlled = typeof currentId === 'string';

React.useEffect(() => {
isControlled && setCurrentStateId(currentId);
}, [currentId]);

const handleClick = (id: string, event: any) => {
if (!isControlled) {
setCurrentStateId(id);
}

onButtonClick(id, event);
};
const buttonSet = buttons.map(({ id, label, loading, disabled }) => {
const activityStyles = id === currentStateId ? styles['btn--active'] : '';
const loadingStatus = id === currentStateId ? false : loading;

return (
<Button
key={id}
fullWidth={fullWidth}
loading={loadingStatus}
disabled={disabled}
kind="secondary"
className={cx(styles['btn'], styles[`btn--${size}`], activityStyles)}
onClick={(event: React.MouseEvent<HTMLElement>) => {
handleClick(id, event);
}}
>
{label}
</Button>
);
});

return (
<div role="group" className={mergedClassName}>
{buttonSet}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SegmentedControl } from './SegmentedControl';
2 changes: 1 addition & 1 deletion packages/react-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export * from './components/Alert';
export * from './components/Avatar';
export * from './components/Badge';
export * from './components/Button';
export * from './components/ButtonGroup';
export * from './components/SegmentedControl';
export * from './components/Card';
export * from './components/Checkbox';
export * from './components/DatePicker';
Expand Down

0 comments on commit 98184c8

Please sign in to comment.