Files
overleaf-cep/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx
T
Rebeka Dekany 1ee87a823d Unify component wrappers and OL-prefix dropdown components (#35152)
* Rename dropdown primitives to OL-prefixed components

* Migrate all consumers to OL-prefixed dropdown components

* Migrate subscription dropdown shells to OL components

* Update pdf-preview story to OL-prefixed dropdown components

* Migrate navbar and spelling dropdowns to OL components

* Move dropdown primitives to ol/ol-dropdown-menu, remove BS5 import aliases

* Rename dropdown prop types to OL-prefixed, clean up BS5 import aliases

* Widen OLDropdownItem/Toggle/Menu props to include AriaAttributes and data-*

GitOrigin-RevId: 0babce46867849b3f176bac8cf1652dc76fd34da
2026-09-17 11:23:43 +00:00

235 lines
7.1 KiB
TypeScript

import { forwardRef, memo, MouseEventHandler } from 'react'
import {
OLDropdown,
OLDropdownMenu,
OLDropdownToggle,
} from '@/shared/components/ol/ol-dropdown-menu'
import DropdownMenuItem from '@/shared/components/dropdown/dropdown-menu-item'
import MaterialIcon from '@/shared/components/material-icon'
import classNames from 'classnames'
import { useTrackChangesStateActionsContext } from '../context/track-changes-state-context'
import { useUserContext } from '@/shared/context/user-context'
import { useTranslation } from 'react-i18next'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import usePersistedState from '@/shared/hooks/use-persisted-state'
import { sendMB } from '@/infrastructure/event-tracking'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useProjectContext } from '@/shared/context/project-context'
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
import { useEditorContext } from '@/shared/context/editor-context'
import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode'
import { useFeatureFlag } from '@/shared/context/split-test-context'
function ReviewModeDropdownItems() {
const { t } = useTranslation()
const user = useUserContext()
const { saveTrackChangesForCurrentUser, saveTrackChanges } =
useTrackChangesStateActionsContext()
const mode = useTrackingChangesMode()
const { permissionsLevel } = useIdeReactContext()
const { write, trackedWrite } = usePermissionsContext()
const { features } = useProjectContext()
const { setUpgradeTrackChangesModal } = useEditorContext()
const showViewOption = permissionsLevel === 'readOnly'
const view = useCodeMirrorViewContext()
return (
<>
<DropdownMenuItem
disabled={!write}
onClick={() => {
if (mode === 'edit') {
view.focus()
return
}
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: 'edit',
})
if (user?.id) {
saveTrackChangesForCurrentUser(false)
} else {
saveTrackChanges({ on_for_guests: false })
}
view.focus()
}}
description={t('edit_content_directly')}
leadingIcon="edit"
active={write && mode === 'edit'}
>
{t('editing')}
</DropdownMenuItem>
<DropdownMenuItem
disabled={permissionsLevel === 'readOnly'}
onClick={() => {
if (mode === 'review') {
view.focus()
return
}
if (!features.trackChanges) {
setUpgradeTrackChangesModal({
show: true,
location: 'review-switcher',
})
} else {
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: 'review',
})
if (user?.id) {
saveTrackChangesForCurrentUser(true)
} else {
saveTrackChanges({ on_for_guests: true })
}
view.focus()
}
}}
description={
permissionsLevel === 'review' && !trackedWrite
? t('comment_only')
: t('edits_become_suggestions')
}
leadingIcon="rate_review"
active={trackedWrite && mode === 'review'}
>
{t('reviewing')}
</DropdownMenuItem>
{showViewOption && (
<DropdownMenuItem
description={t('can_view_content')}
leadingIcon="visibility"
active={mode === 'view'}
>
{t('viewing')}
</DropdownMenuItem>
)}
</>
)
}
function ReviewModeSwitcher() {
const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
if (isToolbarMigration) {
return (
<OLDropdown className="review-mode-switcher" align="end">
<OLDropdownToggle
as={ModeSwitcherToggleButton}
id="review-mode-switcher"
/>
<OLDropdownMenu
flip={false}
popperConfig={{ strategy: 'fixed' }}
// renderOnMount ensures the menu is in the DOM on mount so Popper.js
// can calculate its position correctly when using strategy: 'fixed'
// inside the toolbar's stacking context
renderOnMount
>
<ReviewModeDropdownItems />
</OLDropdownMenu>
</OLDropdown>
)
}
// Portal variant (default): sticky container keeps the button floating
// over the editor at top-right of the scroll area.
return (
<div className="review-mode-switcher-container">
<OLDropdown className="review-mode-switcher" align="end">
<OLDropdownToggle
as={ModeSwitcherToggleButton}
id="review-mode-switcher"
/>
<OLDropdownMenu flip={false}>
<ReviewModeDropdownItems />
</OLDropdownMenu>
</OLDropdown>
</div>
)
}
const ModeSwitcherToggleButton = forwardRef<
HTMLButtonElement,
{ onClick: MouseEventHandler<HTMLButtonElement>; 'aria-expanded': boolean }
>(({ onClick, 'aria-expanded': ariaExpanded }, ref) => {
const { t } = useTranslation()
const mode = useTrackingChangesMode()
if (mode === 'edit') {
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="editing"
iconType="edit"
label={t('editing')}
ariaExpanded={ariaExpanded}
/>
)
} else if (mode === 'review') {
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="reviewing"
iconType="rate_review"
label={t('reviewing')}
ariaExpanded={ariaExpanded}
/>
)
}
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="viewing"
iconType="visibility"
label={t('viewing')}
ariaExpanded={ariaExpanded}
/>
)
})
const ModeSwitcherToggleButtonContent = forwardRef<
HTMLButtonElement,
{
onClick: MouseEventHandler<HTMLButtonElement>
className: string
iconType: string
label: string
ariaExpanded: boolean
}
>(({ onClick, className, iconType, label, ariaExpanded }, ref) => {
const [isFirstTimeUsed, setIsFirstTimeUsed] = usePersistedState(
`modeSwitcherFirstTimeUsed`,
true
)
return (
<button
className={classNames('review-mode-switcher-toggle-button', className, {
'review-mode-switcher-toggle-button-expanded': isFirstTimeUsed,
})}
ref={ref}
onClick={event => {
setIsFirstTimeUsed(false)
onClick(event)
}}
aria-expanded={ariaExpanded}
aria-label={label}
>
<MaterialIcon type={iconType} />
<div className="review-mode-switcher-toggle-label">{label}</div>
<MaterialIcon type="keyboard_arrow_down" />
</button>
)
})
ModeSwitcherToggleButton.displayName = 'ModeSwitcherToggleButton'
ModeSwitcherToggleButtonContent.displayName = 'ModeSwitcherToggleButtonContent'
export default memo(ReviewModeSwitcher)