UNPKG

@atlaskit/select

Version:

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

583 lines (574 loc) 23.7 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn"; import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf"; import _inherits from "@babel/runtime/helpers/inherits"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; var _excluded = ["footer", "label", "maxMenuWidth", "minMenuWidth", "placeholder", "target", "testId", "onMenuOpen", "onMenuClose"]; 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; } function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(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; })(); } import React, { PureComponent } from 'react'; import { bind } from 'bind-event-listener'; import { createPortal } from 'react-dom'; import FocusLock from 'react-focus-lock'; import { Manager as LegacyManager, Popper as LegacyPopper, Reference as LegacyReference } from 'react-popper'; import { shallowEqualObjects } from 'shallow-equal'; import { IdProvider } from '@atlaskit/ds-lib/id-provider'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { Manager as WrappedManager, Popper as WrappedPopper, Reference as WrappedReference } from '@atlaskit/popper/react-popper'; import { mergeStyles } from '@atlaskit/react-select/styles'; import Select from '../select'; import { defaultComponents } from './components'; import { DummyControl } from './dummy-control'; import { MenuDialog } from './menu-dialog'; import { NotifyOpenLayerObserver } from './notify-open-layer-observer'; import { PopupSelectTopLayer } from './popup-select-top-layer'; /** * 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 (fg('platform-dst-popper-consolidation')) { return { Manager: WrappedManager, Popper: WrappedPopper, Reference: WrappedReference }; } return { Manager: LegacyManager, Popper: LegacyPopper, Reference: LegacyReference }; } // ============================== // 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; _classCallCheck(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)); _defineProperty(_this, "menuRef", null); _defineProperty(_this, "selectRef", null); _defineProperty(_this, "targetRef", null); _defineProperty(_this, "unbindWindowClick", null); _defineProperty(_this, "unbindWindowKeydown", null); _defineProperty(_this, "defaultStyles", { groupHeading: function groupHeading(provided) { return _objectSpread(_objectSpread({}, provided), {}, { color: "var(--ds-text-subtlest, #6B6E76)" }); } }); _defineProperty(_this, "isOpenControlled", _this.props.isOpen !== undefined); _defineProperty(_this, "defaultOpenState", _this.isOpenControlled ? _this.props.isOpen : _this.props.defaultIsOpen); _defineProperty(_this, "state", { focusLockEnabled: false, isOpen: (_this$defaultOpenStat = _this.defaultOpenState) !== null && _this$defaultOpenStat !== void 0 ? _this$defaultOpenStat : false, mergedComponents: defaultComponents, mergedPopperProps: defaultPopperProps }); // Event Handlers // ============================== _defineProperty(_this, "handleTargetKeyDown", function (event) { switch (event.key) { case 'ArrowDown': _this.open(); break; default: } }); _defineProperty(_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); } }); _defineProperty(_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(); } }); _defineProperty(_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); } }); _defineProperty(_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 */ _defineProperty(_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 = 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 */ _defineProperty(_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 // ============================== _defineProperty(_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; } } }; }); _defineProperty(_this, "resolveMenuRef", function (popperRef) { return function (ref) { _this.menuRef = ref; if (typeof popperRef === 'function') { popperRef(ref); } else { popperRef.current = ref; } }; }); _defineProperty(_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 _defineProperty(_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; }); _defineProperty(_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 _defineProperty(_this, "showSearchControl", function () { var _this$props4 = _this.props, searchThreshold = _this$props4.searchThreshold, isSearchable = _this$props4.isSearchable; return isSearchable && _this.getItemCount() > searchThreshold; }); // Renderers // ============================== _defineProperty(_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 = _objectWithoutProperties(_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 }); var getLabel = function getLabel() { if (label) { return label; } else if (typeof placeholder === 'string') { return placeholder; } return undefined; }; var popper = /*#__PURE__*/React.createElement(Popper, _extends({}, 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.createElement(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.createElement(FocusLock, { disabled: !focusLockEnabled, returnFocus: true }, /*#__PURE__*/React.createElement(Select, _extends({ label: getLabel(), backspaceRemovesValue: false, controlShouldRenderValue: false, isClearable: false, tabSelectsValue: false, menuIsOpen: true, placeholder: placeholder, ref: _this.getSelectRef }, props, { isSearchable: showSearchControl, 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__*/createPortal(popper, portalDestination); }); _defineProperty(_this, "handleOpenLayerObserverCloseSignal", function () { _this.close(); }); return _this; } _inherits(PopupSelectLegacy, _PureComponent); return _createClass(PopupSelectLegacy, [{ key: "componentDidMount", value: function componentDidMount() { if (typeof window === 'undefined') { return; } this.unbindWindowClick = 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.createElement(Manager, null, /*#__PURE__*/React.createElement(IdProvider, { postfix: "-popup-select" }, function (_ref3) { var id = _ref3.id; return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.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.createElement(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 (!shallowEqualObjects(mergedPopperProps, state.mergedPopperProps)) { newState.mergedPopperProps = mergedPopperProps; } // Merge consumer and default components var mergedComponents = _objectSpread(_objectSpread({}, defaultComponents), props.components); if (!shallowEqualObjects(mergedComponents, state.mergedComponents)) { newState.mergedComponents = mergedComponents; } if (!isEmpty(newState)) { return newState; } return null; } }]); }(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 _defineProperty(PopupSelectLegacy, "defaultProps", { closeMenuOnSelect: true, shouldCloseMenuOnTab: true, components: {}, maxMenuHeight: 300, maxMenuWidth: 440, minMenuWidth: 220, popperProps: {}, isSearchable: true, searchThreshold: 5, styles: {}, options: [] }); export var PopupSelect = /*#__PURE__*/function (_PureComponent2) { function PopupSelect() { var _this4; _classCallCheck(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)); _defineProperty(_this4, "implementation", null); _defineProperty(_this4, "setImplementationRef", function (implementation) { _this4.implementation = implementation; }); _defineProperty(_this4, "open", function (options) { var _this4$implementation; (_this4$implementation = _this4.implementation) === null || _this4$implementation === void 0 || _this4$implementation.open(options); }); _defineProperty(_this4, "close", function (options) { var _this4$implementation2; (_this4$implementation2 = _this4.implementation) === null || _this4$implementation2 === void 0 || _this4$implementation2.close(options); }); return _this4; } _inherits(PopupSelect, _PureComponent2); return _createClass(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 (fg('platform-dst-top-layer')) { return /*#__PURE__*/React.createElement(PopupSelectTopLayer, _extends({ ref: this.setImplementationRef }, this.props)); } return /*#__PURE__*/React.createElement(PopupSelectLegacy, _extends({ ref: this.setImplementationRef }, this.props)); } }]); }(PureComponent);