mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-08-07 07:14:49 +00:00
refactor(splitter): make splitter thinner and totally draggable
It was mentioned that the splitter was too thick and therefore taking up too much screen-space. This commit changes it to be thinner. While the visual representation is just a thin line, the actual trigger zone is wider to make it not a perfect-pixel-matching game. Furthermore, you can now drag the splitter on every point of it, not just on the button. Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
committed by
Philip Molares
parent
da9f1d9b8e
commit
1dc4fea414
@@ -17,9 +17,15 @@ exports[`Splitter resize can change size with mouse 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="divider"
|
||||
data-testid="splitter-divider"
|
||||
id="editor-splitter"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="middle"
|
||||
@@ -34,10 +40,7 @@ exports[`Splitter resize can change size with mouse 1`] = `
|
||||
BootstrapIconMock_ArrowLeft
|
||||
</button>
|
||||
<span
|
||||
aria-orientation="vertical"
|
||||
class="grabber"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
BootstrapIconMock_ArrowLeftRight
|
||||
</span>
|
||||
@@ -82,9 +85,15 @@ exports[`Splitter resize can change size with touch: touch initial 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="divider"
|
||||
data-testid="splitter-divider"
|
||||
id="editor-splitter"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="middle"
|
||||
@@ -99,10 +108,7 @@ exports[`Splitter resize can change size with touch: touch initial 1`] = `
|
||||
BootstrapIconMock_ArrowLeft
|
||||
</button>
|
||||
<span
|
||||
aria-orientation="vertical"
|
||||
class="grabber"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
BootstrapIconMock_ArrowLeftRight
|
||||
</span>
|
||||
@@ -147,9 +153,15 @@ exports[`Splitter resize can change size with touch: touch move to left 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="divider"
|
||||
data-testid="splitter-divider"
|
||||
id="editor-splitter"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="middle"
|
||||
@@ -164,10 +176,7 @@ exports[`Splitter resize can change size with touch: touch move to left 1`] = `
|
||||
BootstrapIconMock_ArrowLeft
|
||||
</button>
|
||||
<span
|
||||
aria-orientation="vertical"
|
||||
class="grabber"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
BootstrapIconMock_ArrowLeftRight
|
||||
</span>
|
||||
@@ -212,9 +221,15 @@ exports[`Splitter resize can change size with touch: touch move to middle 1`] =
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="divider"
|
||||
data-testid="splitter-divider"
|
||||
id="editor-splitter"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="middle"
|
||||
@@ -229,10 +244,7 @@ exports[`Splitter resize can change size with touch: touch move to middle 1`] =
|
||||
BootstrapIconMock_ArrowLeft
|
||||
</button>
|
||||
<span
|
||||
aria-orientation="vertical"
|
||||
class="grabber"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
BootstrapIconMock_ArrowLeftRight
|
||||
</span>
|
||||
@@ -277,9 +289,15 @@ exports[`Splitter resize can change size with touch: touch move to right 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-orientation="vertical"
|
||||
aria-valuemax="100"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="50"
|
||||
class="divider"
|
||||
data-testid="splitter-divider"
|
||||
id="editor-splitter"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="middle"
|
||||
@@ -294,10 +312,7 @@ exports[`Splitter resize can change size with touch: touch move to right 1`] = `
|
||||
BootstrapIconMock_ArrowLeft
|
||||
</button>
|
||||
<span
|
||||
aria-orientation="vertical"
|
||||
class="grabber"
|
||||
role="separator"
|
||||
tabindex="0"
|
||||
>
|
||||
BootstrapIconMock_ArrowLeftRight
|
||||
</span>
|
||||
|
||||
+28
-4
@@ -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 {
|
||||
|
||||
@@ -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<SplitDividerProps> = ({
|
||||
onGrab,
|
||||
@@ -50,7 +53,8 @@ export const SplitDivider: React.FC<SplitDividerProps> = ({
|
||||
dividerButtonsShift,
|
||||
focusLeft,
|
||||
focusRight,
|
||||
forceOpen
|
||||
forceOpen,
|
||||
splitValue
|
||||
}) => {
|
||||
const className = useMemo(() => {
|
||||
return concatCssClasses(styles.middle, {
|
||||
@@ -59,24 +63,53 @@ export const SplitDivider: React.FC<SplitDividerProps> = ({
|
||||
})
|
||||
}, [dividerButtonsShift, forceOpen])
|
||||
|
||||
const stopResizing = useCallback((event: MouseEvent | TouchEvent) => {
|
||||
event.stopPropagation()
|
||||
}, [])
|
||||
|
||||
const startResizingFromIndicator = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
event.stopPropagation()
|
||||
onGrab()
|
||||
},
|
||||
[onGrab]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.divider} {...testId('splitter-divider')} id={'editor-splitter'}>
|
||||
<div className={className}>
|
||||
// oxlint-disable-next-line jsx_a11y/no-static-element-interactions
|
||||
<div
|
||||
className={styles.divider}
|
||||
id={'editor-splitter'}
|
||||
role={'separator'}
|
||||
aria-orientation={'vertical'}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={splitValue}
|
||||
tabIndex={0}
|
||||
{...testId('splitter-divider')}
|
||||
onMouseDown={onGrab}
|
||||
onTouchStart={onGrab}>
|
||||
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
|
||||
<div className={className} onMouseDown={startResizingFromIndicator} onTouchStart={startResizingFromIndicator}>
|
||||
<div className={styles.buttons}>
|
||||
<Button variant={focusLeft ? 'secondary' : 'light'} onClick={onLeftButtonClick}>
|
||||
<Button
|
||||
variant={focusLeft ? 'secondary' : 'light'}
|
||||
onClick={onLeftButtonClick}
|
||||
onMouseDown={stopResizing}
|
||||
onTouchStart={stopResizing}>
|
||||
<UiIcon icon={IconArrowLeft} />
|
||||
</Button>
|
||||
{/* oxlint-disable-next-line jsx_a11y/no-static-element-interactions */}
|
||||
<span
|
||||
role={'separator'}
|
||||
aria-orientation={'vertical'}
|
||||
tabIndex={0}
|
||||
onMouseDown={onGrab}
|
||||
onTouchStart={onGrab}
|
||||
className={styles['grabber']}>
|
||||
className={styles['grabber']}
|
||||
onMouseDownCapture={startResizingFromIndicator}
|
||||
onTouchStartCapture={startResizingFromIndicator}>
|
||||
<UiIcon icon={IconArrowLeftRight} />
|
||||
</span>
|
||||
<Button variant={focusRight ? 'secondary' : 'light'} onClick={onRightButtonClick}>
|
||||
<Button
|
||||
variant={focusRight ? 'secondary' : 'light'}
|
||||
onClick={onRightButtonClick}
|
||||
onMouseDown={stopResizing}
|
||||
onTouchStart={stopResizing}>
|
||||
<UiIcon icon={IconArrowRight} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
||||
fireEvent.mouseUp(window)
|
||||
let moveOverlay = findMoveOverlay(view.container)
|
||||
fireEvent.mouseMove(moveOverlay, Mock.of<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
||||
fireEvent.mouseUp(moveOverlay)
|
||||
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
|
||||
|
||||
fireEvent.mouseDown(divider, {})
|
||||
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 0 }))
|
||||
fireEvent.mouseUp(window)
|
||||
moveOverlay = findMoveOverlay(view.container)
|
||||
fireEvent.mouseMove(moveOverlay, Mock.of<MouseEvent>({ 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<MouseEvent>({ buttons: 1, clientX: 960 }))
|
||||
fireEvent.mouseUp(moveOverlay)
|
||||
expect(setEditorSplitPosition).toHaveBeenCalledWith(50)
|
||||
|
||||
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ 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<TouchEvent>({
|
||||
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<TouchEvent>({
|
||||
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<TouchEvent>({
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -169,6 +169,7 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
|
||||
forceOpen={resizingInProgress}
|
||||
focusLeft={relativeSplitValue < SNAP_PERCENTAGE}
|
||||
focusRight={relativeSplitValue > 100 - SNAP_PERCENTAGE}
|
||||
splitValue={adjustedRelativeSplitValue}
|
||||
dividerButtonsShift={dividerButtonsShift}
|
||||
/>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user