Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

import {useBooleanState} from 'src/react-utils';

import {FloatingPopup, type FloatingPopupProps} from '../FloatingPopup';
import {FloatingPopup, type FloatingPopupProps, type ReferenceType} from '../FloatingPopup';

const popupOffset: FloatingPopupProps['offset'] = {
mainAxis: -9.5,
Expand All @@ -21,7 +21,7 @@ const popupOffset: FloatingPopupProps['offset'] = {
export type FloatingMenuProps = {
dirtype: 'row' | 'column';
canDrag: boolean;
anchorElement: Element;
anchorElement: ReferenceType;
dropdownItems: DropdownMenuProps<unknown>['items'];
switcherMouseProps?: Pick<
ButtonButtonProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {useMemo} from 'react';

// eslint-disable-next-line import/no-extraneous-dependencies
import type {ClientRectObject, VirtualElement} from '@floating-ui/dom';
import {
ArrowDown,
ArrowLeft,
Expand All @@ -16,10 +18,13 @@ import {i18n} from 'src/i18n/yfm-table';
import type {DnDControlHandler} from '../../dnd/dnd';
import {FloatingMenu, type FloatingMenuProps} from '../FloatingMenu/FloatingMenu';

type ControlType = FloatingMenuProps['dirtype'];

export type FloatingMenuControlProps = {
acnhorElement: Element;
cellElement: Element;
tableElement: Element;
multiple: boolean;
type: FloatingMenuProps['dirtype'];
type: ControlType;
dndHandler?: DnDControlHandler;
onMenuOpenToggle: FloatingMenuProps['onOpenToggle'];
onClearCellsClick: () => void;
Expand All @@ -34,7 +39,8 @@ export const FloatingMenuControl: React.FC<FloatingMenuControlProps> =
type,
multiple,
dndHandler,
acnhorElement,
cellElement,
tableElement,
onMenuOpenToggle,
onClearCellsClick,
onInsertBeforeClick,
Expand Down Expand Up @@ -94,12 +100,17 @@ export const FloatingMenuControl: React.FC<FloatingMenuControlProps> =
],
);

const anchor = useMemo(
() => getVirtualAnchor(type, tableElement, cellElement),
[type, tableElement, cellElement],
);

return (
<FloatingMenu
dirtype={type}
canDrag={dndHandler ? dndHandler.canDrag() : false}
onOpenToggle={onMenuOpenToggle}
anchorElement={acnhorElement}
anchorElement={anchor}
switcherMouseProps={
dndHandler
? {
Expand All @@ -114,3 +125,73 @@ export const FloatingMenuControl: React.FC<FloatingMenuControlProps> =
/>
);
};

function getVirtualAnchor(
type: ControlType,
tableElem: Element,
cellElem: Element,
): VirtualElement {
if (type === 'row') {
return {
contextElement: cellElem,
getBoundingClientRect() {
const cellRect = cellElem.getBoundingClientRect();
const tableRect: ClientRectObject = tableElem.getBoundingClientRect().toJSON();

{
// fix table rect
tableRect.x += 1;
tableRect.width -= 2;
tableRect.left += 1;
tableRect.right -= 1;
}

return {
// from table
x: tableRect.x,
width: tableRect.width,
left: tableRect.left,
right: tableRect.right,
// from cell
y: cellRect.y,
height: cellRect.height,
top: cellRect.top,
bottom: cellRect.top,
};
},
};
}

if (type === 'column') {
return {
contextElement: cellElem,
getBoundingClientRect() {
const cellRect: ClientRectObject = cellElem.getBoundingClientRect().toJSON();
const tableRect = tableElem.getBoundingClientRect();

const EDGE_OFFSET = 16;

const cellMiddle = cellRect.x + cellRect.width / 2;

// left border of table
if (cellMiddle - EDGE_OFFSET <= tableRect.left) {
const visible = cellRect.right - tableRect.left;
cellRect.width = (visible - EDGE_OFFSET) * 2;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential issue when the cell is barely visible: visible - EDGE_OFFSET can become negative, leading to negative width values.

I suggest clamping the adjusted value

    const visible = cellRect.right - tableRect.left;
    const adjusted = Math.max(0, visible - EDGE_OFFSET);
    cellRect.width = adjusted * 2;

cellRect.left = cellRect.right - cellRect.width;
cellRect.x = cellRect.left;
}

// right border of table
if (cellMiddle + EDGE_OFFSET >= tableRect.right) {
const visible = tableRect.right - cellRect.left;
cellRect.width = (visible - EDGE_OFFSET) * 2;
cellRect.right = cellRect.left + cellRect.width;
}

return cellRect;
},
};
}

throw new Error(`Unknown control type: ${type}`);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useState} from 'react';
import {forwardRef, useState} from 'react';

import {FloatingPopup, type FloatingPopupProps} from '../FloatingPopup';
import {FloatingPopup, type FloatingPopupProps, type FloatingPopupRef} from '../FloatingPopup';

import {InsertCursor, type InsertCursorProps} from './InsertCursor';
import {PlusButton, type PlusButtonProps} from './PlusButton';
Expand All @@ -18,18 +18,23 @@ const offsetByType: Record<InsertCursorProps['type'], FloatingPopupProps['offset
column: {alignmentAxis: -10},
};

export type FloatingPlusButtonProps = Pick<PlusButtonProps, 'onClick'> &
export type FloatingPlusButtonRef = FloatingPopupRef & {};

export type FloatingPlusButtonProps = Pick<FloatingPopupProps, 'floatingStyles'> &
Pick<PlusButtonProps, 'onClick'> &
Pick<InsertCursorProps, 'type' | 'anchor'>;

export const FloatingPlusButton: React.FC<FloatingPlusButtonProps> =
function YfmTableFloatingPlusButton({anchor, type, ...btnProps}) {
export const FloatingPlusButton = forwardRef<FloatingPlusButtonRef, FloatingPlusButtonProps>(
function YfmTableFloatingPlusButton({anchor, type, floatingStyles, ...btnProps}, ref) {
const [hovered, setHovered] = useState(false);

return (
<>
<FloatingPopup
open
ref={ref}
anchorElement={anchor}
floatingStyles={floatingStyles}
placement={placementByType[type]}
offset={offsetByType[type]}
style={styles}
Expand All @@ -43,4 +48,5 @@ export const FloatingPlusButton: React.FC<FloatingPlusButtonProps> =
{hovered && <InsertCursor anchor={anchor} type={type} />}
</>
);
};
},
);
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import {memo, useMemo} from 'react';
import {memo, useEffect, useMemo, useRef, useState} from 'react';

// eslint-disable-next-line import/no-extraneous-dependencies
import type {VirtualElement} from '@floating-ui/dom';
import {useEffectOnce} from 'react-use';

import {FloatingPlusButton, type FloatingPlusButtonProps} from '../FloatingPlusButton';
import {useRafThrottle} from '../../hooks/use-raf-throttle';
import {
FloatingPlusButton,
type FloatingPlusButtonProps,
type FloatingPlusButtonRef,
} from '../FloatingPlusButton';

type ControlType = FloatingPlusButtonProps['type'];

export type FloatingPlusControlProps = {
type: FloatingPlusButtonProps['type'];
type: ControlType;
index: number;
cellElem: Element;
tableElem: Element;
Expand Down Expand Up @@ -48,6 +56,57 @@ export const FloatingPlusControl = memo<FloatingPlusControlProps>(
[cellElem, tableElem, type],
);

return <FloatingPlusButton anchor={anchor} type={type} onClick={() => onClick(index)} />;
const [visible, setVisible] = useState(() => shouldBeVisible(type, cellElem, tableElem));
const buttonRef = useRef<FloatingPlusButtonRef>(null);

const updateVisibility = () => {
const newVisible = shouldBeVisible(type, cellElem, tableElem);
if (visible !== newVisible) setVisible(newVisible);
};

const onChange = useRafThrottle(() => {
buttonRef.current?.forceUpdate();
updateVisibility();
});

// Update after first render
useEffectOnce(updateVisibility);

useEffect(() => {
if (type !== 'column') return undefined;

const observer = new ResizeObserver(onChange);
observer.observe(tableElem);
tableElem.addEventListener('scroll', onChange);

return () => {
observer.unobserve(tableElem);
tableElem.removeEventListener('scroll', onChange);
};
}, [tableElem, onChange, type]);

return (
<FloatingPlusButton
ref={buttonRef}
anchor={anchor}
type={type}
onClick={() => onClick(index)}
floatingStyles={visible ? undefined : {display: 'none'}}
/>
);
},
);

function shouldBeVisible(type: ControlType, cellElem: Element, tableElem: Element): boolean {
if (type !== 'column') return true;

const THRESHOLD = 4; // px

const cellRect = cellElem.getBoundingClientRect();
const tableRect = tableElem.getBoundingClientRect();

return (
tableRect.left - cellRect.right <= THRESHOLD &&
cellRect.right - tableRect.right <= THRESHOLD
);
}
Loading
Loading