UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

162 lines (156 loc) 8.37 kB
import React, { useCallback, useContext, useMemo, useRef } from 'react'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { Popup } from '@atlaskit/editor-common/ui'; import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners } from '@atlaskit/editor-common/ui-react'; import { akEditorFloatingDialogZIndex, akEditorFloatingOverlapPanelZIndex } from '@atlaskit/editor-shared-styles'; import { CellSelection } from '@atlaskit/editor-tables/cell-selection'; import { ToolbarKeyboardNavigationProvider } from '@atlaskit/editor-toolbar/toolbar-keyboard-navigation-provider'; import { fg } from '@atlaskit/platform-feature-flags'; import { closeActiveTableMenu } from '../../pm-plugins/commands'; import { TableCssClassName as ClassName } from '../../types'; import { dragTableInsertColumnButtonSize, tablePopupMenuFitHeight } from '../consts'; import { COLUMN_MENU } from '../TableMenu/column/keys'; import { ROW_MENU } from '../TableMenu/row/keys'; import { TABLE_MENU_WIDTH } from '../TableMenu/shared/consts'; import { TableMenu } from '../TableMenu/shared/TableMenu'; const PopupWithListeners = withReactEditorViewOuterListeners(Popup); // Defer drag-handle clicks to the drag handle's own toggle/select handlers — those own // the open/switch/close semantics for moving between rows/columns. const DRAG_HANDLE_CONTROLS_SELECTOR = `.${ClassName.DRAG_ROW_CONTROLS}, .${ClassName.DRAG_COLUMN_CONTROLS}`; const NESTED_DROPDOWN_SELECTOR = '[data-toolbar-nested-dropdown-menu]'; // Marks the menu subtree that ToolbarKeyboardNavigationProvider scopes its // keyboard handling to. The provider only reacts to events whose target sits // inside this selector. const TABLE_MENU_NAV_SELECTOR = '[data-table-drag-menu-nav="true"]'; const TABLE_MENU_OFFSET = dragTableInsertColumnButtonSize + 4; const POPUP_OFFSET = [TABLE_MENU_OFFSET, 0]; /** * Row and column menu for table. */ const FloatingTableMenu = ({ api, boundariesElement, editorView, mountPoint, scrollableElement, stickyHeaders, tableWrapper, targetCellPosition }) => { const { activeTableMenu } = useSharedPluginStateWithSelector(api, ['table'], states => { var _states$tableState; return { activeTableMenu: (_states$tableState = states.tableState) === null || _states$tableState === void 0 ? void 0 : _states$tableState.activeTableMenu }; }); const isDragMenuOpen = (activeTableMenu === null || activeTableMenu === void 0 ? void 0 : activeTableMenu.type) === 'row' || (activeTableMenu === null || activeTableMenu === void 0 ? void 0 : activeTableMenu.type) === 'column'; const dragMenuDirection = isDragMenuOpen ? activeTableMenu.type : undefined; const isOpenedByKeyboard = isDragMenuOpen && activeTableMenu.openedBy === 'keyboard'; const popupContentRef = useRef(null); const setOutsideClickTargetRef = useContext(OutsideClickTargetRefContext); const navWrapperRef = useRef(null); const handlePopupRef = useCallback(el => { popupContentRef.current = el; setOutsideClickTargetRef === null || setOutsideClickTargetRef === void 0 ? void 0 : setOutsideClickTargetRef(el); }, [setOutsideClickTargetRef]); const returnFocusToDragHandle = useCallback(() => { // Match legacy DragMenu's closeMenu('handle') behaviour. const handleId = dragMenuDirection === 'row' ? '#drag-handle-button-row' : '#drag-handle-button-column'; const handle = document.querySelector(handleId); handle === null || handle === void 0 ? void 0 : handle.focus(); }, [dragMenuDirection]); const focusFirstMenuItem = useCallback(() => { const root = navWrapperRef.current; if (!root) { return; } const firstItem = root.querySelector('[role="menuitem"]:not([disabled]), [role="menuitemcheckbox"]:not([disabled]), [role="menuitemradio"]:not([disabled]), button:not([disabled])'); firstItem === null || firstItem === void 0 ? void 0 : firstItem.focus(); }, []); // Focus the first menu item when the menu opens via keyboard (Enter/Space on // the drag handle). Mouse-opened menus leave focus where the user clicked. const setNavWrapperRef = useCallback(el => { navWrapperRef.current = el; if (el && isOpenedByKeyboard) { // rAF allows the popup to finish positioning before focusing. requestAnimationFrame(() => { focusFirstMenuItem(); }); } }, [focusFirstMenuItem, isOpenedByKeyboard]); const handleKeyboardFocus = useCallback(_event => { focusFirstMenuItem(); }, [focusFirstMenuItem]); const handleEscape = useCallback(event => { event.preventDefault(); event.stopPropagation(); api === null || api === void 0 ? void 0 : api.core.actions.execute(closeActiveTableMenu(api)); returnFocusToDragHandle(); }, [api, returnFocusToDragHandle]); // Memoize the editor DOM reference so the provider doesn't re-bind listeners // on every render (the provider depends on `dom` in its effect's deps). const editorDom = useMemo(() => editorView.dom instanceof HTMLElement ? editorView.dom : undefined, [editorView.dom]); // The drag menu is opened by interacting with the drag handle directly, not // by a global page-level shortcut. const isShortcutToFocusToolbar = useCallback(() => false, []); const handleClickOutside = useCallback(event => { var _popupContentRef$curr; const target = event.target; // Ignore clicks handled by this popup, drag handles, or nested portalled // dropdowns so those controls can manage their own open/close behavior. if (target instanceof Node && (_popupContentRef$curr = popupContentRef.current) !== null && _popupContentRef$curr !== void 0 && _popupContentRef$curr.contains(target) || target instanceof Element && (target.closest(DRAG_HANDLE_CONTROLS_SELECTOR) || target.closest(NESTED_DROPDOWN_SELECTOR))) { return; } api === null || api === void 0 ? void 0 : api.core.actions.execute(closeActiveTableMenu(api)); }, [api]); if (!isDragMenuOpen || !targetCellPosition || editorView.state.doc.nodeSize <= targetCellPosition) { return null; } const inStickyMode = (stickyHeaders === null || stickyHeaders === void 0 ? void 0 : stickyHeaders.sticky) || (tableWrapper === null || tableWrapper === void 0 ? void 0 : tableWrapper.classList.contains(ClassName.TABLE_NODE_WRAPPER_NO_OVERFLOW)) && fg('platform_editor_table_sticky_header_patch_7'); const targetHandleRef = dragMenuDirection === 'row' ? document.querySelector('#drag-handle-button-row') : document.querySelector('#drag-handle-button-column'); if (!targetHandleRef || !(editorView.state.selection instanceof CellSelection)) { return null; } return /*#__PURE__*/React.createElement(PopupWithListeners, { alignX: dragMenuDirection === 'row' ? 'right' : undefined, alignY: dragMenuDirection === 'row' ? 'start' : undefined // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting , target: targetHandleRef, mountTo: mountPoint, boundariesElement: boundariesElement, scrollableElement: scrollableElement, fitWidth: TABLE_MENU_WIDTH, fitHeight: tablePopupMenuFitHeight // z-index value below is to ensure that this menu is above other floating menu // in table, but below floating dialogs like typeaheads, pickers, etc. // In sticky mode, we want to show the menu above the sticky header , zIndex: inStickyMode ? akEditorFloatingDialogZIndex : akEditorFloatingOverlapPanelZIndex, forcePlacement: true, preventOverflow: dragMenuDirection === 'row', offset: POPUP_OFFSET, stick: true, handleClickOutside: handleClickOutside }, /*#__PURE__*/React.createElement("div", { ref: handlePopupRef }, /*#__PURE__*/React.createElement(ToolbarKeyboardNavigationProvider, { childComponentSelector: TABLE_MENU_NAV_SELECTOR, dom: editorDom, isShortcutToFocusToolbar: isShortcutToFocusToolbar, handleFocus: handleKeyboardFocus, handleEscape: handleEscape }, /*#__PURE__*/React.createElement("div", { "data-table-drag-menu-nav": "true", ref: setNavWrapperRef }, /*#__PURE__*/React.createElement(TableMenu, { api: api, editorView: editorView, surface: dragMenuDirection === 'row' ? ROW_MENU : COLUMN_MENU }))))); }; FloatingTableMenu.displayName = 'FloatingTableMenu'; export default FloatingTableMenu;