refactor: simplify the scroll area API for sidebar layouts (#33761)

This commit is contained in:
yyh
2026-03-19 19:54:16 +08:00
committed by GitHub
parent bb1a6f8a57
commit 70a68f0a86
5 changed files with 138 additions and 56 deletions

View File

@ -4,6 +4,7 @@ import {
ScrollArea, ScrollArea,
ScrollAreaContent, ScrollAreaContent,
ScrollAreaCorner, ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar, ScrollAreaScrollbar,
ScrollAreaThumb, ScrollAreaThumb,
ScrollAreaViewport, ScrollAreaViewport,
@ -19,7 +20,7 @@ const renderScrollArea = (options: {
horizontalThumbClassName?: string horizontalThumbClassName?: string
} = {}) => { } = {}) => {
return render( return render(
<ScrollArea className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root"> <ScrollAreaRoot className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
<ScrollAreaViewport data-testid="scroll-area-viewport" className={options.viewportClassName}> <ScrollAreaViewport data-testid="scroll-area-viewport" className={options.viewportClassName}>
<ScrollAreaContent data-testid="scroll-area-content"> <ScrollAreaContent data-testid="scroll-area-content">
<div className="h-48 w-48">Scrollable content</div> <div className="h-48 w-48">Scrollable content</div>
@ -43,7 +44,7 @@ const renderScrollArea = (options: {
className={options.horizontalThumbClassName} className={options.horizontalThumbClassName}
/> />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea>, </ScrollAreaRoot>,
) )
} }
@ -62,6 +63,38 @@ describe('scroll-area wrapper', () => {
expect(screen.getByTestId('scroll-area-horizontal-thumb')).toBeInTheDocument() expect(screen.getByTestId('scroll-area-horizontal-thumb')).toBeInTheDocument()
}) })
}) })
it('should render the convenience wrapper and apply slot props', async () => {
render(
<>
<p id="installed-apps-label">Installed apps</p>
<ScrollArea
className="h-40 w-40"
slotClassNames={{
content: 'custom-content-class',
scrollbar: 'custom-scrollbar-class',
viewport: 'custom-viewport-class',
}}
labelledBy="installed-apps-label"
data-testid="scroll-area-wrapper-root"
>
<div className="h-48 w-20">Scrollable content</div>
</ScrollArea>
</>,
)
await waitFor(() => {
const root = screen.getByTestId('scroll-area-wrapper-root')
const viewport = screen.getByRole('region', { name: 'Installed apps' })
const content = screen.getByText('Scrollable content').parentElement
expect(root).toBeInTheDocument()
expect(viewport).toHaveClass('custom-viewport-class')
expect(viewport).toHaveAccessibleName('Installed apps')
expect(content).toHaveClass('custom-content-class')
expect(screen.getByText('Scrollable content')).toBeInTheDocument()
})
})
}) })
describe('Scrollbar', () => { describe('Scrollbar', () => {
@ -219,7 +252,7 @@ describe('scroll-area wrapper', () => {
try { try {
render( render(
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root"> <ScrollAreaRoot className="h-40 w-40" data-testid="scroll-area-root">
<ScrollAreaViewport data-testid="scroll-area-viewport"> <ScrollAreaViewport data-testid="scroll-area-viewport">
<ScrollAreaContent data-testid="scroll-area-content"> <ScrollAreaContent data-testid="scroll-area-content">
<div className="h-48 w-48">Scrollable content</div> <div className="h-48 w-48">Scrollable content</div>
@ -236,7 +269,7 @@ describe('scroll-area wrapper', () => {
<ScrollAreaThumb data-testid="scroll-area-horizontal-thumb" /> <ScrollAreaThumb data-testid="scroll-area-horizontal-thumb" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
<ScrollAreaCorner data-testid="scroll-area-corner" /> <ScrollAreaCorner data-testid="scroll-area-corner" />
</ScrollArea>, </ScrollAreaRoot>,
) )
await waitFor(() => { await waitFor(() => {

View File

@ -4,9 +4,9 @@ import * as React from 'react'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import { import {
ScrollArea,
ScrollAreaContent, ScrollAreaContent,
ScrollAreaCorner, ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar, ScrollAreaScrollbar,
ScrollAreaThumb, ScrollAreaThumb,
ScrollAreaViewport, ScrollAreaViewport,
@ -14,7 +14,7 @@ import {
const meta = { const meta = {
title: 'Base/Layout/ScrollArea', title: 'Base/Layout/ScrollArea',
component: ScrollArea, component: ScrollAreaRoot,
parameters: { parameters: {
layout: 'padded', layout: 'padded',
docs: { docs: {
@ -24,7 +24,7 @@ const meta = {
}, },
}, },
tags: ['autodocs'], tags: ['autodocs'],
} satisfies Meta<typeof ScrollArea> } satisfies Meta<typeof ScrollAreaRoot>
export default meta export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
@ -135,7 +135,7 @@ const StoryCard = ({
const VerticalPanelPane = () => ( const VerticalPanelPane = () => (
<div className={cn(panelClassName, 'h-[360px]')}> <div className={cn(panelClassName, 'h-[360px]')}>
<ScrollArea className={insetScrollAreaClassName}> <ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={insetViewportClassName}> <ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="space-y-3 p-4 pr-6"> <ScrollAreaContent className="space-y-3 p-4 pr-6">
<div className="space-y-1"> <div className="space-y-1">
@ -161,13 +161,13 @@ const VerticalPanelPane = () => (
<ScrollAreaScrollbar className={insetScrollbarClassName}> <ScrollAreaScrollbar className={insetScrollbarClassName}>
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
) )
const StickyListPane = () => ( const StickyListPane = () => (
<div className={cn(panelClassName, 'h-[360px]')}> <div className={cn(panelClassName, 'h-[360px]')}>
<ScrollArea className={insetScrollAreaClassName}> <ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={cn(insetViewportClassName, '[mask-image:linear-gradient(to_bottom,transparent_0px,black_10px,black_calc(100%-14px),transparent_100%)]')}> <ScrollAreaViewport className={cn(insetViewportClassName, '[mask-image:linear-gradient(to_bottom,transparent_0px,black_10px,black_calc(100%-14px),transparent_100%)]')}>
<ScrollAreaContent className="min-h-full"> <ScrollAreaContent className="min-h-full">
<div className="sticky top-0 z-10 border-b border-divider-subtle bg-components-panel-bg px-4 pb-3 pt-4"> <div className="sticky top-0 z-10 border-b border-divider-subtle bg-components-panel-bg px-4 pb-3 pt-4">
@ -200,7 +200,7 @@ const StickyListPane = () => (
<ScrollAreaScrollbar className={insetScrollbarClassName}> <ScrollAreaScrollbar className={insetScrollbarClassName}>
<ScrollAreaThumb className="rounded-full" /> <ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
) )
@ -216,7 +216,7 @@ const WorkbenchPane = ({
className?: string className?: string
}) => ( }) => (
<div className={cn(panelClassName, 'min-h-0', className)}> <div className={cn(panelClassName, 'min-h-0', className)}>
<ScrollArea className={insetScrollAreaClassName}> <ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={insetViewportClassName}> <ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="space-y-3 p-4 pr-6"> <ScrollAreaContent className="space-y-3 p-4 pr-6">
<div className="space-y-1"> <div className="space-y-1">
@ -229,13 +229,13 @@ const WorkbenchPane = ({
<ScrollAreaScrollbar className={insetScrollbarClassName}> <ScrollAreaScrollbar className={insetScrollbarClassName}>
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
) )
const HorizontalRailPane = () => ( const HorizontalRailPane = () => (
<div className={cn(panelClassName, 'h-[272px] min-w-0 max-w-full')}> <div className={cn(panelClassName, 'h-[272px] min-w-0 max-w-full')}>
<ScrollArea className={insetScrollAreaClassName}> <ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={insetViewportClassName}> <ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6"> <ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6">
<div className="space-y-1"> <div className="space-y-1">
@ -262,7 +262,7 @@ const HorizontalRailPane = () => (
<ScrollAreaScrollbar orientation="horizontal" className={insetScrollbarClassName}> <ScrollAreaScrollbar orientation="horizontal" className={insetScrollbarClassName}>
<ScrollAreaThumb className="rounded-full" /> <ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
) )
@ -319,7 +319,7 @@ const ScrollbarStatePane = ({
<p className="text-text-secondary system-sm-regular">{description}</p> <p className="text-text-secondary system-sm-regular">{description}</p>
</div> </div>
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3"> <div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
<ScrollArea className="h-[320px] p-1"> <ScrollAreaRoot className="h-[320px] p-1">
<ScrollAreaViewport id={viewportId} className="rounded-[20px] bg-components-panel-bg"> <ScrollAreaViewport id={viewportId} className="rounded-[20px] bg-components-panel-bg">
<ScrollAreaContent className="min-w-0 space-y-2 p-4 pr-6"> <ScrollAreaContent className="min-w-0 space-y-2 p-4 pr-6">
{scrollbarShowcaseRows.map(item => ( {scrollbarShowcaseRows.map(item => (
@ -333,7 +333,7 @@ const ScrollbarStatePane = ({
<ScrollAreaScrollbar className={insetScrollbarClassName}> <ScrollAreaScrollbar className={insetScrollbarClassName}>
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
</div> </div>
) )
@ -347,7 +347,7 @@ const HorizontalScrollbarShowcasePane = () => (
<p className="text-text-secondary system-sm-regular">Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.</p> <p className="text-text-secondary system-sm-regular">Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.</p>
</div> </div>
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3"> <div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
<ScrollArea className="h-[240px] p-1"> <ScrollAreaRoot className="h-[240px] p-1">
<ScrollAreaViewport className="rounded-[20px] bg-components-panel-bg"> <ScrollAreaViewport className="rounded-[20px] bg-components-panel-bg">
<ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6"> <ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6">
<div className="space-y-1"> <div className="space-y-1">
@ -367,7 +367,7 @@ const HorizontalScrollbarShowcasePane = () => (
<ScrollAreaScrollbar orientation="horizontal" className={insetScrollbarClassName}> <ScrollAreaScrollbar orientation="horizontal" className={insetScrollbarClassName}>
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
</div> </div>
) )
@ -375,7 +375,7 @@ const HorizontalScrollbarShowcasePane = () => (
const OverlayPane = () => ( const OverlayPane = () => (
<div className="flex h-[420px] min-w-0 items-center justify-center rounded-[28px] bg-[radial-gradient(circle_at_top,_rgba(21,90,239,0.12),_transparent_45%),linear-gradient(180deg,rgba(16,24,40,0.03),transparent)] p-6"> <div className="flex h-[420px] min-w-0 items-center justify-center rounded-[28px] bg-[radial-gradient(circle_at_top,_rgba(21,90,239,0.12),_transparent_45%),linear-gradient(180deg,rgba(16,24,40,0.03),transparent)] p-6">
<div className={cn(blurPanelClassName, 'w-full max-w-[360px]')}> <div className={cn(blurPanelClassName, 'w-full max-w-[360px]')}>
<ScrollArea className="h-[320px] p-1"> <ScrollAreaRoot className="h-[320px] p-1">
<ScrollAreaViewport className="overscroll-contain rounded-[20px] bg-components-panel-bg-blur"> <ScrollAreaViewport className="overscroll-contain rounded-[20px] bg-components-panel-bg-blur">
<ScrollAreaContent className="space-y-2 p-3 pr-6"> <ScrollAreaContent className="space-y-2 p-3 pr-6">
<div className="sticky top-0 z-10 rounded-xl border border-divider-subtle bg-components-panel-bg-blur px-3 py-3 backdrop-blur-[6px]"> <div className="sticky top-0 z-10 rounded-xl border border-divider-subtle bg-components-panel-bg-blur px-3 py-3 backdrop-blur-[6px]">
@ -400,14 +400,14 @@ const OverlayPane = () => (
<ScrollAreaScrollbar className={insetScrollbarClassName}> <ScrollAreaScrollbar className={insetScrollbarClassName}>
<ScrollAreaThumb className="rounded-full bg-state-base-handle hover:bg-state-base-handle-hover" /> <ScrollAreaThumb className="rounded-full bg-state-base-handle hover:bg-state-base-handle-hover" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
</div> </div>
) )
const CornerPane = () => ( const CornerPane = () => (
<div className={cn(panelClassName, 'h-[320px] w-full max-w-[440px]')}> <div className={cn(panelClassName, 'h-[320px] w-full max-w-[440px]')}>
<ScrollArea className={cn(insetScrollAreaClassName, 'overflow-hidden')}> <ScrollAreaRoot className={cn(insetScrollAreaClassName, 'overflow-hidden')}>
<ScrollAreaViewport className={cn(insetViewportClassName, 'bg-[linear-gradient(180deg,var(--color-components-panel-bg),var(--color-components-panel-bg-alt))]')}> <ScrollAreaViewport className={cn(insetViewportClassName, 'bg-[linear-gradient(180deg,var(--color-components-panel-bg),var(--color-components-panel-bg-alt))]')}>
<ScrollAreaContent className="min-h-[420px] min-w-[620px] space-y-4 p-4"> <ScrollAreaContent className="min-h-[420px] min-w-[620px] space-y-4 p-4">
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
@ -443,7 +443,7 @@ const CornerPane = () => (
<ScrollAreaThumb className="rounded-full" /> <ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
<ScrollAreaCorner className="bg-[linear-gradient(180deg,var(--color-components-panel-bg),var(--color-components-panel-bg-alt))]" /> <ScrollAreaCorner className="bg-[linear-gradient(180deg,var(--color-components-panel-bg),var(--color-components-panel-bg-alt))]" />
</ScrollArea> </ScrollAreaRoot>
</div> </div>
) )
@ -475,7 +475,7 @@ const ExploreSidebarWebAppsPane = () => {
</div> </div>
<div className="h-[304px]"> <div className="h-[304px]">
<ScrollArea className={sidebarScrollAreaClassName}> <ScrollAreaRoot className={sidebarScrollAreaClassName}>
<ScrollAreaViewport className={sidebarViewportClassName}> <ScrollAreaViewport className={sidebarViewportClassName}>
<ScrollAreaContent className={sidebarContentClassName}> <ScrollAreaContent className={sidebarContentClassName}>
{webAppsRows.map((item, index) => ( {webAppsRows.map((item, index) => (
@ -519,7 +519,7 @@ const ExploreSidebarWebAppsPane = () => {
<ScrollAreaScrollbar className={sidebarScrollbarClassName}> <ScrollAreaScrollbar className={sidebarScrollbarClassName}>
<ScrollAreaThumb className="rounded-full" /> <ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
</ScrollArea> </ScrollAreaRoot>
</div> </div>
</div> </div>
</div> </div>
@ -654,7 +654,7 @@ export const PrimitiveComposition: Story = {
description="A stripped-down example for teams that want to start from the base API and add their own shell classes around it. The outer shell adds inset padding so the tracks sit inside the rounded surface instead of colliding with the panel corners." description="A stripped-down example for teams that want to start from the base API and add their own shell classes around it. The outer shell adds inset padding so the tracks sit inside the rounded surface instead of colliding with the panel corners."
> >
<div className={cn(panelClassName, 'h-[260px] max-w-[420px]')}> <div className={cn(panelClassName, 'h-[260px] max-w-[420px]')}>
<ScrollArea className={insetScrollAreaClassName}> <ScrollAreaRoot className={insetScrollAreaClassName}>
<ScrollAreaViewport className={insetViewportClassName}> <ScrollAreaViewport className={insetViewportClassName}>
<ScrollAreaContent className="min-w-[560px] space-y-3 p-4 pr-6"> <ScrollAreaContent className="min-w-[560px] space-y-3 p-4 pr-6">
{Array.from({ length: 8 }, (_, index) => ( {Array.from({ length: 8 }, (_, index) => (
@ -673,7 +673,7 @@ export const PrimitiveComposition: Story = {
<ScrollAreaThumb /> <ScrollAreaThumb />
</ScrollAreaScrollbar> </ScrollAreaScrollbar>
<ScrollAreaCorner /> <ScrollAreaCorner />
</ScrollArea> </ScrollAreaRoot>
</div> </div>
</StoryCard> </StoryCard>
), ),

View File

@ -5,12 +5,26 @@ import * as React from 'react'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import styles from './index.module.css' import styles from './index.module.css'
export const ScrollArea = BaseScrollArea.Root export const ScrollAreaRoot = BaseScrollArea.Root
export type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root> export type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root>
export const ScrollAreaContent = BaseScrollArea.Content export const ScrollAreaContent = BaseScrollArea.Content
export type ScrollAreaContentProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Content> export type ScrollAreaContentProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Content>
export type ScrollAreaSlotClassNames = {
viewport?: string
content?: string
scrollbar?: string
}
export type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
children: React.ReactNode
orientation?: 'vertical' | 'horizontal'
slotClassNames?: ScrollAreaSlotClassNames
label?: string
labelledBy?: string
}
export const scrollAreaScrollbarClassName = cn( export const scrollAreaScrollbarClassName = cn(
styles.scrollbar, styles.scrollbar,
'flex touch-none select-none overflow-clip p-1 opacity-100 transition-opacity motion-reduce:transition-none', 'flex touch-none select-none overflow-clip p-1 opacity-100 transition-opacity motion-reduce:transition-none',
@ -88,3 +102,31 @@ export function ScrollAreaCorner({
/> />
) )
} }
export function ScrollArea({
children,
className,
orientation = 'vertical',
slotClassNames,
label,
labelledBy,
...props
}: ScrollAreaProps) {
return (
<ScrollAreaRoot className={className} {...props}>
<ScrollAreaViewport
aria-label={label}
aria-labelledby={labelledBy}
className={slotClassNames?.viewport}
role={label || labelledBy ? 'region' : undefined}
>
<ScrollAreaContent className={slotClassNames?.content}>
{children}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar orientation={orientation} className={slotClassNames?.scrollbar}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
)
}

View File

@ -93,6 +93,13 @@ describe('SideBar', () => {
expect(screen.getByText('explore.sidebar.title')).toBeInTheDocument() 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', () => { it('should render workspace items when installed apps exist', () => {
mockInstalledApps = [createInstalledApp()] mockInstalledApps = [createInstalledApp()]
renderSideBar() renderSideBar()
@ -136,6 +143,15 @@ describe('SideBar', () => {
const dividers = container.querySelectorAll('[class*="divider"], hr') const dividers = container.querySelectorAll('[class*="divider"], hr')
expect(dividers.length).toBeGreaterThan(0) expect(dividers.length).toBeGreaterThan(0)
}) })
it('should render a button for toggling the sidebar and update its accessible name', () => {
renderSideBar()
const toggleButton = screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' })
fireEvent.click(toggleButton)
expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument()
})
}) })
describe('User Interactions', () => { describe('User Interactions', () => {

View File

@ -13,13 +13,7 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogTitle, AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog' } from '@/app/components/base/ui/alert-dialog'
import { import { ScrollArea } from '@/app/components/base/ui/scroll-area'
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@/app/components/base/ui/scroll-area'
import { toast } from '@/app/components/base/ui/toast' import { toast } from '@/app/components/base/ui/toast'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Link from '@/next/link' import Link from '@/next/link'
@ -30,11 +24,9 @@ import Item from './app-nav-item'
import NoApps from './no-apps' import NoApps from './no-apps'
const expandedSidebarScrollAreaClassNames = { const expandedSidebarScrollAreaClassNames = {
root: 'h-full',
viewport: 'overscroll-contain',
content: 'space-y-0.5', content: 'space-y-0.5',
scrollbar: 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]', scrollbar: 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]',
thumb: 'rounded-full', viewport: 'overscroll-contain',
} as const } as const
const SideBar = () => { const SideBar = () => {
@ -104,10 +96,11 @@ const SideBar = () => {
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}> <div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
<Link <Link
href="/explore/apps" href="/explore/apps"
aria-label={isMobile || isFold ? t('sidebar.title', { ns: 'explore' }) : undefined}
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')} className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
> >
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid"> <div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid">
<span className="i-ri-apps-fill size-3.5 text-components-avatar-shape-fill-stop-100" /> <span aria-hidden="true" className="i-ri-apps-fill size-3.5 text-components-avatar-shape-fill-stop-100" />
</div> </div>
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-regular')}>{t('sidebar.title', { ns: 'explore' })}</div>} {!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-regular')}>{t('sidebar.title', { ns: 'explore' })}</div>}
</Link> </Link>
@ -126,19 +119,12 @@ const SideBar = () => {
{shouldUseExpandedScrollArea {shouldUseExpandedScrollArea
? ( ? (
<div className="min-h-0 flex-1"> <div className="min-h-0 flex-1">
<ScrollArea className={expandedSidebarScrollAreaClassNames.root}> <ScrollArea
<ScrollAreaViewport className="h-full"
aria-labelledby={webAppsLabelId} slotClassNames={expandedSidebarScrollAreaClassNames}
className={expandedSidebarScrollAreaClassNames.viewport} labelledBy={webAppsLabelId}
role="region" >
> {installedAppItems}
<ScrollAreaContent className={expandedSidebarScrollAreaClassNames.content}>
{installedAppItems}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className={expandedSidebarScrollAreaClassNames.scrollbar}>
<ScrollAreaThumb className={expandedSidebarScrollAreaClassNames.thumb} />
</ScrollAreaScrollbar>
</ScrollArea> </ScrollArea>
</div> </div>
) )
@ -154,13 +140,18 @@ const SideBar = () => {
{!isMobile && ( {!isMobile && (
<div className="mt-auto flex pb-3 pt-3"> <div className="mt-auto flex pb-3 pt-3">
<div className="flex size-8 cursor-pointer items-center justify-center text-text-tertiary" onClick={toggleIsFold}> <button
type="button"
aria-label={isFold ? t('sidebar.expandSidebar', { ns: 'layout' }) : t('sidebar.collapseSidebar', { ns: 'layout' })}
className="flex size-8 items-center justify-center rounded-lg text-text-tertiary transition-colors hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover"
onClick={toggleIsFold}
>
{isFold {isFold
? <span className="i-ri-expand-right-line" /> ? <span aria-hidden="true" className="i-ri-expand-right-line" />
: ( : (
<span className="i-ri-layout-left-2-line" /> <span aria-hidden="true" className="i-ri-layout-left-2-line" />
)} )}
</div> </button>
</div> </div>
)} )}