diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 9b216ad82e..83fac34879 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -668,6 +668,7 @@
"event_type": "",
"example_project": "",
"existing_plan_active_until_term_end": "",
+ "existing_project_zip": "",
"exit_focus_mode": "",
"expand": "",
"experiment_enabled_refresh_page": "",
@@ -827,6 +828,7 @@
"github_private_description": "",
"github_project_too_large_error": "",
"github_public_description": "",
+ "github_repo": "",
"github_repository_diverged": "",
"github_symlink_error": "",
"github_sync_description": "",
@@ -960,6 +962,7 @@
"image_file": "",
"image_url": "",
"image_width": "",
+ "import": "",
"import_a_bibtex_file_from_your_provider_account": "",
"import_document_description": "",
"import_existing_projects_from_github": "",
@@ -1010,7 +1013,6 @@
"insert_x_columns_right": "",
"insert_x_rows_above": "",
"insert_x_rows_below": "",
- "institution": "",
"institution_account": "",
"institution_acct_successfully_linked_2": "",
"institution_and_role": "",
@@ -1226,6 +1228,7 @@
"manager": "",
"managers_management": "",
"managing_your_subscription": "",
+ "markdown_document": "",
"marked_as_resolved": "",
"math": "",
"math_display": "",
@@ -1269,6 +1272,7 @@
"more_editor_toolbar_item": "",
"more_info": "",
"more_options": "",
+ "more_templates": "",
"move_to_the_left": "",
"move_to_the_right": "",
"my_library": "",
@@ -2363,7 +2367,6 @@
"upload_failed": "",
"upload_file": "",
"upload_from_computer": "",
- "upload_project": "",
"upload_zipped_project": "",
"url_to_fetch_the_file_from": "",
"us_gov_banner_fedramp": "",
@@ -2484,6 +2487,7 @@
"will_lose_edit_access_on_date": "",
"with_overleaf_subscription_you_also_get": "",
"word_count_lower": "",
+ "word_document": "",
"words": "",
"work_in_vim_or_emacs_emulation_mode": "",
"work_offline": "",
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
index cc05886cd3..ecf01bae25 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
@@ -23,6 +23,8 @@ import type { PortalTemplate } from '../../../../../types/portal-template'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useProjectListContext } from '@/features/project-list/context/project-list-context'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { NestableDropdownContextProvider } from '@/shared/context/nestable-dropdown-context'
+import { NestedMenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
type SendTrackingEvent = {
dropdownMenu: string
@@ -54,7 +56,7 @@ function NewProjectButton({
className,
trackingKey,
showAddAffiliationWidget,
- align,
+ align = 'start',
}: NewProjectButtonProps) {
const { t } = useTranslation()
const { templateLinks } = getMeta('ol-ExposedSettings')
@@ -191,132 +193,132 @@ function NewProjectButton({
{buttonText || t('new_project')}
-
-
- handleModalMenuClick(e, {
- modalVariant: 'blank_project',
- dropdownMenuEvent: 'blank-project',
- })
- }
- >
- {t('blank_project')}
-
-
-
-
- handleModalMenuClick(e, {
- modalVariant: 'example_project',
- dropdownMenuEvent: 'example-project',
- })
- }
- >
- {t('example_project')}
-
-
-
-
- handleModalMenuClick(e, {
- modalVariant: 'upload_project',
- dropdownMenuEvent: 'upload-project',
- })
- }
- >
- {t('upload_project')}
-
-
- {docxImportEnabled && (
+
handleModalMenuClick(e, {
- modalVariant: 'import_docx',
- dropdownMenuEvent: 'import-docx',
+ modalVariant: 'blank_project',
+ dropdownMenuEvent: 'blank-project',
})
}
>
- {t('import_word_document')}
+ {t('blank_project')}
- )}
- {markdownImportEnabled && (
+
+
+ {t('import')}
+
handleModalMenuClick(e, {
- modalVariant: 'import_markdown',
- dropdownMenuEvent: 'import-markdown',
+ modalVariant: 'upload_project',
+ dropdownMenuEvent: 'upload-project',
})
}
>
- {t('import_markdown_file')}
+ {t('existing_project_zip')}
- )}
-
- {ImportProjectFromGithubMenu && (
-
- handleModalMenuClick(e, {
- modalVariant: 'import_from_github',
- dropdownMenuEvent: 'import-from-github',
- })
- }
- />
- )}
-
- {portalTemplates.length > 0 ? (
- <>
-
-
- {`${t('institution')} ${t('templates')}`}
-
- {portalTemplates.map((portalTemplate, index) => (
-
- handlePortalTemplateClick(e, portalTemplate)}
- aria-label={`${portalTemplate.name} ${t('template')}`}
- >
- {portalTemplate.name}
-
-
- ))}
- >
- ) : null}
-
- {templateLinks && templateLinks.length > 0 && (
- <>
-
-
- {t('templates')}
-
- >
- )}
- {templateLinks?.map((templateLink, index) => (
-
- handleStaticTemplateClick(e, templateLink)}
- aria-label={`${templateLink.name} ${t('template')}`}
- >
- {templateLink.name === 'view_all'
- ? t('view_all')
- : templateLink.name}
-
-
- ))}
- {showAddAffiliationWidget && enableAddAffiliationWidget ? (
- <>
-
-
-
+ {docxImportEnabled && (
+
+
+ handleModalMenuClick(e, {
+ modalVariant: 'import_docx',
+ dropdownMenuEvent: 'import-docx',
+ })
+ }
+ >
+ {t('word_document')}
+
- >
- ) : null}
+ )}
+ {markdownImportEnabled && (
+
+
+ handleModalMenuClick(e, {
+ modalVariant: 'import_markdown',
+ dropdownMenuEvent: 'import-markdown',
+ })
+ }
+ >
+ {t('markdown_document')}
+
+
+ )}
+ {ImportProjectFromGithubMenu && (
+
+
+ handleModalMenuClick(e, {
+ modalVariant: 'import_from_github',
+ dropdownMenuEvent: 'import-from-github',
+ })
+ }
+ />
+
+ )}
+
+
+ {t('templates')}
+
+
+
+ handleModalMenuClick(e, {
+ modalVariant: 'example_project',
+ dropdownMenuEvent: 'example-project',
+ })
+ }
+ >
+ {t('example_project')}
+
+
+ {portalTemplates.map((portalTemplate, index) => (
+
+ handlePortalTemplateClick(e, portalTemplate)}
+ aria-label={`${portalTemplate.name} ${t('template')}`}
+ >
+ {portalTemplate.name}
+
+
+ ))}
+ {templateLinks && templateLinks.length > 0 && (
+
+ {templateLinks.map((template, i) => (
+
+ handleStaticTemplateClick(e, template)}
+ aria-label={`${template.name === 'view_all' ? t('view_all') : template.name} ${t('template')}`}
+ >
+ {template.name === 'view_all'
+ ? t('view_all')
+ : template.name}
+
+
+ ))}
+
+ )}
+ {showAddAffiliationWidget && enableAddAffiliationWidget ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
)}
>
diff --git a/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx
index 40640c52c3..d4d76901cc 100644
--- a/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx
+++ b/services/web/frontend/js/features/project-list/components/welcome-message-new/welcome-message-create-new-project-dropdown.tsx
@@ -138,7 +138,7 @@ function WelcomeMessageCreateNewProjectDropdown({
}
tabIndex={-1}
>
- {t('upload_project')}
+ {t('existing_project_zip')}
{docxImportEnabled && (
diff --git a/services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx b/services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx
index 2af20d07a7..67966f0857 100644
--- a/services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx
+++ b/services/web/frontend/js/shared/components/menu-bar/menu-bar-dropdown.tsx
@@ -3,7 +3,7 @@ import {
OLDropdownMenu,
OLDropdownToggle,
} from '@/shared/components/ol/ol-dropdown-menu'
-import { FC, forwardRef, useCallback } from 'react'
+import { FC, forwardRef, useCallback, useEffect, useRef } from 'react'
import classNames from 'classnames'
import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
import { NestableDropdownContextProvider } from '@/shared/context/nestable-dropdown-context'
@@ -71,75 +71,109 @@ const NestableDropdownMenu: FC<
)
}
-const NestedDropdownToggle: FC = forwardRef<
- HTMLAnchorElement,
- AnchorProps
->(function NestedDropdownToggle(
- { children, className, onMouseEnter, id },
- ref
-) {
- return (
- // eslint-disable-next-line jsx-a11y/anchor-is-valid
-
- {children}
-
-
- )
-})
+const NestedDropdownToggle = forwardRef(
+ function NestedDropdownToggle(
+ { children, className, onMouseEnter, id, ...rest },
+ ref
+ ) {
+ return (
+ // eslint-disable-next-line jsx-a11y/anchor-is-valid
+
+ {children}
+
+
+ )
+ }
+)
export const NestedMenuBarDropdown: FC<
- React.PropsWithChildren<{ id: string; title: string }>
-> = ({ children, id, title }) => {
+ React.PropsWithChildren<{
+ id: string
+ title: string
+ drop?: 'start' | 'end'
+ }>
+> = ({ children, id, title, drop = 'end' }) => {
const { menuId, selected, setSelected } = useNestableDropdown()
+ const closeTimerRef = useRef(null)
+
+ const clearCloseTimer = useCallback(() => {
+ if (closeTimerRef.current !== null) {
+ window.clearTimeout(closeTimerRef.current)
+ closeTimerRef.current = null
+ }
+ }, [])
+
+ useEffect(() => {
+ return () => clearCloseTimer()
+ }, [clearCloseTimer])
+
const select = useCallback(() => {
+ clearCloseTimer()
setSelected(id)
- }, [id, setSelected])
+ }, [id, setSelected, clearCloseTimer])
+
+ const scheduleClose = useCallback(() => {
+ clearCloseTimer()
+ closeTimerRef.current = window.setTimeout(() => {
+ setSelected(prev => (prev === id ? null : prev))
+ }, 150)
+ }, [clearCloseTimer, setSelected, id])
+
const onToggle = useCallback(
(show: boolean) => {
- // Only handle opening
if (show) {
setSelected(id)
}
},
[setSelected, id]
)
+
const active = selected === id
+
return (
- {
+ if (e.pointerType !== 'touch') scheduleClose()
+ }}
+ onMouseEnter={clearCloseTimer}
>
-
- {active && (
-
- )}
-
+
+ {active && (
+
+ )}
+
+
)
}
diff --git a/services/web/frontend/stylesheets/components/dropdown-menu.scss b/services/web/frontend/stylesheets/components/dropdown-menu.scss
index f93b9ab762..c2073cb457 100644
--- a/services/web/frontend/stylesheets/components/dropdown-menu.scss
+++ b/services/web/frontend/stylesheets/components/dropdown-menu.scss
@@ -301,7 +301,8 @@ $dropdown-item-min-height: 36px;
}
.nested-dropdown-toggle {
- &::after {
+ &::after,
+ &::before {
content: none !important;
}
diff --git a/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss b/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss
index b22ae76085..b210d17b74 100644
--- a/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss
+++ b/services/web/frontend/stylesheets/pages/project-list-ds-nav.scss
@@ -213,7 +213,6 @@ body {
100vh - #{$header-height} - var(--spacing-03) - var(--spacing-08) -
36px - var(--spacing-05)
);
- overflow: auto;
}
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 0372e5d064..a3587ad5a4 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -865,6 +865,7 @@
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",
"exclusive_access_with_labs": "Exclusive access to early-stage experiments",
"existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.",
+ "existing_project_zip": "Existing project (.zip)",
"exit_focus_mode": "Exit focus mode",
"expand": "Expand",
"experiment_enabled_refresh_page": "Experiment is enabled! You need to refresh the page to see the changes.",
@@ -1066,6 +1067,7 @@
"github_private_description": "You choose who can see and commit to this repository.",
"github_project_too_large_error": "This project is too large to sync with GitHub. There’s a limit on the total size of a project’s editable text files. Try removing or shrinking large text files, then sync again.",
"github_public_description": "Anyone can see this repository. You choose who can commit.",
+ "github_repo": "GitHub repo",
"github_repository_diverged": "The default branch of the linked repository has been force-pushed. Pulling GitHub changes after a force push can cause Overleaf and GitHub to get out of sync. You might need to push changes after pulling to get back in sync.",
"github_successfully_linked_description": "Thanks, we’ve successfully linked your GitHub account to __appName__. You can now export your __appName__ projects to GitHub, or import projects from your GitHub repositories.",
"github_symlink_error": "Your GitHub repository contains symbolic link files, which are not currently supported by Overleaf. Please remove these and try again.",
@@ -1222,6 +1224,7 @@
"image_file": "Image file",
"image_url": "Image URL",
"image_width": "Image width",
+ "import": "Import",
"import_a_bibtex_file_from_your_provider_account": "Import a BibTeX file from your __provider__ account",
"import_document_description": "Content will be imported from the selected document. Formatting may not be reproduced exactly.",
"import_existing_projects_from_github": "Import existing projects from GitHub",
@@ -1591,6 +1594,7 @@
"managers_management": "Managers management",
"managing_your_subscription": "Managing your subscription",
"march": "March",
+ "markdown_document": "Markdown document",
"marked_as_resolved": "Marked as resolved",
"math": "Math",
"math_display": "Math Display",
@@ -1644,6 +1648,7 @@
"more_options": "More options",
"more_project_collaborators": "<0>More0> project <0>collaborators0>",
"more_space_to_write_revise_and_collaborate": "More space to write, revise, and collaborate.",
+ "more_templates": "More templates",
"more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",
"most_popular": "most popular",
"most_popular_uppercase": "Most popular",
@@ -3138,6 +3143,7 @@
"with_overleaf_subscription_you_also_get": "With an Overleaf subscription you also get",
"word_count": "Word Count",
"word_count_lower": "Word count",
+ "word_document": "Word document",
"words": "Words",
"work_in_vim_or_emacs_emulation_mode": "Work in Vim or Emacs emulation mode",
"work_offline": "Work offline",
diff --git a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
index f7e6d3b3cb..c63be515f4 100644
--- a/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/new-project-button.test.tsx
@@ -34,17 +34,30 @@ describe('', function () {
})
it('shows the correct dropdown menu', function () {
- // static menu
- screen.getByText('Blank project')
- screen.getByText('Example project')
- screen.getByText('Upload project')
- screen.getByText('Import from GitHub')
-
- // static text
+ // Section headers
+ screen.getByText('Import')
screen.getByText('Templates')
- // dynamic menu based on templateLinks
- screen.getByText('Journal articles')
+ // Items
+ screen.getByText('Blank project')
+ screen.getByText('Existing project (.zip)')
+ screen.getByText('Example project')
+
+ // "More templates" trigger is present (templateLinks exist in beforeEach)
+ screen.getByText('More templates')
+
+ // Template links are NOT directly visible — they're inside the sub-dropdown
+ expect(screen.queryByText('Journal articles')).to.be.null
+ expect(screen.queryByText('View All')).to.be.null
+ })
+
+ it('shows template links in More templates sub-dropdown on hover', async function () {
+ const moreTrigger = screen.getByRole('menuitem', {
+ name: 'More templates',
+ })
+ fireEvent.mouseEnter(moreTrigger)
+ // After hovering the trigger, template links should appear
+ await screen.findByText('Journal articles')
screen.getByText('View All')
})
@@ -106,14 +119,14 @@ describe('', function () {
})
fireEvent.click(newProjectButton)
- // static menu
- screen.getByText('Blank project')
- screen.getByText('Example project')
- screen.getByText('Upload project')
- screen.getByText('Import from GitHub')
+ // Section headers
+ screen.getByText('Import')
+ screen.getByText('Templates')
- // static text for institution templates
- screen.getByText('Institution Templates')
+ // Items
+ screen.getByText('Blank project')
+ screen.getByText('Existing project (.zip)')
+ screen.getByText('Example project')
// dynamic menu based on portalTemplates
const affiliationTemplate = screen.getByRole('menuitem', {
@@ -123,12 +136,12 @@ describe('', function () {
'/edu/test-new-template#templates'
)
- // static text
- screen.getByText('Templates')
+ // "More templates" trigger is present (templateLinks exist in beforeEach)
+ screen.getByText('More templates')
- // dynamic menu based on templateLinks
- screen.getByText('Journal articles')
- screen.getByText('View All')
+ // Template links are NOT directly visible — they're inside the sub-dropdown
+ expect(screen.queryByText('Journal articles')).to.be.null
+ expect(screen.queryByText('View All')).to.be.null
})
})
})
diff --git a/services/web/test/frontend/features/project-list/components/welcome-message.test.tsx b/services/web/test/frontend/features/project-list/components/welcome-message.test.tsx
index f874f3894a..0298925a00 100644
--- a/services/web/test/frontend/features/project-list/components/welcome-message.test.tsx
+++ b/services/web/test/frontend/features/project-list/components/welcome-message.test.tsx
@@ -46,7 +46,7 @@ describe('', function () {
screen.getByText('Blank project')
screen.getByText('Example project')
- screen.getByText('Upload project')
+ screen.getByText('Existing project (.zip)')
screen.getByText('Import Word document')
screen.getByText('Import from GitHub')
})
@@ -83,7 +83,7 @@ describe('', function () {
// static menu
screen.getByText('Blank project')
screen.getByText('Example project')
- screen.getByText('Upload project')
+ screen.getByText('Existing project (.zip)')
screen.getByText('Import from GitHub')
// static text for institution templates
@@ -110,7 +110,7 @@ describe('', function () {
screen.getByText('Blank project')
screen.getByText('Example project')
- screen.getByText('Upload project')
+ screen.getByText('Existing project (.zip)')
screen.getByText('Import from GitHub')
})
@@ -161,7 +161,7 @@ describe('', function () {
screen.getByText('Blank project')
screen.getByText('Example project')
- screen.getByText('Upload project')
+ screen.getByText('Existing project (.zip)')
expect(screen.queryByText('Import from GitHub')).to.not.exist
})