Skip to content

Commit

Permalink
[PB-3786] feature/Create media component for premeet modal (#47)
Browse files Browse the repository at this point in the history
* Added new transparent modal and modified input props

* Added circle button component
  • Loading branch information
CandelR authored Feb 14, 2025
1 parent 06b734d commit b3e6ca5
Show file tree
Hide file tree
Showing 8 changed files with 1,565 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/ui",
"version": "0.0.18",
"version": "0.0.19",
"description": "Library of Internxt components",
"repository": {
"type": "git",
Expand Down
127 changes: 127 additions & 0 deletions src/components/buttonCircle/CircleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { CaretUp, Warning } from '@phosphor-icons/react';
import { useEffect, useState } from 'react';

type ButtonVariant = 'default' | 'warning' | 'cancel';

export interface CircleButtonProps {
children?: JSX.Element | JSX.Element[];
variant?: ButtonVariant;
active?: boolean;
onClick?: () => void;
onClickToggleButton?: () => void;
className?: string;
dropdown?: React.ReactNode;
indicator?: {
icon?: JSX.Element;
className?: string;
};
}

const CircleButton = ({
children,
variant = 'default',
active = false,
onClick,
onClickToggleButton,
className = '',
dropdown,
indicator,
}: CircleButtonProps): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isOpen) {
const target = event.target as HTMLElement;
const circleButton = document.querySelector(`[data-circle-button="${variant}"]`);
if (circleButton && !circleButton.contains(target)) {
setIsOpen(false);
}
}
};

document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [isOpen, variant]);

const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
if (dropdown) {
onClickToggleButton?.();
setIsOpen(!isOpen);
}
};

const handleMainClick = () => {
onClick?.();
};

const getButtonStyles = () => {
switch (variant) {
case 'cancel':
return 'bg-red hover:bg-red/85';
case 'warning':
return active ? 'bg-white/85' : 'bg-white/25 hover:bg-white/35';
default:
return active ? 'bg-white/85' : 'bg-white/25 hover:bg-white/35';
}
};

const renderIndicator = () => {
if (!indicator) return null;

if (variant === 'warning') {
return (
<div className="absolute -top-1 -right-1 h-5 w-5 bg-orange border border-black/35 rounded-full flex items-center justify-center">
<Warning size={12} color="black" weight="bold" />
</div>
);
}

return (
<div
className={`absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center rounded-full ${indicator.className || ''}`}
>
{indicator.icon}
</div>
);
};

const renderDropdownButton = () => {
if (!dropdown || variant === 'cancel' || variant === 'warning') return null;

return (
<button
onClick={handleToggle}
className="absolute -top-1 -right-1 h-5 w-5 border bg-white border-black/35 rounded-full flex items-center justify-center hover:bg-gray-50"
>
<CaretUp size={10} color="black" weight="fill" />
</button>
);
};

return (
<div className="relative w-12 h-12" data-circle-button={variant}>
<button
onClick={handleMainClick}
className={`
h-11 w-11
flex items-center justify-center
rounded-full
transition-all duration-200
${getButtonStyles()}
${className}
`}
>
{children}
</button>
{renderDropdownButton()}
{renderIndicator()}
{isOpen && dropdown && variant !== 'cancel' && <div className="absolute bottom-full mb-2 left-0">{dropdown}</div>}
</div>
);
};

export default CircleButton;
117 changes: 117 additions & 0 deletions src/components/buttonCircle/__test__/CircleButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { CaretUp } from '@phosphor-icons/react';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import CircleButton from '../CircleButton';

describe('CircleButton component', () => {
it('should render default button correctly', () => {
const button = render(<CircleButton />);
expect(button).toMatchSnapshot();
});

it('should render active default button correctly', () => {
const button = render(<CircleButton active />);
expect(button).toMatchSnapshot();
});

it('should render warning button correctly', () => {
const button = render(<CircleButton variant="warning" />);
expect(button).toMatchSnapshot();
});

it('should render active warning button correctly', () => {
const button = render(<CircleButton variant="warning" active />);
expect(button).toMatchSnapshot();
});

it('should render cancel button correctly', () => {
const button = render(<CircleButton variant="cancel" />);
expect(button).toMatchSnapshot();
});

it('should render button with children correctly', () => {
const button = render(
<CircleButton>
<span>Test Child</span>
</CircleButton>,
);
expect(button).toMatchSnapshot();
});

it('should render button with custom className correctly', () => {
const button = render(<CircleButton className="custom-class" />);
expect(button).toMatchSnapshot();
});

it('should render button with dropdown correctly', () => {
const dropdown = <div>Dropdown Content</div>;
const button = render(<CircleButton dropdown={dropdown} />);
expect(button).toMatchSnapshot();
});

it('should render button with warning indicator correctly', () => {
const button = render(<CircleButton variant="warning" indicator={{}} />);
expect(button).toMatchSnapshot();
});

it('should render button with custom indicator correctly', () => {
const indicator = {
icon: <CaretUp size={12} />,
className: 'custom-indicator',
};
const button = render(<CircleButton indicator={indicator} />);
expect(button).toMatchSnapshot();
});

it('should handle main button click correctly', () => {
const onClick = vi.fn();
render(<CircleButton onClick={onClick} />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(onClick).toHaveBeenCalledOnce();
});

it('should handle dropdown toggle click correctly', () => {
const onClickToggleButton = vi.fn();
const dropdown = <div>Dropdown Content</div>;
render(<CircleButton dropdown={dropdown} onClickToggleButton={onClickToggleButton} />);
const toggleButton = screen.getAllByRole('button')[1]; // Get the second button (dropdown toggle)
fireEvent.click(toggleButton);
expect(onClickToggleButton).toHaveBeenCalledOnce();
});

it('should show dropdown content when toggle is clicked', () => {
const dropdown = <div>Dropdown Content</div>;
render(<CircleButton dropdown={dropdown} />);
const toggleButton = screen.getAllByRole('button')[1];
fireEvent.click(toggleButton);
expect(screen.getByText('Dropdown Content')).toBeInTheDocument();
});

it('should handle dropdown visibility correctly', () => {
const dropdown = <div>Dropdown Content</div>;
render(<CircleButton dropdown={dropdown} />);
const toggleButton = screen.getAllByRole('button')[1];

// Open dropdown
fireEvent.click(toggleButton);
expect(screen.getByText('Dropdown Content')).toBeInTheDocument();

// Simulate click outside - in this case, we just dispatch a global click event
fireEvent.click(document.body);
expect(screen.queryByText('Dropdown Content')).not.toBeInTheDocument();
});

it('should not render dropdown toggle for cancel variant', () => {
const dropdown = <div>Dropdown Content</div>;
const button = render(<CircleButton variant="cancel" dropdown={dropdown} />);
expect(button).toMatchSnapshot();
});

it('should not render dropdown toggle for warning variant', () => {
const dropdown = <div>Dropdown Content</div>;
const button = render(<CircleButton variant="warning" dropdown={dropdown} />);
expect(button).toMatchSnapshot();
});
});
Loading

0 comments on commit b3e6ca5

Please sign in to comment.