UNPKG

@atlaskit/dropdown-menu

Version:

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

318 lines (304 loc) 13.7 kB
/* dropdown-menu-top-layer.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import "./dropdown-menu-top-layer.compiled.css"; import * as React from 'react'; import { ax, ix } from "@compiled/react/runtime"; 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) { _defineProperty(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; } import { useCallback, useEffect, useMemo, useRef } from 'react'; import { bind } from 'bind-event-listener'; import Button from '@atlaskit/button/default/button'; import { KEY_DOWN } from '@atlaskit/ds-lib/keycodes'; import noop from '@atlaskit/ds-lib/noop'; import useControlledState from '@atlaskit/ds-lib/use-controlled'; import useFocus from '@atlaskit/ds-lib/use-focus-event'; import ExpandIcon from '@atlaskit/icon/core/chevron-down'; import MenuGroup from '@atlaskit/menu/menu-group'; import Spinner from '@atlaskit/spinner/spinner'; import { getAriaForTrigger } from '@atlaskit/top-layer/get-aria-for-trigger'; import { fromLegacyPlacement } from '@atlaskit/top-layer/placement-map/index'; import { Popover } from '@atlaskit/top-layer/popover/popover'; import { PopoverSurface } from '@atlaskit/top-layer/popover-surface'; import { useAnchoredPopover } from '@atlaskit/top-layer/use-anchored-popover'; import { isAtCurrentMenuLevel } from '@atlaskit/top-layer/is-at-current-menu-level'; import { useArrowNavigation } from '@atlaskit/top-layer/use-arrow-navigation/use-arrow-navigation'; import { usePopoverId } from '@atlaskit/top-layer/use-popover-id'; import SelectionStore from './internal/context/selection-store'; var MAX_HEIGHT = "calc(100vh - 16px)"; var styles = { spinnerContainer: "_1e0c1txw _1ul91lit _1bah1h6o _1q51v47k _y4tiv47k _85i5v47k _bozgv47k" }; /** * 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 */ function getRootMenuPopover(_ref) { var _menu$parentElement; var menu = _ref.menu; var parentMenu = (_menu$parentElement = menu.parentElement) === null || _menu$parentElement === void 0 ? void 0 : _menu$parentElement.closest('[popover][role="menu"]'); if (!parentMenu) { return menu; } return getRootMenuPopover({ menu: parentMenu }); } /** * Loading indicator for the dropdown menu. */ function LoadingIndicator(_ref2) { var _ref2$statusLabel = _ref2.statusLabel, statusLabel = _ref2$statusLabel === void 0 ? 'Loading' : _ref2$statusLabel, testId = _ref2.testId; return /*#__PURE__*/React.createElement("div", { role: "menuitem", className: ax([styles.spinnerContainer]) }, /*#__PURE__*/React.createElement(Spinner, { 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(_ref3) { var _ref5; var children = _ref3.children, _ref3$defaultOpen = _ref3.defaultOpen, defaultOpen = _ref3$defaultOpen === void 0 ? false : _ref3$defaultOpen, _ref3$isLoading = _ref3.isLoading, isLoading = _ref3$isLoading === void 0 ? false : _ref3$isLoading, isOpenProp = _ref3.isOpen, _ref3$onOpenChange = _ref3.onOpenChange, onOpenChange = _ref3$onOpenChange === void 0 ? noop : _ref3$onOpenChange, _ref3$placement = _ref3.placement, placement = _ref3$placement === void 0 ? 'bottom-start' : _ref3$placement, _ref3$shouldFitContai = _ref3.shouldFitContainer, shouldFitContainer = _ref3$shouldFitContai === void 0 ? false : _ref3$shouldFitContai, returnFocusRef = _ref3.returnFocusRef, spacing = _ref3.spacing, statusLabel = _ref3.statusLabel, testId = _ref3.testId, trigger = _ref3.trigger, label = _ref3.label, interactionName = _ref3.interactionName, menuLabel = _ref3.menuLabel; var _useControlledState = useControlledState(isOpenProp, function () { return defaultOpen; }), _useControlledState2 = _slicedToArray(_useControlledState, 2), isLocalOpen = _useControlledState2[0], setLocalIsOpen = _useControlledState2[1]; var triggerRef = useRef(null); var popoverRef = useRef(null); var popoverId = usePopoverId(); var topLayerPlacement = useMemo(function () { return fromLegacyPlacement({ legacy: placement }); }, [placement]); // DropdownMenu has no `shouldFitViewport` equivalent, so neither axis ever // fits and `blockSize` stays `'content'` - `MenuGroup`'s `maxHeight` owns the // block axis. useAnchoredPopover({ anchorRef: triggerRef, popoverRef: popoverRef, placement: topLayerPlacement, isOpen: isLocalOpen, inlineSize: shouldFitContainer ? 'min-anchor' : 'content' }); // 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 = useCallback(function () { 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 = useCallback(function (event) { var nextIsOpen = !isLocalOpen; setLocalIsOpen(nextIsOpen); // Extract the native DOM event for onOpenChange var nativeEvent = 'nativeEvent' in event ? event.nativeEvent : event; onOpenChange({ isOpen: nextIsOpen, event: nativeEvent }); }, [isLocalOpen, setLocalIsOpen, onOpenChange]); var _useFocus = useFocus(), 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. useEffect(function () { var _triggerRef$current; if (!isFocused || isLocalOpen) { return; } // 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 = Boolean((_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 ? void 0 : _triggerRef$current.closest('[role="menu"]')); return bind(window, { type: 'keydown', listener: function openOnKeyDown(e) { if (e.key === KEY_DOWN && !isNestedTrigger) { e.preventDefault(); handleTriggerClicked(e); } } }); }, [isFocused, isLocalOpen, handleTriggerClicked]); var handleNestedOpen = useCallback(function (_ref4) { var trigger = _ref4.trigger; trigger.click(); }, []); var handleTabClose = useCallback(function () { var currentMenu = popoverRef.current; if (!currentMenu) { return; } // Hiding the root native popover synchronously closes its descendant popovers and restores // focus to the root trigger. The browser can then perform the Tab default action from there. getRootMenuPopover({ menu: currentMenu }).hidePopover(); }, []); useArrowNavigation({ containerRef: popoverRef, onClose: handleTabClose, onNestedOpen: handleNestedOpen, onNestedClose: handleOnClose, isEnabled: isLocalOpen, filter: isAtCurrentMenuLevel }); // 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 = useCallback(function (e) { var _e$target$closest, _e$target; if (!(e.target instanceof Element)) { return; } var menuItem = (_e$target$closest = (_e$target = e.target).closest) === null || _e$target$closest === void 0 ? void 0 : _e$target$closest.call(_e$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 = getAriaForTrigger({ // Intentionally fudging the type here to allow the fudged aria-haspopup cast below, // without having to use `as unknown as ...` which would lose even more type safety. 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; /** * Custom trigger consumers historically receive a callback ref, * and their code assumes it is one. * Using this wrapper for compatibility... */ var setTriggerRef = useCallback(function (node) { triggerRef.current = node; }, []); var renderTrigger = function renderTrigger() { if (typeof trigger === 'function') { return trigger(_objectSpread(_objectSpread({}, narrowAriaAttributes), {}, { onFocus: bindFocus.onFocus, onBlur: bindFocus.onBlur, triggerRef: setTriggerRef, isSelected: isLocalOpen, onClick: handleTriggerClicked, testId: testId && "".concat(testId, "--trigger") })); } return /*#__PURE__*/React.createElement(Button, _extends({ onFocus: bindFocus.onFocus, onBlur: bindFocus.onBlur, ref: setTriggerRef }, narrowAriaAttributes, { isSelected: isLocalOpen, iconAfter: function iconAfter(iconProps) { return /*#__PURE__*/React.createElement(ExpandIcon, _extends({}, iconProps, { size: "small" })); }, onClick: handleTriggerClicked, testId: testId && "".concat(testId, "--trigger"), "aria-label": label, interactionName: interactionName }), trigger); }; return /*#__PURE__*/React.createElement(SelectionStore, null, renderTrigger(), /*#__PURE__*/React.createElement(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(PopoverSurface, null, /*#__PURE__*/React.createElement(MenuGroup, { maxHeight: MAX_HEIGHT, maxWidth: shouldFitContainer ? undefined : 800, onClick: handleMenuClick, spacing: spacing, testId: testId && "".concat(testId, "--menu-wrapper--menu-group") }, isLoading ? /*#__PURE__*/React.createElement(LoadingIndicator, { statusLabel: statusLabel, testId: testId && "".concat(testId, "--menu-wrapper--loading-indicator") }) : children)))); } export default DropdownMenuTopLayer;