Skip to content

Commit 7bf4b75

Browse files
committed
fix(YfmTable): fixed positioning of floating controls when scrolling horizontally in table
1 parent 7bdb5c3 commit 7bf4b75

File tree

11 files changed

+293
-113
lines changed

11 files changed

+293
-113
lines changed

src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenu/FloatingMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const popupOffset: FloatingPopupProps['offset'] = {
2121
export type FloatingMenuProps = {
2222
dirtype: 'row' | 'column';
2323
canDrag: boolean;
24-
anchorElement: Element;
24+
anchorElement: NonNullable<FloatingPopupProps['anchorElement']>;
2525
dropdownItems: DropdownMenuProps<unknown>['items'];
2626
switcherMouseProps?: Pick<
2727
ButtonButtonProps,

src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingMenuControl/FloatingMenuControl.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {useMemo} from 'react';
22

3+
// eslint-disable-next-line import/no-extraneous-dependencies
4+
import type {ReferenceElement} from '@floating-ui/dom';
35
import {
46
ArrowDown,
57
ArrowLeft,
@@ -17,7 +19,7 @@ import type {DnDControlHandler} from '../../dnd/dnd';
1719
import {FloatingMenu, type FloatingMenuProps} from '../FloatingMenu/FloatingMenu';
1820

1921
export type FloatingMenuControlProps = {
20-
acnhorElement: Element;
22+
anchorElement: ReferenceElement;
2123
multiple: boolean;
2224
type: FloatingMenuProps['dirtype'];
2325
dndHandler?: DnDControlHandler;
@@ -34,7 +36,7 @@ export const FloatingMenuControl: React.FC<FloatingMenuControlProps> =
3436
type,
3537
multiple,
3638
dndHandler,
37-
acnhorElement,
39+
anchorElement,
3840
onMenuOpenToggle,
3941
onClearCellsClick,
4042
onInsertBeforeClick,
@@ -99,7 +101,7 @@ export const FloatingMenuControl: React.FC<FloatingMenuControlProps> =
99101
dirtype={type}
100102
canDrag={dndHandler ? dndHandler.canDrag() : false}
101103
onOpenToggle={onMenuOpenToggle}
102-
anchorElement={acnhorElement}
104+
anchorElement={anchorElement}
103105
switcherMouseProps={
104106
dndHandler
105107
? {

src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingPlusButton/FloatingPlusButton.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {useState} from 'react';
1+
import {forwardRef, useState} from 'react';
22

3-
import {FloatingPopup, type FloatingPopupProps} from '../FloatingPopup';
3+
import {FloatingPopup, type FloatingPopupProps, type FloatingPopupRef} from '../FloatingPopup';
44

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

21-
export type FloatingPlusButtonProps = Pick<PlusButtonProps, 'onClick'> &
21+
export type FloatingPlusButtonRef = FloatingPopupRef & {};
22+
23+
export type FloatingPlusButtonProps = Pick<FloatingPopupProps, 'floatingStyles'> &
24+
Pick<PlusButtonProps, 'onClick'> &
2225
Pick<InsertCursorProps, 'type' | 'anchor'>;
2326

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

2831
return (
2932
<>
3033
<FloatingPopup
3134
open
35+
ref={ref}
3236
anchorElement={anchor}
37+
floatingStyles={floatingStyles}
3338
placement={placementByType[type]}
3439
offset={offsetByType[type]}
3540
style={styles}
@@ -43,4 +48,5 @@ export const FloatingPlusButton: React.FC<FloatingPlusButtonProps> =
4348
{hovered && <InsertCursor anchor={anchor} type={type} />}
4449
</>
4550
);
46-
};
51+
},
52+
);

src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingPlusControl/FloatingPlusControl.tsx

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import {memo, useMemo} from 'react';
1+
import {memo, useEffect, useMemo, useRef, useState} from 'react';
22

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

6-
import {FloatingPlusButton, type FloatingPlusButtonProps} from '../FloatingPlusButton';
7+
import {
8+
FloatingPlusButton,
9+
type FloatingPlusButtonProps,
10+
type FloatingPlusButtonRef,
11+
} from '../FloatingPlusButton';
12+
13+
import {useRafThrottle} from './use-raf-throttle';
14+
15+
type ControlType = FloatingPlusButtonProps['type'];
716

817
export type FloatingPlusControlProps = {
9-
type: FloatingPlusButtonProps['type'];
18+
type: ControlType;
1019
index: number;
1120
cellElem: Element;
1221
tableElem: Element;
@@ -48,6 +57,56 @@ export const FloatingPlusControl = memo<FloatingPlusControlProps>(
4857
[cellElem, tableElem, type],
4958
);
5059

51-
return <FloatingPlusButton anchor={anchor} type={type} onClick={() => onClick(index)} />;
60+
const [visible, setVisible] = useState(() => shouldBeVisible(type, cellElem, tableElem));
61+
const buttonRef = useRef<FloatingPlusButtonRef>(null);
62+
63+
const updateVisibility = () => {
64+
const newVisible = shouldBeVisible(type, cellElem, tableElem);
65+
if (visible !== newVisible) setVisible(newVisible);
66+
};
67+
68+
const onChange = useRafThrottle(() => {
69+
buttonRef.current?.forceUpdate();
70+
updateVisibility();
71+
});
72+
73+
// Update after first render
74+
useEffectOnce(updateVisibility);
75+
76+
useEffect(() => {
77+
if (type !== 'column') return undefined;
78+
79+
tableElem.addEventListener('scroll', onChange);
80+
window.addEventListener('resize', onChange);
81+
82+
return () => {
83+
tableElem.removeEventListener('scroll', onChange);
84+
window.removeEventListener('resize', onChange);
85+
};
86+
}, [tableElem, onChange, type]);
87+
88+
return (
89+
<FloatingPlusButton
90+
ref={buttonRef}
91+
anchor={anchor}
92+
type={type}
93+
onClick={() => onClick(index)}
94+
floatingStyles={visible ? undefined : {display: 'none'}}
95+
/>
96+
);
5297
},
5398
);
99+
100+
function shouldBeVisible(type: ControlType, cellElem: Element, tableElem: Element): boolean {
101+
if (type !== 'column') return true;
102+
103+
const THRESHOLD = 4; // px
104+
105+
const cellRect = cellElem.getBoundingClientRect();
106+
const tableRect = tableElem.getBoundingClientRect();
107+
108+
return (
109+
tableRect.left - cellRect.right <= THRESHOLD &&
110+
cellRect.right - tableRect.right <= THRESHOLD
111+
);
112+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {useCallback, useEffect, useRef} from 'react';
2+
3+
import {useLatest} from 'react-use';
4+
5+
export function useRafThrottle(fn: () => void): () => void {
6+
const refFn = useLatest(fn);
7+
const refHandle = useRef<number | null>(null);
8+
9+
useEffect(() => {
10+
return () => {
11+
if (refHandle.current !== null) {
12+
cancelAnimationFrame(refHandle.current);
13+
}
14+
};
15+
}, [refFn]);
16+
17+
return useCallback(() => {
18+
if (refHandle.current === null) {
19+
refHandle.current = requestAnimationFrame(() => {
20+
refHandle.current = null;
21+
refFn.current();
22+
});
23+
}
24+
}, [refFn]);
25+
}

src/extensions/yfm/YfmTable/plugins/YfmTableControls/components/FloatingPopup/FloatingPopup.tsx

Lines changed: 87 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// This fork does not use floating focus manager
44
///
55

6-
import {useEffect, useMemo} from 'react';
6+
import {forwardRef, useEffect, useImperativeHandle, useMemo} from 'react';
77

88
// eslint-disable-next-line import/no-extraneous-dependencies
99
import {
@@ -27,6 +27,10 @@ import './FloatingPopup.scss';
2727

2828
const b = cn('yfm-table-floating-popup');
2929

30+
export type FloatingPopupRef = {
31+
forceUpdate: () => void;
32+
};
33+
3034
export type FloatingPopupProps = QAProps &
3135
DOMProps & {
3236
open?: boolean;
@@ -38,87 +42,88 @@ export type FloatingPopupProps = QAProps &
3842
floatingStyles?: React.CSSProperties;
3943
};
4044

41-
export const FloatingPopup: React.FC<FloatingPopupProps> = function YfmTableFloatingPopup(
42-
props: FloatingPopupProps,
43-
) {
44-
const {
45-
anchorElement,
46-
zIndex = 1000,
47-
style,
48-
className,
49-
qa,
50-
children,
51-
floatingStyles: floatingStylesFromProps,
52-
} = props;
53-
54-
const nodeId = useFloatingNodeId();
55-
const parentId = useFloatingParentNodeId();
56-
57-
const middleware = useMemo(() => [offsetMiddleware(props.offset)], [props.offset]);
58-
59-
const {
60-
refs,
61-
elements,
62-
floatingStyles,
63-
placement,
64-
context: {open},
65-
update,
66-
} = useFloating({
67-
nodeId,
68-
open: props.open,
69-
placement: props.placement,
70-
middleware,
71-
});
72-
73-
const {getFloatingProps} = useInteractions();
74-
75-
useEffect(() => {
76-
if (anchorElement !== undefined && anchorElement !== refs.reference.current) {
77-
refs.setReference(anchorElement);
78-
}
79-
}, [anchorElement, refs]);
80-
81-
useEffect(() => {
82-
if (elements.reference && elements.floating) {
83-
return autoUpdate(elements.reference, elements.floating, update);
45+
export const FloatingPopup = forwardRef<FloatingPopupRef, FloatingPopupProps>(
46+
function YfmTableFloatingPopup(props, ref) {
47+
const {
48+
anchorElement,
49+
zIndex = 1000,
50+
style,
51+
className,
52+
qa,
53+
children,
54+
floatingStyles: floatingStylesFromProps,
55+
} = props;
56+
57+
const nodeId = useFloatingNodeId();
58+
const parentId = useFloatingParentNodeId();
59+
60+
const middleware = useMemo(() => [offsetMiddleware(props.offset)], [props.offset]);
61+
62+
const {
63+
refs,
64+
floatingStyles,
65+
placement,
66+
context: {open},
67+
update,
68+
} = useFloating({
69+
nodeId,
70+
open: props.open,
71+
placement: props.placement,
72+
middleware,
73+
whileElementsMounted: autoUpdate,
74+
});
75+
76+
useImperativeHandle<FloatingPopupRef, FloatingPopupRef>(
77+
ref,
78+
() => ({
79+
forceUpdate: update,
80+
}),
81+
[update],
82+
);
83+
84+
const {getFloatingProps} = useInteractions();
85+
86+
useEffect(() => {
87+
if (anchorElement !== undefined && anchorElement !== refs.reference.current) {
88+
refs.setReference(anchorElement);
89+
}
90+
}, [anchorElement, refs]);
91+
92+
function wrapper(node: JSX.Element) {
93+
if (parentId === null) {
94+
return <FloatingTree>{node}</FloatingTree>;
95+
}
96+
97+
return node;
8498
}
85-
return undefined;
86-
}, [elements, update]);
8799

88-
function wrapper(node: JSX.Element) {
89-
if (parentId === null) {
90-
return <FloatingTree>{node}</FloatingTree>;
91-
}
92-
93-
return node;
94-
}
95-
96-
return wrapper(
97-
<FloatingNode id={nodeId}>
98-
{open ? (
99-
<Portal>
100-
<div
101-
ref={refs.setFloating}
102-
style={{
103-
position: 'absolute',
104-
top: 0,
105-
left: 0,
106-
zIndex,
107-
width: 'max-content',
108-
pointerEvents: 'auto',
109-
outline: 'none',
110-
...floatingStylesFromProps,
111-
...floatingStyles,
112-
}}
113-
data-floating-ui-placement={placement}
114-
{...getFloatingProps()}
115-
>
116-
<div className={b(null, className)} style={style} data-qa={qa}>
117-
{children}
100+
return wrapper(
101+
<FloatingNode id={nodeId}>
102+
{open ? (
103+
<Portal>
104+
<div
105+
ref={refs.setFloating}
106+
style={{
107+
position: 'absolute',
108+
top: 0,
109+
left: 0,
110+
zIndex,
111+
width: 'max-content',
112+
pointerEvents: 'auto',
113+
outline: 'none',
114+
...floatingStylesFromProps,
115+
...floatingStyles,
116+
}}
117+
data-floating-ui-placement={placement}
118+
{...getFloatingProps()}
119+
>
120+
<div className={b(null, className)} style={style} data-qa={qa}>
121+
{children}
122+
</div>
118123
</div>
119-
</div>
120-
</Portal>
121-
) : null}
122-
</FloatingNode>,
123-
);
124-
};
124+
</Portal>
125+
) : null}
126+
</FloatingNode>,
127+
);
128+
},
129+
);

0 commit comments

Comments
 (0)