Skip to content

Commit

Permalink
FCT-1200: add iconRight to secondary- and primary-button component …
Browse files Browse the repository at this point in the history
…+ 2 new icons (#2968)

* feat(primary-button): add `iconRight` property

* feat(secondary-button): add `iconRight` property

* chore(readmes): update component readme's

* feat(icons): new caret icons added

* chore: changeset added

* fix(secondary-button): use custom icon component

* fix(primary-button): use custom icon component
  • Loading branch information
misama-ct authored Oct 28, 2024
1 parent 77241f9 commit 1ec85b2
Show file tree
Hide file tree
Showing 16 changed files with 513 additions and 39 deletions.
7 changes: 7 additions & 0 deletions .changeset/funny-queens-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@commercetools-uikit/secondary-button': minor
'@commercetools-uikit/primary-button': minor
'@commercetools-uikit/icons': minor
---

new property `iconRight` added to primary- & secondary-button, 2 new caret icons added
1 change: 1 addition & 0 deletions packages/components/buttons/primary-button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default Example;
| `type` | `union`<br/>Possible values:<br/>`'button' , 'reset' , 'submit'` | | `'button'` | Used as the HTML type attribute. |
| `label` | `string` || | Should describe what the button does, for accessibility purposes (screen-reader users) |
| `iconLeft` | `ReactElement` | | | The left icon displayed within the button. |
| `iconRight` | `ReactElement` | | | The right icon displayed within the button. |
| `isToggleButton` | `boolean` | | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled |
| `isToggled` | `boolean` | | | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is `false`. |
| `isDisabled` | `boolean` | | | Tells when the button should present a disabled state. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,32 @@ describe('rendering', () => {
expect(screen.getByLabelText('Add')).toBeInTheDocument();
expect(screen.getByLabelText('Add')).toBeEnabled();
});
it('should render icon', () => {
it('should render left icon', () => {
render(<PrimaryButton {...props} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('should render right icon', () => {
render(
<PrimaryButton
{...props}
iconLeft={undefined}
iconRight={<PlusBoldIcon data-testid="icon-right" />}
/>
);
expect(screen.getByTestId('icon-right')).toBeInTheDocument();
});

it('should render both icons', () => {
render(
<PrimaryButton
{...props}
iconRight={<PlusBoldIcon data-testid="icon-right" />}
/>
);
expect(screen.getByTestId('icon')).toBeInTheDocument();
expect(screen.getByTestId('icon-right')).toBeInTheDocument();
});

it('should not render icon', () => {
const { queryByTestId } = render(
<PrimaryButton {...props} iconLeft={undefined} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const meta: Meta<typeof PrimaryButton> = {
control: 'text',
},
iconLeft: iconArgType,
iconRight: iconArgType,
size: {
control: 'select',
options: ['10', '20'],
Expand Down
62 changes: 45 additions & 17 deletions packages/components/buttons/primary-button/src/primary-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import omit from 'lodash/omit';
import { css } from '@emotion/react';
import Inline from '@commercetools-uikit/spacings-inline';
import { designTokens } from '@commercetools-uikit/design-system';
import {
filterInvalidAttributes,
useWarning,
Expand Down Expand Up @@ -38,6 +37,32 @@ const sizeMapping: Record<TLegacySizes, TSizes> = {
big: '20',
};

const PositionedIcon = ({
icon,
size,
isDisabled,
}: {
icon: TPrimaryButtonProps['iconRight'] | TPrimaryButtonProps['iconLeft'];
size: TPrimaryButtonProps['size'];
isDisabled: TPrimaryButtonProps['isDisabled'];
}) => {
if (!icon) return null;
return (
<span
css={css`
display: flex;
align-items: center;
justify-content: center;
`}
>
{cloneElement(icon, {
color: isDisabled ? 'neutral60' : 'surface',
size: size === 'big' || size === '20' ? '40' : '20',
})}
</span>
);
};

export type TPrimaryButtonProps<
TStringOrComponent extends ElementType = 'button'
> = {
Expand All @@ -60,6 +85,10 @@ export type TPrimaryButtonProps<
* The left icon displayed within the button.
*/
iconLeft?: ReactElement;
/**
* The right icon displayed within the button.
*/
iconRight?: ReactElement;
/**
* If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled
*/
Expand Down Expand Up @@ -127,6 +156,7 @@ const PrimaryButton = <TStringOrComponent extends ElementType = 'button'>(
);

const isActive = Boolean(props.isToggleButton && props.isToggled);

return (
<AccessibleButton
as={props.as}
Expand All @@ -139,24 +169,22 @@ const PrimaryButton = <TStringOrComponent extends ElementType = 'button'>(
isDisabled={props.isDisabled}
css={getButtonStyles(props.isDisabled, isActive, props.tone, props.size)}
>
<Inline alignItems="center" scale="xs">
{Boolean(props.iconLeft) && (
<span
css={css`
margin: 0 ${designTokens.spacing10} 0 0;
display: flex;
align-items: center;
justify-content: center;
`}
>
{props.iconLeft &&
cloneElement(props.iconLeft, {
color: props.isDisabled ? 'neutral60' : 'surface',
size: props.size === 'big' || props.size === '20' ? '40' : '20',
})}
</span>
<Inline alignItems="center" scale="s">
{props.iconLeft && (
<PositionedIcon
icon={props.iconLeft}
isDisabled={props.isDisabled}
size={props.size}
/>
)}
<span>{props.label}</span>
{props.iconRight && (
<PositionedIcon
icon={props.iconRight}
isDisabled={props.isDisabled}
size={props.size}
/>
)}
</Inline>
</AccessibleButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,22 @@ export const component = () => (
isDisabled={true}
/>
</Spec>

<Spec label="with icon right">
<PrimaryButton
label="A label text"
onClick={() => {}}
iconRight={<InformationIcon />}
/>
</Spec>

<Spec label="with icons left + right">
<PrimaryButton
label="A label text"
onClick={() => {}}
iconLeft={<InformationIcon />}
iconRight={<InformationIcon />}
/>
</Spec>
</Suite>
);
3 changes: 2 additions & 1 deletion packages/components/buttons/secondary-button/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import SecondaryButton from '@commercetools-uikit/secondary-button';
| Props | Type | Required | Values | Default | Description |
| ------------------ | --------------------- | :------: | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `label` | `string` || - | - | Should describe what the button does, for accessibility purposes (screen-reader users) |
| `iconLeft` | `node` || - | - | The left icon displayed within the button |
| `iconLeft` | `node` | - | - | - | The left icon displayed within the button |
| `iconRight` | `node` | - | - | - | The right icon displayed within the button |
| `isToggleButton` | `bool` || - | `false` | If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled |
| `isToggled` | `bool` | - | - | - | Tells when the button should present a toggled state. It does not have any effect when `isToggleButton` is false |
| `theme` | `string` | - | `default`, `info` | `default` | The component may have a theme only if `isToggleButton` is true. &#xA;This property has been **deprecated** in favor of `tone`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,32 @@ describe('rendering', () => {
expect(screen.getByLabelText('Add')).toBeInTheDocument();
expect(screen.getByLabelText('Add')).toBeEnabled();
});
it('should render icon', () => {
it('should render left icon', () => {
render(<SecondaryButton {...props} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('should render right icon', () => {
render(
<SecondaryButton
{...props}
iconLeft={undefined}
iconRight={<PlusBoldIcon data-testid="icon-right" />}
/>
);
expect(screen.getByTestId('icon-right')).toBeInTheDocument();
});

it('should render left + right icon', () => {
render(
<SecondaryButton
{...props}
iconRight={<PlusBoldIcon data-testid="icon-right" />}
/>
);
expect(screen.getByTestId('icon')).toBeInTheDocument();
expect(screen.getByTestId('icon-right')).toBeInTheDocument();
});

it('should not render icon', () => {
render(<SecondaryButton {...props} iconLeft={undefined} />);
expect(screen.queryByTestId('icon')).not.toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const meta: Meta<typeof SecondaryButton> = {
component: SecondaryButton,
argTypes: {
iconLeft: iconArgType,
iconRight: iconArgType,
as: {
control: 'text',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export type TSecondaryButtonProps<
* The left icon displayed within the button.
*/
iconLeft?: ReactElement;
/**
* The righr icon displayed within the button.
*/
iconRight?: ReactElement;
/**
* If this is active, it means the button will persist in an "active" state when toggled (see `isToggled`), and back to normal state when untoggled
*/
Expand Down Expand Up @@ -108,18 +112,19 @@ export type TSecondaryButtonProps<
export const getIconColor = (
props: Pick<
TSecondaryButtonProps,
'isToggleButton' | 'isToggled' | 'theme' | 'isDisabled' | 'iconLeft'
'isToggleButton' | 'isToggled' | 'theme' | 'isDisabled'
> & {
isActive?: boolean;
}
},
icon: TSecondaryButtonProps['iconLeft'] | TSecondaryButtonProps['iconRight']
) => {
const isActive = props.isToggleButton && props.isToggled;
// if button has a theme, icon should be the same color as the theme on active state
if (props.theme !== 'default' && isActive && !props.isDisabled) return 'info'; // returns the passed in theme without overwriting
// if button is disabled, icon should be grey
if (props.isDisabled) return 'neutral60';
// if button is not disabled nor has a theme, return icon's default color
return props.iconLeft?.props.color;
return icon?.props.color;
};

const defaultProps: Pick<
Expand All @@ -133,6 +138,31 @@ const defaultProps: Pick<
isToggleButton: false,
};

const PositionedIcon = ({
size,
icon,
color,
}: {
size: string;
icon: ReactElement;
color: string;
}) => {
return (
<span
css={css`
display: flex;
align-items: center;
justify-content: center;
`}
>
{cloneElement(icon, {
color,
size: size === 'big' || size === '20' ? '40' : '20',
})}
</span>
);
};

export const SecondaryButton = <
TStringOrComponent extends ElementType = 'button'
>(
Expand Down Expand Up @@ -195,24 +225,22 @@ export const SecondaryButton = <
isDisabled={props.isDisabled}
css={containerStyles}
>
<Inline alignItems="center" scale="xs">
{Boolean(props.iconLeft) && (
<span
css={css`
margin: 0 ${designTokens.spacing10} 0 0;
display: flex;
align-items: center;
justify-content: center;
`}
>
{props.iconLeft &&
cloneElement(props.iconLeft, {
color: getIconColor(props),
size: props.size === 'big' || props.size === '20' ? '40' : '20',
})}
</span>
<Inline alignItems="center" scale="s">
{props.iconLeft && (
<PositionedIcon
icon={props.iconLeft}
size={props.size}
color={getIconColor(props, props.iconLeft)}
/>
)}
<span>{props.label}</span>
{props.iconRight && (
<PositionedIcon
icon={props.iconRight}
size={props.size}
color={getIconColor(props, props.iconRight)}
/>
)}
</Inline>
</AccessibleButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,21 @@ export const component = () => (
<Spec label='when tone is "info"'>
<SecondaryButton label="A label text" onClick={() => {}} tone="info" />
</Spec>

<Spec label="with icon right">
<SecondaryButton
label="A label text"
onClick={() => {}}
iconRight={<InformationIcon />}
/>
</Spec>
<Spec label="with icon left + right">
<SecondaryButton
label="A label text"
onClick={() => {}}
iconLeft={<InformationIcon />}
iconRight={<InformationIcon />}
/>
</Spec>
</Suite>
);
Loading

0 comments on commit 1ec85b2

Please sign in to comment.