From 98b79d9532e5bba6eb0c4a51980dbef3360d6959 Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Wed, 5 Aug 2026 09:16:47 +0100 Subject: [PATCH] [WEB] Simplify "New project" dropdown (#34290) * Simplify the new project dropdown * Simplify the New project dropdown * removing artefact of rebase GitOrigin-RevId: e7e4dc48c10f2297abcd7c5b489ff2308bf9e18b --- .../web/frontend/extracted-translations.json | 8 +- .../components/new-project-button.tsx | 218 +++++++++--------- .../components/project-list-ds-nav.tsx | 1 + ...me-message-create-new-project-dropdown.tsx | 2 +- .../components/menu-bar/menu-bar-dropdown.tsx | 140 ++++++----- .../stylesheets/components/dropdown-menu.scss | 3 +- .../pages/project-list-ds-nav.scss | 1 - services/web/locales/en.json | 6 + .../components/new-project-button.test.tsx | 55 +++-- .../components/welcome-message.test.tsx | 8 +- 10 files changed, 251 insertions(+), 191 deletions(-) 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 && ( + +
  • 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 ? ( - <> - - - {portalTemplates.map((portalTemplate, index) => ( -
  • - handlePortalTemplateClick(e, portalTemplate)} - aria-label={`${portalTemplate.name} ${t('template')}`} - > - {portalTemplate.name} - -
  • - ))} - - ) : null} - - {templateLinks && templateLinks.length > 0 && ( - <> - - - - )} - {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', + }) + } + /> +
  • + )} + + +
  • + + 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} > - - {title} - - {active && ( - - {children} - - )} - + + {title} + + {active && ( + + {children} + + )} + + ) } 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>More project <0>collaborators", "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 })