UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

148 lines (145 loc) 6.84 kB
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;