UNPKG

@atlaskit/editor-common

Version:

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

430 lines (423 loc) • 16.5 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, useCallback, useContext, useMemo } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic import { css, jsx } from '@emotion/react'; import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles'; import { CustomItem, MenuGroup, Section } from '@atlaskit/menu'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; 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, #4688EC)"}`, outline: 'none' }); const buttonStyles = (isActive, submenuActive) => { if (isActive) { if (editorExperiment('platform_editor_controls', 'variant1')) { /** * 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` position: relative; &::before { display: block; height: 100%; width: 2px; position: absolute; left: 0; top: 0; background: ${"var(--ds-border-selected, #1868DB)"}; content: ''; } > span, > span:hover, > span:active { background: ${"var(--ds-background-selected, #E9F2FE)"}; color: ${"var(--ds-text-selected, #1868DB)"}; } :focus > span[aria-disabled='false'] { ${focusedMenuItemStyle}; } :focus-visible, :focus-visible > span[aria-disabled='false'] { outline: none; } `; } /** * 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, #E9F2FE)"}; color: ${"var(--ds-text-selected, #1868DB)"}; } :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, #292A2E)"}; background-color: ${"var(--ds-background-neutral-subtle-hovered, #0515240F)"}; } ${!submenuActive && ` > span:active[aria-disabled='false'] { background-color: ${"var(--ds-background-neutral-subtle-pressed, #0B120E24)"}; }`} > span[aria-disabled='true'] { color: ${"var(--ds-text-disabled, #080F214A)"}; } :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); // eslint-disable-next-line react/jsx-props-no-spreading -- Spreading props to pass through dynamic component props 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 }); } }); _defineProperty(this, "handleEnterKeydown", e => { if (!this.props.allowEnterDefaultBehavior) { e.preventDefault(); } e.stopPropagation(); }); } renderDropdownMenu() { const { target, popupPlacement } = this.state; const { items, mountTo, boundariesElement, scrollableElement, offset, fitHeight, fitWidth, isOpen, zIndex, shouldUseDefaultRole, onItemActivated, arrowKeyNavigationProviderOptions, section, 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 // eslint-disable-next-line react/jsx-props-no-spreading -- Spreading navigationProviderProps to pass through dynamic component props , _extends({}, navigationProviderProps, { handleClose: this.handleCloseAndFocus, closeOnTab: true }), jsx(DropListWithOutsideListeners, { isOpen: true, position: popupPlacement.join(' '), shouldFitContainer: true, handleClickOutside: this.handleClose, handleEscapeKeydown: handleEscapeKeydown || this.handleCloseAndFocus, handleEnterKeydown: this.handleEnterKeydown, 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 // eslint-disable-next-line react/jsx-props-no-spreading -- Spreading rest to pass through dynamic component props }, 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); const memoizedOnClick = useCallback(() => onItemActivated && onItemActivated({ item }), [onItemActivated, item]); const onClick = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedOnClick : () => onItemActivated && onItemActivated({ item }); const memoizedOnMouseDown = useCallback(e => { e.preventDefault(); }, []); const onMouseDown = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedOnMouseDown : e => { e.preventDefault(); }; const memoizedOnMouseEnter = useCallback(() => onMouseEnter && onMouseEnter({ item }), [onMouseEnter, item]); const onMouseEnterHandler = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedOnMouseEnter : () => onMouseEnter && onMouseEnter({ item }); const memoizedOnMouseLeave = useCallback(() => onMouseLeave && onMouseLeave({ item }), [onMouseLeave, item]); const onMouseLeaveHandler = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedOnMouseLeave : () => onMouseLeave && onMouseLeave({ item }); // 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 = jsx("div", { css: () => buttonStyles(item.isActive, submenuActive), role: expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? shouldUseDefaultRole ? undefined : 'menuitem' : undefined, tabIndex: tabIndex, "aria-disabled": item.isDisabled ? 'true' : 'false', "aria-expanded": expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? item['aria-expanded'] : undefined, 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' : expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? undefined : 'menuitem', iconBefore: item.elemBefore, iconAfter: item.elemAfter, isDisabled: item.isDisabled, onClick: onClick, "aria-label": ariaLabel, "aria-pressed": shouldUseDefaultRole ? item.isActive : undefined, "aria-keyshortcuts": item['aria-keyshortcuts'], onMouseDown: onMouseDown, component: DropdownMenuItemCustomComponent, onMouseEnter: onMouseEnterHandler, onMouseLeave: onMouseLeaveHandler, "aria-expanded": expValEquals('platform_editor_august_a11y', 'isEnabled', true) ? undefined : 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 const memoizedArrowKeyNavOptions = useMemo(() => ({ ...props.arrowKeyNavigationProviderOptions, keyDownHandlerContext }), [props.arrowKeyNavigationProviderOptions, keyDownHandlerContext]); const arrowKeyNavOptions = expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedArrowKeyNavOptions : { ...props.arrowKeyNavigationProviderOptions, keyDownHandlerContext }; return jsx(DropdownMenuWrapper, _extends({ arrowKeyNavigationProviderOptions: arrowKeyNavOptions // eslint-disable-next-line react/jsx-props-no-spreading -- Spreading props to pass through dynamic component props }, props)); });