@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
JavaScript
/* 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);