UNPKG

@atlaskit/dropdown-menu

Version:

A dropdown menu displays a list of actions or options to a user.

362 lines (348 loc) 16.3 kB
/* dropdown-menu-top-layer.tsx generated by @compiled/babel-plugin v2.0.0 */ "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("./dropdown-menu-top-layer.compiled.css"); var _react = _interopRequireWildcard(require("react")); var React = _react; var _runtime = require("@compiled/react/runtime"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _bindEventListener = require("bind-event-listener"); var _new = _interopRequireDefault(require("@atlaskit/button/new")); var _keycodes = require("@atlaskit/ds-lib/keycodes"); var _mergeRefs = _interopRequireDefault(require("@atlaskit/ds-lib/merge-refs")); var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop")); var _useControlled = _interopRequireDefault(require("@atlaskit/ds-lib/use-controlled")); var _useFocusEvent = _interopRequireDefault(require("@atlaskit/ds-lib/use-focus-event")); var _chevronDown = _interopRequireDefault(require("@atlaskit/icon/core/chevron-down")); var _menuGroup = _interopRequireDefault(require("@atlaskit/menu/menu-group")); var _spinner = _interopRequireDefault(require("@atlaskit/spinner")); var _focus = require("@atlaskit/top-layer/focus"); var _getAriaForTrigger = require("@atlaskit/top-layer/get-aria-for-trigger"); var _placementMap = require("@atlaskit/top-layer/placement-map"); var _popover = require("@atlaskit/top-layer/popover"); var _useAnchorPosition = require("@atlaskit/top-layer/use-anchor-position"); var _useArrowNavigation = require("@atlaskit/top-layer/use-arrow-navigation"); var _usePopoverId = require("@atlaskit/top-layer/use-popover-id"); var _useWidthFromAnchor = require("@atlaskit/top-layer/use-width-from-anchor"); var _selectionStore = _interopRequireDefault(require("./internal/context/selection-store")); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var MAX_HEIGHT = "calc(100vh - 16px)"; var styles = { spinnerContainer: "_1e0c1txw _1ul91lit _1bah1h6o _1q51v47k _y4tiv47k _85i5v47k _bozgv47k", menuContent: "_2rkofajl _bfhk1bhr _16qs130s" }; /** * Event types produced by trigger interactions. * * - `React.MouseEvent<Element>`: from the trigger's `onClick` handler * - `React.KeyboardEvent<Element>`: from the trigger's `onClick` when activated via keyboard * - `KeyboardEvent`: native event from the ArrowDown `bind(window, ...)` listener */ /** * Determines whether a trigger interaction was keyboard-initiated. * * Keyboard signals: * - `type === 'keydown'` (native KeyboardEvent from ArrowDown handler) * - `clientX/clientY === 0` (assistive technology click) * - `detail === 0` (keyboard-activated click via Enter/Space) */ function isKeyboardTriggered(event) { if (event.type === 'keydown') { return true; } if ('clientX' in event && (event.clientX === 0 || event.clientY === 0)) { return true; } if (event.detail === 0) { return true; } return false; } /** * Loading indicator for the dropdown menu. */ function LoadingIndicator(_ref) { var _ref$statusLabel = _ref.statusLabel, statusLabel = _ref$statusLabel === void 0 ? 'Loading' : _ref$statusLabel, testId = _ref.testId; return /*#__PURE__*/React.createElement("div", { role: "menuitem", className: (0, _runtime.ax)([styles.spinnerContainer]) }, /*#__PURE__*/React.createElement(_spinner.default, { size: "small", label: statusLabel, testId: testId })); } /** * Top-layer implementation of DropdownMenu. * * Replaces the legacy `@atlaskit/popup` + `@atlaskit/portal` + `@atlaskit/layering` pipeline * with the native Popover API via `@atlaskit/top-layer`. * * What is no longer needed: * - Portal: top layer handles stacking natively * - FocusLock / react-focus-lock: popover=auto provides light dismiss * - z-index: top layer is always above everything * - FocusManager (ref registration): replaced by DOM-query-based `useArrowNavigation` * - handle-focus.tsx: replaced by `useArrowNavigation` * - Layering context: top layer nesting is handled by the browser * - Fallback placements / Popper: CSS Anchor Positioning handles positioning */ function DropdownMenuTopLayer(_ref2) { var _ref5; var _ref2$autoFocus = _ref2.autoFocus, autoFocus = _ref2$autoFocus === void 0 ? false : _ref2$autoFocus, children = _ref2.children, _ref2$defaultOpen = _ref2.defaultOpen, defaultOpen = _ref2$defaultOpen === void 0 ? false : _ref2$defaultOpen, _ref2$isLoading = _ref2.isLoading, isLoading = _ref2$isLoading === void 0 ? false : _ref2$isLoading, isOpenProp = _ref2.isOpen, _ref2$onOpenChange = _ref2.onOpenChange, onOpenChange = _ref2$onOpenChange === void 0 ? _noop.default : _ref2$onOpenChange, _ref2$placement = _ref2.placement, placement = _ref2$placement === void 0 ? 'bottom-start' : _ref2$placement, _ref2$shouldFitContai = _ref2.shouldFitContainer, shouldFitContainer = _ref2$shouldFitContai === void 0 ? false : _ref2$shouldFitContai, returnFocusRef = _ref2.returnFocusRef, spacing = _ref2.spacing, statusLabel = _ref2.statusLabel, testId = _ref2.testId, trigger = _ref2.trigger, label = _ref2.label, interactionName = _ref2.interactionName, menuLabel = _ref2.menuLabel; var _useControlledState = (0, _useControlled.default)(isOpenProp, function () { return defaultOpen; }), _useControlledState2 = (0, _slicedToArray2.default)(_useControlledState, 2), isLocalOpen = _useControlledState2[0], setLocalIsOpen = _useControlledState2[1]; var triggerRef = (0, _react.useRef)(null); var popoverRef = (0, _react.useRef)(null); var menuRef = (0, _react.useRef)(null); var _useState = (0, _react.useState)(false), _useState2 = (0, _slicedToArray2.default)(_useState, 2), isTriggeredUsingKeyboard = _useState2[0], setTriggeredUsingKeyboard = _useState2[1]; var popoverId = (0, _usePopoverId.usePopoverId)(); var topLayerPlacement = (0, _react.useMemo)(function () { return (0, _placementMap.fromLegacyPlacement)({ legacy: placement }); }, [placement]); (0, _useAnchorPosition.useAnchorPosition)({ anchorRef: triggerRef, popoverRef: popoverRef, placement: topLayerPlacement, isOpen: isLocalOpen }); (0, _useWidthFromAnchor.useWidthFromAnchor)({ mode: shouldFitContainer ? 'min-anchor' : 'none', popoverRef: popoverRef, anchorRef: triggerRef, isOpen: isLocalOpen }); // Close handling. // Focus restoration is handled natively by the Popover API: // - Escape: browser restores focus to the trigger automatically // - Click-outside: browser does NOT restore (correct behavior) // // The only custom focus handling needed is `returnFocusRef`: when provided, // we redirect focus to a different element than the trigger. We do this // in the onClose callback via rAF, which runs after the browser's native // restoration, effectively overriding it. var handleOnClose = (0, _react.useCallback)(function (_ref3) { var _reason = _ref3.reason; if (returnFocusRef) { requestAnimationFrame(function () { var _returnFocusRef$curre; (_returnFocusRef$curre = returnFocusRef.current) === null || _returnFocusRef$curre === void 0 || _returnFocusRef$curre.focus(); }); } setLocalIsOpen(false); onOpenChange({ isOpen: false, event: null }); }, [onOpenChange, returnFocusRef, setLocalIsOpen]); // Trigger click handling. var handleTriggerClicked = (0, _react.useCallback)(function (event) { var newValue = !isLocalOpen; setTriggeredUsingKeyboard(isKeyboardTriggered(event)); setLocalIsOpen(newValue); // Extract the native DOM event for onOpenChange var nativeEvent = 'nativeEvent' in event ? event.nativeEvent : event; onOpenChange({ isOpen: newValue, event: nativeEvent }); }, [isLocalOpen, setLocalIsOpen, onOpenChange]); var _useFocus = (0, _useFocusEvent.default)(), isFocused = _useFocus.isFocused, bindFocus = _useFocus.bindFocus; // When trigger is focused, open dropdown on ArrowDown (top-level only). // Per WAI-ARIA, ArrowDown opens a menu from a menubar/button trigger, // but inside a vertical submenu, ArrowDown navigates between siblings // and ArrowRight opens nested menus instead. (0, _react.useEffect)(function () { var _triggerRef$current; if (!isFocused || isLocalOpen) { return _noop.default; } // Do not open on ArrowDown if this trigger is inside a parent menu. // Nested menus should only be opened via ArrowRight or Enter. var isNestedTrigger = ((_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 ? void 0 : _triggerRef$current.closest('[role="menu"]')) != null; return (0, _bindEventListener.bind)(window, { type: 'keydown', listener: function openOnKeyDown(e) { if (e.key === _keycodes.KEY_DOWN && !isNestedTrigger) { e.preventDefault(); handleTriggerClicked(e); } else if ((e.code === _keycodes.KEY_SPACE || e.key === _keycodes.KEY_ENTER) && e.detail === 0) { setTriggeredUsingKeyboard(true); } } }); }, [isFocused, isLocalOpen, handleTriggerClicked]); // Arrow navigation. // useArrowNavigation handles ArrowUp/Down, Home/End, and Tab-to-close // by querying focusable elements in the menu DOM container. var handleArrowClose = (0, _react.useCallback)(function () { handleOnClose({ reason: 'escape' }); }, [handleOnClose]); var handleNestedOpen = (0, _react.useCallback)(function (_ref4) { var trigger = _ref4.trigger; trigger.click(); }, []); var handleNestedClose = (0, _react.useCallback)(function () { handleOnClose({ reason: 'escape' }); }, [handleOnClose]); (0, _useArrowNavigation.useArrowNavigation)({ containerRef: menuRef, onClose: handleArrowClose, onNestedOpen: handleNestedOpen, onNestedClose: handleNestedClose, isEnabled: isLocalOpen, filter: _useArrowNavigation.isAtCurrentMenuLevel }); // Auto-focus first item on open. (0, _react.useEffect)(function () { if (!isLocalOpen || !isTriggeredUsingKeyboard && !autoFocus) { return; } requestAnimationFrame(function () { var menu = menuRef.current; if (!menu) { return; } var firstItem = (0, _focus.getFirstFocusable)({ container: menu }); firstItem === null || firstItem === void 0 || firstItem.focus(); }); }, [isLocalOpen, isTriggeredUsingKeyboard, autoFocus]); // Close on menu item click. // Close when a regular menuitem is clicked, but not checkboxes/radios // and not nested triggers (items with aria-haspopup). var handleMenuClick = (0, _react.useCallback)(function (e) { var _target$closest; var target = e.target; var menuItem = (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]'); if (!menuItem) { return; } var isCheckboxOrRadio = menuItem.getAttribute('role') === 'menuitemcheckbox' || menuItem.getAttribute('role') === 'menuitemradio'; // Do not close the menu when clicking a nested trigger (aria-haspopup). // The nested dropdown will handle its own open/close. var isNestedTrigger = menuItem.hasAttribute('aria-haspopup'); if (!isCheckboxOrRadio && !isNestedTrigger) { setLocalIsOpen(false); onOpenChange({ isOpen: false, event: e.nativeEvent }); } }, [setLocalIsOpen, onOpenChange]); var ariaAttributes = (0, _getAriaForTrigger.getAriaForTrigger)({ role: 'menu', isOpen: isLocalOpen, popoverId: popoverId }); // FUDGE(top-layer-api): cast `aria-haspopup` to the narrow shape that adopter // public types expect. `@atlaskit/top-layer` types `aria-haspopup` as the wider // WAI-ARIA union, but the public `CustomTriggerProps` (extending `@atlaskit/popup` // `TriggerProps`) is intentionally kept narrow (`boolean | 'dialog'`) because the // top-layer API surface is not yet settled. The runtime value is unchanged; only // the TypeScript-visible type is narrowed at this boundary. var narrowAriaAttributes = ariaAttributes; var renderTrigger = function renderTrigger() { var setRef = function setRef(node) { triggerRef.current = node; }; var combinedRef = (0, _mergeRefs.default)([setRef]); if (typeof trigger === 'function') { return trigger(_objectSpread(_objectSpread(_objectSpread({}, narrowAriaAttributes), bindFocus), {}, { triggerRef: combinedRef, isSelected: isLocalOpen, onClick: handleTriggerClicked, testId: testId && "".concat(testId, "--trigger") })); } return /*#__PURE__*/React.createElement(_new.default, (0, _extends2.default)({}, bindFocus, { ref: combinedRef }, narrowAriaAttributes, { isSelected: isLocalOpen, iconAfter: function iconAfter(iconProps) { return /*#__PURE__*/React.createElement(_chevronDown.default, (0, _extends2.default)({}, iconProps, { size: "small" })); }, onClick: handleTriggerClicked, testId: testId && "".concat(testId, "--trigger"), "aria-label": label, interactionName: interactionName }), trigger); }; return /*#__PURE__*/React.createElement(_selectionStore.default, null, renderTrigger(), /*#__PURE__*/React.createElement(_popover.Popover, { ref: popoverRef, id: popoverId, role: "menu", label: (_ref5 = menuLabel !== null && menuLabel !== void 0 ? menuLabel : label) !== null && _ref5 !== void 0 ? _ref5 : typeof trigger === 'string' ? trigger : 'Menu', isOpen: isLocalOpen, onClose: handleOnClose, shouldAnimate: true, placement: topLayerPlacement, testId: testId && "".concat(testId, "--content") }, /*#__PURE__*/React.createElement("div", { ref: menuRef, className: (0, _runtime.ax)([styles.menuContent]) }, /*#__PURE__*/React.createElement(_menuGroup.default, { isLoading: isLoading, maxHeight: MAX_HEIGHT, maxWidth: shouldFitContainer ? undefined : 800, onClick: handleMenuClick, role: "menu", spacing: spacing, testId: testId && "".concat(testId, "--menu-wrapper--menu-group"), menuLabel: menuLabel }, isLoading ? /*#__PURE__*/React.createElement(LoadingIndicator, { statusLabel: statusLabel, testId: testId && "".concat(testId, "--menu-wrapper--loading-indicator") }) : children)))); } var _default = exports.default = DropdownMenuTopLayer;