@atlaskit/editor-plugin-insert-block
Version:
Insert block plugin for @atlaskit/editor-core
242 lines (241 loc) • 10.4 kB
JavaScript
/* RegisteredInsertMenu.tsx generated by @compiled/babel-plugin v3.0.2 */
import "./RegisteredInsertMenu.compiled.css";
import { ax, ix } from "@compiled/react/runtime";
import React, { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { getDocument } from '@atlaskit/browser-apis';
import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages/insert-block';
import { MENU } from '@atlaskit/editor-common/quick-insert/keys';
import { getQuickInsertMenuModel, getQuickInsertMenuRows, selectQuickInsertCategoryItems } from '@atlaskit/editor-common/quick-insert/registered-menu-model';
import { useQuickInsertSearchAnalytics } from '@atlaskit/editor-common/quick-insert/use-search-analytics';
import { isSectionOverflowItemKey } from '@atlaskit/editor-common/type-ahead-is-section-overflow-item-key';
import { TYPE_AHEAD_SURFACE_CONTEXT } from '@atlaskit/editor-common/type-ahead-surface-context';
import { createSurfaceContext } from '@atlaskit/editor-ui-control-model/create-surface-context';
import { Box } from '@atlaskit/primitives/compiled/box';
import Textfield from '@atlaskit/textfield/text-field';
import { RegisteredInsertMenuList } from './RegisteredInsertMenuList';
const DEFAULT_MENU_MAX_HEIGHT = 520;
const styles = {
menu: "_2rko1mok _1reo15vq _18m915vq _bfhk1bhr _16qs130s _1e0c1txw _2lx21bp4 _1bsb1edt",
search: "_18zrutpp _1o9zidpf _1q51utpp _85i5u2gc"
};
export const RegisteredInsertMenu = ({
components,
emptyStateHandler,
fireAnalyticsEvent,
getItemRank,
editorView,
isOffline,
maxHeight = DEFAULT_MENU_MAX_HEIGHT,
onClose,
onSelect
}) => {
const menuStyle = {
maxHeight
};
const {
formatMessage
} = useIntl();
const listId = useId();
const [menuOpenId] = useState(() => Symbol('registered-insert-menu-open'));
const [query, setQuery] = useState('');
const [selectedItemIndex, setSelectedItemIndex] = useState(0);
const selectedItemIndexRef = useRef(selectedItemIndex);
const [renderedItems, setRenderedItems] = useState({
startIndex: -1,
stopIndex: -1
});
const searchRef = useRef(null);
useLayoutEffect(() => {
var _searchRef$current;
// Popup positions on the next frame; focusing must not scroll to its initial location.
(_searchRef$current = searchRef.current) === null || _searchRef$current === void 0 ? void 0 : _searchRef$current.focus({
preventScroll: true
});
}, []);
const surfaceContext = useMemo(() => createSurfaceContext(TYPE_AHEAD_SURFACE_CONTEXT, {
menuOpenId
}), [menuOpenId]);
const hasSectionOverflowItems = useMemo(() => components.some(({
key
}) => isSectionOverflowItemKey(key)), [components]);
const model = useMemo(() => getQuickInsertMenuModel({
components,
formatMessage,
getItemRank,
query,
rootComponent: MENU,
selectCategoryItems: hasSectionOverflowItems ? selectQuickInsertCategoryItems : undefined,
surfaceContext
}), [components, formatMessage, getItemRank, hasSectionOverflowItems, query, surfaceContext]);
const [previewActivation, setPreviewActivation] = useState();
const activePreviewItemKey = (previewActivation === null || previewActivation === void 0 ? void 0 : previewActivation.model) === model ? previewActivation.itemKey : undefined;
const searchResultCount = useMemo(() => model.sections.reduce((count, section) => count + section.filter(({
key,
type
}) => type === 'menu-item' && !isSectionOverflowItemKey(key)).length, 0), [model.sections]);
const flushSearchAnalytics = useQuickInsertSearchAnalytics({
fireAnalyticsEvent,
query,
resultCount: searchResultCount,
surface: 'toolbar'
});
const closeWithAnalytics = useCallback(() => {
flushSearchAnalytics();
onClose();
}, [flushSearchAnalytics, onClose]);
const selectWithAnalytics = useCallback(() => {
flushSearchAnalytics();
onSelect();
}, [flushSearchAnalytics, onSelect]);
const selectableItems = useMemo(() => getQuickInsertMenuRows(model).filter(row => row.type === 'menu-item'), [model]);
const itemCount = selectableItems.length + (model.footer ? 1 : 0);
const selectableItemKeys = useMemo(() => [...selectableItems, ...(model.footer ? [model.footer] : [])].map(({
key
}) => key).join(','), [model.footer, selectableItems]);
useLayoutEffect(() => {
const initialItemIndex = itemCount > 0 ? 0 : -1;
selectedItemIndexRef.current = initialItemIndex;
setSelectedItemIndex(initialItemIndex);
}, [itemCount, query, selectableItemKeys]);
useLayoutEffect(() => {
setPreviewActivation(undefined);
}, [model, query]);
const isActiveItemMounted = selectedItemIndex >= 0 && selectedItemIndex < itemCount && (selectedItemIndex >= renderedItems.startIndex && selectedItemIndex <= renderedItems.stopIndex || Boolean(model.footer) && selectedItemIndex === itemCount - 1);
const activeItemId = `${listId}-${selectedItemIndex}`;
const setKeyboardPreview = useCallback(itemIndex => {
const item = selectableItems[itemIndex];
setPreviewActivation(item ? {
itemKey: item.key,
model,
source: 'keyboard'
} : undefined);
}, [model, selectableItems]);
const selectNextItem = useCallback(() => {
const currentIndex = selectedItemIndexRef.current;
const nextIndex = itemCount === 0 ? -1 : currentIndex < 0 ? 0 : (currentIndex + 1) % itemCount;
selectedItemIndexRef.current = nextIndex;
setSelectedItemIndex(nextIndex);
setKeyboardPreview(nextIndex);
}, [itemCount, setKeyboardPreview]);
const selectPreviousItem = useCallback(() => {
const currentIndex = selectedItemIndexRef.current;
const previousIndex = itemCount === 0 ? -1 : currentIndex <= 0 ? itemCount - 1 : currentIndex - 1;
selectedItemIndexRef.current = previousIndex;
setSelectedItemIndex(previousIndex);
setKeyboardPreview(previousIndex);
}, [itemCount, setKeyboardPreview]);
const onItemHover = useCallback((itemIndex, itemKey) => {
selectedItemIndexRef.current = itemIndex;
setSelectedItemIndex(itemIndex);
setPreviewActivation(current => {
if ((current === null || current === void 0 ? void 0 : current.itemKey) === itemKey && (current === null || current === void 0 ? void 0 : current.model) === model && (current === null || current === void 0 ? void 0 : current.source) === 'pointer') {
return current;
}
return itemKey ? {
itemKey,
model,
source: 'pointer'
} : undefined;
});
}, [model]);
const onItemFocus = useCallback((itemIndex, itemKey) => {
selectedItemIndexRef.current = itemIndex;
setSelectedItemIndex(itemIndex);
setPreviewActivation(current => {
if ((current === null || current === void 0 ? void 0 : current.itemKey) === itemKey && (current === null || current === void 0 ? void 0 : current.model) === model) {
return current;
}
return itemKey ? {
itemKey,
model,
source: 'keyboard'
} : undefined;
});
}, [model]);
const onItemLeave = useCallback(itemKey => {
setPreviewActivation(current => (current === null || current === void 0 ? void 0 : current.source) === 'pointer' && current.itemKey === itemKey ? undefined : current);
}, []);
const onItemBlur = useCallback(itemKey => {
setPreviewActivation(current => (current === null || current === void 0 ? void 0 : current.itemKey) === itemKey ? undefined : current);
}, []);
const onItemVisibilityChange = useCallback((itemKey, isVisible) => {
if (!isVisible) {
setPreviewActivation(current => (current === null || current === void 0 ? void 0 : current.source) === 'pointer' && current.itemKey === itemKey ? undefined : current);
}
}, []);
const selectActiveItem = useCallback(() => {
if (isActiveItemMounted) {
var _getDocument, _getDocument$getEleme;
(_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : (_getDocument$getEleme = _getDocument.getElementById(activeItemId)) === null || _getDocument$getEleme === void 0 ? void 0 : _getDocument$getEleme.click();
}
}, [activeItemId, isActiveItemMounted]);
const onQueryChange = useCallback(event => {
setQuery(event.currentTarget.value);
}, []);
const onKeyDown = useCallback(event => {
if (event.key === 'Escape') {
event.preventDefault();
closeWithAnalytics();
return;
}
// The search field owns the composite's arrow-key and Enter behaviour. Other
// focusable controls, including consumer-provided empty-state content, need
// to retain their native keyboard behaviour.
if (event.target !== searchRef.current) {
return;
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
selectNextItem();
break;
case 'ArrowUp':
event.preventDefault();
selectPreviousItem();
break;
case 'Enter':
event.preventDefault();
selectActiveItem();
break;
}
}, [closeWithAnalytics, selectActiveItem, selectNextItem, selectPreviousItem]);
return /*#__PURE__*/React.createElement(Box, {
"data-keyboard-navigation-independent": "",
testId: "registered-insert-menu",
onKeyDownCapture: onKeyDown,
xcss: styles.menu,
style: menuStyle
}, /*#__PURE__*/React.createElement(Box, {
xcss: styles.search
}, /*#__PURE__*/React.createElement(Textfield, {
"aria-activedescendant": isActiveItemMounted ? activeItemId : undefined,
"aria-controls": listId,
"aria-expanded": "true",
placeholder: "Search",
ref: searchRef,
role: "combobox",
value: query,
onChange: onQueryChange
})), /*#__PURE__*/React.createElement(RegisteredInsertMenuList, {
editorView: editorView,
emptyStateHandler: emptyStateHandler,
isOffline: isOffline,
listId: listId,
listLabel: formatMessage(messages.insertMenu),
maxHeight: maxHeight,
menuOpenId: menuOpenId,
model: model,
onClose: selectWithAnalytics,
onItemFocus: onItemFocus,
onItemHover: onItemHover,
onItemBlur: onItemBlur,
onItemLeave: onItemLeave,
onItemVisibilityChange: onItemVisibilityChange,
onRenderedItemsChange: setRenderedItems,
query: query,
selectedItemIndex: selectedItemIndex,
surfaceContext: surfaceContext,
activePreviewItemKey: activePreviewItemKey
}));
};