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