Skip to content

Commit

Permalink
Merge pull request #54 from getpingback/feat/split-button
Browse files Browse the repository at this point in the history
Feat/split button
  • Loading branch information
Jessmartins91 authored Jan 7, 2025
2 parents 949a882 + 7d1aaf9 commit c9215ae
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 3 deletions.
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

### [0.0.313](https://github.com/getpingback/ui/compare/v0.0.312...v0.0.313) (2025-01-07)


### Bug Fixes

* changes requested ([65f9c2d](https://github.com/getpingback/ui/commits/65f9c2d25bb8e39dc57c581a8893af148244d337))
* remove props ([ffb5e79](https://github.com/getpingback/ui/commits/ffb5e791a33540d37baade5e9fcb9bf5d4b0436c))
* types ([6c072ff](https://github.com/getpingback/ui/commits/6c072ff427ccdf0d42081621005a3010d9e0fb0e))

### [0.0.311](https://github.com/getpingback/ui/compare/v0.0.310...v0.0.311) (2025-01-06)


### Bug Fixes

* splitbutton font style ([5052395](https://github.com/getpingback/ui/commits/505239574bf2c634e648531a3a409e710674cd06))

### [0.0.309](https://github.com/getpingback/ui/compare/v0.0.308...v0.0.309) (2025-01-06)


### Bug Fixes

* splitbutton styles ([75d1224](https://github.com/getpingback/ui/commits/75d1224157950f57c9420945bf4c5925c7d03d54))

### [0.0.307](https://github.com/getpingback/ui/compare/v0.0.305...v0.0.307) (2025-01-06)


### Features

* create a splitbutton and tests ([ed2e45c](https://github.com/getpingback/ui/commits/ed2e45c04755bedf8e0d657e69d3a3901f63bea6))
* export splitbutton ([e3cdfe3](https://github.com/getpingback/ui/commits/e3cdfe3f5075f3fffe2dd57cbc75744f03ada978))

### [0.0.305](https://github.com/getpingback/ui/compare/v0.0.303...v0.0.305) (2025-01-02)

### [0.0.303](https://github.com/getpingback/ui/compare/v0.0.301...v0.0.303) (2025-01-02)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@getpingback/ui",
"author": "Pingback Team",
"version": "0.0.306",
"version": "0.0.314",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './pagination';
export * from './variable-input';
export * from './radio-group';
export * from './switch';
export * from './split-button';
1 change: 1 addition & 0 deletions src/components/split-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './split-button';
54 changes: 54 additions & 0 deletions src/components/split-button/split-button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { SplitButton } from './split-button';
import { TrashCanIcon, PlusIcon } from '@stash-ui/light-icons';
import { HeartIcon } from '@stash-ui/regular-icons';

const meta = {
title: 'Components/SplitButton',
component: SplitButton,

tags: ['autodocs'],

argTypes: {}
} satisfies Meta<typeof SplitButton>;

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

export const Solid: Story = {
args: {
prefixIcon: <HeartIcon />,
variant: 'solid',
label: 'Button label',
onPrefixClick: () => {},
menuItems: [
{
key: 'add',
icon: <PlusIcon />,
text: 'Add',
onClick: () => console.log('add')
},
{
key: 'delete',
icon: <TrashCanIcon />,
text: 'Delete',
onClick: () => console.log('delete')
}
]
}
};

export const Outlined: Story = {
args: {
...Solid.args,
variant: 'outlined'
}
};

export const Ghost: Story = {
args: {
...Solid.args,
variant: 'ghost'
}
};
104 changes: 104 additions & 0 deletions src/components/split-button/split-button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { composeStories } from '@storybook/testing-react';

import * as stories from './split-button.stories';

const { Solid, Outlined, Ghost } = composeStories(stories);

describe('SplitButton Component', () => {
it('should render the SplitButton component', () => {
render(<Solid />);
const buttonContainer = screen.getByTestId('split-button');
const primaryButton = screen.getByTestId('split-button-primary');
const menuTrigger = screen.getByTestId('split-button-menu-trigger');
expect(buttonContainer).toBeInTheDocument();
expect(primaryButton).toBeInTheDocument();
expect(menuTrigger).toBeInTheDocument();

expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-solid_default)' });
expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-solid_default)' });
});

it('should render the SplitButton menu items', async () => {
render(<Solid />);
const menuTrigger = screen.getByTestId('split-button-menu-trigger');

const user = userEvent.setup();
await user.click(menuTrigger);
const menuContent = screen.getByTestId('split-button-menu-content');
const menuItems = screen.getAllByTestId(/split-button-menu-item/i);
expect(menuContent).toBeInTheDocument();
expect(menuItems[0]).toHaveTextContent('Add');
expect(menuItems[1]).toHaveTextContent('Delete');
});

it('should call the onClick function when a menu item is clicked', async () => {
const onClickMock = jest.fn();
render(
<Solid
{...{
menuItems: [
{
key: 'add',
text: 'Add',
onClick: () => onClickMock('add')
},
{
key: 'delete',
text: 'Delete',
onClick: () => onClickMock('delete')
}
]
}}
/>
);

const user = userEvent.setup();
const menuTrigger = screen.getByTestId('split-button-menu-trigger');
await user.click(menuTrigger);
const menuItems = screen.getAllByTestId(/split-button-menu-item/i);
await user.click(menuItems[0]);

expect(onClickMock).toHaveBeenCalledWith('add');
});

it('should call the onClick function when a primary button is clicked', async () => {
const onClickMock = jest.fn();
render(<Solid {...{ onPrefixClick: () => onClickMock('primary action') }} />);
const user = userEvent.setup();
const primaryButton = screen.getByTestId('split-button-primary');
await user.click(primaryButton);

expect(onClickMock).toHaveBeenCalledWith('primary action');
});

it('should render the outlined SplitButton component', () => {
render(<Outlined />);
const buttonContainer = screen.getByTestId('split-button');
const primaryButton = screen.getByTestId('split-button-primary');
const menuTrigger = screen.getByTestId('split-button-menu-trigger');

expect(buttonContainer).toBeInTheDocument();
expect(primaryButton).toBeInTheDocument();
expect(menuTrigger).toBeInTheDocument();

expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-outlined_default)' });
expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-outlined_default)' });
});

it('should render the ghost SplitButton component', () => {
render(<Ghost />);
const buttonContainer = screen.getByTestId('split-button');
const primaryButton = screen.getByTestId('split-button-primary');
const menuTrigger = screen.getByTestId('split-button-menu-trigger');

expect(buttonContainer).toBeInTheDocument();
expect(primaryButton).toBeInTheDocument();
expect(menuTrigger).toBeInTheDocument();

expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-ghost_bg-color)' });
expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-ghost_bg-color)' });
});
});
104 changes: 104 additions & 0 deletions src/components/split-button/split-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { CaretDownIcon } from '@stash-ui/light-icons';
import { cva } from 'class-variance-authority';
import { DropdownItem } from '../dropdown';

const containerVariants = cva('flex items-center font-primary max-w-fit h-[32px] text-sm rounded-lg ', {
variants: {
type: {
solid: 'bg-button-solid text-button-solid-foreground hover:shadow-solid transition-all duration-200 ease-in-out',
outlined:
'bg-button-outlined text-secondary-foreground border border-button-outlined-border hover:shadow-outlined transition-all duration-200 ease-in-out',
ghost:
'bg-button-ghost-gray text-button-ghost-foreground text-secondary-foreground hover:shadow-ghost transition-all duration-200 ease-in-out'
}
},
defaultVariants: {
type: 'solid'
}
});

const leftButtonVariants = cva(
'h-full w-full flex items-center gap-1 px-3 rounded-l-lg rounded-r-none transition-all duration-200 ease-in-out font-semibold',
{
variants: {
type: {
solid: 'hover:bg-button-solid-hover',
outlined: 'hover:bg-button-outlined-hover opacity-85 hover:opacity-100',
ghost: 'hover:bg-button-ghost-hover opacity-85 hover:opacity-100'
}
}
}
);

const menuTriggerVariants = cva(
'h-full w-full min-w-[32px] max-w-[32px] border-l flex items-center justify-center rounded-r-lg rounded-l-none border-solid transition-all duration-200 ease-in-out',
{
variants: {
type: {
solid: 'hover:bg-button-solid-hover border-[#282C2F1F]',
outlined: 'hover:bg-button-outlined-hover opacity-85 hover:opacity-100 border-[#282C2F1F]',
ghost: 'hover:bg-button-ghost-hover border-[#282C2F1F]'
}
}
}
);

interface SplitButtonProps {
prefixIcon: React.ReactNode;
label: string;
variant?: 'solid' | 'outlined' | 'ghost';
onPrefixClick: () => void;
sufixIcon?: React.ReactNode;
className?: string;
menuItems: {
key: string;
icon: JSX.Element | undefined;
text: string;
onClick: () => void;
}[];
}

function SplitButton({ prefixIcon, label, variant, onPrefixClick, menuItems, sufixIcon, className }: SplitButtonProps) {
const [isMenuActionsOpen, setIsMenuActionsOpen] = useState(false);

return (
<DropdownMenu.Root open={isMenuActionsOpen} modal={false} onOpenChange={(open) => !open && setIsMenuActionsOpen(false)}>
<DropdownMenu.Trigger asChild>
<div className={containerVariants({ type: variant })} data-testid="split-button">
<button className={leftButtonVariants({ type: variant })} onClick={onPrefixClick} data-testid="split-button-primary">
{prefixIcon}
{label}
</button>
<button
onClick={() => setIsMenuActionsOpen(!isMenuActionsOpen)}
className={menuTriggerVariants({ type: variant })}
data-testid="split-button-menu-trigger"
>
{sufixIcon ? sufixIcon : <CaretDownIcon width={20} height={20} />}
</button>
</div>
</DropdownMenu.Trigger>

<DropdownMenu.Portal>
<DropdownMenu.Content
data-testid="split-button-menu-content"
onClick={() => setIsMenuActionsOpen(false)}
side="bottom"
className={`rounded-lg w-[252px] flex-col z-50 min-w-fit overflow-hidden bg-background-accent shadow-modal py-2 ${className}`}
sideOffset={4}
align="center"
>
{menuItems.map((item, index) => (
<DropdownItem icon={item?.icon} key={index} onClick={item.onClick} data-testid="split-button-menu-item">
{item.text}
</DropdownItem>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}

export { SplitButton };
8 changes: 8 additions & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@

--buttons-solid_default: #9061f9;
--button-solid-bg: #9061f9;
--button-solid-hover: #7e3af2;
--buttons-label_color: #7e3af2;
--button-solid-text: #fafafa;
--button-ghost-bg: #9061f91f;
--button-ghost-text: #52525b;
--button-ghost-gray: #52525B14;
--button-ghost-gray-hover: #282C2F14;
--button-outlined-border: #D4D4D8;
--button-outlined-hover: #52525B14;
--buttons-label_inverse: #fafafa;
--button-hover-solid-color: #9061f940;
--buttons-ghost_bg-color: #9061f91f;
Expand All @@ -45,6 +50,8 @@
--switch-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.161) inset;

--button-solid-shadow: 0px 0px 0px 3px #9061f940;
--button-outlined-shadow: 0px 0px 0px 3px #9CA3AF1F;
--button-ghost-shadow: 0px 0px 0px 3px #9CA3AF14;

--list-hightlight--hover: 0px 0px 0px 3px rgba(14, 159, 110, 0.12);
--list-actived: #9061f914;
Expand Down Expand Up @@ -104,6 +111,7 @@
--buttons-solid_default: #9061f9;
--button-solid-bg: #9061f9;
--button-solid-text: #fafafa;
--button-solid-hover: #7e3af2;
--buttons-label: #52525b;
--button-ghost-bg: #ac94fa3d;
--button-ghost-text: #fafafa;
Expand Down
13 changes: 11 additions & 2 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ const config = {
drawer: 'var(--drawer-shadow)',
custom: '0px 0px 0px 1px rgba(0,0,0,0.15)',
solid: 'var(--button-solid-shadow)',
outlined: 'var(--button-outlined-shadow)',
ghost: 'var(--button-ghost-shadow)',
dropdown: 'var(--dropdown-shadow)',
switch: 'var(--switch-shadow)'
},
Expand Down Expand Up @@ -298,11 +300,18 @@ const config = {
button: {
solid: {
DEFAULT: 'var(--button-solid-bg)',
foreground: 'var(--button-solid-text)'
foreground: 'var(--button-solid-text)',
hover: 'var(--button-solid-hover)'
},
ghost: {
DEFAULT: 'var(--button-ghost-bg)',
foreground: 'var(--button-ghost-text)'
foreground: 'var(--button-ghost-text)',
gray: 'var(--button-ghost-gray)',
hover: 'var(--button-ghost-gray-hover)'
},
outlined: {
border: 'var(--button-outlined-border)',
hover: 'var(--button-outlined-hover)'
},
done: {
DEFAULT: 'var(--badge-done-bg)',
Expand Down

0 comments on commit c9215ae

Please sign in to comment.