@datarobot/design-system 28.9.3 → 28.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/floating-panel/constants.d.ts +21 -0
- package/cjs/floating-panel/constants.js +94 -0
- package/cjs/floating-panel/draggable-area.d.ts +12 -0
- package/cjs/floating-panel/draggable-area.js +72 -0
- package/cjs/floating-panel/floating-panel-dock-button.d.ts +5 -0
- package/cjs/floating-panel/floating-panel-dock-button.js +37 -0
- package/cjs/floating-panel/floating-panel-drag-handle.d.ts +5 -0
- package/cjs/floating-panel/floating-panel-drag-handle.js +51 -0
- package/cjs/floating-panel/floating-panel-header.d.ts +11 -0
- package/cjs/floating-panel/floating-panel-header.js +46 -0
- package/cjs/floating-panel/floating-panel.d.ts +66 -0
- package/cjs/floating-panel/floating-panel.js +140 -0
- package/cjs/floating-panel/index.d.ts +4 -0
- package/cjs/floating-panel/index.js +19 -0
- package/cjs/floating-panel/types.d.ts +21 -0
- package/cjs/floating-panel/types.js +5 -0
- package/cjs/floating-panel/use-floating-panel-root.d.ts +6 -0
- package/cjs/floating-panel/use-floating-panel-root.js +31 -0
- package/cjs/floating-panel/use-floating-panel-state.d.ts +27 -0
- package/cjs/floating-panel/use-floating-panel-state.js +316 -0
- package/cjs/floating-panel/use-floating-panel.d.ts +4 -0
- package/cjs/floating-panel/use-floating-panel.js +14 -0
- package/cjs/index.d.ts +1 -0
- package/cjs/index.js +11 -0
- package/esm/floating-panel/constants.d.ts +21 -0
- package/esm/floating-panel/constants.js +86 -0
- package/esm/floating-panel/draggable-area.d.ts +12 -0
- package/esm/floating-panel/draggable-area.js +65 -0
- package/esm/floating-panel/floating-panel-dock-button.d.ts +5 -0
- package/esm/floating-panel/floating-panel-dock-button.js +30 -0
- package/esm/floating-panel/floating-panel-drag-handle.d.ts +5 -0
- package/esm/floating-panel/floating-panel-drag-handle.js +44 -0
- package/esm/floating-panel/floating-panel-header.d.ts +11 -0
- package/esm/floating-panel/floating-panel-header.js +39 -0
- package/esm/floating-panel/floating-panel.d.ts +66 -0
- package/esm/floating-panel/floating-panel.js +132 -0
- package/esm/floating-panel/index.d.ts +4 -0
- package/esm/floating-panel/index.js +2 -0
- package/esm/floating-panel/types.d.ts +21 -0
- package/esm/floating-panel/types.js +1 -0
- package/esm/floating-panel/use-floating-panel-root.d.ts +6 -0
- package/esm/floating-panel/use-floating-panel-root.js +24 -0
- package/esm/floating-panel/use-floating-panel-state.d.ts +27 -0
- package/esm/floating-panel/use-floating-panel-state.js +310 -0
- package/esm/floating-panel/use-floating-panel.d.ts +4 -0
- package/esm/floating-panel/use-floating-panel.js +9 -0
- package/esm/index.d.ts +1 -0
- package/esm/index.js +1 -0
- package/floating-panel/package.json +7 -0
- package/js/bundle/bundle.js +1339 -327
- package/js/bundle/bundle.min.js +1 -1
- package/js/bundle/index.d.ts +94 -1
- package/package.json +1 -1
- package/styles/index.css +149 -0
- package/styles/index.min.css +1 -1
- package/styles/themes/alpine-light.css +12 -0
- package/styles/themes/alpine-light.min.css +1 -1
- package/styles/themes/midnight-gray.css +12 -0
- package/styles/themes/midnight-gray.min.css +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { faXmark } from '@fortawesome/free-solid-svg-icons/faXmark';
|
|
3
|
+
import { Button, ACCENT_TYPES } from '../button';
|
|
4
|
+
import { FontAwesomeIcon } from '../font-awesome-icon';
|
|
5
|
+
import { useTranslation } from '../hooks/use-translation';
|
|
6
|
+
import { FloatingPanelDockButton } from './floating-panel-dock-button';
|
|
7
|
+
import { FloatingPanelDragHandle } from './floating-panel-drag-handle';
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
export const FloatingPanelHeader = /*#__PURE__*/forwardRef(({
|
|
10
|
+
actions = null,
|
|
11
|
+
onClose,
|
|
12
|
+
isDocked,
|
|
13
|
+
setIsDocked,
|
|
14
|
+
getPosition
|
|
15
|
+
}, ref) => {
|
|
16
|
+
const {
|
|
17
|
+
t
|
|
18
|
+
} = useTranslation();
|
|
19
|
+
return /*#__PURE__*/_jsxs("header", {
|
|
20
|
+
ref: ref,
|
|
21
|
+
className: "floating-panel-header",
|
|
22
|
+
children: [/*#__PURE__*/_jsx(FloatingPanelDragHandle, {}), /*#__PURE__*/_jsxs("div", {
|
|
23
|
+
className: "actions",
|
|
24
|
+
children: [actions, /*#__PURE__*/_jsx(FloatingPanelDockButton, {
|
|
25
|
+
isDocked: isDocked,
|
|
26
|
+
setIsDocked: setIsDocked
|
|
27
|
+
}), /*#__PURE__*/_jsx(Button, {
|
|
28
|
+
testId: "floating-panel-close-action",
|
|
29
|
+
accentType: ACCENT_TYPES.ROUND_ICON,
|
|
30
|
+
onClick: () => onClose(getPosition()),
|
|
31
|
+
"aria-label": t('Close'),
|
|
32
|
+
children: /*#__PURE__*/_jsx(FontAwesomeIcon, {
|
|
33
|
+
icon: faXmark
|
|
34
|
+
})
|
|
35
|
+
})]
|
|
36
|
+
})]
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
FloatingPanelHeader.displayName = 'FloatingPanelHeader';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { Modifier, Placement } from '@popperjs/core';
|
|
3
|
+
import './floating-panel.less';
|
|
4
|
+
import type { ElementReference, PanelState } from './types';
|
|
5
|
+
export type FloatingPanelProps = {
|
|
6
|
+
/**
|
|
7
|
+
* Panel's content
|
|
8
|
+
*/
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Pass custom header action buttons here
|
|
12
|
+
*/
|
|
13
|
+
actions?: React.ReactNode;
|
|
14
|
+
minHeight?: number;
|
|
15
|
+
minWidth?: number;
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
maxHeight?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Position panel relative to this element. Alternative use `initialState`
|
|
20
|
+
*/
|
|
21
|
+
anchorEl?: ElementReference;
|
|
22
|
+
/**
|
|
23
|
+
* Dock element. It should be placed after id="full-screen-modal-root"
|
|
24
|
+
*/
|
|
25
|
+
dockEl?: ElementReference;
|
|
26
|
+
/**
|
|
27
|
+
* Pass an element where the main page content is rendered, this will help calculate the header size
|
|
28
|
+
*/
|
|
29
|
+
pageContentEl?: ElementReference;
|
|
30
|
+
/**
|
|
31
|
+
* Popper's placement, used for initial position if `initialState` is not defined
|
|
32
|
+
*/
|
|
33
|
+
placement?: Placement;
|
|
34
|
+
/**
|
|
35
|
+
* Popper modifies which will override default set of modifiers
|
|
36
|
+
*/
|
|
37
|
+
popperModifiers?: Partial<Modifier<any, any>>[];
|
|
38
|
+
testId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Use this to set initial state. Update it in `onClose` handler to preserve last position.
|
|
41
|
+
*/
|
|
42
|
+
initialState?: PanelState;
|
|
43
|
+
/**
|
|
44
|
+
* Use this to control docked state outside the component
|
|
45
|
+
*/
|
|
46
|
+
isDocked?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Use this to sync up the docked state with consumer when controlled outside
|
|
49
|
+
*/
|
|
50
|
+
setIsDocked?: Dispatch<SetStateAction<boolean>>;
|
|
51
|
+
/**
|
|
52
|
+
* Use this in consumer to update `initialState` in order to preserve last position
|
|
53
|
+
*/
|
|
54
|
+
onClose?: (State: PanelState) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Aria label for the floating panel
|
|
57
|
+
*/
|
|
58
|
+
ariaLabel?: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Floating panel
|
|
62
|
+
* @midnight-gray-supported
|
|
63
|
+
* @uxr-only-supported
|
|
64
|
+
* @alpine-light-supported
|
|
65
|
+
*/
|
|
66
|
+
export declare function FloatingPanel({ children, minHeight, minWidth, maxWidth, maxHeight, anchorEl, dockEl, pageContentEl, initialState, testId, placement, popperModifiers, onClose, actions, ...props }: FloatingPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import React, { useLayoutEffect, useRef } from 'react';
|
|
3
|
+
import classnames from 'classnames';
|
|
4
|
+
import { createPopper } from '@popperjs/core';
|
|
5
|
+
import { OFFSET_MODIFIER } from '../react-popper';
|
|
6
|
+
import { useTranslation } from '../hooks';
|
|
7
|
+
import { FloatingPanelContext, resizeHandles, getHtmlElement } from './constants';
|
|
8
|
+
import { useFloatingPanelState } from './use-floating-panel-state';
|
|
9
|
+
import { DraggableArea } from './draggable-area';
|
|
10
|
+
import { FloatingPanelHeader } from './floating-panel-header';
|
|
11
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
+
const noop = () => false;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Floating panel
|
|
16
|
+
* @midnight-gray-supported
|
|
17
|
+
* @uxr-only-supported
|
|
18
|
+
* @alpine-light-supported
|
|
19
|
+
*/
|
|
20
|
+
export function FloatingPanel({
|
|
21
|
+
children,
|
|
22
|
+
minHeight = 648,
|
|
23
|
+
minWidth = 380,
|
|
24
|
+
maxWidth = 900,
|
|
25
|
+
maxHeight = 800,
|
|
26
|
+
anchorEl,
|
|
27
|
+
dockEl,
|
|
28
|
+
pageContentEl,
|
|
29
|
+
initialState,
|
|
30
|
+
testId = 'floating-panel',
|
|
31
|
+
placement = 'bottom',
|
|
32
|
+
popperModifiers = [OFFSET_MODIFIER],
|
|
33
|
+
onClose = noop,
|
|
34
|
+
actions,
|
|
35
|
+
...props
|
|
36
|
+
}) {
|
|
37
|
+
const containerElement = getHtmlElement(dockEl) || document.getElementById('floating-panel-root') || document.body;
|
|
38
|
+
const initialStateAppliedOnce = useRef(false);
|
|
39
|
+
const {
|
|
40
|
+
t
|
|
41
|
+
} = useTranslation();
|
|
42
|
+
const {
|
|
43
|
+
panelRef,
|
|
44
|
+
headerRef,
|
|
45
|
+
style,
|
|
46
|
+
onResize,
|
|
47
|
+
onMovementStart,
|
|
48
|
+
onResizeEnd,
|
|
49
|
+
setState,
|
|
50
|
+
isDocked,
|
|
51
|
+
setIsDocked,
|
|
52
|
+
getPosition,
|
|
53
|
+
context
|
|
54
|
+
} = useFloatingPanelState({
|
|
55
|
+
minHeight,
|
|
56
|
+
minWidth,
|
|
57
|
+
maxWidth,
|
|
58
|
+
maxHeight,
|
|
59
|
+
initialState,
|
|
60
|
+
isDocked: props.isDocked ?? false,
|
|
61
|
+
setIsDocked: props.setIsDocked ?? noop,
|
|
62
|
+
containerElement,
|
|
63
|
+
pageContentElement: getHtmlElement(pageContentEl)
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// use @popperjs/core to set initial position for the panel
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
if (!panelRef.current) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
panelRef.current.focus();
|
|
72
|
+
if (initialState && !initialStateAppliedOnce.current) {
|
|
73
|
+
initialStateAppliedOnce.current = true;
|
|
74
|
+
return setState(initialState);
|
|
75
|
+
}
|
|
76
|
+
if (!anchorEl) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const popperInstance = createPopper(getHtmlElement(anchorEl), panelRef.current, {
|
|
80
|
+
placement,
|
|
81
|
+
strategy: 'fixed',
|
|
82
|
+
modifiers: popperModifiers,
|
|
83
|
+
onFirstUpdate: () => {
|
|
84
|
+
const {
|
|
85
|
+
left,
|
|
86
|
+
top
|
|
87
|
+
} = panelRef.current.getBoundingClientRect();
|
|
88
|
+
setState(state => ({
|
|
89
|
+
...state,
|
|
90
|
+
left,
|
|
91
|
+
top
|
|
92
|
+
}));
|
|
93
|
+
popperInstance.destroy();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}, [placement]);
|
|
97
|
+
return /*#__PURE__*/_jsx(FloatingPanelContext.Provider, {
|
|
98
|
+
value: context,
|
|
99
|
+
children: /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/_jsxs("section", {
|
|
100
|
+
ref: panelRef,
|
|
101
|
+
"test-id": testId,
|
|
102
|
+
className: classnames('floating-panel', {
|
|
103
|
+
docked: isDocked
|
|
104
|
+
})
|
|
105
|
+
/* eslint-disable-next-line react/forbid-dom-props */,
|
|
106
|
+
style: style,
|
|
107
|
+
"aria-modal": "true",
|
|
108
|
+
role: "dialog",
|
|
109
|
+
"aria-label": props.ariaLabel || t('Floating panel'),
|
|
110
|
+
"aria-describedby": "floating-panel-content",
|
|
111
|
+
tabIndex: -1,
|
|
112
|
+
children: [/*#__PURE__*/_jsx(FloatingPanelHeader, {
|
|
113
|
+
ref: headerRef,
|
|
114
|
+
onClose: onClose,
|
|
115
|
+
getPosition: getPosition,
|
|
116
|
+
isDocked: isDocked,
|
|
117
|
+
setIsDocked: setIsDocked,
|
|
118
|
+
actions: actions
|
|
119
|
+
}), /*#__PURE__*/_jsx("div", {
|
|
120
|
+
id: "floating-panel-content",
|
|
121
|
+
className: "floating-panel-content",
|
|
122
|
+
children: children
|
|
123
|
+
}), !isDocked && resizeHandles.map(handle => /*#__PURE__*/_jsx(DraggableArea, {
|
|
124
|
+
areaId: handle.id,
|
|
125
|
+
className: classnames('resize-handle', handle.id),
|
|
126
|
+
onDrag: onResize,
|
|
127
|
+
onDragStart: onMovementStart,
|
|
128
|
+
onDragEnd: onResizeEnd
|
|
129
|
+
}, handle.id))]
|
|
130
|
+
}), containerElement)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { MutableRefObject } from 'react';
|
|
2
|
+
export type PanelState = {
|
|
3
|
+
top: number;
|
|
4
|
+
left: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
export type DraggableEvent = MouseEvent & {
|
|
9
|
+
areaId: string;
|
|
10
|
+
};
|
|
11
|
+
export type FloatingPanelContextType = {
|
|
12
|
+
onDrag: (event: DraggableEvent) => void;
|
|
13
|
+
onDragStart: (event: DraggableEvent) => void;
|
|
14
|
+
onDragEnd: (event: DraggableEvent | string) => void;
|
|
15
|
+
onMoveWithArrows: (event: React.KeyboardEvent) => void;
|
|
16
|
+
isDocked: boolean;
|
|
17
|
+
setIsDocked: (docked: boolean) => void;
|
|
18
|
+
isMoving: boolean;
|
|
19
|
+
getPosition: () => PanelState;
|
|
20
|
+
};
|
|
21
|
+
export type ElementReference = HTMLElement | Element | MutableRefObject<HTMLElement> | (() => HTMLElement) | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import debounce from 'lodash-es/debounce';
|
|
3
|
+
export function useFloatingPanelRoot({
|
|
4
|
+
containerElement,
|
|
5
|
+
pageContentElement
|
|
6
|
+
}) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
// this is not an exceptional case: when there is no header this could be omitted
|
|
9
|
+
if (!containerElement || !pageContentElement) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const onResize = debounce(() => {
|
|
13
|
+
const contentRect = pageContentElement.getBoundingClientRect();
|
|
14
|
+
containerElement.style.top = `${contentRect.top}px`;
|
|
15
|
+
}, 100);
|
|
16
|
+
onResize();
|
|
17
|
+
const resizeObserver = new ResizeObserver(onResize);
|
|
18
|
+
resizeObserver.observe(pageContentElement);
|
|
19
|
+
return () => {
|
|
20
|
+
onResize.cancel();
|
|
21
|
+
resizeObserver.disconnect();
|
|
22
|
+
};
|
|
23
|
+
}, [containerElement, pageContentElement]);
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { CSSProperties, Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { DraggableEvent, FloatingPanelContextType, PanelState } from './types';
|
|
3
|
+
export type UseFloatingPanelHook = {
|
|
4
|
+
style: CSSProperties;
|
|
5
|
+
context: FloatingPanelContextType;
|
|
6
|
+
panelRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
7
|
+
headerRef: React.MutableRefObject<HTMLHeadElement | null>;
|
|
8
|
+
onResize: (event: DraggableEvent) => void;
|
|
9
|
+
onMovementStart: (event: DraggableEvent) => void;
|
|
10
|
+
onResizeEnd: (event: DraggableEvent | string) => void;
|
|
11
|
+
setState: Dispatch<SetStateAction<PanelState>>;
|
|
12
|
+
isDocked: boolean;
|
|
13
|
+
setIsDocked: (docked: boolean) => void;
|
|
14
|
+
getPosition: () => PanelState;
|
|
15
|
+
};
|
|
16
|
+
export type UseFloatingPanelParams = {
|
|
17
|
+
minHeight: number;
|
|
18
|
+
minWidth: number;
|
|
19
|
+
maxWidth: number;
|
|
20
|
+
maxHeight: number;
|
|
21
|
+
isDocked: boolean;
|
|
22
|
+
setIsDocked: Dispatch<SetStateAction<boolean>>;
|
|
23
|
+
initialState?: PanelState;
|
|
24
|
+
containerElement: HTMLElement;
|
|
25
|
+
pageContentElement?: HTMLElement;
|
|
26
|
+
};
|
|
27
|
+
export declare function useFloatingPanelState({ minWidth, minHeight, maxWidth, maxHeight, isDocked: isDockedExternal, setIsDocked: setIsDockedExternal, initialState, containerElement, pageContentElement, }: UseFloatingPanelParams): UseFloatingPanelHook;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useFocusTrap } from '../hooks';
|
|
3
|
+
import { useFloatingPanelRoot } from './use-floating-panel-root';
|
|
4
|
+
import { MOVE_KEYS, MOVE_KEYS_VALUES, HIGHLIGHTED_CLASSNAME, resizeHandlesMap } from './constants';
|
|
5
|
+
const step = 80;
|
|
6
|
+
const dockAreaWidth = 26;
|
|
7
|
+
const delta = {
|
|
8
|
+
[MOVE_KEYS.UP]: {
|
|
9
|
+
dx: 0,
|
|
10
|
+
dy: -step
|
|
11
|
+
},
|
|
12
|
+
[MOVE_KEYS.RIGHT]: {
|
|
13
|
+
dx: step,
|
|
14
|
+
dy: 0
|
|
15
|
+
},
|
|
16
|
+
[MOVE_KEYS.DOWN]: {
|
|
17
|
+
dx: 0,
|
|
18
|
+
dy: step
|
|
19
|
+
},
|
|
20
|
+
[MOVE_KEYS.LEFT]: {
|
|
21
|
+
dx: -step,
|
|
22
|
+
dy: 0
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export function useFloatingPanelState({
|
|
26
|
+
minWidth,
|
|
27
|
+
minHeight,
|
|
28
|
+
maxWidth,
|
|
29
|
+
maxHeight,
|
|
30
|
+
isDocked: isDockedExternal,
|
|
31
|
+
setIsDocked: setIsDockedExternal,
|
|
32
|
+
initialState,
|
|
33
|
+
containerElement,
|
|
34
|
+
pageContentElement
|
|
35
|
+
}) {
|
|
36
|
+
const [isDocked, setIsDocked] = useState(isDockedExternal);
|
|
37
|
+
const [isMoving, setIsMoving] = useState(false);
|
|
38
|
+
const animationRef = useRef(null);
|
|
39
|
+
const headerRef = useRef(null);
|
|
40
|
+
const headerHeightRef = useRef(36);
|
|
41
|
+
const panelRef = useRef(null);
|
|
42
|
+
const dragStartPositionsRef = useRef();
|
|
43
|
+
const isDockHighlightedRef = useRef(false);
|
|
44
|
+
const containerRef = useRef(containerElement);
|
|
45
|
+
const lastPositionRef = useRef({
|
|
46
|
+
top: 0,
|
|
47
|
+
left: 0,
|
|
48
|
+
width: 0,
|
|
49
|
+
height: 0
|
|
50
|
+
});
|
|
51
|
+
const [state, setState] = useState(initialState ? {
|
|
52
|
+
top: initialState.top || lastPositionRef.current.top,
|
|
53
|
+
left: initialState.left || lastPositionRef.current.left,
|
|
54
|
+
width: between(initialState.width || lastPositionRef.current.width, minWidth, maxWidth),
|
|
55
|
+
height: between(initialState.height || lastPositionRef.current.height, minHeight, maxHeight)
|
|
56
|
+
} : {
|
|
57
|
+
top: 0,
|
|
58
|
+
left: 0,
|
|
59
|
+
width: minWidth,
|
|
60
|
+
height: minHeight
|
|
61
|
+
});
|
|
62
|
+
lastPositionRef.current = state;
|
|
63
|
+
containerRef.current = containerElement;
|
|
64
|
+
const style = useMemo(() => isDocked ? {} : {
|
|
65
|
+
transform: `translate3d(${state.left}px, ${state.top}px, 0px)`,
|
|
66
|
+
width: `${state.width}px`,
|
|
67
|
+
height: `${state.height}px`
|
|
68
|
+
}, [state, isDocked]);
|
|
69
|
+
const onDrag = useCallback(event => {
|
|
70
|
+
if (animationRef.current) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
animationRef.current = requestAnimationFrame(() => {
|
|
74
|
+
const start = dragStartPositionsRef.current;
|
|
75
|
+
const maxLeft = window.innerWidth - start.width * 0.25;
|
|
76
|
+
const maxTop = window.innerHeight - headerHeightRef.current;
|
|
77
|
+
const nextLeft = event.clientX - (start.mouseX - start.left);
|
|
78
|
+
const nextTop = event.clientY - (start.mouseY - start.top);
|
|
79
|
+
setState(state => {
|
|
80
|
+
const nextState = {
|
|
81
|
+
...state,
|
|
82
|
+
top: between(nextTop, 0, maxTop),
|
|
83
|
+
left: between(nextLeft, 0, maxLeft)
|
|
84
|
+
};
|
|
85
|
+
const nextIsDockHighlighted = isInDockArea(nextState);
|
|
86
|
+
// DOM manipulation, not state
|
|
87
|
+
if (nextIsDockHighlighted !== isDockHighlightedRef.current) {
|
|
88
|
+
toggleDockHighlight(nextIsDockHighlighted, containerRef.current);
|
|
89
|
+
isDockHighlightedRef.current = nextIsDockHighlighted;
|
|
90
|
+
}
|
|
91
|
+
return nextState;
|
|
92
|
+
});
|
|
93
|
+
animationRef.current = null;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
const onMovementStart = useCallback(event => {
|
|
97
|
+
const element = panelRef.current;
|
|
98
|
+
const header = headerRef.current;
|
|
99
|
+
if (!element || !header) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
headerHeightRef.current = header.getBoundingClientRect().height;
|
|
103
|
+
const mouseX = event.pageX;
|
|
104
|
+
const mouseY = event.pageY;
|
|
105
|
+
dragStartPositionsRef.current = {
|
|
106
|
+
...getPanelStateFromElement(element),
|
|
107
|
+
mouseX,
|
|
108
|
+
mouseY
|
|
109
|
+
};
|
|
110
|
+
lastPositionRef.current = dragStartPositionsRef.current;
|
|
111
|
+
setIsMoving(true);
|
|
112
|
+
}, []);
|
|
113
|
+
const onDragEnd = useCallback(() => {
|
|
114
|
+
if (isDockHighlightedRef.current) {
|
|
115
|
+
const position = dragStartPositionsRef.current;
|
|
116
|
+
setIsDocked(true);
|
|
117
|
+
setIsDockedExternal(true);
|
|
118
|
+
setState(state => {
|
|
119
|
+
const nextState = {
|
|
120
|
+
...state,
|
|
121
|
+
left: window.innerWidth - position.width - dockAreaWidth * 2
|
|
122
|
+
};
|
|
123
|
+
lastPositionRef.current = nextState;
|
|
124
|
+
return nextState;
|
|
125
|
+
});
|
|
126
|
+
isDockHighlightedRef.current = false;
|
|
127
|
+
toggleDockHighlight(false, containerRef.current);
|
|
128
|
+
}
|
|
129
|
+
setIsMoving(false);
|
|
130
|
+
}, []);
|
|
131
|
+
const onResize = useCallback(e => {
|
|
132
|
+
if (animationRef.current) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
animationRef.current = requestAnimationFrame(() => {
|
|
136
|
+
const start = dragStartPositionsRef.current;
|
|
137
|
+
const statePatch = {};
|
|
138
|
+
const resize = resizeHandlesMap.get(e.areaId);
|
|
139
|
+
if (resize?.right) {
|
|
140
|
+
const width = between(start.width + (e.pageX - start.mouseX), minWidth, maxWidth);
|
|
141
|
+
if (width !== start.width) {
|
|
142
|
+
statePatch.width = width;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (resize?.bottom) {
|
|
146
|
+
const height = between(start.height + (e.pageY - start.mouseY), minHeight, maxHeight);
|
|
147
|
+
if (height !== start.height) {
|
|
148
|
+
statePatch.height = height;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (resize?.top) {
|
|
152
|
+
const height = start.height - (e.pageY - start.mouseY);
|
|
153
|
+
const nextTop = start.top + (e.pageY - start.mouseY);
|
|
154
|
+
if (height > minHeight && height <= maxHeight && nextTop > 0) {
|
|
155
|
+
statePatch.height = height;
|
|
156
|
+
statePatch.top = nextTop;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (resize?.left) {
|
|
160
|
+
const width = start.width - (e.pageX - start.mouseX);
|
|
161
|
+
if (width > minWidth && width <= maxWidth) {
|
|
162
|
+
statePatch.width = width;
|
|
163
|
+
statePatch.left = start.left + (e.pageX - start.mouseX);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (Object.keys(statePatch).length) {
|
|
167
|
+
setState(state => ({
|
|
168
|
+
...state,
|
|
169
|
+
...statePatch
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
animationRef.current = null;
|
|
173
|
+
});
|
|
174
|
+
}, [minWidth, minHeight, maxWidth, maxHeight]);
|
|
175
|
+
const onResizeEnd = useCallback(() => {
|
|
176
|
+
setIsMoving(false);
|
|
177
|
+
}, []);
|
|
178
|
+
const setIsDockedHandler = useCallback(docked => {
|
|
179
|
+
const element = panelRef.current;
|
|
180
|
+
if (!element || docked === isDocked) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (docked) {
|
|
184
|
+
setIsDocked(true);
|
|
185
|
+
} else {
|
|
186
|
+
setIsDocked(false);
|
|
187
|
+
}
|
|
188
|
+
if (isDockedExternal !== docked) {
|
|
189
|
+
setIsDockedExternal(docked);
|
|
190
|
+
}
|
|
191
|
+
}, [isDockedExternal, isDocked, setIsDockedExternal]);
|
|
192
|
+
const onMoveWithArrows = useCallback(event => {
|
|
193
|
+
if (!MOVE_KEYS_VALUES.includes(event.key)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const element = panelRef.current;
|
|
197
|
+
if (!element) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const dx = delta[event.key].dx;
|
|
201
|
+
const dy = delta[event.key].dy;
|
|
202
|
+
const currentState = getPanelStateFromElement(element);
|
|
203
|
+
const maxLeft = window.innerWidth - currentState.width * 0.25;
|
|
204
|
+
const maxTop = window.innerHeight - headerHeightRef.current;
|
|
205
|
+
const nextLeft = currentState.left + dx;
|
|
206
|
+
const nextTop = currentState.top + dy;
|
|
207
|
+
let shouldDockInsteadOfMove = false;
|
|
208
|
+
const nextState = {
|
|
209
|
+
...state,
|
|
210
|
+
top: between(nextTop, 0, maxTop),
|
|
211
|
+
left: between(nextLeft, 0, maxLeft)
|
|
212
|
+
};
|
|
213
|
+
shouldDockInsteadOfMove = isInDockArea(nextState) !== isDocked;
|
|
214
|
+
if (shouldDockInsteadOfMove) {
|
|
215
|
+
setIsDocked(true);
|
|
216
|
+
setIsDockedExternal(true);
|
|
217
|
+
} else {
|
|
218
|
+
setState(nextState);
|
|
219
|
+
}
|
|
220
|
+
}, [isDocked, state, setIsDockedExternal]);
|
|
221
|
+
const getPosition = useCallback(() => {
|
|
222
|
+
return lastPositionRef.current;
|
|
223
|
+
}, []);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (isDockedExternal !== isDocked) {
|
|
226
|
+
setIsDockedHandler(isDockedExternal);
|
|
227
|
+
}
|
|
228
|
+
}, [isDockedExternal]);
|
|
229
|
+
|
|
230
|
+
// update position on window resize
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
const header = headerRef.current;
|
|
233
|
+
headerHeightRef.current = header.getBoundingClientRect().height;
|
|
234
|
+
const onResizeHandler = () => {
|
|
235
|
+
const start = lastPositionRef.current;
|
|
236
|
+
const maxLeft = window.innerWidth - start.width;
|
|
237
|
+
const maxTop = window.innerHeight - headerHeightRef.current;
|
|
238
|
+
if (start.left > maxLeft || start.top > maxTop) {
|
|
239
|
+
setState(state => ({
|
|
240
|
+
...state,
|
|
241
|
+
top: between(start.top, 0, maxTop),
|
|
242
|
+
left: between(start.left, 0, maxLeft)
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
window.addEventListener('resize', onResizeHandler);
|
|
247
|
+
return () => {
|
|
248
|
+
window.removeEventListener('resize', onResizeHandler);
|
|
249
|
+
if (animationRef.current) {
|
|
250
|
+
cancelAnimationFrame(animationRef.current);
|
|
251
|
+
animationRef.current = null;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}, []);
|
|
255
|
+
useFocusTrap(panelRef);
|
|
256
|
+
// update style according to content and header height
|
|
257
|
+
useFloatingPanelRoot({
|
|
258
|
+
containerElement,
|
|
259
|
+
pageContentElement
|
|
260
|
+
});
|
|
261
|
+
const context = useMemo(() => ({
|
|
262
|
+
onDrag,
|
|
263
|
+
onDragStart: onMovementStart,
|
|
264
|
+
onDragEnd,
|
|
265
|
+
onMoveWithArrows,
|
|
266
|
+
isDocked,
|
|
267
|
+
setIsDocked: setIsDockedHandler,
|
|
268
|
+
getPosition,
|
|
269
|
+
isMoving
|
|
270
|
+
}), [isDocked, setIsDockedHandler, onDrag, onDragEnd, onMovementStart, onMoveWithArrows, getPosition, isMoving]);
|
|
271
|
+
const api = {
|
|
272
|
+
style,
|
|
273
|
+
panelRef,
|
|
274
|
+
headerRef,
|
|
275
|
+
onResize,
|
|
276
|
+
onMovementStart,
|
|
277
|
+
onResizeEnd,
|
|
278
|
+
setState,
|
|
279
|
+
context,
|
|
280
|
+
isDocked,
|
|
281
|
+
setIsDocked: setIsDockedHandler,
|
|
282
|
+
getPosition
|
|
283
|
+
};
|
|
284
|
+
return useMemo(() => api, Object.values(api));
|
|
285
|
+
}
|
|
286
|
+
function getPanelStateFromElement(element) {
|
|
287
|
+
const bbox = element.getBoundingClientRect();
|
|
288
|
+
return {
|
|
289
|
+
width: bbox.width,
|
|
290
|
+
height: bbox.height,
|
|
291
|
+
left: bbox.x,
|
|
292
|
+
top: bbox.y
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function isInDockArea(panelPosition) {
|
|
296
|
+
return panelPosition.left + panelPosition.width > window.innerWidth - dockAreaWidth;
|
|
297
|
+
}
|
|
298
|
+
function toggleDockHighlight(isDockHighlighted, element) {
|
|
299
|
+
if (!element) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (isDockHighlighted) {
|
|
303
|
+
element.classList.add(HIGHLIGHTED_CLASSNAME);
|
|
304
|
+
} else {
|
|
305
|
+
element.classList.remove(HIGHLIGHTED_CLASSNAME);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function between(value, min, max) {
|
|
309
|
+
return Math.min(Math.max(value, min), max);
|
|
310
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { FloatingPanelContext } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Use inside the content component for FloatingPanel in order to get access to context
|
|
6
|
+
*/
|
|
7
|
+
export function useFloatingPanel() {
|
|
8
|
+
return useContext(FloatingPanelContext);
|
|
9
|
+
}
|
package/esm/index.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export * from './file-upload';
|
|
|
39
39
|
export * from './file-uploader';
|
|
40
40
|
export * from './files-upload-area';
|
|
41
41
|
export * from './filtering-panel';
|
|
42
|
+
export * from './floating-panel';
|
|
42
43
|
export * from './font-awesome-icon';
|
|
43
44
|
export * from './form-field';
|
|
44
45
|
export * from './full-screen-drawer';
|
package/esm/index.js
CHANGED
|
@@ -39,6 +39,7 @@ export * from './file-upload';
|
|
|
39
39
|
export * from './file-uploader';
|
|
40
40
|
export * from './files-upload-area';
|
|
41
41
|
export * from './filtering-panel';
|
|
42
|
+
export * from './floating-panel';
|
|
42
43
|
export * from './font-awesome-icon';
|
|
43
44
|
export * from './form-field';
|
|
44
45
|
export * from './full-screen-drawer';
|