@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
430 lines (423 loc) • 16.5 kB
JavaScript
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));
});