@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
148 lines (145 loc) • 6.84 kB
JavaScript
import React, { useEffect, useMemo, useRef } from 'react';
import rafSchedule from 'raf-schd';
import { createPortal } from 'react-dom';
import { akEditorTableCellOnStickyHeaderZIndex } from '@atlaskit/editor-shared-styles';
import { TableCssClassName as ClassName } from '../../types';
import { insertColumnButtonOffset } from '../common-styles';
const BUTTON_WIDTH = 20;
export const calcLeftPos = ({
buttonWidth,
cellRectLeft,
cellRefWidth,
offset
}) => {
return cellRectLeft + cellRefWidth - buttonWidth - offset;
};
export const calcObserverTargetMargin = (tableWrapper, fixedButtonRefCurrent) => {
const tableWrapperRect = tableWrapper.getBoundingClientRect();
const fixedButtonRect = fixedButtonRefCurrent.getBoundingClientRect();
const scrollLeft = tableWrapper.scrollLeft;
return fixedButtonRect.left - tableWrapperRect.left + scrollLeft;
};
export const FixedButton = ({
children,
isContextualMenuOpen,
mountTo,
offset,
stickyHeader,
tableWrapper,
targetCellPosition,
targetCellRef
}) => {
const fixedButtonRef = useRef(null);
const observerTargetRef = useRef(null);
// Using refs here rather than state to prevent heaps of renders on scroll
const scrollDataRef = useRef(0);
const leftPosDataRef = useRef(0);
useEffect(() => {
const observerTargetRefCurrent = observerTargetRef.current;
const fixedButtonRefCurrent = fixedButtonRef.current;
if (fixedButtonRefCurrent && observerTargetRefCurrent) {
scrollDataRef.current = tableWrapper.scrollLeft;
leftPosDataRef.current = 0;
// Hide the button initially in case there's a flash of the button being
// outside the table before the Intersection Observer fires
fixedButtonRefCurrent.style.visibility = 'hidden';
const margin = calcObserverTargetMargin(tableWrapper, fixedButtonRefCurrent);
// Much more simple and predictable to add this margin to the observer target
// rather than using it to calculate the rootMargin values
observerTargetRefCurrent.style.marginLeft = `${margin}px`;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
fixedButtonRefCurrent.style.visibility = 'visible';
} else {
fixedButtonRefCurrent.style.visibility = 'hidden';
}
});
}, {
root: tableWrapper,
rootMargin: `0px ${insertColumnButtonOffset}px 0px 0px`,
threshold: 1
});
const handleScroll = rafSchedule(event => {
if (fixedButtonRef.current) {
const delta = event.target.scrollLeft - scrollDataRef.current;
const style = `translateX(${leftPosDataRef.current - delta}px)`;
fixedButtonRef.current.style.transform = style;
scrollDataRef.current = event.target.scrollLeft;
leftPosDataRef.current = leftPosDataRef.current - delta;
}
});
observer.observe(observerTargetRefCurrent);
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
tableWrapper.addEventListener('scroll', handleScroll);
return () => {
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
tableWrapper.removeEventListener('scroll', handleScroll);
fixedButtonRefCurrent.style.transform = '';
observer.unobserve(observerTargetRefCurrent);
};
}
}, [fixedButtonRef, observerTargetRef, tableWrapper, targetCellPosition, targetCellRef, isContextualMenuOpen]);
const fixedButtonTop = 0;
const containerLeft = useMemo(() => {
const container = targetCellRef.closest('[data-editor-container="true"]');
return (container === null || container === void 0 ? void 0 : container.getBoundingClientRect().left) || 0;
}, [targetCellRef]);
const left = useMemo(() => {
const targetCellRect = targetCellRef.getBoundingClientRect();
const baseLeft = calcLeftPos({
buttonWidth: BUTTON_WIDTH,
cellRectLeft: targetCellRect.left,
cellRefWidth: targetCellRef.clientWidth,
offset
});
return baseLeft - containerLeft;
}, [containerLeft, targetCellRef, offset]);
// Using a portal here to ensure wrapperRef has the tableWrapper as an
// ancestor. This is required to make the Intersection Observer work.
return /*#__PURE__*/createPortal(
/*#__PURE__*/
// Using observerTargetRef here for our Intersection Observer. There is issues
// getting the observer to work just using the fixedButtonRef, possible due
// to using position fixed on this Element, or possibly due to its position
// being changed on scroll.
React.createElement("div", {
ref: observerTargetRef,
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
position: 'absolute',
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
top: "var(--ds-space-0, 0px)",
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
left: "var(--ds-space-0, 0px)",
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
width: "var(--ds-space-250, 20px)",
// BUTTON_WIDTH
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
height: "var(--ds-space-250, 20px)" // BUTTON_WIDTH
}
}, /*#__PURE__*/React.createElement("div", {
ref: fixedButtonRef,
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
position: 'fixed',
// eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview
top: fixedButtonTop + stickyHeader.padding + offset * 2,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop, @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
zIndex: akEditorTableCellOnStickyHeaderZIndex,
// eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview
left,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
width: "var(--ds-space-250, 20px)",
// BUTTON_WIDTH
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
height: "var(--ds-space-250, 20px)" // BUTTON_WIDTH
}
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: ClassName.CONTEXTUAL_MENU_BUTTON_FIXED
}, children)), mountTo);
};
export default FixedButton;