diff --git a/eslint-suppressions.json b/eslint-suppressions.json index d9862230670..ef487d7e69e 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3299,14 +3299,6 @@ "count": 1 } }, - "web/app/components/explore/sidebar/app-nav-item/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/explore/try-app/app/text-generation.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 diff --git a/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx b/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx index 11c3426f484..cf433b30981 100644 --- a/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx +++ b/web/__tests__/explore/sidebar-lifecycle-flow.test.tsx @@ -204,12 +204,5 @@ describe('Sidebar Lifecycle Flow', () => { expect(screen.getByText('explore.sidebar.noApps.title')).toBeInTheDocument() }) - - it('should hide NoApps on mobile', () => { - mockMediaType = MediaType.mobile - renderSidebar() - - expect(screen.queryByText('explore.sidebar.noApps.title')).not.toBeInTheDocument() - }) }) }) diff --git a/web/app/components/explore/sidebar/__tests__/index.spec.tsx b/web/app/components/explore/sidebar/__tests__/index.spec.tsx index e711b04ce86..6c0e2f7ac87 100644 --- a/web/app/components/explore/sidebar/__tests__/index.spec.tsx +++ b/web/app/components/explore/sidebar/__tests__/index.spec.tsx @@ -1,6 +1,5 @@ import type { InstalledApp } from '@/models/explore' import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { MediaType } from '@/hooks/use-breakpoints' import { expectLoadingButton } from '@/test/button' import { AppModeEnum } from '@/types/app' import SideBar from '../index' @@ -16,7 +15,6 @@ const mockUpdatePinStatus = vi.fn() let mockIsPending = false let mockIsUninstallPending = false let mockInstalledApps: InstalledApp[] = [] -let mockMediaType: string = MediaType.pc vi.mock('@/next/navigation', () => ({ usePathname: () => '/', @@ -26,15 +24,6 @@ vi.mock('@/next/navigation', () => ({ }), })) -vi.mock('@/hooks/use-breakpoints', () => ({ - default: () => mockMediaType, - MediaType: { - mobile: 'mobile', - tablet: 'tablet', - pc: 'pc', - }, -})) - vi.mock('@/service/use-explore', () => ({ useGetInstalledApps: () => ({ isPending: mockIsPending, @@ -87,7 +76,6 @@ describe('SideBar', () => { mockIsPending = false mockIsUninstallPending = false mockInstalledApps = [] - mockMediaType = MediaType.pc }) describe('Rendering', () => { @@ -97,13 +85,6 @@ describe('SideBar', () => { expect(screen.getByText('explore.sidebar.title')).toBeInTheDocument() }) - it('should expose an accessible name for the discovery link when the text is hidden', () => { - mockMediaType = MediaType.mobile - renderSideBar() - - expect(screen.getByRole('link', { name: 'explore.sidebar.title' })).toBeInTheDocument() - }) - it('should render workspace items when installed apps exist', () => { mockInstalledApps = [createInstalledApp()] renderSideBar() @@ -156,6 +137,17 @@ describe('SideBar', () => { expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument() }) + + it('should render icon-only content when folded', () => { + mockInstalledApps = [createInstalledApp()] + renderSideBar() + + fireEvent.click(screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' })) + + expect(screen.getByRole('link', { name: 'explore.sidebar.title' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'My App' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument() + }) }) describe('User Interactions', () => { @@ -229,14 +221,4 @@ describe('SideBar', () => { expectLoadingButton(screen.getByText('common.operation.confirm').closest('button')) }) }) - - describe('Edge Cases', () => { - it('should hide NoApps and app names on mobile', () => { - mockMediaType = MediaType.mobile - renderSideBar() - - expect(screen.queryByText('explore.sidebar.noApps.title')).not.toBeInTheDocument() - expect(screen.queryByText('explore.sidebar.webApps')).not.toBeInTheDocument() - }) - }) }) diff --git a/web/app/components/explore/sidebar/app-nav-item/__tests__/index.spec.tsx b/web/app/components/explore/sidebar/app-nav-item/__tests__/index.spec.tsx index fc4d128d032..b6bd4e85a5d 100644 --- a/web/app/components/explore/sidebar/app-nav-item/__tests__/index.spec.tsx +++ b/web/app/components/explore/sidebar/app-nav-item/__tests__/index.spec.tsx @@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react' import AppNavItem from '../index' const baseProps = { - isMobile: false, name: 'My App', id: 'app-123', icon_type: 'emoji' as const, @@ -22,18 +21,12 @@ describe('AppNavItem', () => { }) describe('Rendering', () => { - it('should render name and item operation on desktop', () => { + it('should render name and item operation when expanded', () => { render() expect(screen.getByText('My App')).toBeInTheDocument() expect(screen.getByTestId('item-operation-trigger')).toBeInTheDocument() }) - - it('should hide name on mobile', () => { - render() - - expect(screen.queryByText('My App')).not.toBeInTheDocument() - }) }) describe('User Interactions', () => { @@ -43,6 +36,26 @@ describe('AppNavItem', () => { const link = screen.getByRole('link', { name: 'My App' }) expect(link).toHaveAttribute('href', '/installed/app-123') + expect(link).toHaveAttribute('aria-label', 'My App') + expect(link).not.toHaveAttribute('aria-current') + }) + + it('should use a contextual accessible name when ariaLabel is provided', () => { + render() + + const link = screen.getByRole('link', { name: 'Open My App web app' }) + + expect(link).toHaveAttribute('href', '/installed/app-123') + expect(link).toHaveAttribute('aria-label', 'Open My App web app') + expect(screen.getByText('My App')).toBeInTheDocument() + }) + + it('should expose selected state through the current link', () => { + render() + + const link = screen.getByRole('link', { name: 'My App' }) + + expect(link).toHaveAttribute('aria-current', 'page') }) it('should call onDelete with app id when delete action is clicked', async () => { diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx index c132ad45e19..be53903e6dc 100644 --- a/web/app/components/explore/sidebar/app-nav-item/index.tsx +++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx @@ -9,9 +9,9 @@ import ItemOperation from '@/app/components/explore/item-operation' import Link from '@/next/link' type IAppNavItemProps = { - isMobile: boolean variant?: 'default' | 'mainNav' name: string + ariaLabel?: string id: string icon_type: AppIconType | null icon: string @@ -25,9 +25,9 @@ type IAppNavItemProps = { } export default function AppNavItem({ - isMobile, variant = 'default', name, + ariaLabel, id, icon_type, icon, @@ -47,43 +47,31 @@ export default function AppNavItem({ key={id} className={cn( isMainNav - ? 'group flex h-8 items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 transition-colors has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid' - : 'group flex h-8 items-center justify-between rounded-lg px-2 system-sm-medium text-sm font-normal text-components-menu-item-text has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid mobile:justify-center mobile:px-1', - isMainNav - ? (isSelected ? 'bg-state-base-hover' : 'hover:bg-state-base-hover') - : (isSelected ? 'bg-state-base-active text-components-menu-item-text-active' : 'hover:bg-state-base-hover hover:text-components-menu-item-text-hover'), + ? 'group flex h-8 items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 transition-colors not-has-[>a[aria-current=page]]:hover:bg-state-base-hover has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid has-[>a[aria-current=page]]:bg-state-base-active' + : cn( + 'group flex h-8 items-center rounded-lg system-sm-medium text-sm font-normal text-components-menu-item-text transition-colors not-has-[>a[aria-current=page]]:hover:bg-state-base-hover not-has-[>a[aria-current=page]]:hover:text-components-menu-item-text-hover has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid has-[>a[aria-current=page]]:bg-state-base-active has-[>a[aria-current=page]]:text-components-menu-item-text-active', + 'w-full justify-start px-2 group-data-[folded=true]/sidebar:justify-center group-data-[folded=true]/sidebar:px-1', + ), )} > - {isMobile && ( - - - - )} - {!isMobile && ( - <> - - -
{name}
- -
e.stopPropagation()}> - onDelete(id)} - /> -
- - )} + + +
{name}
+ +
+ onDelete(id)} + /> +
) } diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index 12890c58566..673175ea0db 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -16,7 +16,6 @@ import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Link from '@/next/link' import { usePathname, useSelectedLayoutSegments } from '@/next/navigation' import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' @@ -34,8 +33,6 @@ const SideBar = () => { const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp() const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() - const media = useBreakpoints() - const isMobile = media === MediaType.mobile const [isFold, { toggle: toggleIsFold, }] = useBoolean(false) @@ -55,12 +52,10 @@ const SideBar = () => { } const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length - const shouldUseExpandedScrollArea = !isMobile && !isFold const webAppsLabelId = React.useId() const installedAppItems = installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => ( { )) return ( -
+
- {!isMobile && !isFold &&
{t('sidebar.title', { ns: 'explore' })}
} + {!isFold &&
{t('sidebar.title', { ns: 'explore' })}
}
- {!isPending && installedApps.length === 0 && !isMobile && !isFold + {!isPending && installedApps.length === 0 && !isFold && (
@@ -104,8 +102,8 @@ const SideBar = () => { {installedApps.length > 0 && (
- {!isMobile && !isFold &&

{t('sidebar.webApps', { ns: 'explore' })}

} - {shouldUseExpandedScrollArea + {!isFold &&

{t('sidebar.webApps', { ns: 'explore' })}

} + {!isFold ? (
{
)} - {!isMobile && ( -
- -
- )} +
+ +
diff --git a/web/app/components/header/account-dropdown/main-nav-menu-content.tsx b/web/app/components/header/account-dropdown/main-nav-menu-content.tsx index 1b84397a1ed..3ae90caffcb 100644 --- a/web/app/components/header/account-dropdown/main-nav-menu-content.tsx +++ b/web/app/components/header/account-dropdown/main-nav-menu-content.tsx @@ -26,10 +26,6 @@ import { useProviderContext } from '@/context/provider-context' import Link from '@/next/link' import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content' -const mainNavMenuGroupClassName = 'p-1' -const mainNavMenuItemClassName = 'mx-0 h-8 gap-1 px-3 py-1' -const mainNavMenuSubPopupClassName = 'w-60 max-h-[360px] bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]' - type MainNavRadioItemContentProps = { iconClassName?: string label: ReactNode @@ -56,7 +52,7 @@ function AppearanceSubmenu() { return ( - + setTheme(value as Theme)}> - + - + - + @@ -97,7 +93,7 @@ export function MainNavMenuContent({ return ( <> - +
@@ -114,9 +110,9 @@ export function MainNavMenuContent({
- + } > setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })} > - + { void onLogout() }} diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index c9c28cd74ce..c4175b44418 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -782,7 +782,8 @@ describe('MainNav', () => { }) expect(screen.queryByText('Alpha App')).not.toBeInTheDocument() - expect(screen.getByRole('link', { name: 'Beta Tool' })).toHaveAttribute('href', '/installed/installed-2') + expect(screen.getByText('Beta Tool')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'common.mainNav.webApps.openApp:{"name":"Beta Tool"}' })).toHaveAttribute('href', '/installed/installed-2') }) it('renders web app skeleton rows while installed apps are loading', () => { diff --git a/web/app/components/main-nav/components/web-apps-section.tsx b/web/app/components/main-nav/components/web-apps-section.tsx index 448207b8ec3..7062cf96cb7 100644 --- a/web/app/components/main-nav/components/web-apps-section.tsx +++ b/web/app/components/main-nav/components/web-apps-section.tsx @@ -148,8 +148,8 @@ const WebAppsSectionContent = () => {