-
Notifications
You must be signed in to change notification settings - Fork 0
[PB-5294]: feat/create-suite-launcher-component #65
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
Changes from all commits
78b261d
0fcc256
731b5f0
6373fb5
68c9051
7ada0bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { DotsNine, Lock } from '@phosphor-icons/react'; | ||
| import { cloneElement, isValidElement } from 'react'; | ||
| import { Popover } from '../popover'; | ||
|
|
||
| export interface SuiteLauncherProps { | ||
| className?: string; | ||
| suiteArray: { | ||
| icon: JSX.Element; | ||
| title: string; | ||
| onClick: () => void; | ||
| isMain?: boolean; | ||
| availableSoon?: boolean; | ||
| isLocked?: boolean; | ||
| }[]; | ||
| soonText?: string; | ||
| } | ||
|
|
||
| /** | ||
| * SuiteLauncher renders a dropdown menu with a list of suite applications. | ||
| * | ||
| * @param {suiteLauncherProps} props | ||
| * - Object containing properties for the suiteLauncher component. | ||
| * | ||
| * @param {suiteLauncherProps['suiteArray']} props.suiteArray | ||
| * - Array of objects containing the suite applications. | ||
| * | ||
| * @param {string} [props.className] | ||
| * - Optional CSS class name for the suiteLauncher component. | ||
| * | ||
| * @param {string} [props.soonText] | ||
| * - Optional text to display when a suite application is available soon. It should be a translated string. Defaults to "Soon". | ||
| * | ||
| * @returns {JSX.Element} | ||
| * - The rendered suiteLauncher component. | ||
| */ | ||
| export default function SuiteLauncher({ | ||
| className = '', | ||
| suiteArray, | ||
| soonText, | ||
| }: Readonly<SuiteLauncherProps>): JSX.Element { | ||
| const SuiteButton = ( | ||
| <div className="flex h-10 w-10 items-center justify-center text-black dark:text-white"> | ||
| <DotsNine size={26} className="h-7 w-7" weight="bold" /> | ||
| </div> | ||
| ); | ||
|
|
||
| const panel = ( | ||
| <div className="w-64 flex flex-wrap p-2" data-testid="suite-launcher-panel"> | ||
| {suiteArray.map((suiteApp, idx) => ( | ||
| <div | ||
| key={idx} | ||
| className={`w-1/3 flex items-center justify-center rounded-md ${suiteApp.isMain ? 'bg-primary/10 dark:bg-primary/20' : ''}`} | ||
| > | ||
| <div | ||
| role="none" | ||
| className={ | ||
| `flex items-center px-3 py-2 text-gray-80 w-full rounded-md ` + | ||
| `${suiteApp.availableSoon ? '' : 'cursor-pointer hover:bg-gray-1 dark:hover:bg-gray-10'}` | ||
| } | ||
| style={{ lineHeight: 1.25 }} | ||
| onClick={suiteApp.availableSoon ? undefined : suiteApp.onClick} | ||
| > | ||
| <div className="flex flex-col items-center w-full rounded-md"> | ||
| {suiteApp.isLocked ? ( | ||
| <Lock size={26} weight="regular" data-testid="suite-launcher-lock-icon" /> | ||
| ) : isValidElement(suiteApp.icon as JSX.Element) ? ( | ||
| cloneElement(suiteApp.icon as JSX.Element, { | ||
| size: 26, | ||
| className: | ||
| `${suiteApp.icon.props?.className ?? ''} ${suiteApp.isMain ? 'text-primary' : ''} ` + | ||
| `${suiteApp.availableSoon || suiteApp.isLocked ? 'opacity-50 filter grayscale' : ''}`, | ||
| weight: suiteApp.isMain ? 'fill' : 'regular', | ||
| }) | ||
| ) : ( | ||
| suiteApp.icon | ||
| )} | ||
|
|
||
| <div className="mt-1 flex items-center"> | ||
| <span | ||
| className={`text-xs ${suiteApp.isMain ? 'text-primary font-medium' : 'text-gray-60'}`} | ||
| style={{ lineHeight: 1, opacity: suiteApp.availableSoon || suiteApp.isLocked ? 0.5 : 1 }} | ||
| > | ||
| {suiteApp.title} | ||
| </span> | ||
|
|
||
| {suiteApp.availableSoon && ( | ||
| <div className="flex rounded-sm px-1 ml-1 py-0.5 bg-purple-1 dark:bg-purple-10 items-center"> | ||
| <span | ||
| className="font-medium dark:font-normal text-purple-10 dark:text-purple-1" | ||
| style={{ lineHeight: 1, fontSize: 'xx-small' }} | ||
| > | ||
| {soonText ?? 'Soon'} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can pass the label as a prop😄
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be a good option, but for now I think its better this way so its easier to config (you only decide if its availableSoon, if its disabled or if its main, and ui just decides its css. |
||
| </span> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ); | ||
|
|
||
| return ( | ||
| <Popover className={className} childrenButton={SuiteButton} panel={() => panel} data-testid="app-suite-dropdown" /> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { render, screen, fireEvent } from '@testing-library/react'; | ||
| import SuiteLauncher from '../SuiteLauncher'; | ||
| import { describe, it, expect, vi } from 'vitest'; | ||
|
|
||
| describe('SuiteLauncher', () => { | ||
| it('renders Popover button and panel', () => { | ||
| render(<SuiteLauncher suiteArray={[]} />); | ||
| expect(screen.getByTestId('popover-button')).toBeInTheDocument(); | ||
| fireEvent.click(screen.getByTestId('popover-button')); | ||
| expect(screen.getByTestId('suite-launcher-panel')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders suite items, shows soon text, renders lock icon and handles clicks correctly', () => { | ||
| const onClick1 = vi.fn(); | ||
| const onClick2 = vi.fn(); | ||
| const onClick3 = vi.fn(); | ||
|
|
||
| const Icon = (props: any) => ( | ||
| <span data-testid="mock-icon" {...props}> | ||
| Icon | ||
| </span> | ||
| ); | ||
|
|
||
| const suiteArray = [ | ||
| { icon: <Icon className="orig" />, title: 'App 1', onClick: onClick1, isMain: true }, | ||
| { icon: <Icon />, title: 'App 2', onClick: onClick2, availableSoon: true }, | ||
| { icon: <Icon />, title: 'App 3', onClick: onClick3, isLocked: true }, | ||
| ]; | ||
|
|
||
| render(<SuiteLauncher suiteArray={suiteArray} soonText="Pronto" />); | ||
| fireEvent.click(screen.getByTestId('popover-button')); | ||
|
|
||
| // Titles are rendered | ||
| expect(screen.getByText('App 1')).toBeInTheDocument(); | ||
| expect(screen.getByText('App 2')).toBeInTheDocument(); | ||
| expect(screen.getByText('App 3')).toBeInTheDocument(); | ||
|
|
||
| // Custom soonText is shown for availableSoon items | ||
| expect(screen.getByText('Pronto')).toBeInTheDocument(); | ||
|
|
||
| // Locked item shows the Lock icon | ||
| expect(screen.getByTestId('suite-launcher-lock-icon')).toBeInTheDocument(); | ||
|
|
||
| // Clicking available item triggers its onClick | ||
| fireEvent.click(screen.getByText('App 1')); | ||
| expect(onClick1).toHaveBeenCalled(); | ||
|
|
||
| // Clicking "availableSoon" item should NOT trigger its onClick | ||
| fireEvent.click(screen.getByText('App 2')); | ||
| expect(onClick2).not.toHaveBeenCalled(); | ||
|
|
||
| // Even if locked, clicking should call its onClick because isLocked keeps the icon displayed | ||
| // the onClick handler is attached but opacity/grayscale is applied | ||
| fireEvent.click(screen.getByText('App 3')); | ||
| expect(onClick3).toHaveBeenCalled(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { default as SuiteLauncher } from './SuiteLauncher'; | ||
| export type { SuiteLauncherProps } from './SuiteLauncher'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! 🚀