UNPKG

@atlaskit/editor-common

Version:

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

376 lines (372 loc) • 13.6 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _extends from "@babel/runtime/helpers/extends"; /* eslint-disable @repo/internal/react/no-class-components */ /** * @jsxRuntime classic * @jsx jsx */ import React, { PureComponent, useContext } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { css, jsx } from '@emotion/react'; import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles'; import { CustomItem, MenuGroup, Section } from '@atlaskit/menu'; import Tooltip from '@atlaskit/tooltip'; import { DropdownMenuSharedCssClassName } from '../../styles'; import { KeyDownHandlerContext } from '../../ui-menu/ToolbarArrowKeyNavigationProvider'; import { OutsideClickTargetRefContext, 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 */ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766 '& > div > div': { display: 'flex' } }); const focusedMenuItemStyle = css({ boxShadow: `inset 0px 0px 0px 2px ${"var(--ds-border-focused, #388BFF)"}`, outline: 'none' }); const buttonStyles = (isActive, submenuActive) => { if (isActive) { /** * Hack for item to imitate old dropdown-menu selected styles */ // eslint-disable-next-line @atlaskit/design-system/no-css-tagged-template-expression -- needs manual remediation 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 { // eslint-disable-next-line @atlaskit/design-system/no-css-tagged-template-expression -- needs manual remediation return css` > span:hover[aria-disabled='false'] { color: ${"var(--ds-text, #172B4D)"}; 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, #091E424F)"}; } :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 DropListWithOutsideClickTargetRef = props => { const setOutsideClickTargetRef = React.useContext(OutsideClickTargetRefContext); // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading return jsx(DropList, _extends({ onDroplistRef: setOutsideClickTargetRef }, props)); }; const DropListWithOutsideListeners = withReactEditorViewOuterListeners(DropListWithOutsideClickTargetRef); /** * 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. */ // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/react/no-class-components 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", event => { 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(event); }); _defineProperty(this, "handleClose", event => { const { onOpenChange } = this.props; if (onOpenChange) { onOpenChange({ isOpen: false, event: event }); } }); } renderDropdownMenu() { const { target, popupPlacement } = this.state; const { items, mountTo, boundariesElement, scrollableElement, offset, fitHeight, fitWidth, isOpen, zIndex, shouldUseDefaultRole, onItemActivated, arrowKeyNavigationProviderOptions, section, allowEnterDefaultBehavior, handleEscapeKeydown } = 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 // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , _extends({}, navigationProviderProps, { handleClose: this.handleCloseAndFocus, closeOnTab: true }), jsx(DropListWithOutsideListeners, { isOpen: true, position: popupPlacement.join(' '), shouldFitContainer: true, handleClickOutside: this.handleClose, handleEscapeKeydown: handleEscapeKeydown || this.handleCloseAndFocus, handleEnterKeydown: e => { if (!allowEnterDefaultBehavior) { e.preventDefault(); } e.stopPropagation(); }, targetRef: this.state.target }, jsx("div", { style: { height: 0, minWidth: fitWidth || 0 } }), jsx("div", { ref: this.popupRef }, 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 // Ignored via go/ees005 // eslint-disable-next-line react/no-array-index-key , 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 }); })))))))); } render() { const { children, isOpen } = this.props; return jsx("div", { css: wrapper }, jsx("div", { ref: this.handleRef }, children), isOpen ? this.renderDropdownMenu() : null); } componentDidUpdate(previousProps) { const { mountTo, isOpen } = this.props; const isOpenToggled = isOpen !== previousProps.isOpen; if (isOpen && isOpenToggled) { if (typeof this.props.shouldFocusFirstItem === 'function' && this.props.shouldFocusFirstItem()) { var _this$state$target2; const keyboardEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); if (mountTo) { mountTo.dispatchEvent(keyboardEvent); return; } (_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 // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading }, 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. // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 position: 'static' } }), children); }); export function DropdownMenuItem({ item, onItemActivated, shouldUseDefaultRole, onMouseEnter, onMouseLeave }) { var _item$key2; 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(Boolean(event.target instanceof HTMLElement && event.target.closest(`.${DropdownMenuSharedCssClassName.SUBMENU}`))); }; const ariaLabel = item['aria-label'] === '' ? undefined : item['aria-label'] || String(item.content); const testId = item['data-testid'] || `dropdown-item__${item.content}`; // From time to time we don't want to have any tabIndex on item wrapper // especially when we pass any interactive element as a item.content const tabIndex = item.wrapperTabIndex === null ? undefined : item.wrapperTabIndex || -1; const dropListItem = // eslint-disable-next-line jsx-a11y/no-static-element-interactions jsx("div", { css: () => buttonStyles(item.isActive, submenuActive), tabIndex: tabIndex, "aria-disabled": item.isDisabled ? 'true' : 'false', onMouseDown: _handleSubmenuActive }, jsx(CustomItem, { item: item, key: (_item$key2 = item.key) !== null && _item$key2 !== void 0 ? _item$key2 : String(item.content), testId: testId, role: shouldUseDefaultRole ? 'button' : 'menuitem', iconBefore: item.elemBefore, iconAfter: item.elemAfter, isDisabled: item.isDisabled, onClick: () => onItemActivated && onItemActivated({ item }), "aria-label": ariaLabel, "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 }), "aria-expanded": item['aria-expanded'] }, item.content)); if (item.tooltipDescription) { var _item$key3; return jsx(Tooltip, { key: (_item$key3 = item.key) !== null && _item$key3 !== void 0 ? _item$key3 : String(item.content), content: item.tooltipDescription, position: item.tooltipPosition }, dropListItem); } return dropListItem; } export const DropdownMenuWithKeyboardNavigation = /*#__PURE__*/React.memo( // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any ({ ...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 } // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading }, props)); });