Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#396 ButtonGroup rename SegmentedControl + component improvements #422

Merged
merged 9 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you follow the story convention from eg. Avatar?

We're trying to avoid the renaming of the component using the Component suffix. This happens thanks to naming the first story Default.

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