@atlaskit/editor-plugin-floating-toolbar
Version:
Floating toolbar plugin for @atlaskit/editor-core
256 lines (250 loc) • 8.96 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/**
* @jsxRuntime classic
* @jsx jsx
* @jsxFrag
*/
import React, { Component } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { css, jsx } from '@emotion/react';
import { FloatingToolbarButton as Button } from '@atlaskit/editor-common/ui';
import { ArrowKeyNavigationType, DropdownContainer as UiDropdown } from '@atlaskit/editor-common/ui-menu';
import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
import { Divider } from './Divider';
import DropdownMenu, { itemSpacing, menuItemDimensions } from './DropdownMenu';
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
const dropdownExpandContainer = css({
margin: `0px ${"var(--ds-space-negative-050, -4px)"}`
});
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
const iconGroup = css({
display: 'flex'
});
const CompositeIcon = ({
icon
}) => jsx("div", {
css: iconGroup
}, icon, jsx("span", {
css: dropdownExpandContainer
}, jsx(ChevronDownIcon, {
color: "currentColor",
spacing: "spacious",
label: "Expand dropdown menu",
size: "small"
})));
// eslint-disable-next-line @repo/internal/react/no-class-components
export default class Dropdown extends Component {
constructor(...args) {
super(...args);
_defineProperty(this, "state", {
isOpen: false,
isOpenedByKeyboard: false
});
_defineProperty(this, "triggerRef", /*#__PURE__*/React.createRef());
_defineProperty(this, "makeArrayOptionsFromCallback", makeOptions => {
const options = makeOptions();
return options;
});
_defineProperty(this, "renderArrayOptions", options => {
const {
showSelected,
dispatchCommand,
editorView,
areAnyNewToolbarFlagsEnabled
} = this.props;
return jsx(DropdownMenu, {
hide: this.hide,
dispatchCommand: dispatchCommand,
items: options,
showSelected: showSelected,
editorView: editorView,
areAnyNewToolbarFlagsEnabled: areAnyNewToolbarFlagsEnabled
});
});
_defineProperty(this, "toggleOpen", () => {
const onClick = this.props.onClick;
if (onClick) {
onClick();
}
this.setState({
isOpen: !this.state.isOpen,
isOpenedByKeyboard: false
});
const onToggle = this.props.onToggle;
if (!onToggle) {
return;
}
requestAnimationFrame(() => {
this.props.dispatchCommand(onToggle);
});
});
_defineProperty(this, "toggleOpenByKeyboard", event => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.setState({
isOpen: !this.state.isOpen,
isOpenedByKeyboard: true
});
}
});
_defineProperty(this, "hide", () => {
this.setState({
...this.state,
isOpen: false
});
});
_defineProperty(this, "hideOnEsc", () => {
var _document$querySelect;
// Focus the trigger button only on Escape
// Focus is done before hiding to ensure onBlur is called
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
(_document$querySelect = document.querySelector(`[data-testid=${this.props.buttonTestId}]`)) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.focus();
this.hide();
});
_defineProperty(this, "onOpenChanged", openChangedEvent => {
if (!openChangedEvent.isOpen && openChangedEvent.event instanceof KeyboardEvent) {
var _openChangedEvent$eve;
((_openChangedEvent$eve = openChangedEvent.event) === null || _openChangedEvent$eve === void 0 ? void 0 : _openChangedEvent$eve.key) === 'Escape' ? this.hideOnEsc() : this.hide();
}
});
}
render() {
const {
isOpen
} = this.state;
const {
title,
icon,
iconBefore,
options,
dispatchCommand,
mountPoint,
boundariesElement,
scrollableElement,
hideExpandIcon,
disabled,
tooltip,
buttonTestId,
dropdownWidth,
dropdownListId,
alignDropdownWithToolbar,
footer,
onMount,
pulse,
shouldFitContainer,
alignX,
areAnyNewToolbarFlagsEnabled
} = this.props;
let trigger;
if (icon) {
const TriggerIcon = hideExpandIcon ? icon : jsx(CompositeIcon, {
icon: icon
});
trigger = jsx(Button, {
testId: buttonTestId,
title: title,
icon: TriggerIcon,
onClick: this.toggleOpen,
onKeyDown: this.toggleOpenByKeyboard,
selected: isOpen,
disabled: disabled,
tooltipContent: tooltip,
ariaHasPopup: areAnyNewToolbarFlagsEnabled ? true : undefined,
onMount: onMount,
pulse: pulse,
areAnyNewToolbarFlagsEnabled: areAnyNewToolbarFlagsEnabled
});
} else {
trigger = jsx(Button, {
testId: buttonTestId,
iconAfter: jsx("span", {
css: dropdownExpandContainer
}, jsx(ChevronDownIcon, {
color: "currentColor",
spacing: "spacious",
label: "Expand dropdown menu",
size: "small"
})),
icon: iconBefore,
onClick: this.toggleOpen,
onKeyDown: this.toggleOpenByKeyboard,
selected: isOpen,
disabled: disabled,
tooltipContent: tooltip,
ariaHasPopup: true,
areaControls: dropdownListId,
onMount: onMount,
pulse: pulse,
areAnyNewToolbarFlagsEnabled: areAnyNewToolbarFlagsEnabled
}, title);
}
/**
* We want to change direction of our dropdowns a bit early,
* not exactly when it hits the boundary.
*/
const fitTolerance = 10;
const fitWidth = Array.isArray(options) || typeof options === 'function' ? dropdownWidth || menuItemDimensions.width : options.width;
const fitHeight = Array.isArray(options) ? options.length * menuItemDimensions.height + itemSpacing * 2 : typeof options === 'function' ? this.makeArrayOptionsFromCallback(options).length : options.height;
return (
/**
* At the moment footer diver is rendered along with footer, if it's provided
* This is to provide some level of consistency
* Refer to the PR for more details:
* https://stash.atlassian.com/projects/ATLASSIAN/repos/atlassian-frontend-monorepo/pull-requests/137394/overview?commentId=8130003
*/
jsx(UiDropdown, {
alignX: alignX,
mountTo: mountPoint,
boundariesElement: boundariesElement,
scrollableElement: scrollableElement,
isOpen: isOpen,
handleClickOutside: this.hide,
handleEscapeKeydown: this.hideOnEsc,
onOpenChange: this.onOpenChanged,
fitWidth: fitWidth + fitTolerance,
fitHeight: fitHeight + fitTolerance,
trigger: trigger,
dropdownListId: dropdownListId,
alignDropdownWithParentElement: alignDropdownWithToolbar
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
arrowKeyNavigationProviderOptions: {
type: ArrowKeyNavigationType.MENU
},
shouldFitContainer: shouldFitContainer
}, Array.isArray(options) ? this.renderArrayOptions(options) : typeof options === 'function' ? this.renderArrayOptions(this.makeArrayOptionsFromCallback(options)) : options.render({
hide: this.hide,
dispatchCommand
}), footer && jsx(React.Fragment, null, jsx(Divider, null), footer))
);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.isOpen !== this.state.isOpen) {
if (this.props.setDisableParentScroll) {
this.props.setDisableParentScroll(this.state.isOpen);
}
// ECA11Y-235: no sense in sending keyboard event since the menu popup mounted to the custom element,
// we will ensure first element focused asap as 'MenuArrowKeyNavigationProvider' is mounted
if (this.props.mountPoint) {
return;
}
if (this.state.isOpen && this.state.isOpenedByKeyboard) {
const dropList = document.querySelector('[data-role="droplistContent"]');
if (dropList) {
// Add setTimeout so that if a dropdown item has tooltip,
// the tooltip won't be rendered until next render cycle
// when the droplist is correctly positioned.
// This makes tooltip appears at the correct position for the first dropdown item.
setTimeout(() => {
const keyboardEvent = new KeyboardEvent('keydown', {
bubbles: true,
key: 'ArrowDown'
});
dropList.dispatchEvent(keyboardEvent);
}, 0);
}
}
}
}
}