From fe9b5c36f35be5455b781bbcc55f073605f458a0 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Thu, 7 May 2026 15:46:09 -0600 Subject: [PATCH 1/4] feat(sidenav): add notification slot above storage --- src/components/navigation/sidenav/Sidenav.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/navigation/sidenav/Sidenav.tsx b/src/components/navigation/sidenav/Sidenav.tsx index 3986f93..cdcc014 100644 --- a/src/components/navigation/sidenav/Sidenav.tsx +++ b/src/components/navigation/sidenav/Sidenav.tsx @@ -39,6 +39,7 @@ export interface SidenavProps { showSubsections?: boolean; isCollapsed?: boolean; storage?: SidenavStorageProps; + notification?: ReactNode; onToggleCollapse?: () => void; } @@ -55,6 +56,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 {ReactNode} notification - Optional notification node rendered above the storage section (hidden when collapsed) * @property {() => void} onToggleCollapse - A callback function triggered when the collapse button is clicked */ const Sidenav = ({ @@ -66,6 +68,7 @@ const Sidenav = ({ showSubsections, isCollapsed = false, storage, + notification, onToggleCollapse, }: SidenavProps) => { return ( @@ -94,18 +97,21 @@ const Sidenav = ({ - {storage && ( + {(notification || storage) && (
- + {notification &&
{notification}
} + {storage && ( + + )}
)} From eaf8967826a95d20d23b289af6775fe165027690 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Thu, 7 May 2026 15:51:49 -0600 Subject: [PATCH 2/4] chore: bump version to 0.1.16 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From abecabce1ec1fb4e8fb041f67412a1e829d31596 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Thu, 7 May 2026 20:44:39 -0600 Subject: [PATCH 3/4] fix(sidenav): update snapshot in Sidenav component --- .../sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..4b12928 100644 --- a/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap +++ b/src/components/navigation/sidenav/__test__/__snapshots__/Sidenav.test.tsx.snap @@ -717,7 +717,7 @@ exports[`Sidenav Component > should match snapshot with storage 1`] = `
Date: Mon, 11 May 2026 09:01:48 -0600 Subject: [PATCH 4/4] feat(sidenav): enhance notification handling and add animation on collapse --- src/components/navigation/sidenav/Sidenav.tsx | 51 ++++- .../sidenav/__test__/Sidenav.test.tsx | 68 +++++- .../__snapshots__/Sidenav.test.tsx.snap | 212 +++++++++++++++++- 3 files changed, 316 insertions(+), 15 deletions(-) diff --git a/src/components/navigation/sidenav/Sidenav.tsx b/src/components/navigation/sidenav/Sidenav.tsx index cdcc014..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,7 +40,12 @@ export interface SidenavProps { showSubsections?: boolean; isCollapsed?: boolean; storage?: SidenavStorageProps; - notification?: ReactNode; + notification?: { + message: string; + actionLabel: string; + onAction: () => void; + type?: 'warning'; + }; onToggleCollapse?: () => void; } @@ -56,7 +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 {ReactNode} notification - Optional notification node rendered above the storage section (hidden when collapsed) + * @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 = ({ @@ -71,6 +77,21 @@ const Sidenav = ({ 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 (
- {(notification || storage) && ( -
- {notification &&
{notification}
} + {(notification || storage) && showContent && ( +
+ {notification && ( +
+
+ +
+

{notification.message}

+ +
+
+
+ )} {storage && ( { }); 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 4b12928..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`] = `