BootstrapIconMock_ArrowLeftRight
diff --git a/frontend/src/components/editor-page/splitter/split-divider/split-divider.module.scss b/frontend/src/components/editor-page/splitter/split-divider/split-divider.module.scss
index bab48be62..990bb31c1 100644
--- a/frontend/src/components/editor-page/splitter/split-divider/split-divider.module.scss
+++ b/frontend/src/components/editor-page/splitter/split-divider/split-divider.module.scss
@@ -1,22 +1,46 @@
/*
- * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+ * SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.divider {
- width: 15px;
- background: var(--bs-body-bg);
+ width: 5px;
+ // This ensures that the handle area is thicker but the visual representation is still thin
+ background: linear-gradient(
+ to right,
+ transparent 0,
+ transparent 2px,
+ var(--bs-border-color) 2px,
+ var(--bs-border-color) 3px,
+ transparent 3px
+ );
z-index: 1;
- box-shadow: 0 0 6px var(--bs-gray-400);
+ cursor: col-resize;
+ position: relative;
display: flex;
align-items: center;
justify-content: center;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0 -5px;
+ }
+
+ :global(.btn) {
+ cursor: default;
+ }
}
.grabber {
cursor: col-resize;
+ user-select: none;
+
+ > * {
+ pointer-events: none;
+ }
}
.middle {
diff --git a/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx b/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx
index 423ab8b25..7bc22df3e 100644
--- a/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx
+++ b/frontend/src/components/editor-page/splitter/split-divider/split-divider.tsx
@@ -7,7 +7,8 @@ import { concatCssClasses } from '../../../../utils/concat-css-classes'
import { testId } from '../../../../utils/test-id'
import { UiIcon } from '../../../common/icons/ui-icon'
import styles from './split-divider.module.scss'
-import React, { useMemo } from 'react'
+import type { MouseEvent, TouchEvent } from 'react'
+import React, { useCallback, useMemo } from 'react'
import { Button } from 'react-bootstrap'
import {
ArrowLeft as IconArrowLeft,
@@ -28,6 +29,7 @@ export interface SplitDividerProps {
forceOpen: boolean
focusLeft: boolean
focusRight: boolean
+ splitValue: number
dividerButtonsShift: DividerButtonsShift
}
@@ -42,6 +44,7 @@ export interface SplitDividerProps {
* @param focusLeft defines if the left button should be focused
* @param focusRight defines if the right button should be focused
* @param forceOpen defines if the arrow buttons should always be visible
+ * @param splitValue the currently selected split position in percent
*/
export const SplitDivider: React.FC
= ({
onGrab,
@@ -50,7 +53,8 @@ export const SplitDivider: React.FC = ({
dividerButtonsShift,
focusLeft,
focusRight,
- forceOpen
+ forceOpen,
+ splitValue
}) => {
const className = useMemo(() => {
return concatCssClasses(styles.middle, {
@@ -59,24 +63,53 @@ export const SplitDivider: React.FC = ({
})
}, [dividerButtonsShift, forceOpen])
+ const stopResizing = useCallback((event: MouseEvent | TouchEvent) => {
+ event.stopPropagation()
+ }, [])
+
+ const startResizingFromIndicator = useCallback(
+ (event: MouseEvent | TouchEvent) => {
+ event.stopPropagation()
+ onGrab()
+ },
+ [onGrab]
+ )
+
return (
-
-
+ // oxlint-disable-next-line jsx_a11y/no-static-element-interactions
+
+ {/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
+
-
diff --git a/frontend/src/components/editor-page/splitter/splitter.spec.tsx b/frontend/src/components/editor-page/splitter/splitter.spec.tsx
index c31ff503e..508c70230 100644
--- a/frontend/src/components/editor-page/splitter/splitter.spec.tsx
+++ b/frontend/src/components/editor-page/splitter/splitter.spec.tsx
@@ -15,6 +15,26 @@ jest.mock('../../../redux/editor-config/methods')
const setEditorSplitPosition = jest.spyOn(EditorConfigModule, 'setEditorSplitPosition').mockReturnValue()
+const findMoveOverlay = (container: HTMLElement): Element => {
+ const overlay = container.querySelector('.move-overlay')
+
+ if (!overlay) {
+ throw new Error('Move overlay was not rendered')
+ }
+
+ return overlay
+}
+
+const findGrabber = (container: HTMLElement): Element => {
+ const grabber = container.querySelector('.grabber')
+
+ if (!grabber) {
+ throw new Error('Grabber was not rendered')
+ }
+
+ return grabber
+}
+
describe('Splitter', () => {
describe('resize', () => {
beforeEach(() => {
@@ -44,15 +64,24 @@ describe('Splitter', () => {
const divider = await screen.findByTestId('splitter-divider')
fireEvent.mouseDown(divider, {})
- fireEvent.mouseMove(window, Mock.of
({ buttons: 1, clientX: 1920 }))
- fireEvent.mouseUp(window)
+ let moveOverlay = findMoveOverlay(view.container)
+ fireEvent.mouseMove(moveOverlay, Mock.of({ buttons: 1, clientX: 1920 }))
+ fireEvent.mouseUp(moveOverlay)
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
fireEvent.mouseDown(divider, {})
- fireEvent.mouseMove(window, Mock.of({ buttons: 1, clientX: 0 }))
- fireEvent.mouseUp(window)
+ moveOverlay = findMoveOverlay(view.container)
+ fireEvent.mouseMove(moveOverlay, Mock.of({ buttons: 1, clientX: 0 }))
+ fireEvent.mouseUp(moveOverlay)
expect(setEditorSplitPosition).toHaveBeenCalledWith(0)
+ const grabber = findGrabber(view.container)
+ fireEvent.mouseDown(grabber, {})
+ moveOverlay = findMoveOverlay(view.container)
+ fireEvent.mouseMove(moveOverlay, Mock.of({ buttons: 1, clientX: 960 }))
+ fireEvent.mouseUp(moveOverlay)
+ expect(setEditorSplitPosition).toHaveBeenCalledWith(50)
+
fireEvent.mouseMove(window, Mock.of({ buttons: 1, clientX: 1920 }))
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
})
@@ -77,8 +106,9 @@ describe('Splitter', () => {
}
fireEvent.touchStart(divider, {})
+ let moveOverlay = findMoveOverlay(view.container)
fireEvent.touchMove(
- window,
+ moveOverlay,
Mock.of({
touches: [
{ ...defaultTouchEvent, clientX: 1920 },
@@ -86,12 +116,14 @@ describe('Splitter', () => {
]
})
)
- fireEvent.touchEnd(window)
+ fireEvent.touchEnd(moveOverlay)
+ expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
expect(view.container).toMatchSnapshot('touch move to left')
fireEvent.touchStart(divider, {})
+ moveOverlay = findMoveOverlay(view.container)
fireEvent.touchMove(
- window,
+ moveOverlay,
Mock.of({
touches: [
{ ...defaultTouchEvent, clientX: 0 },
@@ -99,11 +131,15 @@ describe('Splitter', () => {
]
})
)
- fireEvent.touchCancel(window)
+ fireEvent.touchCancel(moveOverlay)
+ expect(setEditorSplitPosition).toHaveBeenCalledWith(0)
expect(view.container).toMatchSnapshot('touch move to right')
+ const grabber = findGrabber(view.container)
+ fireEvent.touchStart(grabber, {})
+ moveOverlay = findMoveOverlay(view.container)
fireEvent.touchMove(
- window,
+ moveOverlay,
Mock.of({
touches: [
{ ...defaultTouchEvent, clientX: 500 },
@@ -111,6 +147,8 @@ describe('Splitter', () => {
]
})
)
+ fireEvent.touchEnd(moveOverlay)
+ expect(setEditorSplitPosition).toHaveBeenCalledWith(expect.closeTo(26, 1))
expect(view.container).toMatchSnapshot('touch move to middle')
})
})
diff --git a/frontend/src/components/editor-page/splitter/splitter.tsx b/frontend/src/components/editor-page/splitter/splitter.tsx
index be59bea4a..6a913455b 100644
--- a/frontend/src/components/editor-page/splitter/splitter.tsx
+++ b/frontend/src/components/editor-page/splitter/splitter.tsx
@@ -169,6 +169,7 @@ export const Splitter: React.FC = ({ additionalContainerClassName
forceOpen={resizingInProgress}
focusLeft={relativeSplitValue < SNAP_PERCENTAGE}
focusRight={relativeSplitValue > 100 - SNAP_PERCENTAGE}
+ splitValue={adjustedRelativeSplitValue}
dividerButtonsShift={dividerButtonsShift}
/>