@atlaskit/editor-plugin-floating-toolbar
Version:
Floating toolbar plugin for @atlaskit/editor-core
143 lines (142 loc) • 5.97 kB
JavaScript
import React, { useEffect, useState } from 'react';
import { bind } from 'bind-event-listener';
import rafSchedule from 'raf-schd';
import { IconButton } from '@atlaskit/button/new';
import { messages } from '@atlaskit/editor-common/floating-toolbar';
import ChevronLeftLargeIcon from '@atlaskit/icon/core/chevron-left';
import ChevronRightLargeIcon from '@atlaskit/icon/core/chevron-right';
// eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss
import { Box, xcss } from '@atlaskit/primitives';
const rightSideStyles = xcss({
borderLeft: `solid ${"var(--ds-border, #0B120E24)"} 1px`,
right: 'space.0',
borderTopRightRadius: 'radius.small',
borderBottomRightRadius: 'radius.small'
});
const leftSideStyles = xcss({
borderRight: `solid ${"var(--ds-border, #0B120E24)"} 1px`,
left: 'space.0',
borderTopLeftRadius: 'radius.small',
borderBottomLeftRadius: 'radius.small'
});
const buttonCommonStyles = xcss({
backgroundColor: 'elevation.surface.overlay',
zIndex: '1',
position: 'absolute'
});
export const ScrollButton = ({
intl,
scrollContainerRef,
node,
disabled,
side
}) => {
const [needScroll, setNeedScroll] = useState(false);
const [canScrollToSide, setCanScrollToSide] = useState(true);
const setCanScrollDebounced = rafSchedule(() => {
// Refs are null before mounting and after unmount
if (!scrollContainerRef.current) {
return;
}
const {
scrollLeft,
scrollWidth,
offsetWidth
} = scrollContainerRef.current;
setCanScrollToSide(
// -1 to account for pixel rounding error
side === 'left' ? scrollLeft > 0 : scrollLeft < scrollWidth - offsetWidth - 1);
});
const onScroll = () => {
setCanScrollDebounced();
};
const onClick = () => {
var _scrollContainerRef$c, _scrollContainerRef$c2, _scrollContainerRef$c3;
const {
width: scrollContainerWidth = 0
} = ((_scrollContainerRef$c = scrollContainerRef.current) === null || _scrollContainerRef$c === void 0 ? void 0 : _scrollContainerRef$c.getBoundingClientRect()) || {};
const scrollLeft = ((_scrollContainerRef$c2 = scrollContainerRef.current) === null || _scrollContainerRef$c2 === void 0 ? void 0 : _scrollContainerRef$c2.scrollLeft) || 0;
const scrollTo = side === 'left' ? scrollLeft - scrollContainerWidth : scrollLeft + scrollContainerWidth;
(_scrollContainerRef$c3 = scrollContainerRef.current) === null || _scrollContainerRef$c3 === void 0 ? void 0 : _scrollContainerRef$c3.scrollTo({
top: 0,
left: scrollTo,
behavior: 'smooth'
});
};
const resizeObserver = new ResizeObserver(_t => {
var _scrollContainerRef$c4, _scrollContainerRef$c5;
const widthNeededToShowAllItems = ((_scrollContainerRef$c4 = scrollContainerRef.current) === null || _scrollContainerRef$c4 === void 0 ? void 0 : _scrollContainerRef$c4.scrollWidth) || 0;
const parentNode = (_scrollContainerRef$c5 = scrollContainerRef.current) === null || _scrollContainerRef$c5 === void 0 ? void 0 : _scrollContainerRef$c5.parentNode;
let availableSpace = -1;
if (parentNode instanceof HTMLElement) {
availableSpace = parentNode.offsetWidth;
}
if (availableSpace === -1) {
return;
}
if (availableSpace >= widthNeededToShowAllItems) {
setNeedScroll(false);
} else {
setNeedScroll(true);
onScroll();
}
});
useEffect(() => {
onScroll();
const scrollContainerRefCurrent = scrollContainerRef.current;
let unbind;
if (scrollContainerRefCurrent) {
// Adding/removing scroll button depending on scroll position
unbind = bind(scrollContainerRefCurrent, {
type: 'scroll',
listener: onScroll
});
// watch for toolbar resize and show/hide scroll buttons if needed
resizeObserver.observe(scrollContainerRefCurrent);
}
return () => {
if (scrollContainerRefCurrent) {
var _unbind;
(_unbind = unbind) === null || _unbind === void 0 ? void 0 : _unbind();
resizeObserver.unobserve(scrollContainerRefCurrent);
}
setCanScrollDebounced.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const scrollContainerRefCurrent = scrollContainerRef.current;
if (scrollContainerRefCurrent) {
var _scrollContainerRefCu;
// reset scroll position when switching from one node with toolbar to another
// scroll to made optional as it may not be rendered in testing env
(_scrollContainerRefCu = scrollContainerRefCurrent.scrollTo) === null || _scrollContainerRefCu === void 0 ? void 0 : _scrollContainerRefCu.call(scrollContainerRefCurrent, {
left: 0
});
}
}, [node.type, scrollContainerRef]);
const Icon = side === 'left' ? ChevronLeftLargeIcon : ChevronRightLargeIcon;
return needScroll && (side === 'left' && canScrollToSide || side === 'right' && canScrollToSide) && /*#__PURE__*/React.createElement(Box, {
padding: "space.050"
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
xcss: [side === 'left' ? leftSideStyles : rightSideStyles, buttonCommonStyles]
}, /*#__PURE__*/React.createElement(IconButton, {
appearance: "subtle",
label: intl.formatMessage(side === 'left' ? messages.floatingToolbarScrollLeft : messages.floatingToolbarScrollRight),
onClick: onClick,
isDisabled: disabled
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
icon: iconProps => /*#__PURE__*/React.createElement(Icon, {
label: iconProps.label,
size: "small"
}),
isTooltipDisabled: false
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
tooltip: {
position: 'top'
}
}));
};