decap-cms-ui-default 3.6.0 → 3.7.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 +6 -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/package.json +2 -2
- package/src/DropDown/getCoords.js +68 -0
- package/src/DropDown/useDropDownCoords.js +110 -0
- package/src/Dropdown.js +15 -5
|
@@ -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>
|