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 = () => {