diff --git a/package.json b/package.json index f60e025..c18ae22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@internxt/ui", - "version": "0.1.15", + "version": "0.1.16", "description": "Library of Internxt components", "repository": { "type": "git", @@ -98,4 +98,4 @@ "prettier --write" ] } -} +} \ No newline at end of file diff --git a/src/components/navigation/sidenav/Sidenav.tsx b/src/components/navigation/sidenav/Sidenav.tsx index 3986f93..68f8c89 100644 --- a/src/components/navigation/sidenav/Sidenav.tsx +++ b/src/components/navigation/sidenav/Sidenav.tsx @@ -1,4 +1,5 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { WarningIcon } from '@phosphor-icons/react'; import SidenavOptions, { SidenavOption } from './SidenavOptions'; import SidenavHeader from './SidenavHeader'; import SidenavStorage from './SidenavStorage'; @@ -39,6 +40,12 @@ export interface SidenavProps { showSubsections?: boolean; isCollapsed?: boolean; storage?: SidenavStorageProps; + notification?: { + message: string; + actionLabel: string; + onAction: () => void; + type?: 'warning'; + }; onToggleCollapse?: () => void; } @@ -55,6 +62,7 @@ export interface SidenavProps { * @property {boolean} showSubsections - Determines whether to display the subsections of the sidenav * @property {boolean} isCollapsed - Determines whether the sidenav is collapsed or not * @property {SidenavStorage} storage - The storage information displayed at the bottom of the sidenav + * @property {object} notification - Optional structured notification rendered above the storage section (hidden when collapsed). Accepts message, actionLabel, onAction, and an optional type ('warning'). * @property {() => void} onToggleCollapse - A callback function triggered when the collapse button is clicked */ const Sidenav = ({ @@ -66,8 +74,24 @@ const Sidenav = ({ showSubsections, isCollapsed = false, storage, + notification, onToggleCollapse, }: SidenavProps) => { + const [showContent, setShowContent] = useState(!isCollapsed); + const timerRef = useRef | null>(null); + + useEffect(() => { + if (timerRef.current) clearTimeout(timerRef.current); + if (isCollapsed) { + setShowContent(false); + } else { + timerRef.current = setTimeout(() => setShowContent(true), 300); + } + return () => { + if (timerRef.current) clearTimeout(timerRef.current); + }; + }, [isCollapsed]); + return (
- {storage && ( -
- + {(notification || storage) && showContent && ( +
+ {notification && ( +
+
+ +
+

{notification.message}

+ +
+
+
+ )} + {storage && ( + + )}
)}
diff --git a/src/components/navigation/sidenav/__test__/Sidenav.test.tsx b/src/components/navigation/sidenav/__test__/Sidenav.test.tsx index 0c6e7e4..142a7e1 100644 --- a/src/components/navigation/sidenav/__test__/Sidenav.test.tsx +++ b/src/components/navigation/sidenav/__test__/Sidenav.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, act } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import '@testing-library/jest-dom'; import Sidenav, { SidenavProps } from '../Sidenav'; @@ -217,7 +217,7 @@ describe('Sidenav Component', () => { }); it('hides storage when collapsed', () => { - const { getByText } = renderSidenav({ + const { queryByText } = renderSidenav({ isCollapsed: true, storage: { usage: '2.8 GB', @@ -228,10 +228,8 @@ describe('Sidenav Component', () => { isLoading: false, }, }); - const usageText = getByText('2.8 GB'); - const storageContainer = usageText.closest('div')?.parentElement?.parentElement?.parentElement; - expect(storageContainer).toHaveClass('opacity-0'); - expect(storageContainer).toHaveClass('invisible'); + expect(queryByText('2.8 GB')).not.toBeInTheDocument(); + expect(queryByText('4 GB')).not.toBeInTheDocument(); }); it('hides subsections when collapsed even if showSubsections is true', () => { @@ -260,6 +258,64 @@ describe('Sidenav Component', () => { }); }); + describe('Notification', () => { + const onAction = vi.fn(); + const notification = { + message: 'Your account will be deleted in 14 days.', + actionLabel: 'Upgrade plan', + onAction, + }; + + it('should match snapshot with notification', () => { + const { container } = renderSidenav({ notification }); + expect(container).toMatchSnapshot(); + }); + + it('renders notification message and action label', () => { + const { getByText } = renderSidenav({ notification }); + expect(getByText('Your account will be deleted in 14 days.')).toBeInTheDocument(); + expect(getByText('Upgrade plan')).toBeInTheDocument(); + }); + + it('calls onAction when action button is clicked', () => { + const { getByText } = renderSidenav({ notification }); + fireEvent.click(getByText('Upgrade plan')); + expect(onAction).toHaveBeenCalledOnce(); + }); + + it('does not render notification when isCollapsed is true', () => { + const { queryByText } = renderSidenav({ notification, isCollapsed: true }); + expect(queryByText('Your account will be deleted in 14 days.')).not.toBeInTheDocument(); + }); + + it('hides notification immediately when sidenav collapses', () => { + vi.useFakeTimers(); + const { getByText, queryByText, rerender } = renderSidenav({ notification, isCollapsed: false }); + expect(getByText('Your account will be deleted in 14 days.')).toBeInTheDocument(); + + rerender(); + expect(queryByText('Your account will be deleted in 14 days.')).not.toBeInTheDocument(); + + vi.useRealTimers(); + }); + + it('shows notification after 300ms when sidenav expands', () => { + vi.useFakeTimers(); + const { getByText, queryByText, rerender } = renderSidenav({ notification, isCollapsed: true }); + expect(queryByText('Your account will be deleted in 14 days.')).not.toBeInTheDocument(); + + rerender(); + expect(queryByText('Your account will be deleted in 14 days.')).not.toBeInTheDocument(); + + act(() => { + vi.advanceTimersByTime(300); + }); + expect(getByText('Your account will be deleted in 14 days.')).toBeInTheDocument(); + + vi.useRealTimers(); + }); + }); + describe('Collapse toggle button', () => { it('does not render collapse button when onToggleCollapse is not provided', () => { const { container } = renderSidenav(); diff --git a/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap b/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap index f9c9a4d..b944554 100644 --- a/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap +++ b/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap @@ -1,5 +1,215 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Sidenav Component > Notification > should match snapshot with notification 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+ + + +
+

+ Your account will be deleted in 14 days. +

+ +
+
+
+
+
+
+`; + exports[`Sidenav Component > should match snapshot 1`] = `
should match snapshot with storage 1`] = `