Merge pull request #27715 from overleaf/dp-rail-overflow-2

Add new editor rail overflow menu

GitOrigin-RevId: f93da19a2687c099ece4509c22a374a47e94f5ad
This commit is contained in:
David
2025-08-11 14:14:13 +01:00
committed by Copybot
parent 7556e1fd03
commit f921b27155
7 changed files with 132 additions and 17 deletions
@@ -22,6 +22,7 @@ export default /** @type {const} */ ([
'info',
'integration_instructions',
'lightbulb',
'more_vert',
'note_add',
'picture_as_pdf',
'rate_review',
@@ -13,6 +13,7 @@ type RailActionButton = {
icon: AvailableUnfilledIcon
title: string
action: () => void
hide?: boolean
}
type RailDropdown = {
@@ -20,6 +21,7 @@ type RailDropdown = {
icon: AvailableUnfilledIcon
title: string
dropdown: ReactElement
hide?: boolean
}
export type RailAction = RailDropdown | RailActionButton
@@ -31,6 +33,10 @@ export default function RailActionElement({ action }: { action: RailAction }) {
}
}, [action])
if (action.hide) {
return null
}
if ('dropdown' in action) {
return (
<Dropdown align="end" drop="end">
@@ -41,7 +47,7 @@ export default function RailActionElement({ action }: { action: RailAction }) {
>
<span>
<DropdownToggle
id="rail-help-dropdown-btn"
id={`rail-dropdown-btn-${action.key}`}
className="ide-rail-tab-link ide-rail-tab-button ide-rail-tab-dropdown"
as="button"
aria-label={action.title}
@@ -0,0 +1,32 @@
import { DropdownMenu } from '@/shared/components/dropdown/dropdown-menu'
import { RailTabKey } from '../../contexts/rail-context'
import { RailElement } from '../../utils/rail-types'
import RailTab from './rail-tab'
export default function RailOverflowDropdown({
tabs,
isOpen,
selectedTab,
}: {
tabs: RailElement[]
isOpen: boolean
selectedTab: RailTabKey
}) {
return (
<DropdownMenu className="ide-rail-overflow-dropdown">
{tabs
.filter(({ hide }) => !hide)
.map(({ icon, key, indicator, title, disabled }) => (
<RailTab
open={isOpen && selectedTab === key}
key={key}
eventKey={key}
icon={icon}
indicator={indicator}
title={title}
disabled={disabled}
/>
))}
</DropdownMenu>
)
}
@@ -29,6 +29,8 @@ import { RailElement } from '../../utils/rail-types'
import RailPanel from './rail-panel'
import RailResizeHandle from './rail-resize-handle'
import RailModals from './rail-modals'
import RailOverflowDropdown from './rail-overflow-dropdown'
import useRailOverflow from '../../hooks/use-rail-overflow'
export const RailLayout = () => {
const { sendEvent } = useEditorAnalytics()
@@ -166,6 +168,25 @@ export const RailLayout = () => {
const isReviewPanelOpen = selectedTab === 'review-panel'
const { tabsInRail, tabsInOverflow, tabWrapperRef } =
useRailOverflow(railTabs)
const moreOptionsAction: RailAction = useMemo(() => {
return {
key: 'more-options',
icon: 'more_vert',
title: t('more_options'),
hide: tabsInOverflow.length === 0,
dropdown: (
<RailOverflowDropdown
tabs={tabsInOverflow}
isOpen={isOpen}
selectedTab={selectedTab}
/>
),
}
}, [t, isOpen, selectedTab, tabsInOverflow])
return (
<TabContainer
mountOnEnter // Only render when necessary (so that we can lazy load tab content)
@@ -183,22 +204,24 @@ export const RailLayout = () => {
aria-label={t('files_collaboration_integrations_logs')}
>
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
{railTabs
.filter(({ hide }) => !hide)
.map(({ icon, key, indicator, title, disabled }) => (
<RailTab
open={isOpen && selectedTab === key}
key={key}
eventKey={key}
icon={icon}
indicator={indicator}
title={title}
disabled={disabled}
/>
))}
<div className="flex-grow-1" />
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
{tabsInRail
.filter(({ hide }) => !hide)
.map(({ icon, key, indicator, title, disabled }) => (
<RailTab
open={isOpen && selectedTab === key}
key={key}
eventKey={key}
icon={icon}
indicator={indicator}
title={title}
disabled={disabled}
/>
))}
<RailActionElement key="more-options" action={moreOptionsAction} />
</div>
<nav aria-label={t('help_editor_settings')}>
{railActions?.map(action => (
{railActions.map(action => (
<RailActionElement key={action.key} action={action} />
))}
</nav>
@@ -0,0 +1,35 @@
import { useCallback, useState } from 'react'
import { RailElement } from '../utils/rail-types'
import { useResizeObserver } from '@/shared/hooks/use-resize-observer'
const useRailOverflow = (railTabs: RailElement[]) => {
const [tabsInRail, setTabsInRail] = useState<RailElement[]>(railTabs)
const [tabsInOverflow, setTabsInOverflow] = useState<RailElement[]>([])
const handleResize = useCallback(
(element: Element) => {
const height = (element as HTMLElement).offsetHeight
const tabHeight =
(element.querySelector('.ide-rail-tab-link')?.clientHeight ?? 0) + 4 // 4px gap between tabs
const numTabsToFit = Math.floor(height / tabHeight)
if (numTabsToFit >= railTabs.length) {
setTabsInRail(railTabs)
setTabsInOverflow([])
} else {
const sliceIndex = Math.max(numTabsToFit - 1, 0)
setTabsInRail(railTabs.slice(0, sliceIndex))
setTabsInOverflow(railTabs.slice(sliceIndex))
}
},
[railTabs]
)
const { elementRef: tabWrapperRef } = useResizeObserver(handleResize)
return { tabsInRail, tabsInOverflow, tabWrapperRef }
}
export default useRailOverflow
@@ -148,11 +148,18 @@ body {
.ide-rail-tabs-nav {
height: 100%;
display: flex;
flex-direction: column;
flex-flow: column nowrap;
gap: var(--spacing-02);
padding-bottom: var(--spacing-04);
}
.ide-rail-tabs-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-02);
}
.ide-rail-tab-dropdown {
border: 0;
@@ -161,6 +168,17 @@ body {
}
}
.ide-rail-overflow-dropdown {
min-width: 0;
background-color: var(--ide-rail-background);
&.show {
display: flex;
flex-direction: column;
gap: var(--spacing-02);
}
}
.new-error-logs-promo {
display: flex;
gap: var(--spacing-04);