[WEB] Simplify "New project" dropdown (#34290)

* Simplify the new project dropdown

* Simplify the New project dropdown

* removing artefact of rebase

GitOrigin-RevId: e7e4dc48c10f2297abcd7c5b489ff2308bf9e18b
This commit is contained in:
Davinder Singh
2026-09-17 11:47:56 +00:00
committed by Copybot
parent deec57e54e
commit 98b79d9532
10 changed files with 251 additions and 191 deletions
@@ -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": "",
@@ -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')}
</OLDropdownToggle>
<OLDropdownMenu>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{t('blank_project')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{t('example_project')}
</OLDropdownItem>
</li>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('upload_project')}
</OLDropdownItem>
</li>
{docxImportEnabled && (
<NestableDropdownContextProvider id={id}>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_docx',
dropdownMenuEvent: 'import-docx',
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
})
}
>
{t('import_word_document')}
{t('blank_project')}
</OLDropdownItem>
</li>
)}
{markdownImportEnabled && (
<OLDropdownDivider />
<OLDropdownHeader aria-hidden="true">
{t('import')}
</OLDropdownHeader>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_markdown',
dropdownMenuEvent: 'import-markdown',
modalVariant: 'upload_project',
dropdownMenuEvent: 'upload-project',
})
}
>
{t('import_markdown_file')}
{t('existing_project_zip')}
</OLDropdownItem>
</li>
)}
<li role="none">
{ImportProjectFromGithubMenu && (
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
)}
</li>
{portalTemplates.length > 0 ? (
<>
<OLDropdownDivider />
<OLDropdownHeader aria-hidden="true">
{`${t('institution')} ${t('templates')}`}
</OLDropdownHeader>
{portalTemplates.map((portalTemplate, index) => (
<li role="none" key={`portal-template-${index}`}>
<OLDropdownItem
key={`portal-template-${index}`}
href={`${portalTemplate.url}#templates`}
onClick={e => handlePortalTemplateClick(e, portalTemplate)}
aria-label={`${portalTemplate.name} ${t('template')}`}
>
{portalTemplate.name}
</OLDropdownItem>
</li>
))}
</>
) : null}
{templateLinks && templateLinks.length > 0 && (
<>
<OLDropdownDivider />
<OLDropdownHeader aria-hidden="true">
{t('templates')}
</OLDropdownHeader>
</>
)}
{templateLinks?.map((templateLink, index) => (
<li role="none" key={`new-project-button-template-${index}`}>
<OLDropdownItem
href={templateLink.url}
onClick={e => handleStaticTemplateClick(e, templateLink)}
aria-label={`${templateLink.name} ${t('template')}`}
>
{templateLink.name === 'view_all'
? t('view_all')
: templateLink.name}
</OLDropdownItem>
</li>
))}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<OLDropdownDivider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
{docxImportEnabled && (
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_docx',
dropdownMenuEvent: 'import-docx',
})
}
>
{t('word_document')}
</OLDropdownItem>
</li>
</>
) : null}
)}
{markdownImportEnabled && (
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_markdown',
dropdownMenuEvent: 'import-markdown',
})
}
>
{t('markdown_document')}
</OLDropdownItem>
</li>
)}
{ImportProjectFromGithubMenu && (
<li role="none">
<ImportProjectFromGithubMenu
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'import_from_github',
dropdownMenuEvent: 'import-from-github',
})
}
/>
</li>
)}
<OLDropdownDivider />
<OLDropdownHeader aria-hidden="true">
{t('templates')}
</OLDropdownHeader>
<li role="none">
<OLDropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
})
}
>
{t('example_project')}
</OLDropdownItem>
</li>
{portalTemplates.map((portalTemplate, index) => (
<li role="none" key={`portal-template-${index}`}>
<OLDropdownItem
href={`${portalTemplate.url}#templates`}
onClick={e => handlePortalTemplateClick(e, portalTemplate)}
aria-label={`${portalTemplate.name} ${t('template')}`}
>
{portalTemplate.name}
</OLDropdownItem>
</li>
))}
{templateLinks && templateLinks.length > 0 && (
<NestedMenuBarDropdown
id="more-templates"
title={t('more_templates')}
drop={align === 'end' ? 'start' : 'end'}
>
{templateLinks.map((template, i) => (
<li role="none" key={`more-template-${i}`}>
<OLDropdownItem
href={template.url}
onClick={e => 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}
</OLDropdownItem>
</li>
))}
</NestedMenuBarDropdown>
)}
{showAddAffiliationWidget && enableAddAffiliationWidget ? (
<>
<OLDropdownDivider />
<li className="add-affiliation-mobile-wrapper">
<AddAffiliation className="is-mobile" />
</li>
</>
) : null}
</NestableDropdownContextProvider>
</OLDropdownMenu>
</OLDropdown>
<NewProjectButtonModal
@@ -64,6 +64,7 @@ export function ProjectListDsNav() {
<NewProjectButton
id="new-project-button-projects-table"
showAddAffiliationWidget
align="end"
/>
)}
</>
@@ -138,7 +138,7 @@ function WelcomeMessageCreateNewProjectDropdown({
}
tabIndex={-1}
>
{t('upload_project')}
{t('existing_project_zip')}
</OLDropdownItem>
</li>
{docxImportEnabled && (
@@ -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<React.PropsWithChildren> = forwardRef<
HTMLAnchorElement,
AnchorProps
>(function NestedDropdownToggle(
{ children, className, onMouseEnter, id },
ref
) {
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a
id={id}
href="#"
ref={ref}
onMouseEnter={onMouseEnter}
onClick={onMouseEnter}
className={classNames(
className,
'nested-dropdown-toggle',
'dropdown-item'
)}
role="menuitem"
aria-haspopup
>
{children}
<MaterialIcon type="chevron_right" />
</a>
)
})
const NestedDropdownToggle = forwardRef<HTMLAnchorElement, AnchorProps>(
function NestedDropdownToggle(
{ children, className, onMouseEnter, id, ...rest },
ref
) {
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a
id={id}
href="#"
ref={ref}
{...rest}
onMouseEnter={onMouseEnter}
onClick={onMouseEnter}
className={classNames(
className,
'nested-dropdown-toggle',
'dropdown-item'
)}
role="menuitem"
aria-haspopup="menu"
>
{children}
<MaterialIcon type="chevron_right" />
</a>
)
}
)
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<number | null>(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 (
<OLDropdown
align="start"
drop="end"
show={active}
autoClose
onToggle={onToggle}
as="li"
<li
role="none"
onPointerLeave={e => {
if (e.pointerType !== 'touch') scheduleClose()
}}
onMouseEnter={clearCloseTimer}
>
<OLDropdownToggle
id={`${menuId}-${id}`}
onMouseEnter={select}
className={classNames({ 'nested-dropdown-toggle-shown': active })}
as={NestedDropdownToggle}
<OLDropdown
align="start"
drop={drop}
show={active}
autoClose
onToggle={onToggle}
className="w-100"
>
{title}
</OLDropdownToggle>
{active && (
<NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
{children}
</NestableDropdownMenu>
)}
</OLDropdown>
<OLDropdownToggle
id={`${menuId}-${id}`}
onMouseEnter={select}
className={classNames({ 'nested-dropdown-toggle-shown': active })}
as={NestedDropdownToggle}
>
{title}
</OLDropdownToggle>
{active && (
<NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
{children}
</NestableDropdownMenu>
)}
</OLDropdown>
</li>
)
}
@@ -301,7 +301,8 @@ $dropdown-item-min-height: 36px;
}
.nested-dropdown-toggle {
&::after {
&::after,
&::before {
content: none !important;
}
@@ -213,7 +213,6 @@ body {
100vh - #{$header-height} - var(--spacing-03) - var(--spacing-08) -
36px - var(--spacing-05)
);
overflow: auto;
}
}
+6
View File
@@ -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>More</0> project <0>collaborators</0>",
"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",
@@ -34,17 +34,30 @@ describe('<NewProjectButton />', 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('<NewProjectButton />', 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('<NewProjectButton />', 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
})
})
})
@@ -46,7 +46,7 @@ describe('<WelcomeMessage />', 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('<WelcomeMessage />', 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('<WelcomeMessage />', 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('<WelcomeMessage />', 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
})