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

feat(button): allow icons to be placed left and right of button #866

Merged
merged 4 commits into from
May 1, 2024
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
43 changes: 43 additions & 0 deletions packages/react/src/components/buttons/button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { shallow } from 'enzyme';
import { doNothing } from '../../test-utils/callbacks';
import { mountWithProviders, renderWithProviders } from '../../test-utils/renderer';
import { Button } from './button';
import { getByTestId } from '../../test-utils/enzyme-selectors';

describe('Button', () => {
test('onClick callback is called when clicked', () => {
Expand Down Expand Up @@ -176,4 +178,45 @@ describe('Button', () => {
modifier: ':focus',
});
});

test('icons can be placed left and right of the button', () => {
const wrapper = shallow(
<Button
buttonType="primary"
label="Primary Button"
leftIconName="chevronLeft"
rightIconName="chevronRight"
/>,
);

expect(getByTestId(wrapper, 'left-icon')).toHaveLength(1);
expect(getByTestId(wrapper, 'right-icon')).toHaveLength(1);
});

test('icons can be placed on one side of the button', () => {
const wrapper = shallow(
<Button
buttonType="primary"
label="Primary Button"
rightIconName="chevronRight"
/>,
);

expect(getByTestId(wrapper, 'left-icon')).toHaveLength(0);
expect(getByTestId(wrapper, 'right-icon')).toHaveLength(1);
});

test('has left and right icons', () => {
const tree = renderWithProviders(
<Button
buttonType="primary"
label="Primary Button"
leftIconName="chevronLeft"
rightIconName="chevronRight"

/>,
);

expect(tree).toMatchSnapshot();
});
});
122 changes: 122 additions & 0 deletions packages/react/src/components/buttons/button.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,128 @@ exports[`Button has destructive-secondary styles 1`] = `
</button>
`;

exports[`Button has left and right icons 1`] = `
.c0 {
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: inherit;
border: 1px solid;
border-radius: 1.5rem;
box-sizing: border-box;
color: inherit;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
font-family: inherit;
font-size: 0.75rem;
font-weight: var(--font-bold);
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-letter-spacing: 0.025rem;
-moz-letter-spacing: 0.025rem;
-ms-letter-spacing: 0.025rem;
letter-spacing: 0.025rem;
line-height: 1rem;
min-height: var(--size-2x);
min-width: 2rem;
outline: none;
padding: 0 var(--spacing-2x);
text-transform: uppercase;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.c0 {
outline: 2px solid transparent;
outline-offset: -2px;
}
.c0:focus {
box-shadow: 0 0 0 2px #006296;
outline: 2px solid #84C6EA;
outline-offset: -2px;
}
.c0 > svg {
color: inherit;
height: var(--size-1x);
width: var(--size-1x);
}
.c2 {
margin-right: var(--spacing-1x);
}
.c3 {
margin-left: var(--spacing-1x);
}
.c1 {
background-color: #006296;
border-color: #006296;
color: #FFFFFF;
}
.c1 {
outline: 2px solid transparent;
outline-offset: -2px;
}
.c1:focus {
box-shadow: 0 0 0 2px #006296;
outline: 2px solid #84C6EA;
outline-offset: -2px;
}
.c1:hover,
.c1[aria-expanded='true'] {
background-color: #003A5A;
border-color: #003A5A;
color: #FFFFFF;
}
.c1:disabled {
background-color: #84C6EA;
border-color: #84C6EA;
color: #FFFFFF;
}
<button
class="c0 c1"
type="button"
>
<svg
aria-hidden="true"
class="c2"
color="currentColor"
data-testid="left-icon"
focusable="false"
height="24"
width="24"
/>
Primary Button
<svg
aria-hidden="true"
class="c3"
color="currentColor"
data-testid="right-icon"
focusable="false"
height="24"
width="24"
/>
</button>
`;

exports[`Button has mobile styles 1`] = `
.c0 {
-webkit-align-items: center;
Expand Down
44 changes: 43 additions & 1 deletion packages/react/src/components/buttons/button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { FocusEventHandler, forwardRef, KeyboardEvent, MouseEvent, PropsWithChildren, ReactElement, Ref } from 'react';
import {
FocusEventHandler,
forwardRef,
KeyboardEvent,
MouseEvent,
PropsWithChildren,
ReactElement,
Ref,
} from 'react';
import styled from 'styled-components';
import {
Icon,
IconName,
} from '../icon/icon';
import { ResolvedTheme } from '../../themes/theme';
import { useDeviceContext } from '../device-context-provider/device-context-provider';
import { AbstractButton, ButtonType, getButtonTypeStyles } from './abstract-button';
Expand Down Expand Up @@ -32,12 +44,23 @@ export interface ButtonProps {
title?: string;
type?: Type;

leftIconName?: IconName;
rightIconName?: IconName;

onClick?(event: MouseEvent<HTMLButtonElement>): void;
onFocus?: FocusEventHandler<HTMLButtonElement>;
onBlur?: FocusEventHandler<HTMLButtonElement>;
onKeyDown?(event: KeyboardEvent<HTMLButtonElement>): void;
}

const LeftIcon = styled(Icon)`
margin-right: var(--spacing-1x);
`;

const RightIcon = styled(Icon)`
margin-left: var(--spacing-1x);
`;

const StyledButton = styled(AbstractButton)<{ theme: ResolvedTheme } & ButtonProps>`
${getButtonTypeStyles}
`;
Expand All @@ -55,10 +78,13 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
onBlur,
onKeyDown,
title,
leftIconName,
rightIconName,
type = 'button',
...props
}: PropsWithChildren<ButtonProps>, ref: Ref<HTMLButtonElement>): ReactElement => {
const { isMobile } = useDeviceContext();
const iconSize = props?.size === 'small' && !isMobile ? '16' : '24';

return (
<StyledButton
Expand All @@ -78,7 +104,23 @@ export const Button = forwardRef<HTMLButtonElement, PropsWithChildren<ButtonProp
{...props /* eslint-disable-line react/jsx-props-no-spreading *//* To spread aria-* and data-* */}
>
{children}
{leftIconName && (
<LeftIcon
aria-hidden="true"
data-testid="left-icon"
name={leftIconName}
size={iconSize}
/>
)}
{label}
{rightIconName && (
<RightIcon
aria-hidden="true"
data-testid="right-icon"
name={rightIconName}
size={iconSize}
/>
)}
</StyledButton>
);
});
Expand Down
13 changes: 13 additions & 0 deletions packages/storybook/stories/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,16 @@ export const PrimaryWithIcon: Story = {
</Button>
),
};

export const PrimaryWithIconsOnBothSides: Story = {
args: {
buttonType: 'primary',
label: 'Icons on both sides',
size: 'medium',
leftIconName: 'chevronLeft',
rightIconName: 'chevronRight',
},
render: (args) => (
<Button {...args /* eslint-disable-line react/jsx-props-no-spreading */} />
),
};
Loading