UNPKG

@atlaskit/select

Version:

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

592 lines (582 loc) 25.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.PopupSelect = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _bindEventListener = require("bind-event-listener"); var _reactDom = require("react-dom"); var _reactFocusLock = _interopRequireDefault(require("react-focus-lock")); var _reactPopper = require("react-popper"); var _shallowEqual = require("shallow-equal"); var _idProvider = require("@atlaskit/ds-lib/id-provider"); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _reactPopper2 = require("@atlaskit/popper/react-popper"); var _styles = require("@atlaskit/react-select/styles"); var _select = _interopRequireDefault(require("../select")); var _components = require("./components"); var _dummyControl = require("./dummy-control"); var _menuDialog = require("./menu-dialog"); var _notifyOpenLayerObserver = require("./notify-open-layer-observer"); var _popupSelectTopLayer = require("./popup-select-top-layer"); var _excluded = ["footer", "label", "maxMenuWidth", "minMenuWidth", "placeholder", "target", "testId", "onMenuOpen", "onMenuClose"]; 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; } function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } /** * Are we rendering on the client or server? */ var canUseDOM = function canUseDOM() { return Boolean(typeof window !== 'undefined' && window.document && window.document.createElement); }; function getPopperComponents() { if ((0, _fg.fg)('platform-dst-popper-consolidation')) { return { Manager: _reactPopper2.Manager, Popper: _reactPopper2.Popper, Reference: _reactPopper2.Reference }; } return { Manager: _reactPopper.Manager, Popper: _reactPopper.Popper, Reference: _reactPopper.Reference }; } // ============================== // Types // ============================== // ============================== // Class // ============================== var modifiers = [{ name: 'offset', options: { offset: [0, 8] } }, { name: 'preventOverflow', enabled: true, options: { padding: 5, boundary: 'clippingParents', altAxis: true, altBoundary: true } }]; var defaultPopperProps = { modifiers: modifiers, placement: 'bottom-start' }; var isEmpty = function isEmpty(obj) { return Object.keys(obj).length === 0; }; // eslint-disable-next-line @repo/internal/react/no-class-components var PopupSelectLegacy = /*#__PURE__*/function (_PureComponent) { function PopupSelectLegacy() { var _this$defaultOpenStat; var _this; (0, _classCallCheck2.default)(this, PopupSelectLegacy); for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _callSuper(this, PopupSelectLegacy, [].concat(args)); (0, _defineProperty2.default)(_this, "menuRef", null); (0, _defineProperty2.default)(_this, "selectRef", null); (0, _defineProperty2.default)(_this, "targetRef", null); (0, _defineProperty2.default)(_this, "unbindWindowClick", null); (0, _defineProperty2.default)(_this, "unbindWindowKeydown", null); (0, _defineProperty2.default)(_this, "defaultStyles", { groupHeading: function groupHeading(provided) { return _objectSpread(_objectSpread({}, provided), {}, { color: "var(--ds-text-subtlest, #6B6E76)" }); } }); (0, _defineProperty2.default)(_this, "isOpenControlled", _this.props.isOpen !== undefined); (0, _defineProperty2.default)(_this, "defaultOpenState", _this.isOpenControlled ? _this.props.isOpen : _this.props.defaultIsOpen); (0, _defineProperty2.default)(_this, "state", { focusLockEnabled: false, isOpen: (_this$defaultOpenStat = _this.defaultOpenState) !== null && _this$defaultOpenStat !== void 0 ? _this$defaultOpenStat : false, mergedComponents: _components.defaultComponents, mergedPopperProps: defaultPopperProps }); // Event Handlers // ============================== (0, _defineProperty2.default)(_this, "handleTargetKeyDown", function (event) { switch (event.key) { case 'ArrowDown': _this.open(); break; default: } }); (0, _defineProperty2.default)(_this, "handleKeyDown", function (event) { //We shouldn't close PopupSelect on tab event if there are custom interactive element. var tabEvent = event.key === 'Tab' && event.shiftKey || event.key === 'Tab'; if (_this.props.shouldCloseMenuOnTab && tabEvent) { _this.close(); } switch (event.key) { case 'Escape': case 'Esc': _this.close(); if (_this.props.shouldPreventEscapePropagation) { event.stopPropagation(); } break; default: } if (_this.props.onKeyDown) { /* @ts-ignore - updating type of event React.KeyboardEvent effects the unbindWindowsKeyDown listener. Check if this can be fixed once the component gets refactor to functional */ _this.props.onKeyDown(event); } }); (0, _defineProperty2.default)(_this, "handleClick", function (_ref) { var target = _ref.target; var isOpen = _this.state.isOpen; // appease flow if (!(target instanceof Element)) { return; } // NOTE: Why not use the <Blanket /> component to close? // We don't want to interupt the user's flow. Taking this approach allows // user to click "through" to other elements and close the popout. if (isOpen && _this.menuRef && !_this.menuRef.contains(target)) { _this.close(); } // open on target click -- we can't trust consumers to spread the onClick // property to the target if (!isOpen && _this.targetRef && _this.targetRef.contains(target)) { _this.open(); } }); (0, _defineProperty2.default)(_this, "handleSelectChange", function (value, actionMeta) { var _this$props = _this.props, closeMenuOnSelect = _this$props.closeMenuOnSelect, onChange = _this$props.onChange; if (closeMenuOnSelect && actionMeta.action !== 'clear') { _this.close(); } if (onChange) { onChange(value, actionMeta); } }); (0, _defineProperty2.default)(_this, "handleFirstPopperUpdate", function () { // When the popup opens it's focused into. Since the popup is inside a portal, it's position is // initially set to 0,0 - this causes the window scroll position to jump to the top. To prevent // this we defer enabling the focus-lock until after Popper has positioned the popup the first time. _this.setState({ focusLockEnabled: true }); }); // Internal Lifecycle // ============================== /** * Opens the popup * * @param options.controlOverride - Force the popup to open when it's open state is being controlled */ (0, _defineProperty2.default)(_this, "open", function (options) { var _this$props2 = _this.props, onOpen = _this$props2.onOpen, onMenuOpen = _this$props2.onMenuOpen; if (!(options !== null && options !== void 0 && options.controlOverride) && _this.isOpenControlled) { // Prevent popup opening if it's open state is already being controlled return; } if (onOpen) { onOpen(); } if (onMenuOpen) { onMenuOpen(); } _this.setState({ isOpen: true }, function () { if (_this.selectRef) { var _this$selectRef$selec; (_this$selectRef$selec = _this.selectRef.select) === null || _this$selectRef$selec === void 0 || _this$selectRef$selec.openMenu('first'); } }); if (typeof window === 'undefined') { return; } _this.unbindWindowKeydown = (0, _bindEventListener.bind)(window, { type: 'keydown', listener: _this.handleKeyDown, options: { capture: true } }); }); /** * Closes the popup * * @param options.controlOverride - Force the popup to close when it's open state is being controlled */ (0, _defineProperty2.default)(_this, "close", function (options) { var _this$unbindWindowKey, _this2; var _this$props3 = _this.props, onClose = _this$props3.onClose, onMenuClose = _this$props3.onMenuClose; if (!(options !== null && options !== void 0 && options.controlOverride) && _this.isOpenControlled) { // Prevent popup closing if it's open state is already being controlled return; } if (onClose) { onClose(); } if (onMenuClose) { onMenuClose(); } _this.setState({ isOpen: false }); _this.setState({ focusLockEnabled: false }); if (_this.targetRef != null) { _this.targetRef.focus(); } if (typeof window === 'undefined') { return; } (_this$unbindWindowKey = (_this2 = _this).unbindWindowKeydown) === null || _this$unbindWindowKey === void 0 || _this$unbindWindowKey.call(_this2); _this.unbindWindowKeydown = null; }); // Refs // ============================== (0, _defineProperty2.default)(_this, "resolveTargetRef", function (popperRef) { return function (ref) { // avoid thrashing fn calls if (!_this.targetRef && popperRef && ref) { _this.targetRef = ref; if (typeof popperRef === 'function') { popperRef(ref); } else { popperRef.current = ref; } } }; }); (0, _defineProperty2.default)(_this, "resolveMenuRef", function (popperRef) { return function (ref) { _this.menuRef = ref; if (typeof popperRef === 'function') { popperRef(ref); } else { popperRef.current = ref; } }; }); (0, _defineProperty2.default)(_this, "getSelectRef", function (ref) { _this.selectRef = ref; }); // Utils // ============================== // account for groups when counting options // this may need to be recursive, right now it just counts one level (0, _defineProperty2.default)(_this, "getItemCount", function () { var options = _this.props.options; var count = 0; options.forEach(function (groupOrOption) { if (groupOrOption.options) { groupOrOption.options.forEach(function () { return count++; }); } else { count++; } }); return count; }); (0, _defineProperty2.default)(_this, "getMaxHeight", function () { var _this$selectRef$selec2; var maxMenuHeight = _this.props.maxMenuHeight; if (!_this.selectRef) { return maxMenuHeight; } // subtract the control height to maintain consistency var showSearchControl = _this.showSearchControl(); var controlRef = (_this$selectRef$selec2 = _this.selectRef.select) === null || _this$selectRef$selec2 === void 0 ? void 0 : _this$selectRef$selec2.controlRef; var offsetHeight = showSearchControl && controlRef ? controlRef.offsetHeight : 0; var maxHeight = maxMenuHeight - offsetHeight; return maxHeight; }); // if the threshold is exceeded, AND isSearchable is true, then display the search control (0, _defineProperty2.default)(_this, "showSearchControl", function () { var _this$props4 = _this.props, searchThreshold = _this$props4.searchThreshold, isSearchable = _this$props4.isSearchable; return isSearchable && _this.getItemCount() > searchThreshold; }); // Renderers // ============================== (0, _defineProperty2.default)(_this, "renderSelect", function (id) { var _this$props5 = _this.props, footer = _this$props5.footer, label = _this$props5.label, maxMenuWidth = _this$props5.maxMenuWidth, minMenuWidth = _this$props5.minMenuWidth, placeholder = _this$props5.placeholder, _target = _this$props5.target, testId = _this$props5.testId, _onMenuOpen = _this$props5.onMenuOpen, _onMenuClose = _this$props5.onMenuClose, props = (0, _objectWithoutProperties2.default)(_this$props5, _excluded); var _this$state = _this.state, focusLockEnabled = _this$state.focusLockEnabled, isOpen = _this$state.isOpen, mergedComponents = _this$state.mergedComponents, mergedPopperProps = _this$state.mergedPopperProps; var _getPopperComponents = getPopperComponents(), Popper = _getPopperComponents.Popper; var showSearchControl = _this.showSearchControl(); var portalDestination = canUseDOM() ? document.body : null; if (!portalDestination || !isOpen) { return null; } var selectComponents = _objectSpread(_objectSpread({}, mergedComponents), {}, { Control: showSearchControl ? mergedComponents.Control : _dummyControl.DummyControl }); var getLabel = function getLabel() { if (label) { return label; } else if (typeof placeholder === 'string') { return placeholder; } return undefined; }; var popper = /*#__PURE__*/_react.default.createElement(Popper, (0, _extends2.default)({}, mergedPopperProps, { onFirstUpdate: function onFirstUpdate(state) { var _mergedPopperProps$on; _this.handleFirstPopperUpdate(); (_mergedPopperProps$on = mergedPopperProps.onFirstUpdate) === null || _mergedPopperProps$on === void 0 || _mergedPopperProps$on.call(mergedPopperProps, state); } }), function (_ref2) { var placement = _ref2.placement, ref = _ref2.ref, style = _ref2.style; return /*#__PURE__*/_react.default.createElement(_menuDialog.MenuDialog // There is not a limited amount of values for the widths, so they need to remain dynamic. // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 , { style: _objectSpread(_objectSpread({}, style), {}, { maxWidth: maxMenuWidth, minWidth: minMenuWidth }), "data-placement": placement, id: id, testId: testId, ref: _this.resolveMenuRef(ref) }, /*#__PURE__*/_react.default.createElement(_reactFocusLock.default, { disabled: !focusLockEnabled, returnFocus: true }, /*#__PURE__*/_react.default.createElement(_select.default, (0, _extends2.default)({ label: getLabel(), backspaceRemovesValue: false, controlShouldRenderValue: false, isClearable: false, tabSelectsValue: false, menuIsOpen: true, placeholder: placeholder, ref: _this.getSelectRef }, props, { isSearchable: showSearchControl, styles: (0, _styles.mergeStyles)(_this.defaultStyles, props.styles || {}), maxMenuHeight: _this.getMaxHeight(), components: selectComponents, onChange: _this.handleSelectChange, testId: testId // Note: We are intentionally not using `onMenuClose={this.close}` here, due to state management conflicts // between PopupSelect's popup and the internal select menu. This can result in the PopupSelect closing // or opening unexpectedly. })), footer)); }); return mergedPopperProps.strategy === 'fixed' ? popper : /*#__PURE__*/(0, _reactDom.createPortal)(popper, portalDestination); }); (0, _defineProperty2.default)(_this, "handleOpenLayerObserverCloseSignal", function () { _this.close(); }); return _this; } (0, _inherits2.default)(PopupSelectLegacy, _PureComponent); return (0, _createClass2.default)(PopupSelectLegacy, [{ key: "componentDidMount", value: function componentDidMount() { if (typeof window === 'undefined') { return; } this.unbindWindowClick = (0, _bindEventListener.bind)(window, { type: 'click', listener: this.handleClick, options: { capture: true } }); } }, { key: "componentWillUnmount", value: function componentWillUnmount() { var _this$unbindWindowCli, _this$unbindWindowKey2; if (typeof window === 'undefined') { return; } (_this$unbindWindowCli = this.unbindWindowClick) === null || _this$unbindWindowCli === void 0 || _this$unbindWindowCli.call(this); this.unbindWindowClick = null; (_this$unbindWindowKey2 = this.unbindWindowKeydown) === null || _this$unbindWindowKey2 === void 0 || _this$unbindWindowKey2.call(this); this.unbindWindowKeydown = null; } }, { key: "componentDidUpdate", value: function componentDidUpdate(prevProps) { var isOpen = this.props.isOpen; if (prevProps.isOpen !== isOpen) { if (isOpen === true) { this.open({ controlOverride: true }); } else if (isOpen === false) { this.close({ controlOverride: true }); } } } }, { key: "render", value: function render() { var _this3 = this; var target = this.props.target; var isOpen = this.state.isOpen; var _getPopperComponents2 = getPopperComponents(), Manager = _getPopperComponents2.Manager, Reference = _getPopperComponents2.Reference; return /*#__PURE__*/_react.default.createElement(Manager, null, /*#__PURE__*/_react.default.createElement(_idProvider.IdProvider, { postfix: "-popup-select" }, function (_ref3) { var id = _ref3.id; return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(Reference, null, function (_ref4) { var ref = _ref4.ref; return target && target({ isOpen: isOpen, onKeyDown: _this3.handleTargetKeyDown, ref: _this3.resolveTargetRef(ref), // Should technically be 'dialog' by default instead of 'true' via AriaAttributes, but will require further changes. // See go/DSP-22283 'aria-haspopup': 'true', 'aria-expanded': isOpen, 'aria-controls': isOpen ? id : undefined }); }), _this3.renderSelect(id)); }), /*#__PURE__*/_react.default.createElement(_notifyOpenLayerObserver.NotifyOpenLayerObserver, { isOpen: this.state.isOpen, onClose: this.handleOpenLayerObserverCloseSignal })); } }], [{ key: "getDerivedStateFromProps", value: function getDerivedStateFromProps(props, state) { var newState = {}; // Merge consumer and default popper props var mergedPopperProps = _objectSpread(_objectSpread({}, defaultPopperProps), props.popperProps); if (!(0, _shallowEqual.shallowEqualObjects)(mergedPopperProps, state.mergedPopperProps)) { newState.mergedPopperProps = mergedPopperProps; } // Merge consumer and default components var mergedComponents = _objectSpread(_objectSpread({}, _components.defaultComponents), props.components); if (!(0, _shallowEqual.shallowEqualObjects)(mergedComponents, state.mergedComponents)) { newState.mergedComponents = mergedComponents; } if (!isEmpty(newState)) { return newState; } return null; } }]); }(_react.PureComponent); // This facade preserves the historical class type and imperative ref API while the feature gate // selects between independently implemented legacy and top-layer render paths. // We cannot just branch inside the `render()` method of `PopupSelectLegacy` class because // it has life cycle methods which would still affect the top layer rendering path. // eslint-disable-next-line @repo/internal/react/no-class-components (0, _defineProperty2.default)(PopupSelectLegacy, "defaultProps", { closeMenuOnSelect: true, shouldCloseMenuOnTab: true, components: {}, maxMenuHeight: 300, maxMenuWidth: 440, minMenuWidth: 220, popperProps: {}, isSearchable: true, searchThreshold: 5, styles: {}, options: [] }); var PopupSelect = exports.PopupSelect = /*#__PURE__*/function (_PureComponent2) { function PopupSelect() { var _this4; (0, _classCallCheck2.default)(this, PopupSelect); for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { args[_key2] = arguments[_key2]; } _this4 = _callSuper(this, PopupSelect, [].concat(args)); (0, _defineProperty2.default)(_this4, "implementation", null); (0, _defineProperty2.default)(_this4, "setImplementationRef", function (implementation) { _this4.implementation = implementation; }); (0, _defineProperty2.default)(_this4, "open", function (options) { var _this4$implementation; (_this4$implementation = _this4.implementation) === null || _this4$implementation === void 0 || _this4$implementation.open(options); }); (0, _defineProperty2.default)(_this4, "close", function (options) { var _this4$implementation2; (_this4$implementation2 = _this4.implementation) === null || _this4$implementation2 === void 0 || _this4$implementation2.close(options); }); return _this4; } (0, _inherits2.default)(PopupSelect, _PureComponent2); return (0, _createClass2.default)(PopupSelect, [{ key: "selectRef", get: function get() { var _this$implementation$, _this$implementation; return (_this$implementation$ = (_this$implementation = this.implementation) === null || _this$implementation === void 0 ? void 0 : _this$implementation.selectRef) !== null && _this$implementation$ !== void 0 ? _this$implementation$ : null; } }, { key: "menuRef", get: function get() { var _this$implementation$2, _this$implementation2; return (_this$implementation$2 = (_this$implementation2 = this.implementation) === null || _this$implementation2 === void 0 ? void 0 : _this$implementation2.menuRef) !== null && _this$implementation$2 !== void 0 ? _this$implementation$2 : null; } }, { key: "targetRef", get: function get() { var _this$implementation$3, _this$implementation3; return (_this$implementation$3 = (_this$implementation3 = this.implementation) === null || _this$implementation3 === void 0 ? void 0 : _this$implementation3.targetRef) !== null && _this$implementation$3 !== void 0 ? _this$implementation$3 : null; } }, { key: "render", value: function render() { if ((0, _fg.fg)('platform-dst-top-layer')) { return /*#__PURE__*/_react.default.createElement(_popupSelectTopLayer.PopupSelectTopLayer, (0, _extends2.default)({ ref: this.setImplementationRef }, this.props)); } return /*#__PURE__*/_react.default.createElement(PopupSelectLegacy, (0, _extends2.default)({ ref: this.setImplementationRef }, this.props)); } }]); }(_react.PureComponent);