Improve landmarks on the Project dashboard and Editor pages (#27401)

* Improve landmarks for the Project Dasbhboard

* Improve landmarks for the IDE page

* Improve landmarks for the new redesigned IDE page

* Sort locales

* Fix typo OlButtonToolbar -> OLButtonToolbar

* Update project navbar translation

* Update labels

* Redundant main landmark

* Fix failing test

* Descriptive name for the rails tab

* Header should not be in a button

* Update translation to Account and help

* Update translation to Project categories and tags

* Add explanations

* Show landmark for the survey widget when it's rendered

* Suggestions for nav stretch/scroll/borders

* Source format

---------

Co-authored-by: Antoine Clausse <antoine.clausse@overleaf.com>
GitOrigin-RevId: d05a738e782d2edb229529aadf92b9004dfd973a
This commit is contained in:
Rebeka Dekany
2025-07-29 08:05:16 +00:00
committed by Copybot
co-authored by Antoine Clausse
parent 1abb98fac2
commit 7c97cbbfe4
43 changed files with 340 additions and 165 deletions
@@ -72,7 +72,7 @@ const ChatPane = React.memo(function ChatPane() {
}
return (
<aside className="chat">
<aside className="chat" aria-label={t('chat')}>
<InfiniteScroll
atEnd={atEnd}
className="messages"
@@ -88,11 +88,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
const shouldDisplayPublishButton = hasPublishPermissions && PublishButton
return (
<header
className="toolbar toolbar-header"
role="navigation"
aria-label={t('project_layout_sharing_submission')}
>
<nav className="toolbar toolbar-header" aria-label={t('project_actions')}>
<div className="toolbar-left">
<MenuButton onClick={onShowLeftMenuClick} />
{cobranding && cobranding.logoImgUrl && (
@@ -158,7 +154,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
</>
)}
</div>
</header>
</nav>
)
})
@@ -9,7 +9,7 @@ function ChangeList() {
const { t } = useTranslation()
return (
<aside className="change-list" aria-label={t('project_history_list')}>
<aside className="change-list" aria-label={t('project_history_labels')}>
<div className="history-header history-toggle-switch-container">
<ToggleSwitch labelsOnly={labelsOnly} setLabelsOnly={setLabelsOnly} />
</div>
@@ -55,6 +55,8 @@ export const EditorAndPdf: FC = () => {
'ide-panel-group-resizing': resizing,
hidden: !editorIsOpen,
})}
tagName="section"
aria-label={t('editor')}
>
{selectedEntityCount === 0 && <NoSelectionPane />}
{selectedEntityCount === 1 && openEntity?.type === 'fileRef' && (
@@ -105,6 +107,8 @@ export const EditorAndPdf: FC = () => {
onCollapse={handlePdfPaneCollapse}
onExpand={handlePdfPaneExpand}
className="ide-react-panel"
tagName="section"
aria-label={t('pdf_preview_logs')}
>
<PdfPreview />
{/* ensure that "sync to code" is available in PDF only layout */}
@@ -7,6 +7,7 @@ import { OutlineContainer } from '@/features/outline/components/outline-containe
import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
import React, { ElementType } from 'react'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { t } from 'i18next'
const editorSidebarComponents = importOverleafModules(
'editorSidebarComponents'
@@ -18,10 +19,11 @@ export default function EditorSidebar() {
const { outlineEnabled, outlinePanelRef } = useOutlinePane()
return (
<aside
<nav
className={classNames('ide-react-editor-sidebar', {
hidden: view === 'history',
})}
aria-label={t('project_files_outline')}
>
{editorSidebarComponents.map(
({ import: { default: Component }, path }) => (
@@ -53,6 +55,6 @@ export default function EditorSidebar() {
<OutlineContainer />
</Panel>
</PanelGroup>
</aside>
</nav>
)
}
@@ -1,10 +1,13 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
export function HistorySidebar() {
const { t } = useTranslation()
return (
<aside
<nav
id="history-file-tree"
className="ide-react-editor-sidebar history-file-tree"
aria-label={t('project_files_history')}
/>
)
}
@@ -69,7 +69,7 @@ export const ChatPane = () => {
<div className="chat-panel">
<RailPanelHeader title={t('collaborator_chat')} />
<div className="chat-wrapper">
<aside className="chat">
<aside className="chat" aria-label={t('chat')}>
<InfiniteScroll
atEnd={atEnd}
className="messages"
@@ -22,7 +22,7 @@ export default function IntegrationCard({
<div className="integrations-panel-card-contents">
{icon}
<div className="integrations-panel-card-inner">
<header className="integrations-panel-card-header">
<div className="integrations-panel-card-header">
<div className="integrations-panel-card-title">{title}</div>
{showPaywallBadge && (
<OLBadge
@@ -33,7 +33,7 @@ export default function IntegrationCard({
{t('premium')}
</OLBadge>
)}
</header>
</div>
<p className="integrations-panel-card-description">{description}</p>
</div>
</div>
@@ -68,6 +68,8 @@ export default function MainLayout() {
})}
minSize={5}
defaultSize={50}
tagName="section"
aria-label={t('editor')}
>
<div className="ide-redesign-editor-container">
<EditorPanel />
@@ -108,6 +110,8 @@ export default function MainLayout() {
ref={pdfPanelRef}
onExpand={handlePdfPaneExpand}
onCollapse={handlePdfPaneCollapse}
tagName="section"
aria-label={t('pdf_preview')}
>
<PdfPreview />
{pdfLayout === 'flat' && view === 'pdf' && (
@@ -1,14 +1,19 @@
import { memo } from 'react'
import OlButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import { useTranslation } from 'react-i18next'
import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import PdfCompileButton from '@/features/pdf-preview/components/pdf-compile-button'
import PdfHybridDownloadButton from '@/features/pdf-preview/components/pdf-hybrid-download-button'
import { DetachedSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
import SwitchToEditorButton from '@/features/pdf-preview/components/switch-to-editor-button'
function PdfPreviewHybridToolbar() {
const { t } = useTranslation()
// TODO: add detached pdf logic
return (
<OlButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
<OLButtonToolbar
className="toolbar toolbar-pdf toolbar-pdf-hybrid"
aria-label={t('pdf')}
>
<div className="toolbar-pdf-left">
<PdfCompileButton />
<PdfHybridDownloadButton />
@@ -19,7 +24,7 @@ function PdfPreviewHybridToolbar() {
<DetachedSynctexControl />
{/* TODO: should we have code check? */}
</div>
</OlButtonToolbar>
</OLButtonToolbar>
)
}
@@ -14,7 +14,7 @@ export default function RailPanelHeader({
const { t } = useTranslation()
const { handlePaneCollapse } = useRailContext()
return (
<header className="rail-panel-header">
<div className="rail-panel-header">
<h4 className="rail-panel-title">{title}</h4>
<div className="rail-panel-header-actions">
@@ -33,6 +33,6 @@ export default function RailPanelHeader({
/>
</OLTooltip>
</div>
</header>
</div>
)
}
@@ -258,7 +258,13 @@ export const RailLayout = () => {
onSelect={onTabSelect}
id="ide-rail-tabs"
>
<div className={classNames('ide-rail', { hidden: isHistoryView })}>
{/* The <Nav> element is a "div" and has a "role="tablist"".
But it should be identified as a navigation landmark.
Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */}
<nav
className={classNames('ide-rail', { hidden: isHistoryView })}
aria-label={t('files_collaboration_integrations_logs')}
>
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
{railTabs
.filter(({ hide }) => !hide)
@@ -274,11 +280,13 @@ export const RailLayout = () => {
/>
))}
<div className="flex-grow-1" />
{railActions?.map(action => (
<RailActionElement key={action.key} action={action} />
))}
<nav aria-label={t('help_editor_settings')}>
{railActions?.map(action => (
<RailActionElement key={action.key} action={action} />
))}
</nav>
</Nav>
</div>
</nav>
<Panel
id={
newErrorlogs
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next'
import { ToolbarMenuBar } from './menu-bar'
import { ToolbarProjectTitle } from './project-title'
import { OnlineUsers } from './online-users'
@@ -23,6 +24,7 @@ export const Toolbar = () => {
const { view, restoreView } = useLayoutContext()
const { cobranding } = useEditorContext()
const { permissionsLevel } = useIdeReactContext()
const { t } = useTranslation()
const shouldDisplaySubmitButton =
(permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
SubmitProjectButton
@@ -34,18 +36,18 @@ export const Toolbar = () => {
if (view === 'history') {
return (
<div className="ide-redesign-toolbar">
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
<div className="d-flex align-items-center">
<BackToEditorButton onClick={handleBackToEditorClick} />
</div>
<ToolbarProjectTitle />
<div /> {/* Empty div used for spacing */}
</div>
</nav>
)
}
return (
<div className="ide-redesign-toolbar">
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
<div className="ide-redesign-toolbar-menu">
<ToolbarLogos cobranding={cobranding} />
<ToolbarMenuBar />
@@ -62,6 +64,6 @@ export const Toolbar = () => {
<ShareProjectButton />
{getMeta('ol-showUpgradePrompt') && <UpgradeButton />}
</div>
</div>
</nav>
)
}
@@ -39,7 +39,7 @@ const OutlinePane = React.memo<{
return (
<div className={headerClasses}>
<header className="outline-header">
<div className="outline-header">
<OutlineToggleButton
toggleExpanded={toggleExpanded}
expanded={expanded}
@@ -47,7 +47,7 @@ const OutlinePane = React.memo<{
isPartial={isPartial}
isTexFile={isTexFile}
/>
</header>
</div>
{isOpen && (
<div className="outline-body">
<OutlineRoot
@@ -487,6 +487,21 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
const toolbarInfoLoaded =
rawScale !== null && page !== null && totalPages !== null
// Remove the 'region' role from each PDF page container.
// This prevents polluting the landmark navigation menu for every page,
// which creates a poor screen reader experience. Page navigation should be handled
// by the toolbar controls.
useEffect(() => {
if (!initialised || !pdfJsWrapper) return
const pageElements = pdfJsWrapper.container.querySelectorAll(
'div[data-page-number][role="region"]'
)
pageElements.forEach(element => {
element.removeAttribute('role')
})
}, [initialised, pdfJsWrapper])
/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
return (
@@ -1,6 +1,6 @@
import { memo, useState, useEffect, useRef } from 'react'
import OlButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import { useTranslation } from 'react-i18next'
import OLButtonToolbar from '@/features/ui/components/ol/ol-button-toolbar'
import { useLayoutContext } from '@/shared/context/layout-context'
import PdfCompileButton from './pdf-compile-button'
import SwitchToEditorButton from './switch-to-editor-button'
@@ -15,7 +15,7 @@ const ORPHAN_UI_TIMEOUT_MS = 5000
function PdfPreviewHybridToolbar() {
const { detachRole, detachIsLinked } = useLayoutContext()
const { t } = useTranslation()
const uiTimeoutRef = useRef<number>()
const [orphanPdfTabAfterDelay, setOrphanPdfTabAfterDelay] = useState(false)
@@ -47,9 +47,12 @@ function PdfPreviewHybridToolbar() {
}
return (
<OlButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
<OLButtonToolbar
className="toolbar toolbar-pdf toolbar-pdf-hybrid"
aria-label={t('pdf')}
>
{ToolbarInner}
</OlButtonToolbar>
</OLButtonToolbar>
)
}
@@ -99,7 +99,7 @@ function PreviewLogEntryHeader({
const headerTitleText = logType ? `${logType} ${headerTitle}` : headerTitle
return (
<header className={logEntryHeaderClasses}>
<div className={logEntryHeaderClasses}>
{headerIcon ? (
<div className="log-entry-header-icon-container">{headerIcon}</div>
) : null}
@@ -116,7 +116,7 @@ function PreviewLogEntryHeader({
) : (
locationLink
)}
</header>
</div>
)
}
@@ -1,4 +1,5 @@
import { JSXElementConstructor } from 'react'
import { useTranslation } from 'react-i18next'
import Common from './groups/common'
import Institution from './groups/institution'
import ConfirmEmail from './groups/confirm-email'
@@ -32,9 +33,13 @@ const USGovBanner: JSXElementConstructor<Record<string, never>> =
function UserNotifications() {
const groupSubscriptionsPendingEnrollment =
getMeta('ol-groupSubscriptionsPendingEnrollment') || []
const { t } = useTranslation()
return (
<div className="user-notifications notification-list">
<section
className="user-notifications notification-list"
aria-label={t('notification')}
>
<ul className="list-unstyled">
{EnrollmentNotification &&
groupSubscriptionsPendingEnrollment.map(subscription => (
@@ -58,7 +63,7 @@ function UserNotifications() {
{isDeprecatedBrowser() && <DeprecatedBrowser />}
</ul>
</div>
</section>
)
}
@@ -62,73 +62,75 @@ export function ProjectListDsNav() {
overleafLogo={overleafLogo}
showCloseIcon
/>
<main className="project-list-wrapper">
<div className="project-list-wrapper">
<SidebarDsNav />
<div className="project-ds-nav-content-and-messages">
<div className="project-ds-nav-content">
<div className="project-ds-nav-main">
{error ? <DashApiError /> : ''}
<UserNotifications />
<div className="project-list-header-row">
<ProjectListTitle
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className="text-truncate d-none d-md-block"
/>
<div className="project-tools">
<div className="d-none d-md-block">
{selectedProjects.length === 0 ? (
<main aria-labelledby="main-content">
<div className="project-list-header-row">
<ProjectListTitle
filter={filter}
selectedTag={selectedTag}
selectedTagId={selectedTagId}
className="text-truncate d-none d-md-block"
/>
<div className="project-tools">
<div className="d-none d-md-block">
{selectedProjects.length === 0 ? (
<CurrentPlanWidget />
) : (
<ProjectTools />
)}
</div>
<div className="d-md-none">
<CurrentPlanWidget />
) : (
<ProjectTools />
)}
</div>
<div className="d-md-none">
<CurrentPlanWidget />
</div>
</div>
</div>
</div>
<div className="project-ds-nav-project-list">
<OLRow className="d-none d-md-block">
<OLCol lg={7}>
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
/>
</OLCol>
</OLRow>
<div className="project-list-sidebar-survey-wrapper d-md-none">
{/* Omit the survey card in mobile view for now */}
</div>
<div className="mt-1 d-md-none">
<div
role="toolbar"
className="projects-toolbar"
aria-label={t('projects')}
>
<ProjectsDropdown />
<SortByDropdown />
<div className="project-ds-nav-project-list">
<OLRow className="d-none d-md-block">
<OLCol lg={7}>
<SearchForm
inputValue={searchText}
setInputValue={setSearchText}
filter={filter}
selectedTag={selectedTag}
/>
</OLCol>
</OLRow>
<div className="project-list-sidebar-survey-wrapper d-md-none">
{/* Omit the survey card in mobile view for now */}
</div>
<div className="mt-1 d-md-none">
<div
role="toolbar"
className="projects-toolbar"
aria-label={t('projects')}
>
<ProjectsDropdown />
<SortByDropdown />
</div>
</div>
<div className="mt-3">
<TableContainer bordered>
{tableTopArea}
<ProjectListTable />
</TableContainer>
</div>
<div className="mt-3">
<LoadMore />
</div>
</div>
<div className="mt-3">
<TableContainer bordered>
{tableTopArea}
<ProjectListTable />
</TableContainer>
</div>
<div className="mt-3">
<LoadMore />
</div>
</div>
</main>
</div>
<Footer {...footerProps} />
</div>
<CookieBanner />
</div>
</main>
</div>
</div>
)
}
@@ -50,8 +50,8 @@ function DefaultNavbarAndFooter({ children }: { children: ReactNode }) {
<>
<DefaultNavbar {...navbarProps} />
<main
id="main-content"
className="content content-alt project-list-react"
aria-labelledby="main-content"
>
{children}
</main>
@@ -1,4 +1,5 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import classnames from 'classnames'
import { Question, User } from '@phosphor-icons/react'
import NewProjectButton from '../new-project-button'
@@ -8,7 +9,6 @@ import { usePersistedResize } from '@/shared/hooks/use-resize'
import { Dropdown } from 'react-bootstrap'
import getMeta from '@/utils/meta'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import { useTranslation } from 'react-i18next'
import { NavDropdownMenuItems } from '@/features/ui/components/bootstrap-5/navbar/nav-dropdown-from-data'
import { NavbarDropdownItemData } from '@/features/ui/components/types/navbar'
import { useContactUsModal } from '@/shared/hooks/use-contact-us-modal'
@@ -45,19 +45,24 @@ function SidebarDsNav() {
},
})}
>
<NewProjectButton
id="new-project-button-sidebar"
className={scrolledDown ? 'show-shadow' : undefined}
/>
<div
className="project-list-sidebar-scroll"
ref={containerRef}
data-testid="project-list-sidebar-scroll"
<nav
className="flex-grow flex-shrink"
aria-label={t('project_categories_tags')}
>
<SidebarFilters />
{showAddAffiliationWidget && <hr />}
<AddAffiliation />
</div>
<NewProjectButton
id="new-project-button-sidebar"
className={scrolledDown ? 'show-shadow' : undefined}
/>
<div
className="project-list-sidebar-scroll"
ref={containerRef}
data-testid="project-list-sidebar-scroll"
>
<SidebarFilters />
{showAddAffiliationWidget && <hr />}
<AddAffiliation />
</div>
</nav>
<div
className={classnames(
'ds-nav-sidebar-lower',
@@ -67,7 +72,10 @@ function SidebarDsNav() {
<div className="project-list-sidebar-survey-wrapper">
<SurveyWidgetDsNav />
</div>
<div className="d-flex gap-3 mb-2">
<nav
className="d-flex flex-row gap-3 mb-2"
aria-label={t('account_help')}
>
{helpItem && (
<Dropdown
className="ds-nav-icon-dropdown"
@@ -157,7 +165,7 @@ function SidebarDsNav() {
<UserProvider>{contactUsModal}</UserProvider>
</>
)}
</div>
</nav>
<div className="ds-nav-ds-name" translate="no">
<span>Digital Science</span>
</div>
@@ -22,7 +22,7 @@ export function SurveyWidgetDsNav() {
}
return (
<div className="user-notifications">
<aside className="user-notifications" aria-label={t('feedback')}>
<div className="notification-entry">
<div role="alert" className="survey-notification">
<div className="notification-body">
@@ -48,6 +48,6 @@ export function SurveyWidgetDsNav() {
</OLButton>
</div>
</div>
</div>
</aside>
)
}
@@ -44,12 +44,14 @@ function ProjectListTitle({
}
return (
<div
<h1
id="main-content"
tabIndex={-1}
className={classnames('project-list-title', className)}
{...extraProps}
>
{message}
</div>
</h1>
)
}
@@ -23,7 +23,7 @@ function FatFooterBase() {
const currentYear = new Date().getFullYear()
return (
<footer className="fat-footer-base">
<div className="fat-footer-base">
<div className="fat-footer-base-section fat-footer-base-meta">
<div className="fat-footer-base-item">
<div className="fat-footer-base-copyright" translate="no">
@@ -62,7 +62,7 @@ function FatFooterBase() {
/>
</div>
</div>
</footer>
</div>
)
}
@@ -91,11 +91,7 @@ function FatFooter() {
return (
<footer className="fat-footer hidden-print">
<div
role="navigation"
aria-label={t('footer_navigation')}
className="fat-footer-container"
>
<div className="fat-footer-container">
<div className={`fat-footer-sections ${hideFatFooter ? 'hidden' : ''}`}>
<div className="footer-section" id="footer-brand">
<a href="/" aria-label={t('overleaf')} className="footer-brand">
@@ -61,6 +61,7 @@ function DefaultNavbar(
'--navbar-brand-image-redesign-url': `url("${overleafBlackLogo}")`,
} as CSSPropertiesWithVariables
}
aria-label={t('primary')}
>
<Container className="navbar-container" fluid>
<div className="navbar-header">
@@ -92,7 +93,7 @@ function DefaultNavbar(
<Navbar.Toggle
aria-controls="navbar-main-collapse"
aria-expanded="false"
aria-label={t('main_navigation')}
aria-label={t('primary')}
>
{showCloseIcon && expanded ? (
<X />