; observer?: ResizeObserver; viewport?: DOMRect; raf?: number }}
+ */
+const viewportState = { callbacks: new Map() };
+
+/** @type {ResizeObserverCallback} */
+function onResize([entry]) {
+ viewportState.viewport = entry.contentRect;
+ if (viewportState.raf) cancelAnimationFrame(viewportState.raf);
+ viewportState.raf = requestAnimationFrame(() => {
+ viewportState.callbacks.forEach(cb => cb(viewportState.viewport));
+ });
+}
+
+/**
+ * Initializes a `ResizeObserver` to track viewport changes and notify subscribers.
+ * We cache this single observer to avoid the overhead of creating a new one for every dropdown.
+ */
+function initObserver() {
+ if (!viewportState.observer && typeof ResizeObserver !== 'undefined') {
+ viewportState.observer = new ResizeObserver(onResize);
+ viewportState.observer.observe(document.documentElement);
+ }
+}
+
+/**
+ * Registers a callback to be called with the viewport rect on changes.
+ * @param {(viewport: DOMRect | undefined) => void} callback
+ * @returns {() => void} An unsubscribe function to stop listening for viewport changes.
+ */
+function subscribeToViewportRect(callback) {
+ initObserver();
+ callback(viewportState.viewport);
+ viewportState.callbacks.set(callback, callback);
+
+ // Unsubscribe function that cleans up the callback and also the observer if no-one is listening anymore.
+ return () => {
+ viewportState.callbacks.delete(callback);
+ if (viewportState.callbacks.size === 0 && viewportState.observer) {
+ viewportState.observer.disconnect();
+ delete viewportState.observer;
+ }
+ };
+}
+
+/** React hook providing the DOMRect of the viewport. */
+function useViewportRect() {
+ const [viewport, setViewport] = useState(/** @type {DOMRect | undefined} */ (undefined));
+ useEffect(() => subscribeToViewportRect(setViewport), []);
+ return viewport;
+}
+
+/**
+ * Get co-ordinates for a dropdown based on its source element and the viewport.
+ * @param {{ dropdownPosition?: import('./getCoords').Position; open?: boolean }} options
+ *
+ * @example
+ * const [open, setOpen] = useState(false);
+ * const { refs, coords } = useDropDownCoords({ dropdownPosition: 'right', open });
+ *
+ * return (
+ *
+ * // Pass the source ref to the button to attach to.
+ *
+ *
+ *
+ * );
+ */
+export function useDropDownCoords({ dropdownPosition = 'left', open } = {}) {
+ const [x, setX] = useState(0);
+ const [y, setY] = useState(0);
+ const viewport = useViewportRect();
+ const source = useRef(/** @type {HTMLElement | null} */ (null));
+ const dropdown = useRef(/** @type {HTMLElement | null} */ (null));
+
+ useLayoutEffect(() => {
+ if (!open || !viewport || !source.current || !dropdown.current) {
+ return;
+ }
+
+ const { x, y } = getCoords({
+ reference: source.current.getBoundingClientRect(),
+ target: dropdown.current.getBoundingClientRect(),
+ viewport,
+ dropdownPosition,
+ });
+
+ setX(x);
+ setY(y);
+ }, [dropdownPosition, source.current, dropdown.current, viewport, open]);
+
+ return { refs: { source, dropdown }, coords: { x, y } };
+}
diff --git a/packages/decap-cms-ui-default/src/Dropdown.js b/packages/decap-cms-ui-default/src/Dropdown.js
index c64f4aad4d55..be3113b28819 100644
--- a/packages/decap-cms-ui-default/src/Dropdown.js
+++ b/packages/decap-cms-ui-default/src/Dropdown.js
@@ -1,9 +1,10 @@
-import React from 'react';
+import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Wrapper, Button as DropdownButton, Menu, MenuItem } from 'react-aria-menubutton';
+import { useDropDownCoords } from './DropDown/useDropDownCoords';
import { colors, buttons, components, zIndex } from './styles';
import Icon from './Icon';
@@ -11,6 +12,7 @@ const StyledWrapper = styled(Wrapper)`
position: relative;
font-size: 14px;
user-select: none;
+ touch-action: manipulation;
`;
const StyledDropdownButton = styled(DropdownButton)`
@@ -20,6 +22,7 @@ const StyledDropdownButton = styled(DropdownButton)`
padding-left: 20px;
padding-right: 40px;
position: relative;
+ white-space: nowrap;
&:after {
${components.caretDown};
@@ -44,8 +47,7 @@ const DropdownList = styled.ul`
${props => css`
width: ${props.width};
top: ${props.top};
- left: ${props.position === 'left' ? 0 : 'auto'};
- right: ${props.position === 'right' ? 0 : 'auto'};
+ left: ${props.left};
`};
`;
@@ -91,15 +93,23 @@ function Dropdown({
className,
children,
}) {
+ const [open, setOpen] = useState(false);
+ const { coords, refs } = useDropDownCoords({ dropdownPosition, open });
return (
handler()}
+ onMenuToggle={({ isOpen }) => setOpen(isOpen)}
className={className}
>
- {renderButton()}
+ {renderButton()}