Skip to content

Commit 1b286c4

Browse files
authored
Merge pull request #404 from vtfk/dialog-draggable
Dialog draggable
2 parents 3925341 + 43ea060 commit 1b286c4

File tree

5 files changed

+214
-29
lines changed

5 files changed

+214
-29
lines changed

src/ui/Dialog/Dialog.stories.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,54 @@ export function PreventScrollingBehind () {
161161
</>
162162
)
163163
}
164+
165+
export function Draggable () {
166+
const [dialogOpen, setIsDialogOpen] = useState(false)
167+
const [dialog2Open, setDialog2Open] = useState(false)
168+
169+
return (
170+
<>
171+
<div>
172+
<Button onClick={() => { setIsDialogOpen(!dialogOpen) }}>{`${!dialogOpen ? 'Åpne' : 'Lukk'} modal`}</Button>
173+
</div>
174+
<p />
175+
176+
<Dialog
177+
isOpen={dialogOpen}
178+
onDismiss={() => { setIsDialogOpen(false) }}
179+
showCloseButton
180+
draggable
181+
contained
182+
width='60%'
183+
height='50%'
184+
>
185+
<DialogTitle>This is the first dialog</DialogTitle>
186+
<DialogBody>
187+
Click and drag the top part of the dialog to move it.<br />
188+
This dialog is <b>contained</b> and is not allowed to move outside it's parents bounds
189+
</DialogBody>
190+
<DialogActions>
191+
<Button size='small' onClick={() => setDialog2Open(true)}>Open nested dialog</Button>
192+
<Button size='small' type='secondary' onClick={() => { setIsDialogOpen(false) }}>Close</Button>
193+
</DialogActions>
194+
</Dialog>
195+
<Dialog
196+
isOpen={dialog2Open}
197+
onDismiss={() => setDialog2Open(false)}
198+
width='50%'
199+
draggable
200+
height='30%'
201+
>
202+
<DialogTitle>
203+
Draggable nested dialog
204+
</DialogTitle>
205+
<DialogBody>
206+
This dialog is also draggable, but it is not <b>contained</b>
207+
</DialogBody>
208+
<DialogActions>
209+
<Button size='small' onClick={() => setDialog2Open(false)}>Close</Button>
210+
</DialogActions>
211+
</Dialog>
212+
</>
213+
)
214+
}

src/ui/Dialog/index.js

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,14 @@ import { ReactComponent as CloseIcon } from './icon-close.svg'
66

77
import './styles.scss'
88
import ScrollLock from 'react-scrolllock'
9+
import { Draggable } from '../Draggable'
910

10-
export function Dialog ({ isOpen, title, className, persistent, width, height, showCloseButton, onDismiss, onCloseBtnClick, onClickOutside, onPressEscape, style, ...props }) {
11+
export function Dialog ({ isOpen, title, className, persistent, width, height, draggable, contained, showCloseButton, onDismiss, onCloseBtnClick, onClickOutside, onPressEscape, style, ...props }) {
1112
// Set an unique ID for the dialog
1213
const [id] = useState(`${nanoid()}`)
1314

1415
const [clickStartedInsideDialog, setClickStartedInsideDialog] = useState(undefined)
15-
16-
const parsedStyles = useMemo(() => {
17-
const _style = { ...style }
18-
if (width) _style.width = width
19-
if (height) _style.height = height
20-
return _style
21-
}, [style, width, height])
16+
const [isDragging, setIsDragging] = useState(false)
2217

2318
// onCreated lifecycle-hook
2419
useEffect(() => {
@@ -40,19 +35,27 @@ export function Dialog ({ isOpen, title, className, persistent, width, height, s
4035
}
4136
}
4237

43-
function handleMouseUp(e) {
44-
setClickStartedInsideDialog(false);
38+
function handleMouseUp (e) {
39+
setClickStartedInsideDialog(false)
40+
setIsDragging(false)
4541
}
4642

4743
// Register eventhandlers
48-
document.addEventListener('keydown', handleKeyDown);
49-
document.addEventListener('mouseup', handleMouseUp);
44+
document.addEventListener('keydown', handleKeyDown)
45+
document.addEventListener('mouseup', handleMouseUp)
5046
return () => {
51-
document.removeEventListener('keydown', handleKeyDown);
47+
document.removeEventListener('keydown', handleKeyDown)
5248
document.removeEventListener('mouseup', handleMouseUp)
5349
}
5450
})
5551

52+
const dialogClasses = useMemo(() => {
53+
let classes = 'dialog'
54+
if (isDragging) classes += ' dialog-no-select'
55+
56+
return classes
57+
}, [isDragging])
58+
5659
// Plays the shaking animation-effect when the dialog is persistent
5760
function shakeDialogBox () {
5861
const dialog = document.getElementById(`dialog-${id}`)
@@ -66,7 +69,7 @@ export function Dialog ({ isOpen, title, className, persistent, width, height, s
6669
}
6770

6871
function handleBackdropClick (e) {
69-
if(clickStartedInsideDialog) return;
72+
if (clickStartedInsideDialog) return
7073
const clickedBackdrop = e.target.id === `dialog-backdrop-${id}`
7174

7275
if (isOpen && clickedBackdrop) {
@@ -86,22 +89,26 @@ export function Dialog ({ isOpen, title, className, persistent, width, height, s
8689
isOpen === true &&
8790
<ScrollLock isActive={isOpen}>
8891
<div id={`dialog-backdrop-${id}`} className={`dialog-backdrop ${className}`} onMouseUp={(e) => handleBackdropClick(e)}>
89-
<div
90-
id={`dialog-${id}`}
91-
className='dialog'
92-
aria-label='dialog'
93-
aria-modal='true'
94-
role='dialog'
95-
style={parsedStyles}
96-
onMouseDown={(e) => { setClickStartedInsideDialog(true); e.preventDefault(); e.stopPropagation(); }}
97-
>
98-
{!persistent && showCloseButton &&
99-
<button className='dialog-close-btn' onClick={(e) => { handleCloseBtnClick(); e.preventDefault() }} aria-label='Lukk'>
100-
<CloseIcon alt='' />
101-
</button>}
102-
{props.children}
103-
</div>
92+
<Draggable active={draggable && isDragging} contained={contained} width={width} height={height}>
93+
<div
94+
id={`dialog-${id}`}
95+
className={dialogClasses}
96+
aria-label='dialog'
97+
aria-modal='true'
98+
role='dialog'
99+
style={style}
100+
onMouseDown={() => { setClickStartedInsideDialog(true) }}
101+
>
102+
{ draggable && <div className='dialog-drag-area' onMouseDown={() => setIsDragging(true)} /> }
103+
{!persistent && showCloseButton &&
104+
<button className='dialog-close-btn' onClick={(e) => { handleCloseBtnClick(); e.preventDefault() }} aria-label='Lukk'>
105+
<CloseIcon alt='' />
106+
</button>}
107+
{props.children}
108+
</div>
109+
</Draggable>
104110
</div>
111+
105112
</ScrollLock>
106113
)
107114
}
@@ -133,6 +140,8 @@ export function DialogActions ({ children, style }) {
133140
Dialog.propTypes = {
134141
children: PropTypes.any,
135142
className: PropTypes.string,
143+
contained: PropTypes.bool,
144+
draggable: PropTypes.bool,
136145
height: PropTypes.string,
137146
isOpen: PropTypes.bool.isRequired,
138147
onClickOutside: PropTypes.func,

src/ui/Dialog/styles.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
position: relative;
2222
display: flex;
2323
flex-direction: column;
24+
width: 100%;
25+
height: 100%;
2426
gap: 0.5rem;
2527
border-radius: 10px;
2628
min-width: 200px;
@@ -39,6 +41,19 @@
3941
z-index: 1001;
4042
}
4143

44+
.dialog-drag-area {
45+
position: absolute;
46+
top: 0;
47+
left: 0;
48+
right: 0;
49+
height: $baseUnit * 4;
50+
cursor: move;
51+
}
52+
53+
.dialog-no-select {
54+
user-select: none!important;
55+
}
56+
4257
.dialog-title {
4358
font-weight: 500;
4459
font-size: x-large;

src/ui/Draggable/index.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React, { useEffect, useMemo, useRef, useState } from 'react'
2+
import PropTypes from 'prop-types'
3+
import './styles.scss'
4+
5+
export function Draggable ({ children, active, contained, width, height }) {
6+
const [isDragging, setIsDragging] = useState(false)
7+
const [coordinates, setCoordinates] = useState(undefined)
8+
const [mouseSelfDelta, setMouseSelfDelta] = useState(undefined)
9+
const thisRef = useRef(undefined)
10+
11+
const parsedStyle = useMemo(() => {
12+
const _s = { width, height }
13+
if (coordinates?.x && coordinates?.y) {
14+
_s.top = `${coordinates.y}px`
15+
_s.left = `${coordinates.x}px`
16+
}
17+
return _s
18+
}, [width, height, coordinates])
19+
20+
useEffect(() => {
21+
/**
22+
*
23+
* @param {MouseEvent} e
24+
* @returns
25+
*/
26+
function handleMouseMove (e) {
27+
if (!active || !isDragging) return
28+
29+
if (thisRef?.current) {
30+
let x = e.clientX - mouseSelfDelta.x
31+
let y = e.clientY - mouseSelfDelta.y
32+
33+
const width = thisRef.current.clientWidth
34+
const height = thisRef.current.clientHeight
35+
const bboxRight = x + thisRef.current.clientWidth
36+
const bboxBottom = y + height
37+
const parentWidth = thisRef.current.parentNode.clientWidth
38+
const parentHeight = thisRef.current.parentNode.clientHeight
39+
40+
if (contained) {
41+
if (x <= 1) x = 1
42+
else if (bboxRight >= parentWidth) x = parentWidth - width
43+
if (y <= 1) y = 1
44+
else if (bboxBottom >= parentHeight) y = parentHeight - height
45+
} else {
46+
if (x === 0) x = -1
47+
if (y === 0) y = -1
48+
}
49+
50+
setCoordinates({
51+
x,
52+
y
53+
})
54+
}
55+
}
56+
57+
document.addEventListener('mousemove', handleMouseMove)
58+
59+
return () => {
60+
document.removeEventListener('mousemove', handleMouseMove)
61+
}
62+
}, [active, isDragging])
63+
64+
useEffect(() => {
65+
function handleMouseUp () {
66+
setIsDragging(false)
67+
}
68+
69+
document.addEventListener('mouseup', handleMouseUp)
70+
71+
return () => {
72+
document.removeEventListener('mouseup', handleMouseUp)
73+
}
74+
})
75+
76+
function handleDragStart (e) {
77+
if (thisRef) {
78+
setIsDragging(true)
79+
setMouseSelfDelta({
80+
x: e.clientX - thisRef.current.offsetLeft,
81+
y: e.clientY - thisRef.current.offsetTop
82+
})
83+
setCoordinates({
84+
x: thisRef.current.offsetLeft,
85+
y: thisRef.current.offsetTop
86+
})
87+
}
88+
}
89+
90+
function handleDragEnd (e) {
91+
setIsDragging(false)
92+
}
93+
94+
return (
95+
<div className='draggable' ref={thisRef} onMouseDown={(e) => handleDragStart(e)} style={parsedStyle} onMouseUp={(e) => handleDragEnd(e)}>
96+
{children && children}
97+
</div>
98+
)
99+
}
100+
101+
Draggable.propTypes = {
102+
active: PropTypes.bool,
103+
children: PropTypes.node,
104+
contained: PropTypes.bool,
105+
height: PropTypes.string,
106+
width: PropTypes.string
107+
}

src/ui/Draggable/styles.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.draggable {
2+
position: absolute;
3+
}

0 commit comments

Comments
 (0)