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/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.6.0",
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": "45c9f5b9a1a12f74321ce4658b71ec88d6365ec1"
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.position === 'left' ? 0 : 'auto'};
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 width={dropdownWidth} top={dropdownTopOverlap} position={dropdownPosition}>
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: 28px;
317
+ margin-bottom: 22px;
317
318
  `,
318
319
  cardTopHeading: css`
319
- font-size: 22px;
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};