UNPKG

@atlaskit/select

Version:

Select allows users to make a single selection or multiple selections from a list of options.

360 lines (347 loc) 15.5 kB
/* popup-select-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 _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; var _excluded = ["closeMenuOnSelect", "shouldCloseMenuOnTab", "footer", "searchThreshold", "maxMenuWidth", "minMenuWidth", "maxMenuHeight", "target", "label", "placeholder", "testId", "isOpen", "defaultIsOpen", "spacing", "options", "isSearchable", "components", "styles", "onChange", "onKeyDown", "onOpen", "onClose", "onMenuOpen", "onMenuClose", "popperProps", "shouldPreventEscapePropagation"]; import "./popup-select-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 { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { bind } from 'bind-event-listener'; import { mergeStyles } from '@atlaskit/react-select/styles'; 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 { usePopoverId } from '@atlaskit/top-layer/use-popover-id'; import Select from '../select'; import { defaultComponents } from './components'; import { DummyControl } from './dummy-control'; var menuDialogStyles = null; /** * Top-layer implementation of PopupSelect. * * Replaces the legacy PopupSelect rendering pipeline * (react-popper + createPortal + react-focus-lock + @atlaskit/layering + z-index) * with the native Popover API + CSS Anchor Positioning via @atlaskit/top-layer. * * Gated behind the `platform-dst-top-layer` feature flag. */ function PopupSelectTopLayerInner(_ref, forwardedRef) { var _popperProps$placemen, _getLabel; var _ref$closeMenuOnSelec = _ref.closeMenuOnSelect, closeMenuOnSelect = _ref$closeMenuOnSelec === void 0 ? true : _ref$closeMenuOnSelec, _ref$shouldCloseMenuO = _ref.shouldCloseMenuOnTab, shouldCloseMenuOnTab = _ref$shouldCloseMenuO === void 0 ? true : _ref$shouldCloseMenuO, footer = _ref.footer, _ref$searchThreshold = _ref.searchThreshold, searchThreshold = _ref$searchThreshold === void 0 ? 5 : _ref$searchThreshold, _ref$maxMenuWidth = _ref.maxMenuWidth, maxMenuWidth = _ref$maxMenuWidth === void 0 ? 440 : _ref$maxMenuWidth, _ref$minMenuWidth = _ref.minMenuWidth, minMenuWidth = _ref$minMenuWidth === void 0 ? 220 : _ref$minMenuWidth, _ref$maxMenuHeight = _ref.maxMenuHeight, maxMenuHeight = _ref$maxMenuHeight === void 0 ? 300 : _ref$maxMenuHeight, target = _ref.target, label = _ref.label, placeholder = _ref.placeholder, testId = _ref.testId, controlledIsOpen = _ref.isOpen, defaultIsOpen = _ref.defaultIsOpen, _spacing = _ref.spacing, _ref$options = _ref.options, options = _ref$options === void 0 ? [] : _ref$options, _ref$isSearchable = _ref.isSearchable, isSearchable = _ref$isSearchable === void 0 ? true : _ref$isSearchable, consumerComponents = _ref.components, consumerStyles = _ref.styles, onChange = _ref.onChange, onKeyDown = _ref.onKeyDown, onOpen = _ref.onOpen, onClose = _ref.onClose, onMenuOpen = _ref.onMenuOpen, onMenuClose = _ref.onMenuClose, popperProps = _ref.popperProps, _shouldPreventEscapePropagation = _ref.shouldPreventEscapePropagation, selectProps = _objectWithoutProperties(_ref, _excluded); // State. var isControlled = controlledIsOpen !== undefined; var _useState = useState(isControlled ? Boolean(controlledIsOpen) : Boolean(defaultIsOpen)), _useState2 = _slicedToArray(_useState, 2), internalIsOpen = _useState2[0], setInternalIsOpen = _useState2[1]; var isOpen = isControlled ? Boolean(controlledIsOpen) : internalIsOpen; var selectRef = useRef(null); var triggerRef = useRef(null); var menuRef = useRef(null); var popoverRef = useRef(null); var popoverId = usePopoverId(); // Placement. var legacyPlacement = (_popperProps$placemen = popperProps === null || popperProps === void 0 ? void 0 : popperProps.placement) !== null && _popperProps$placemen !== void 0 ? _popperProps$placemen : 'bottom-start'; // `popup-select` historically defaulted to an `[along, away] = [0, 8]` popper offset // modifier; consumers can override it via `popperProps.modifiers`. Honour both here so // the top-layer rendering matches the legacy popper output. var legacyOffset = useMemo(function () { var _modifiers$find; var modifiers = popperProps === null || popperProps === void 0 ? void 0 : popperProps.modifiers; var value = modifiers === null || modifiers === void 0 || (_modifiers$find = modifiers.find(function (modifier) { return (modifier === null || modifier === void 0 ? void 0 : modifier.name) === 'offset'; })) === null || _modifiers$find === void 0 || (_modifiers$find = _modifiers$find.options) === null || _modifiers$find === void 0 ? void 0 : _modifiers$find.offset; if (Array.isArray(value) && value.length === 2) { return [Number(value[0]) || 0, Number(value[1]) || 0]; } return [0, 8]; }, [popperProps === null || popperProps === void 0 ? void 0 : popperProps.modifiers]); var topLayerPlacement = useMemo(function () { return fromLegacyPlacement({ legacy: legacyPlacement, offset: legacyOffset }); }, [legacyPlacement, legacyOffset]); useAnchoredPopover({ anchorRef: triggerRef, popoverRef: popoverRef, placement: topLayerPlacement, isOpen: isOpen }); // Merged components. var mergedComponents = useMemo(function () { return _objectSpread(_objectSpread({}, defaultComponents), consumerComponents); }, [consumerComponents]); // Default styles. var defaultStyles = useMemo(function () { return { groupHeading: function groupHeading(provided) { return _objectSpread(_objectSpread({}, provided), {}, { color: "var(--ds-text-subtlest, #6B6E76)" }); } }; }, []); // Utils. var getItemCount = useCallback(function () { return options.reduce(function (count, groupOrOption) { var group = groupOrOption; if (group.options) { return count + group.options.length; } return count + 1; }, 0); }, [options]); var showSearchControl = isSearchable && getItemCount() > searchThreshold; var getMaxHeight = useCallback(function () { var _selectRef$current$se; if (!selectRef.current) { return maxMenuHeight; } var controlRef = (_selectRef$current$se = selectRef.current.select) === null || _selectRef$current$se === void 0 ? void 0 : _selectRef$current$se.controlRef; var offsetHeight = showSearchControl && controlRef ? controlRef.offsetHeight : 0; return maxMenuHeight - offsetHeight; }, [maxMenuHeight, showSearchControl]); var getLabel = useCallback(function () { if (label) { return label; } if (typeof placeholder === 'string') { return placeholder; } return undefined; }, [label, placeholder]); // Open / Close. var open = useCallback(function () { if (!isControlled) { setInternalIsOpen(true); } onOpen === null || onOpen === void 0 || onOpen(); onMenuOpen === null || onMenuOpen === void 0 || onMenuOpen(); }, [isControlled, onOpen, onMenuOpen]); var close = useCallback(function () { if (!isControlled) { setInternalIsOpen(false); } onClose === null || onClose === void 0 || onClose(); onMenuClose === null || onMenuClose === void 0 || onMenuClose(); }, [isControlled, onClose, onMenuClose]); /** * For backwards compatibility */ useImperativeHandle(forwardedRef, function () { return { open: open, close: close, get selectRef() { return selectRef.current; }, get menuRef() { return menuRef.current; }, get targetRef() { return triggerRef.current; } }; }, [close, open]); // Sync controlled isOpen. useEffect(function () { if (!isControlled) { return; } if (controlledIsOpen) { onOpen === null || onOpen === void 0 || onOpen(); onMenuOpen === null || onMenuOpen === void 0 || onMenuOpen(); } else { onClose === null || onClose === void 0 || onClose(); onMenuClose === null || onMenuClose === void 0 || onMenuClose(); } // Only react to changes in the controlled value // eslint-disable-next-line react-hooks/exhaustive-deps }, [controlledIsOpen]); // Open the react-select menu when isOpen becomes true. useEffect(function () { if (isOpen && selectRef.current) { var _selectRef$current$se2; (_selectRef$current$se2 = selectRef.current.select) === null || _selectRef$current$se2 === void 0 || _selectRef$current$se2.openMenu('first'); } }, [isOpen]); // Event Handlers. var handleTargetKeyDown = useCallback(function (event) { if (event.key === 'ArrowDown') { open(); } }, [open]); // Open the menu when the trigger is clicked. Mirrors the legacy // `PopupSelect` global-click listener: consumers are not required to // spread an `onClick`, so we walk up from the event target to the // trigger ref instead. useEffect(function () { if (typeof window === 'undefined') { return; } return bind(window, { type: 'click', listener: function listener(event) { var target = event.target; if (!(target instanceof Element)) { return; } var trigger = triggerRef.current; if (!trigger || !trigger.contains(target)) { return; } if (isOpen) { close(); return; } open(); }, options: { capture: true } }); }, [isOpen, open, close]); // Focus restoration on close is handled automatically by Popover // based on the content role (role="dialog" -> auto-restore). var handleOnClose = useCallback(function (_ref2) { var _reason = _ref2.reason; if (isControlled) { // For controlled mode, notify consumer but do not change internal state onClose === null || onClose === void 0 || onClose(); onMenuClose === null || onMenuClose === void 0 || onMenuClose(); return; } close(); }, [close, isControlled, onClose, onMenuClose]); var handleSelectChange = useCallback(function (value, actionMeta) { if (closeMenuOnSelect && actionMeta.action !== 'clear') { var _triggerRef$current; close(); // Return focus to trigger after selection (_triggerRef$current = triggerRef.current) === null || _triggerRef$current === void 0 || _triggerRef$current.focus(); } onChange === null || onChange === void 0 || onChange(value, actionMeta); }, [closeMenuOnSelect, close, onChange]); var handleKeyDown = useCallback(function (event) { var tabEvent = event.key === 'Tab'; if (shouldCloseMenuOnTab && tabEvent) { var _triggerRef$current2; close(); (_triggerRef$current2 = triggerRef.current) === null || _triggerRef$current2 === void 0 || _triggerRef$current2.focus(); } onKeyDown === null || onKeyDown === void 0 || onKeyDown(event); }, [shouldCloseMenuOnTab, close, onKeyDown]); // Select components. // Type assertion needed because defaultComponents uses concrete OptionType/boolean // but Select expects the generic Option/IsMulti parameters. The components are // structurally compatible regardless of the generic parameters. var selectComponentsMerged = useMemo(function () { return _objectSpread(_objectSpread({}, mergedComponents), {}, { Control: showSearchControl ? mergedComponents.Control : DummyControl }); }, [mergedComponents, showSearchControl]); var getSelectRef = useCallback(function (ref) { selectRef.current = ref; }, []); var triggerProps = { ref: function ref(node) { triggerRef.current = node; }, // No `onClick` on `triggerProps`: the trigger click is observed via // the capture-phase global `click` listener in the effect above, // matching the legacy `PopupSelect` contract for consumers that do // not spread `triggerProps` onto their target. onKeyDown: handleTargetKeyDown, // Maintain existing aria attribute format for backwards compatibility. // Should technically be 'dialog' instead of 'true', but preserving // legacy behavior. See go/DSP-22283 'aria-haspopup': 'true', 'aria-expanded': isOpen, // Always emit aria-controls referencing the controlled element. // Matches the behavior of `getAriaForTrigger` (see // `@atlaskit/top-layer/get-aria-for-trigger`) and aligns with the // WAI-ARIA spec recommendation. 'aria-controls': popoverId, isOpen: isOpen }; return /*#__PURE__*/React.createElement(Fragment, null, target === null || target === void 0 ? void 0 : target(triggerProps), /*#__PURE__*/React.createElement(Popover, { ref: popoverRef, id: popoverId, role: "dialog", label: (_getLabel = getLabel()) !== null && _getLabel !== void 0 ? _getLabel : 'Popup select', isOpen: isOpen, onClose: handleOnClose, shouldAnimate: true, placement: topLayerPlacement, testId: testId && "".concat(testId, "--content") }, /*#__PURE__*/React.createElement(PopoverSurface, null, /*#__PURE__*/React.createElement("div", { ref: menuRef, // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop style: { maxWidth: maxMenuWidth, minWidth: minMenuWidth }, "data-testid": testId && "".concat(testId, "--menu"), className: ax(["_2rko1mok _bfhk1bhr _16qs130s"]) }, /*#__PURE__*/React.createElement(Select, _extends({ label: getLabel(), backspaceRemovesValue: false, controlShouldRenderValue: false, isClearable: false, tabSelectsValue: false, placeholder: placeholder, ref: getSelectRef // eslint-disable-next-line @repo/internal/react/no-unsafe-spread-props -- preserving legacy PopupSelect API during migration }, selectProps, { menuRenderMode: "inline", options: options, isSearchable: showSearchControl, styles: mergeStyles(defaultStyles, consumerStyles || {}), maxMenuHeight: getMaxHeight(), components: selectComponentsMerged, onChange: handleSelectChange, onKeyDown: handleKeyDown, testId: testId })), footer)))); } export var PopupSelectTopLayer = /*#__PURE__*/forwardRef(PopupSelectTopLayerInner);