UNPKG

@atlaskit/editor-plugin-floating-toolbar

Version:

Floating toolbar plugin for @atlaskit/editor-core

256 lines (250 loc) 8.96 kB
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); } } } } }