@atlaskit/editor-plugin-quick-insert
Version:
Quick insert plugin for @atlaskit/editor-core
400 lines (399 loc) • 20.1 kB
JavaScript
/* RegistryElementBrowser.tsx generated by @compiled/babel-plugin v3.0.2 */
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import "./RegistryElementBrowser.compiled.css";
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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import IconButton from '@atlaskit/button/icon/button';
// eslint-disable-next-line @atlaskit/platform/no-barrel-entry-imports
import { messages } from '@atlaskit/editor-common/quick-insert';
import { MENU, RECOMMENDED_SECTION } from '@atlaskit/editor-common/quick-insert/keys';
import { createRegisteredMenuSurfaceContext } from '@atlaskit/editor-common/registered-menu/surface-context';
import { getMenuFooterSectionKey } from '@atlaskit/editor-common/type-ahead-get-menu-footer-section-key';
import { getSectionOverflowItemKey } from '@atlaskit/editor-common/type-ahead-get-section-overflow-item-key';
import CrossIcon from '@atlaskit/icon/core/cross';
import ModalBody from '@atlaskit/modal-dialog/modal-body';
import Modal from '@atlaskit/modal-dialog/modal-dialog';
import ModalFooter from '@atlaskit/modal-dialog/modal-footer';
import ModalHeader from '@atlaskit/modal-dialog/modal-header';
import ModalTitle from '@atlaskit/modal-dialog/modal-title';
import ModalTransition from '@atlaskit/modal-dialog/modal-transition';
import Textfield from '@atlaskit/textfield/text-field';
import { useQuickInsertSearchAnalytics } from '../registered-menu/useQuickInsertSearchAnalytics';
import { getArrowKeyTargetIndex, RETURN_TO_SEARCH } from './getArrowKeyTargetIndex';
import { createRegistryElementBrowserModel, getInitialRegistryElementBrowserSection, getRegistryElementBrowserItems } from './model';
import { getFirstEnabledOption, isOptionDisabled } from './options';
import { RegistryElementBrowserCategories } from './RegistryElementBrowserCategories';
import { RegistryElementBrowserFooter } from './RegistryElementBrowserFooter';
import { RegistryElementBrowserSearchResults } from './RegistryElementBrowserSearchResults';
var browserGridTemplateColumns = '182px minmax(0, 1fr)';
var categoryColumnGap = "calc(".concat("var(--ds-space-200, 16px)", " + ", "var(--ds-space-150, 12px)", ")");
var browserLayoutStyles = null;
// The modal header keeps its 24px inline padding, so a narrower gap lines the search field up
// with the results below (16px body padding + 182px + 28px gap).
var headerContentStyles = null;
var headerSearchStyles = null;
var EMPTY_ITEMS = [];
var resultsId = 'registry-element-browser-results';
var modalTestId = 'registry-element-browser-modal';
var arrowKeys = new Set(['ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp']);
var isArrowKey = function isArrowKey(key) {
return arrowKeys.has(key);
};
var toArrowKeyOption = function toArrowKeyOption(option) {
return {
getRect: function getRect() {
return option.getBoundingClientRect();
},
isDisabled: isOptionDisabled(option)
};
};
export var RegistryElementBrowser = function RegistryElementBrowser(_ref) {
var _model$fallbackItems, _model$fallbackItems$, _model$fallbackItems2;
var components = _ref.components,
emptyStateHandler = _ref.emptyStateHandler,
fireAnalyticsEvent = _ref.fireAnalyticsEvent,
defaultSection = _ref.defaultSection,
getRecommendedItems = _ref.getRecommendedItems,
helpUrl = _ref.helpUrl,
editorView = _ref.editorView,
_ref$isLoading = _ref.isLoading,
isLoading = _ref$isLoading === void 0 ? false : _ref$isLoading,
isOffline = _ref.isOffline,
isOpen = _ref.isOpen,
onClearSelection = _ref.onClearSelection,
onClose = _ref.onClose,
onCloseComplete = _ref.onCloseComplete,
onConfirmInsert = _ref.onConfirmInsert,
onSelect = _ref.onSelect;
var _useIntl = useIntl(),
formatMessage = _useIntl.formatMessage;
var _useState = useState(function () {
return {
isOpen: isOpen,
menuOpenId: Symbol('element-browser')
};
}),
_useState2 = _slicedToArray(_useState, 2),
session = _useState2[0],
setSession = _useState2[1];
if (session.isOpen !== isOpen) {
setSession({
isOpen: isOpen,
menuOpenId: isOpen ? Symbol('element-browser') : session.menuOpenId
});
}
var surfaceContext = useMemo(function () {
return createRegisteredMenuSurfaceContext({
menuOpenId: session.menuOpenId,
surface: 'element-browser'
});
}, [session.menuOpenId]);
var _useState3 = useState({
query: '',
section: defaultSection
}),
_useState4 = _slicedToArray(_useState3, 2),
browserState = _useState4[0],
setBrowserState = _useState4[1];
var query = browserState.query,
section = browserState.section,
selectedRegistration = browserState.selectedRegistration;
var selectedRegistrationRef = useRef(undefined);
// Recommendations depend only on the registry, so typing a query does not re-run the resolver.
var recommendedRegistrations = useMemo(function () {
return getRecommendedItems === null || getRecommendedItems === void 0 ? void 0 : getRecommendedItems({
components: components,
surfaceContext: surfaceContext
});
}, [components, getRecommendedItems, surfaceContext]);
var browserModel = useMemo(function () {
var model = createRegistryElementBrowserModel(components, {
footerKey: getMenuFooterSectionKey(MENU.key),
surfaceContext: surfaceContext,
overflowKeys: new Set(components.filter(function (component) {
return component.type === 'menu-section';
}).map(function (component) {
return getSectionOverflowItemKey(component.key);
})),
recommendedRegistrations: recommendedRegistrations,
recommendedSectionKey: RECOMMENDED_SECTION.key,
rootKey: MENU.key
});
var effectiveSection = section && model.sections.some(function (_ref2) {
var key = _ref2.key;
return key === section;
}) ? section : undefined;
return _objectSpread({
model: model,
effectiveSection: effectiveSection
}, getRegistryElementBrowserItems({
formatMessage: formatMessage,
model: model,
query: query,
section: effectiveSection
}));
}, [components, formatMessage, query, recommendedRegistrations, section, surfaceContext]);
var model = browserModel.model,
effectiveSection = browserModel.effectiveSection,
items = browserModel.items,
recommendedItems = browserModel.recommendedItems;
var resultItems = useMemo(function () {
return [].concat(_toConsumableArray(recommendedItems), _toConsumableArray(items));
}, [items, recommendedItems]);
var visibleItems = resultItems.length === 0 && !isLoading && query.trim().length > 0 ? (_model$fallbackItems = model.fallbackItems) !== null && _model$fallbackItems !== void 0 ? _model$fallbackItems : EMPTY_ITEMS : resultItems;
var isSelectedRegistrationCurrent = Boolean(selectedRegistration && browserState.selectionIsOffline === isOffline && visibleItems.some(function (_ref3) {
var registration = _ref3.registration;
return registration === selectedRegistration;
}));
var selectedKey = isSelectedRegistrationCurrent ? selectedRegistration === null || selectedRegistration === void 0 ? void 0 : selectedRegistration.key : undefined;
var invalidSection = Boolean(section && section !== effectiveSection);
var invalidSelection = Boolean(selectedRegistration && !isSelectedRegistrationCurrent);
if (isOpen && (invalidSection || invalidSelection)) {
setBrowserState(_objectSpread(_objectSpread({}, browserState), {}, {
section: effectiveSection,
selectedRegistration: undefined,
selectionIsOffline: undefined,
invalidation: {
section: invalidSection ? section : undefined,
selection: invalidSelection
}
}));
}
var searchInputRef = useRef(null);
var selectedCategoryRef = useRef(null);
var focusedCategoryRef = useRef();
var handledInvalidationRef = useRef(browserState.invalidation);
var wasOpen = useRef(false);
useEffect(function () {
if (isOpen && !wasOpen.current) {
selectedRegistrationRef.current = undefined;
setBrowserState({
query: '',
section: getInitialRegistryElementBrowserSection(model, defaultSection),
selectedRegistration: undefined
});
onClearSelection();
}
wasOpen.current = isOpen;
}, [defaultSection, isOpen, model, onClearSelection]);
useEffect(function () {
var _searchInputRef$curre;
var invalidation = browserState.invalidation;
if (!isOpen || !invalidation || handledInvalidationRef.current === invalidation) {
return;
}
handledInvalidationRef.current = invalidation;
selectedRegistrationRef.current = undefined;
onClearSelection();
var ownerDocument = (_searchInputRef$curre = searchInputRef.current) === null || _searchInputRef$curre === void 0 ? void 0 : _searchInputRef$curre.ownerDocument;
var activeElement = ownerDocument === null || ownerDocument === void 0 ? void 0 : ownerDocument.activeElement;
if (invalidation.section && focusedCategoryRef.current === invalidation.section && activeElement === (ownerDocument === null || ownerDocument === void 0 ? void 0 : ownerDocument.body)) {
var _selectedCategoryRef$;
(_selectedCategoryRef$ = selectedCategoryRef.current) === null || _selectedCategoryRef$ === void 0 || _selectedCategoryRef$.focus();
} else if (invalidation.selection && (activeElement === (ownerDocument === null || ownerDocument === void 0 ? void 0 : ownerDocument.body) || activeElement !== null && activeElement !== void 0 && activeElement.closest('[role="option"]'))) {
var _searchInputRef$curre2;
(_searchInputRef$curre2 = searchInputRef.current) === null || _searchInputRef$curre2 === void 0 || _searchInputRef$curre2.focus();
}
}, [browserState.invalidation, isOpen, onClearSelection]);
var resultCount = resultItems.length || (!isLoading && query.trim() ? (_model$fallbackItems$ = (_model$fallbackItems2 = model.fallbackItems) === null || _model$fallbackItems2 === void 0 ? void 0 : _model$fallbackItems2.length) !== null && _model$fallbackItems$ !== void 0 ? _model$fallbackItems$ : 0 : 0);
var flushSearchAnalytics = useQuickInsertSearchAnalytics({
query: query,
resultCount: resultItems.length,
surface: 'element-browser',
category: effectiveSection !== null && effectiveSection !== void 0 ? effectiveSection : 'all',
isOpen: isOpen && !isLoading,
fireAnalyticsEvent: fireAnalyticsEvent
});
var closeBrowser = useCallback(function () {
flushSearchAnalytics();
onClose();
}, [flushSearchAnalytics, onClose]);
var onSearchFocus = useCallback(function () {
focusedCategoryRef.current = undefined;
}, []);
var onBrowserFocusCapture = useCallback(function (event) {
var _category$dataset$sec;
var categories = event.currentTarget.querySelector('[data-testid="registry-element-browser-categories"]');
var category = event.target instanceof HTMLElement && categories !== null && categories !== void 0 && categories.contains(event.target) ? event.target.closest('[data-section]') : null;
focusedCategoryRef.current = category ? (_category$dataset$sec = category.dataset.section) !== null && _category$dataset$sec !== void 0 ? _category$dataset$sec : '' : undefined;
}, []);
var onSearchChange = useCallback(function (event) {
var nextQuery = event.currentTarget.value;
selectedRegistrationRef.current = undefined;
setBrowserState(function (current) {
return _objectSpread(_objectSpread({}, current), {}, {
query: nextQuery,
selectedRegistration: undefined
});
});
onClearSelection();
}, [onClearSelection]);
var onSectionClick = useCallback(function (event) {
var nextSection = event.currentTarget.dataset.section || undefined;
selectedRegistrationRef.current = undefined;
setBrowserState({
query: '',
section: nextSection,
selectedRegistration: undefined
});
onClearSelection();
}, [onClearSelection]);
var onRegistrySelect = useCallback(function (key, handler) {
var _model$items$find;
var registration = (_model$items$find = model.items.find(function (_ref4) {
var registration = _ref4.registration;
return registration.key === key;
})) === null || _model$items$find === void 0 ? void 0 : _model$items$find.registration;
selectedRegistrationRef.current = registration;
setBrowserState(function (current) {
return _objectSpread(_objectSpread({}, current), {}, {
selectedRegistration: registration,
selectionIsOffline: isOffline
});
});
onSelect(handler);
}, [isOffline, model.items, onSelect]);
var confirmInsert = useCallback(function () {
var selectedRegistration = selectedRegistrationRef.current;
if (!selectedRegistration || !visibleItems.some(function (_ref5) {
var registration = _ref5.registration;
return registration === selectedRegistration;
})) {
return;
}
onConfirmInsert();
}, [onConfirmInsert, visibleItems]);
var onModalOpenComplete = useCallback(function (node) {
node.style.borderRadius = "var(--ds-radius-large, 8px)";
var footer = node.querySelector("[data-testid=\"".concat(modalTestId, "--footer\"]"));
footer === null || footer === void 0 || footer.style.setProperty('box-sizing', 'border-box');
footer === null || footer === void 0 || footer.style.setProperty('height', '64px');
footer === null || footer === void 0 || footer.style.setProperty('padding-block', "var(--ds-space-200, 16px)");
var scrollableBody = node.querySelector("[data-testid=\"".concat(modalTestId, "--scrollable\"]"));
scrollableBody === null || scrollableBody === void 0 || scrollableBody.style.setProperty('border-block-start', 'none');
scrollableBody === null || scrollableBody === void 0 || scrollableBody.style.setProperty('border-block-end', 'none');
}, []);
var onKeyDown = useCallback(function (event) {
if (event.key === 'Tab' && event.shiftKey && event.target instanceof HTMLElement && event.target.getAttribute('role') === 'option') {
var _selectedCategoryRef$2;
event.preventDefault();
(_selectedCategoryRef$2 = selectedCategoryRef.current) === null || _selectedCategoryRef$2 === void 0 || _selectedCategoryRef$2.focus();
return;
}
if (isArrowKey(event.key)) {
var _resultCollection$que;
if (event.currentTarget === searchInputRef.current && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
return;
}
var resultCollection = event.currentTarget.ownerDocument.getElementById(resultsId);
var options = Array.from((_resultCollection$que = resultCollection === null || resultCollection === void 0 ? void 0 : resultCollection.querySelectorAll('[role="option"]')) !== null && _resultCollection$que !== void 0 ? _resultCollection$que : []);
var currentIndex = options.findIndex(function (option) {
return option === event.currentTarget.ownerDocument.activeElement;
});
var targetIndex = getArrowKeyTargetIndex(options.map(toArrowKeyOption), currentIndex, event.key);
var nextOption = targetIndex === undefined ? undefined : options[targetIndex];
if (nextOption) {
event.preventDefault();
nextOption.focus();
nextOption.click();
} else if (targetIndex === RETURN_TO_SEARCH) {
var _searchInputRef$curre3;
event.preventDefault();
(_searchInputRef$curre3 = searchInputRef.current) === null || _searchInputRef$curre3 === void 0 || _searchInputRef$curre3.focus();
}
return;
}
if (event.key === 'Escape') {
event.preventDefault();
closeBrowser();
return;
}
if (event.key === 'Enter') {
var focusedOption = event.target instanceof HTMLElement ? event.target.closest('[role="option"]') : null;
var firstOption = getFirstEnabledOption(event.currentTarget.ownerDocument.getElementById(resultsId));
var optionToActivate = focusedOption !== null && focusedOption !== void 0 ? focusedOption : firstOption;
if (isSelectedRegistrationCurrent || optionToActivate) {
event.preventDefault();
if (!isSelectedRegistrationCurrent) {
optionToActivate === null || optionToActivate === void 0 || optionToActivate.click();
}
confirmInsert();
}
}
}, [closeBrowser, confirmInsert, isSelectedRegistrationCurrent]);
return /*#__PURE__*/React.createElement(ModalTransition, null, isOpen && /*#__PURE__*/React.createElement(Modal, {
label: formatMessage({
defaultMessage: 'Insert elements',
id: 'editor.quick-insert.title'
}),
height: "804px",
onClose: closeBrowser,
onCloseComplete: onCloseComplete,
onOpenComplete: onModalOpenComplete,
testId: modalTestId,
width: "min(1072px, calc(100vw - 32px))"
}, /*#__PURE__*/React.createElement(ModalHeader, null, /*#__PURE__*/React.createElement("div", {
className: ax(["_zulpv47k _1e0c11p5 _yv0e1xuv _1bsb1osq _1ikai47z"])
}, /*#__PURE__*/React.createElement(ModalTitle, null, formatMessage({
defaultMessage: 'Insert elements',
id: 'editor.quick-insert.title'
})), /*#__PURE__*/React.createElement("div", {
className: ax(["_zulp1ejb _4cvr1h6o _1e0c11p5 _yv0ehpfy _1bsb1osq"])
}, /*#__PURE__*/React.createElement(Textfield, {
autoFocus: true,
"aria-controls": resultsId,
"aria-label": formatMessage({
defaultMessage: 'Search elements',
id: 'editor.quick-insert.search'
}),
isCompact: true,
name: "registry-element-browser-search",
onFocus: onSearchFocus,
onChange: onSearchChange,
onKeyDown: onKeyDown,
ref: searchInputRef,
value: query
}), /*#__PURE__*/React.createElement(IconButton, {
appearance: "subtle",
icon: CrossIcon,
label: formatMessage(messages.close),
onClick: closeBrowser,
spacing: "compact"
})))), /*#__PURE__*/React.createElement(ModalBody, {
hasInlinePadding: false
}, /*#__PURE__*/React.createElement("div", {
onFocusCapture: onBrowserFocusCapture,
className: ax(["_zulp1fka _18zrpxbi _1e0c11p5 _yv0e1xuv _1q511b66 _1ikai47z _uzjvutpp"])
}, /*#__PURE__*/React.createElement(RegistryElementBrowserCategories, {
section: effectiveSection,
sections: model.sections,
onSectionClick: onSectionClick,
selectedButtonRef: selectedCategoryRef
}), /*#__PURE__*/React.createElement(RegistryElementBrowserSearchResults, {
editorView: editorView,
emptyStateHandler: emptyStateHandler,
isLoading: isLoading,
isOffline: isOffline,
items: items,
fallbackItems: model.fallbackItems,
query: query,
recommendedItems: recommendedItems,
resultsId: resultsId,
surfaceContext: surfaceContext,
section: effectiveSection,
selectedKey: selectedKey,
onKeyDown: onKeyDown,
onSelect: onRegistrySelect
}))), /*#__PURE__*/React.createElement(ModalFooter, null, /*#__PURE__*/React.createElement(RegistryElementBrowserFooter, {
helpUrl: helpUrl,
isInsertEnabled: isSelectedRegistrationCurrent,
onClose: closeBrowser,
onConfirmInsert: confirmInsert,
resultCount: resultCount
}))));
};