From 78b261d48c1f472bfac884abbd8212543117b95b Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 1 Dec 2025 12:22:08 +0100 Subject: [PATCH 1/6] feat: add SuiteLauncher component with tests --- src/components/index.ts | 1 + .../suiteLauncher/SuiteLauncher.tsx | 107 ++++++++++++++++++ .../__test__/SuiteLauncher.test.tsx | 57 ++++++++++ src/components/suiteLauncher/index.ts | 2 + 4 files changed, 167 insertions(+) create mode 100644 src/components/suiteLauncher/SuiteLauncher.tsx create mode 100644 src/components/suiteLauncher/__test__/SuiteLauncher.test.tsx create mode 100644 src/components/suiteLauncher/index.ts diff --git a/src/components/index.ts b/src/components/index.ts index 28b9097..0a49e0b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -22,6 +22,7 @@ export * from './popover'; export * from './radioButton'; export * from './skeletonLoader'; export * from './slider'; +export * from './suiteLauncher'; export * from './switch'; export * from './table/Table'; export * from './textArea'; diff --git a/src/components/suiteLauncher/SuiteLauncher.tsx b/src/components/suiteLauncher/SuiteLauncher.tsx new file mode 100644 index 0000000..99b9fdf --- /dev/null +++ b/src/components/suiteLauncher/SuiteLauncher.tsx @@ -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): JSX.Element { + const SuiteButton = ( +
+ +
+ ); + + const panel = ( +
+ {suiteArray.map((suiteApp, idx) => ( +
+
+
+ {suiteApp.isLocked ? ( + + ) : 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 + )} + +
+ + {suiteApp.title} + + + {suiteApp.availableSoon && ( +
+ + {soonText ?? 'Soon'} + +
+ )} +
+
+
+
+ ))} +
+ ); + + return ( + panel} data-testid="app-suite-dropdown" /> + ); +} diff --git a/src/components/suiteLauncher/__test__/SuiteLauncher.test.tsx b/src/components/suiteLauncher/__test__/SuiteLauncher.test.tsx new file mode 100644 index 0000000..93d0e94 --- /dev/null +++ b/src/components/suiteLauncher/__test__/SuiteLauncher.test.tsx @@ -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(); + 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) => ( + + Icon + + ); + + const suiteArray = [ + { icon: , title: 'App 1', onClick: onClick1, isMain: true }, + { icon: , title: 'App 2', onClick: onClick2, availableSoon: true }, + { icon: , title: 'App 3', onClick: onClick3, isLocked: true }, + ]; + + render(); + 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(); + }); +}); diff --git a/src/components/suiteLauncher/index.ts b/src/components/suiteLauncher/index.ts new file mode 100644 index 0000000..fcb22a6 --- /dev/null +++ b/src/components/suiteLauncher/index.ts @@ -0,0 +1,2 @@ +export { default as SuiteLauncher } from './SuiteLauncher'; +export type { SuiteLauncherProps } from './SuiteLauncher'; From 0fcc256143ad69d8a025299813ddeef8b883f692 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 1 Dec 2025 12:23:22 +0100 Subject: [PATCH 2/6] feat: improve dialog component by allowing support to JSX elements and adding maxWidth prop --- src/components/dialog/Dialog.tsx | 41 ++++++++++++++------------------ 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/components/dialog/Dialog.tsx b/src/components/dialog/Dialog.tsx index cf26845..658f499 100644 --- a/src/components/dialog/Dialog.tsx +++ b/src/components/dialog/Dialog.tsx @@ -9,10 +9,11 @@ export interface DialogProps { onSecondaryAction: () => void; title: string; subtitle: string; - primaryAction: string; - secondaryAction: string; + primaryAction: string | JSX.Element; + secondaryAction: string | JSX.Element; primaryActionColor: 'primary' | 'danger'; isLoading?: boolean; + maxWidth?: 'sm' | 'md' | 'lg'; } /** @@ -36,11 +37,11 @@ export interface DialogProps { * @property {string} subtitle * - A subtitle for the dialog, displayed below the title. * - * @property {string} primaryAction - * - The label for the primary action button. + * @property {string | JSX.Element} primaryAction + * - The label or content for the primary action button. * * @property {string} secondaryAction - * - The label for the secondary action button. + * - The label or content for the secondary action button. * * @property {('primary' | 'danger')} primaryActionColor * - Defines the color of the primary action button. Can either be 'primary' or 'danger'. @@ -48,6 +49,9 @@ export interface DialogProps { * @property {boolean} [isLoading] * - Optional flag to indicate if the buttons should show a loading state. Defaults to false. * + * @property {'sm' | 'md' | 'lg'} [maxWidth] + * - Optional maximum width for the dialog. Can be 'sm', 'md', or 'lg'. + * * @returns {JSX.Element} * - The rendered dialog component. */ @@ -63,6 +67,7 @@ const Dialog = ({ secondaryAction, primaryActionColor, isLoading, + maxWidth = 'sm', }: DialogProps): JSX.Element => { const [isVisible, setIsVisible] = useState(isOpen); const [transitionOpacity, setTransitionOpacity] = useState('opacity-0'); @@ -107,29 +112,19 @@ const Dialog = ({ {isVisible && (

{title}

From 731b5f07c5175919c3f26a61f1d300320399819d Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 1 Dec 2025 12:23:50 +0100 Subject: [PATCH 3/6] feat: streamline button and panel class handling, add data-testid for testing --- src/components/popover/Popover.tsx | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/components/popover/Popover.tsx b/src/components/popover/Popover.tsx index 0ecd7ee..8651d49 100644 --- a/src/components/popover/Popover.tsx +++ b/src/components/popover/Popover.tsx @@ -77,34 +77,19 @@ const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps
{showContent && (
{panel(closePopover)}
From 6373fb5ed962f8d70b3a97c8aaf712e2e4e8dba5 Mon Sep 17 00:00:00 2001 From: larryrider Date: Mon, 1 Dec 2025 12:26:34 +0100 Subject: [PATCH 4/6] fix: test snapshots --- .../__snapshots__/Dialog.test.tsx.snap | 20 ++----------------- .../__snapshots__/Popover.test.tsx.snap | 2 ++ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/components/dialog/__test__/__snapshots__/Dialog.test.tsx.snap b/src/components/dialog/__test__/__snapshots__/Dialog.test.tsx.snap index 6583e8f..f437270 100644 --- a/src/components/dialog/__test__/__snapshots__/Dialog.test.tsx.snap +++ b/src/components/dialog/__test__/__snapshots__/Dialog.test.tsx.snap @@ -6,27 +6,11 @@ exports[`Dialog > should match snapshot 1`] = ` class="fixed inset-0 z-50 " >
should match snapshot 1`] = `