@atlaskit/select
Version:
Select allows users to make a single selection or multiple selections from a list of options.
369 lines (355 loc) • 17 kB
JavaScript
/* popup-select-top-layer.tsx generated by @compiled/babel-plugin v3.0.2 */
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PopupSelectTopLayer = void 0;
require("./popup-select-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 _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _bindEventListener = require("bind-event-listener");
var _styles = require("@atlaskit/react-select/styles");
var _index = require("@atlaskit/top-layer/placement-map/index");
var _popover = require("@atlaskit/top-layer/popover/popover");
var _popoverSurface = require("@atlaskit/top-layer/popover-surface");
var _useAnchoredPopover = require("@atlaskit/top-layer/use-anchored-popover");
var _usePopoverId = require("@atlaskit/top-layer/use-popover-id");
var _select = _interopRequireDefault(require("../select"));
var _components = require("./components");
var _dummyControl = require("./dummy-control");
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"];
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 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 = (0, _objectWithoutProperties2.default)(_ref, _excluded);
// State.
var isControlled = controlledIsOpen !== undefined;
var _useState = (0, _react.useState)(isControlled ? Boolean(controlledIsOpen) : Boolean(defaultIsOpen)),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
internalIsOpen = _useState2[0],
setInternalIsOpen = _useState2[1];
var isOpen = isControlled ? Boolean(controlledIsOpen) : internalIsOpen;
var selectRef = (0, _react.useRef)(null);
var triggerRef = (0, _react.useRef)(null);
var menuRef = (0, _react.useRef)(null);
var popoverRef = (0, _react.useRef)(null);
var popoverId = (0, _usePopoverId.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 = (0, _react.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 = (0, _react.useMemo)(function () {
return (0, _index.fromLegacyPlacement)({
legacy: legacyPlacement,
offset: legacyOffset
});
}, [legacyPlacement, legacyOffset]);
(0, _useAnchoredPopover.useAnchoredPopover)({
anchorRef: triggerRef,
popoverRef: popoverRef,
placement: topLayerPlacement,
isOpen: isOpen
});
// Merged components.
var mergedComponents = (0, _react.useMemo)(function () {
return _objectSpread(_objectSpread({}, _components.defaultComponents), consumerComponents);
}, [consumerComponents]);
// Default styles.
var defaultStyles = (0, _react.useMemo)(function () {
return {
groupHeading: function groupHeading(provided) {
return _objectSpread(_objectSpread({}, provided), {}, {
color: "var(--ds-text-subtlest, #6B6E76)"
});
}
};
}, []);
// Utils.
var getItemCount = (0, _react.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 = (0, _react.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 = (0, _react.useCallback)(function () {
if (label) {
return label;
}
if (typeof placeholder === 'string') {
return placeholder;
}
return undefined;
}, [label, placeholder]);
// Open / Close.
var open = (0, _react.useCallback)(function () {
if (!isControlled) {
setInternalIsOpen(true);
}
onOpen === null || onOpen === void 0 || onOpen();
onMenuOpen === null || onMenuOpen === void 0 || onMenuOpen();
}, [isControlled, onOpen, onMenuOpen]);
var close = (0, _react.useCallback)(function () {
if (!isControlled) {
setInternalIsOpen(false);
}
onClose === null || onClose === void 0 || onClose();
onMenuClose === null || onMenuClose === void 0 || onMenuClose();
}, [isControlled, onClose, onMenuClose]);
/**
* For backwards compatibility
*/
(0, _react.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.
(0, _react.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.
(0, _react.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 = (0, _react.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.
(0, _react.useEffect)(function () {
if (typeof window === 'undefined') {
return;
}
return (0, _bindEventListener.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 = (0, _react.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 = (0, _react.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 = (0, _react.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 = (0, _react.useMemo)(function () {
return _objectSpread(_objectSpread({}, mergedComponents), {}, {
Control: showSearchControl ? mergedComponents.Control : _dummyControl.DummyControl
});
}, [mergedComponents, showSearchControl]);
var getSelectRef = (0, _react.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(_react.Fragment, null, target === null || target === void 0 ? void 0 : target(triggerProps), /*#__PURE__*/React.createElement(_popover.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.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: (0, _runtime.ax)(["_2rko1mok _bfhk1bhr _16qs130s"])
}, /*#__PURE__*/React.createElement(_select.default, (0, _extends2.default)({
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: (0, _styles.mergeStyles)(defaultStyles, consumerStyles || {}),
maxMenuHeight: getMaxHeight(),
components: selectComponentsMerged,
onChange: handleSelectChange,
onKeyDown: handleKeyDown,
testId: testId
})), footer))));
}
var PopupSelectTopLayer = exports.PopupSelectTopLayer = /*#__PURE__*/(0, _react.forwardRef)(PopupSelectTopLayerInner);