decap-cms-ui-default 3.6.0 → 3.8.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/CHANGELOG.md +17 -0
- package/dist/decap-cms-ui-default.js +2 -2
- package/dist/decap-cms-ui-default.js.map +1 -1
- package/dist/esm/DropDown/getCoords.js +106 -0
- package/dist/esm/DropDown/useDropDownCoords.js +122 -0
- package/dist/esm/Dropdown.js +27 -12
- package/dist/esm/Icon/images/_index.js +24 -0
- package/dist/esm/styles.js +41 -40
- package/package.json +3 -2
- package/src/DropDown/getCoords.js +68 -0
- package/src/DropDown/useDropDownCoords.js +110 -0
- package/src/Dropdown.js +15 -5
- package/src/Icon/images/_index.js +4 -0
- package/src/Icon/images/copy.svg +1 -0
- package/src/Icon/images/download.svg +1 -0
- package/src/styles.js +9 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decap-cms-ui-default",
|
|
3
3
|
"description": "Default UI components for Decap CMS.",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.8.0",
|
|
5
5
|
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-ui-default",
|
|
6
6
|
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
|
7
7
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"react-aria-menubutton": "^7.0.0",
|
|
21
|
+
"react-immutable-proptypes": "^2.1.0",
|
|
21
22
|
"react-transition-group": "^4.4.5"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
@@ -27,5 +28,5 @@
|
|
|
27
28
|
"prop-types": "^15.7.2",
|
|
28
29
|
"react": "^19.1.0"
|
|
29
30
|
},
|
|
30
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "567a80101f4846853701ad7d8abdc29b5e4fab56"
|
|
31
32
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** @typedef {'left' | 'right' | 'center' | undefined} Position */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate relative co-ordinates for a dropdown to position it below a reference element.
|
|
5
|
+
* @param {{ reference: DOMRect; target: DOMRect; viewport: DOMRect; dropdownPosition: Position }} options
|
|
6
|
+
*/
|
|
7
|
+
export function getCoords({ reference, target, dropdownPosition, viewport }) {
|
|
8
|
+
let { x, y } = computeCoordsFromPlacement({ reference, target, dropdownPosition });
|
|
9
|
+
({ x, y } = constrain({ x, y, viewport, target }));
|
|
10
|
+
return relativize({ x, y, reference });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{ reference: DOMRect; target: DOMRect; dropdownPosition: Position }} options
|
|
15
|
+
* @returns {{ x: number; y: number }} co-ordinates
|
|
16
|
+
*/
|
|
17
|
+
function computeCoordsFromPlacement({ reference, target, dropdownPosition }) {
|
|
18
|
+
const commonAlign = reference.width / 2 - target.width / 2;
|
|
19
|
+
|
|
20
|
+
const coords = {
|
|
21
|
+
x: reference.x + commonAlign,
|
|
22
|
+
y: reference.y + reference.height,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
switch (dropdownPosition) {
|
|
26
|
+
case 'left':
|
|
27
|
+
coords.x -= commonAlign;
|
|
28
|
+
break;
|
|
29
|
+
case 'right':
|
|
30
|
+
coords.x += commonAlign;
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return coords;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Constrain co-ordinates within the viewport.
|
|
40
|
+
* @param {{ x: number; y: number, viewport: DOMRect, target: DOMRect }} options
|
|
41
|
+
*/
|
|
42
|
+
function constrain({ x, y, viewport, target }) {
|
|
43
|
+
const overflow = {
|
|
44
|
+
left: x,
|
|
45
|
+
right: x + target.width - viewport.width,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
x = clamp(x - overflow.left, x, x - overflow.right);
|
|
49
|
+
|
|
50
|
+
return { x, y };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {number} min
|
|
55
|
+
* @param {number} value
|
|
56
|
+
* @param {number} max
|
|
57
|
+
*/
|
|
58
|
+
function clamp(min, value, max) {
|
|
59
|
+
return Math.min(Math.max(min, value), max);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert absolute viewport co-ordinates into element-relative co-ordinates.
|
|
64
|
+
* @param {{ x: number; y: number; reference: DOMRect }} options
|
|
65
|
+
*/
|
|
66
|
+
function relativize({ x, y, reference }) {
|
|
67
|
+
return { x: x - reference.x, y: y - reference.y };
|
|
68
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { getCoords } from './getCoords';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @type {{ callbacks: Map<Function, Function>; observer?: ResizeObserver; viewport?: DOMRect; raf?: number }}
|
|
7
|
+
*/
|
|
8
|
+
const viewportState = { callbacks: new Map() };
|
|
9
|
+
|
|
10
|
+
/** @type {ResizeObserverCallback} */
|
|
11
|
+
function onResize([entry]) {
|
|
12
|
+
viewportState.viewport = entry.contentRect;
|
|
13
|
+
if (viewportState.raf) cancelAnimationFrame(viewportState.raf);
|
|
14
|
+
viewportState.raf = requestAnimationFrame(() => {
|
|
15
|
+
viewportState.callbacks.forEach(cb => cb(viewportState.viewport));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initializes a `ResizeObserver` to track viewport changes and notify subscribers.
|
|
21
|
+
* We cache this single observer to avoid the overhead of creating a new one for every dropdown.
|
|
22
|
+
*/
|
|
23
|
+
function initObserver() {
|
|
24
|
+
if (!viewportState.observer && typeof ResizeObserver !== 'undefined') {
|
|
25
|
+
viewportState.observer = new ResizeObserver(onResize);
|
|
26
|
+
viewportState.observer.observe(document.documentElement);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Registers a callback to be called with the viewport rect on changes.
|
|
32
|
+
* @param {(viewport: DOMRect | undefined) => void} callback
|
|
33
|
+
* @returns {() => void} An unsubscribe function to stop listening for viewport changes.
|
|
34
|
+
*/
|
|
35
|
+
function subscribeToViewportRect(callback) {
|
|
36
|
+
initObserver();
|
|
37
|
+
callback(viewportState.viewport);
|
|
38
|
+
viewportState.callbacks.set(callback, callback);
|
|
39
|
+
|
|
40
|
+
// Unsubscribe function that cleans up the callback and also the observer if no-one is listening anymore.
|
|
41
|
+
return () => {
|
|
42
|
+
viewportState.callbacks.delete(callback);
|
|
43
|
+
if (viewportState.callbacks.size === 0 && viewportState.observer) {
|
|
44
|
+
viewportState.observer.disconnect();
|
|
45
|
+
delete viewportState.observer;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** React hook providing the DOMRect of the viewport. */
|
|
51
|
+
function useViewportRect() {
|
|
52
|
+
const [viewport, setViewport] = useState(/** @type {DOMRect | undefined} */ (undefined));
|
|
53
|
+
useEffect(() => subscribeToViewportRect(setViewport), []);
|
|
54
|
+
return viewport;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get co-ordinates for a dropdown based on its source element and the viewport.
|
|
59
|
+
* @param {{ dropdownPosition?: import('./getCoords').Position; open?: boolean }} options
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const [open, setOpen] = useState(false);
|
|
63
|
+
* const { refs, coords } = useDropDownCoords({ dropdownPosition: 'right', open });
|
|
64
|
+
*
|
|
65
|
+
* return (
|
|
66
|
+
* <div style={{ position: 'relative' }}>
|
|
67
|
+
* // Pass the source ref to the button to attach to.
|
|
68
|
+
* <button ref={refs.source} onClick={() => setOpen(!open)}>
|
|
69
|
+
* Open Dropdown
|
|
70
|
+
* </button>
|
|
71
|
+
* <ul
|
|
72
|
+
* // Pass the dropdown ref to the dropdown menu to measure it.
|
|
73
|
+
* ref={refs.dropdown}
|
|
74
|
+
* style={{
|
|
75
|
+
* position: 'absolute',
|
|
76
|
+
* // Use the calculated co-ordinates to position the dropdown.
|
|
77
|
+
* left: coords.x,
|
|
78
|
+
* top: coords.y,
|
|
79
|
+
* display: open ? 'block' : 'none'
|
|
80
|
+
* }}>
|
|
81
|
+
* ...
|
|
82
|
+
* </ul>
|
|
83
|
+
* </div>
|
|
84
|
+
* );
|
|
85
|
+
*/
|
|
86
|
+
export function useDropDownCoords({ dropdownPosition = 'left', open } = {}) {
|
|
87
|
+
const [x, setX] = useState(0);
|
|
88
|
+
const [y, setY] = useState(0);
|
|
89
|
+
const viewport = useViewportRect();
|
|
90
|
+
const source = useRef(/** @type {HTMLElement | null} */ (null));
|
|
91
|
+
const dropdown = useRef(/** @type {HTMLElement | null} */ (null));
|
|
92
|
+
|
|
93
|
+
useLayoutEffect(() => {
|
|
94
|
+
if (!open || !viewport || !source.current || !dropdown.current) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { x, y } = getCoords({
|
|
99
|
+
reference: source.current.getBoundingClientRect(),
|
|
100
|
+
target: dropdown.current.getBoundingClientRect(),
|
|
101
|
+
viewport,
|
|
102
|
+
dropdownPosition,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
setX(x);
|
|
106
|
+
setY(y);
|
|
107
|
+
}, [dropdownPosition, source.current, dropdown.current, viewport, open]);
|
|
108
|
+
|
|
109
|
+
return { refs: { source, dropdown }, coords: { x, y } };
|
|
110
|
+
}
|
package/src/Dropdown.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { css } from '@emotion/react';
|
|
4
4
|
import styled from '@emotion/styled';
|
|
5
5
|
import { Wrapper, Button as DropdownButton, Menu, MenuItem } from 'react-aria-menubutton';
|
|
6
6
|
|
|
7
|
+
import { useDropDownCoords } from './DropDown/useDropDownCoords';
|
|
7
8
|
import { colors, buttons, components, zIndex } from './styles';
|
|
8
9
|
import Icon from './Icon';
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ const StyledWrapper = styled(Wrapper)`
|
|
|
11
12
|
position: relative;
|
|
12
13
|
font-size: 14px;
|
|
13
14
|
user-select: none;
|
|
15
|
+
touch-action: manipulation;
|
|
14
16
|
`;
|
|
15
17
|
|
|
16
18
|
const StyledDropdownButton = styled(DropdownButton)`
|
|
@@ -20,6 +22,7 @@ const StyledDropdownButton = styled(DropdownButton)`
|
|
|
20
22
|
padding-left: 20px;
|
|
21
23
|
padding-right: 40px;
|
|
22
24
|
position: relative;
|
|
25
|
+
white-space: nowrap;
|
|
23
26
|
|
|
24
27
|
&:after {
|
|
25
28
|
${components.caretDown};
|
|
@@ -44,8 +47,7 @@ const DropdownList = styled.ul`
|
|
|
44
47
|
${props => css`
|
|
45
48
|
width: ${props.width};
|
|
46
49
|
top: ${props.top};
|
|
47
|
-
left: ${props.
|
|
48
|
-
right: ${props.position === 'right' ? 0 : 'auto'};
|
|
50
|
+
left: ${props.left};
|
|
49
51
|
`};
|
|
50
52
|
`;
|
|
51
53
|
|
|
@@ -91,15 +93,23 @@ function Dropdown({
|
|
|
91
93
|
className,
|
|
92
94
|
children,
|
|
93
95
|
}) {
|
|
96
|
+
const [open, setOpen] = useState(false);
|
|
97
|
+
const { coords, refs } = useDropDownCoords({ dropdownPosition, open });
|
|
94
98
|
return (
|
|
95
99
|
<StyledWrapper
|
|
96
100
|
closeOnSelection={closeOnSelection}
|
|
97
101
|
onSelection={handler => handler()}
|
|
102
|
+
onMenuToggle={({ isOpen }) => setOpen(isOpen)}
|
|
98
103
|
className={className}
|
|
99
104
|
>
|
|
100
|
-
{renderButton()}
|
|
105
|
+
<div ref={refs.source}>{renderButton()}</div>
|
|
101
106
|
<Menu>
|
|
102
|
-
<DropdownList
|
|
107
|
+
<DropdownList
|
|
108
|
+
ref={refs.dropdown}
|
|
109
|
+
width={dropdownWidth}
|
|
110
|
+
top={dropdownTopOverlap}
|
|
111
|
+
left={coords.x + 'px'}
|
|
112
|
+
>
|
|
103
113
|
{children}
|
|
104
114
|
</DropdownList>
|
|
105
115
|
</Menu>
|
|
@@ -11,6 +11,8 @@ import iconCircle from './circle.svg';
|
|
|
11
11
|
import iconClose from './close.svg';
|
|
12
12
|
import iconCode from './code.svg';
|
|
13
13
|
import iconCodeBlock from './code-block.svg';
|
|
14
|
+
import iconCopy from './copy.svg';
|
|
15
|
+
import iconDownload from './download.svg';
|
|
14
16
|
import iconDragHandle from './drag-handle.svg';
|
|
15
17
|
import iconEye from './eye.svg';
|
|
16
18
|
import iconFolder from './folder.svg';
|
|
@@ -63,6 +65,8 @@ const images = {
|
|
|
63
65
|
close: iconClose,
|
|
64
66
|
code: iconCode,
|
|
65
67
|
'code-block': iconCodeBlock,
|
|
68
|
+
copy: iconCopy,
|
|
69
|
+
download: iconDownload,
|
|
66
70
|
'drag-handle': iconDragHandle,
|
|
67
71
|
eye: iconEye,
|
|
68
72
|
folder: iconFolder,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
|
package/src/styles.js
CHANGED
|
@@ -94,6 +94,7 @@ const lengths = {
|
|
|
94
94
|
borderWidth: '2px',
|
|
95
95
|
topCardWidth: '682px',
|
|
96
96
|
pageMargin: '28px 18px',
|
|
97
|
+
pageMarginMobile: '12px 8px',
|
|
97
98
|
objectWidgetTopBarContainerPadding: '0 14px 14px',
|
|
98
99
|
};
|
|
99
100
|
|
|
@@ -313,12 +314,16 @@ const components = {
|
|
|
313
314
|
width: ${lengths.topCardWidth};
|
|
314
315
|
max-width: 100%;
|
|
315
316
|
padding: 18px 20px;
|
|
316
|
-
margin-bottom:
|
|
317
|
+
margin-bottom: 22px;
|
|
317
318
|
`,
|
|
318
319
|
cardTopHeading: css`
|
|
319
|
-
font-size:
|
|
320
|
+
font-size: 20px;
|
|
321
|
+
line-height: 24px;
|
|
322
|
+
@media (min-width: 500px) {
|
|
323
|
+
font-size: 22px;
|
|
324
|
+
line-height: 26px;
|
|
325
|
+
}
|
|
320
326
|
font-weight: 600;
|
|
321
|
-
line-height: 37px;
|
|
322
327
|
margin: 0;
|
|
323
328
|
padding: 0;
|
|
324
329
|
`,
|
|
@@ -327,6 +332,7 @@ const components = {
|
|
|
327
332
|
color: ${colors.text};
|
|
328
333
|
font-size: 14px;
|
|
329
334
|
margin-top: 8px;
|
|
335
|
+
margin-bottom: 0;
|
|
330
336
|
`,
|
|
331
337
|
objectWidgetTopBarContainer: css`
|
|
332
338
|
padding: ${lengths.objectWidgetTopBarContainerPadding};
|