UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

339 lines (334 loc) • 12.1 kB
import _extends from "@babel/runtime/helpers/extends"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; /** @jsx jsx */ import React, { PureComponent, useContext } from 'react'; import { css, jsx } from '@emotion/react'; import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles'; import { CustomItem, MenuGroup, Section } from '@atlaskit/menu'; import { getBooleanFF } from '@atlaskit/platform-feature-flags'; import { B100, N70, N900 } from '@atlaskit/theme/colors'; import Tooltip from '@atlaskit/tooltip'; import { DropdownMenuSharedCssClassName } from '../../styles'; import { KeyDownHandlerContext } from '../../ui-menu/ToolbarArrowKeyNavigationProvider'; import { withReactEditorViewOuterListeners } from '../../ui-react'; import DropList from '../../ui/DropList'; import Popup from '../../ui/Popup'; import { ArrowKeyNavigationProvider } from '../ArrowKeyNavigationProvider'; import { ArrowKeyNavigationType } from '../ArrowKeyNavigationProvider/types'; const wrapper = css` /* tooltip in ToolbarButton is display:block */ & > div > div { display: flex; } `; const focusedMenuItemStyle = css` box-shadow: inset 0px 0px 0px 2px ${`var(--ds-border-focused, ${B100})`}; outline: none; `; const buttonStyles = (isActive, submenuActive) => { if (isActive) { /** * Hack for item to imitate old dropdown-menu selected styles */ return css` > span, > span:hover, > span:active { background: ${"var(--ds-background-selected, #6c798f)"}; color: ${"var(--ds-text, #fff)"}; } :focus > span[aria-disabled='false'] { ${focusedMenuItemStyle}; } :focus-visible, :focus-visible > span[aria-disabled='false'] { outline: none; } `; } else { return css` > span:hover[aria-disabled='false'] { color: ${`var(--ds-text, ${N900})`}; background-color: ${"var(--ds-background-neutral-subtle-hovered, rgb(244, 245, 247))"}; } ${!submenuActive && ` > span:active[aria-disabled='false'] { background-color: ${"var(--ds-background-neutral-subtle-pressed, rgb(179, 212, 255))"}; }`} > span[aria-disabled='true'] { color: ${`var(--ds-text-disabled, ${N70})`}; } :focus > span[aria-disabled='false'] { ${focusedMenuItemStyle}; } :focus-visible, :focus-visible > span[aria-disabled='false'] { outline: none; } `; // The default focus-visible style is removed to ensure consistency across browsers } }; const DropListWithOutsideListeners = withReactEditorViewOuterListeners(DropList); /** * Wrapper around @atlaskit/droplist which uses Popup and Portal to render * dropdown-menu outside of "overflow: hidden" containers when needed. * * Also it controls popper's placement. */ export default class DropdownMenuWrapper extends PureComponent { constructor(...args) { super(...args); _defineProperty(this, "state", { popupPlacement: ['bottom', 'left'], selectionIndex: -1 }); _defineProperty(this, "popupRef", /*#__PURE__*/React.createRef()); _defineProperty(this, "handleRef", target => { this.setState({ target: target || undefined }); }); _defineProperty(this, "updatePopupPlacement", placement => { const { popupPlacement: previousPlacement } = this.state; if (placement[0] !== previousPlacement[0] || placement[1] !== previousPlacement[1]) { this.setState({ popupPlacement: placement }); } }); _defineProperty(this, "handleCloseAndFocus", () => { var _this$state$target, _this$state$target$qu; (_this$state$target = this.state.target) === null || _this$state$target === void 0 ? void 0 : (_this$state$target$qu = _this$state$target.querySelector('button')) === null || _this$state$target$qu === void 0 ? void 0 : _this$state$target$qu.focus(); this.handleClose(); }); _defineProperty(this, "handleClose", () => { if (this.props.onOpenChange) { this.props.onOpenChange({ isOpen: false }); } }); } renderDropdownMenu() { const { target, popupPlacement } = this.state; const { items, mountTo, boundariesElement, scrollableElement, offset, fitHeight, fitWidth, isOpen, zIndex, shouldUseDefaultRole, onItemActivated, arrowKeyNavigationProviderOptions, section } = this.props; // Note that this onSelection function can't be refactored to useMemo for // performance gains as it is being used as a dependency in a useEffect in // MenuArrowKeyNavigationProvider in order to check for re-renders to adjust // focus for accessibility. If this needs to be refactored in future refer // back to ED-16740 for context. const navigationProviderProps = arrowKeyNavigationProviderOptions.type === ArrowKeyNavigationType.COLOR ? arrowKeyNavigationProviderOptions : { ...arrowKeyNavigationProviderOptions, onSelection: index => { let result = []; if (typeof onItemActivated === 'function') { result = items.reduce((result, group) => { return result.concat(group.items); }, result); onItemActivated({ item: result[index], shouldCloseMenu: false }); } } }; return jsx(Popup, { target: isOpen ? target : undefined, mountTo: mountTo, boundariesElement: boundariesElement, scrollableElement: scrollableElement, onPlacementChanged: this.updatePopupPlacement, fitHeight: fitHeight, fitWidth: fitWidth, zIndex: zIndex || akEditorFloatingPanelZIndex, offset: offset }, jsx(ArrowKeyNavigationProvider, _extends({}, navigationProviderProps, { handleClose: this.handleCloseAndFocus, closeOnTab: true }), jsx(DropListWithOutsideListeners, { isOpen: true, appearance: "tall", position: popupPlacement.join(' '), shouldFlip: false, shouldFitContainer: true, isTriggerNotTabbable: true, handleClickOutside: this.handleClose, handleEscapeKeydown: this.handleCloseAndFocus, handleEnterKeydown: e => { e.preventDefault(); e.stopPropagation(); }, targetRef: this.state.target }, jsx("div", { style: { height: 0, minWidth: fitWidth || 0 } }), jsx("div", { ref: this.popupRef }, getBooleanFF('platform.editor.menu.group-items') && jsx(MenuGroup, { role: shouldUseDefaultRole ? 'group' : 'menu' }, items.map((group, index) => jsx(Section, { hasSeparator: (section === null || section === void 0 ? void 0 : section.hasSeparator) && index > 0, title: section === null || section === void 0 ? void 0 : section.title, key: index }, group.items.map(item => { var _item$key; return jsx(DropdownMenuItem, { key: (_item$key = item.key) !== null && _item$key !== void 0 ? _item$key : String(item.content), item: item, onItemActivated: this.props.onItemActivated, shouldUseDefaultRole: this.props.shouldUseDefaultRole, onMouseEnter: this.props.onMouseEnter, onMouseLeave: this.props.onMouseLeave }); })))), !getBooleanFF('platform.editor.menu.group-items') && items.map((group, index) => jsx(MenuGroup, { key: index, role: shouldUseDefaultRole ? 'group' : 'menu' }, group.items.map(item => { var _item$key2; return jsx(DropdownMenuItem, { key: (_item$key2 = item.key) !== null && _item$key2 !== void 0 ? _item$key2 : String(item.content), item: item, onItemActivated: this.props.onItemActivated, shouldUseDefaultRole: this.props.shouldUseDefaultRole, onMouseEnter: this.props.onMouseEnter, onMouseLeave: this.props.onMouseLeave }); }))))))); } render() { const { children, isOpen } = this.props; return jsx("div", { css: wrapper }, jsx("div", { ref: this.handleRef }, children), isOpen ? this.renderDropdownMenu() : null); } componentDidUpdate(previousProps) { const isOpenToggled = this.props.isOpen !== previousProps.isOpen; if (this.props.isOpen && isOpenToggled) { if (typeof this.props.shouldFocusFirstItem === 'function' && this.props.shouldFocusFirstItem()) { var _this$state$target2; const keyboardEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); (_this$state$target2 = this.state.target) === null || _this$state$target2 === void 0 ? void 0 : _this$state$target2.dispatchEvent(keyboardEvent); } } } } const DropdownMenuItemCustomComponent = /*#__PURE__*/React.forwardRef((props, ref) => { const { children, ...rest } = props; return jsx("span", _extends({ ref: ref }, rest, { style: { // This forces the item container back to be `position: static`, the default value. // This ensures the custom nested menu for table color picker still works as now // menu items from @atlaskit/menu all have `position: relative` set for the selected borders. // The current implementation unfortunately is very brittle. Design System Team will // be prioritizing official support for accessible nested menus that we want you to move // to in the future. position: 'static' } }), children); }); export function DropdownMenuItem({ item, onItemActivated, shouldUseDefaultRole, onMouseEnter, onMouseLeave }) { var _item$key3; const [submenuActive, setSubmenuActive] = React.useState(false); // onClick and value.name are the action indicators in the handlers // If neither are present, don't wrap in an Item. if (!item.onClick && !(item.value && item.value.name)) { return jsx("span", { key: String(item.content) }, item.content); } const _handleSubmenuActive = event => { setSubmenuActive(!!event.target.closest(`.${DropdownMenuSharedCssClassName.SUBMENU}`)); }; const dropListItem = jsx("div", { css: () => buttonStyles(item.isActive, submenuActive), tabIndex: -1, "aria-disabled": item.isDisabled ? 'true' : 'false', onMouseDown: _handleSubmenuActive }, jsx(CustomItem, { item: item, key: (_item$key3 = item.key) !== null && _item$key3 !== void 0 ? _item$key3 : String(item.content), testId: `dropdown-item__${String(item.content)}`, role: shouldUseDefaultRole ? 'button' : 'menuitem', iconBefore: item.elemBefore, iconAfter: item.elemAfter, isDisabled: item.isDisabled, onClick: () => onItemActivated && onItemActivated({ item }), "aria-label": item['aria-label'] || String(item.content), "aria-pressed": shouldUseDefaultRole ? item.isActive : undefined, "aria-keyshortcuts": item['aria-keyshortcuts'], onMouseDown: e => { e.preventDefault(); }, component: DropdownMenuItemCustomComponent, onMouseEnter: () => onMouseEnter && onMouseEnter({ item }), onMouseLeave: () => onMouseLeave && onMouseLeave({ item }) }, item.content)); if (item.tooltipDescription) { var _item$key4; return jsx(Tooltip, { key: (_item$key4 = item.key) !== null && _item$key4 !== void 0 ? _item$key4 : String(item.content), content: item.tooltipDescription, position: item.tooltipPosition }, dropListItem); } return dropListItem; } export const DropdownMenuWithKeyboardNavigation = /*#__PURE__*/React.memo(({ ...props }) => { const keyDownHandlerContext = useContext(KeyDownHandlerContext); // This context is to handle the tab, Arrow Right/Left key events for dropdown. // Default context has the void callbacks for above key events return jsx(DropdownMenuWrapper, _extends({ arrowKeyNavigationProviderOptions: { ...props.arrowKeyNavigationProviderOptions, keyDownHandlerContext } }, props)); });