mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(app): show the format toolbar on double-click selections (#2566)
This commit is contained in:
1 parent
977b6767cd
commit
10ba2ca896
4 files changed
+78
-7
No files matched your search
@@ -0,0 +1,56 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import {useAntiShiftFloating} from '@app/features/app/hooks/useAntiShiftFloating';
|
||||||
|
import {act, createElement, useLayoutEffect, useState} from 'react';
|
||||||
|
import {createRoot, type Root} from 'react-dom/client';
|
||||||
|
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
||||||
|
|
||||||
|
let host: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
let target: HTMLDivElement;
|
||||||
|
|
||||||
|
function PortalLikeFloating({onReady}: {onReady: (isReady: boolean) => void}) {
|
||||||
|
const {setFloating, state} = useAntiShiftFloating(target, true, {placement: 'top'});
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
onReady(state.isReady);
|
||||||
|
return mounted ? createElement('div', {ref: setFloating, 'data-testid': 'floating'}) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settle(): Promise<void> {
|
||||||
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(() => resolve(null)));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
host = document.createElement('div');
|
||||||
|
target = document.createElement('div');
|
||||||
|
document.body.append(host, target);
|
||||||
|
root = createRoot(host);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
act(() => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
document.body.replaceChildren();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useAntiShiftFloating', () => {
|
||||||
|
it('positions a floating element that mounts after the first commit', async () => {
|
||||||
|
const readyStates: Array<boolean> = [];
|
||||||
|
act(() => {
|
||||||
|
root.render(createElement(PortalLikeFloating, {onReady: (isReady) => readyStates.push(isReady)}));
|
||||||
|
});
|
||||||
|
await settle();
|
||||||
|
expect(readyStates.at(-1)).toBe(true);
|
||||||
|
expect(host.querySelector<HTMLElement>('[data-testid="floating"]')?.style.visibility).toBe('visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -279,6 +279,11 @@ export function useAntiShiftFloating(
|
|||||||
constrainHeight = false,
|
constrainHeight = false,
|
||||||
} = options;
|
} = options;
|
||||||
const floatingRef = useRef<HTMLElement>(null);
|
const floatingRef = useRef<HTMLElement>(null);
|
||||||
|
const [floatingElement, setFloatingElement] = useState<HTMLElement | null>(null);
|
||||||
|
const setFloating = useCallback((element: HTMLElement | null) => {
|
||||||
|
floatingRef.current = element;
|
||||||
|
setFloatingElement(element);
|
||||||
|
}, []);
|
||||||
const [state, setState] = useState<FloatingState>(() => {
|
const [state, setState] = useState<FloatingState>(() => {
|
||||||
const {x, y} = target ? getInitialGuess(target, placement, offsetMainAxis, offsetCrossAxis) : {x: -9999, y: -9999};
|
const {x, y} = target ? getInitialGuess(target, placement, offsetMainAxis, offsetCrossAxis) : {x: -9999, y: -9999};
|
||||||
return {x, y, isReady: false, offsetX: 0, offsetY: 0, placement};
|
return {x, y, isReady: false, offsetX: 0, offsetY: 0, placement};
|
||||||
@@ -481,7 +486,7 @@ export function useAntiShiftFloating(
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!enabled || target == null || floatingRef.current == null) {
|
if (!enabled || target == null || floatingElement == null) {
|
||||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -489,7 +494,7 @@ export function useAntiShiftFloating(
|
|||||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const floating = floatingRef.current;
|
const floating = floatingElement;
|
||||||
updatePosition();
|
updatePosition();
|
||||||
if (shouldAutoUpdate) {
|
if (shouldAutoUpdate) {
|
||||||
const cleanupCallbacks = [
|
const cleanupCallbacks = [
|
||||||
@@ -502,7 +507,7 @@ export function useAntiShiftFloating(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if (shouldObserveFloatingResize) {
|
} else if (shouldObserveFloatingResize) {
|
||||||
cleanupRef.current = observeFloatingResize(floatingRef.current, updatePosition);
|
cleanupRef.current = observeFloatingResize(floating, updatePosition);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
positionRevisionRef.current += 1;
|
positionRevisionRef.current += 1;
|
||||||
@@ -519,9 +524,18 @@ export function useAntiShiftFloating(
|
|||||||
}
|
}
|
||||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||||
};
|
};
|
||||||
}, [enabled, target, shouldAutoUpdate, shouldObserveFloatingResize, updatePosition, updatePositionNow]);
|
}, [
|
||||||
|
enabled,
|
||||||
|
target,
|
||||||
|
floatingElement,
|
||||||
|
shouldAutoUpdate,
|
||||||
|
shouldObserveFloatingResize,
|
||||||
|
updatePosition,
|
||||||
|
updatePositionNow,
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
ref: floatingRef,
|
ref: floatingRef,
|
||||||
|
setFloating,
|
||||||
state,
|
state,
|
||||||
style: {
|
style: {
|
||||||
position: 'fixed' as const,
|
position: 'fixed' as const,
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ function SelectionToolbarSurface({
|
|||||||
}),
|
}),
|
||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
const {ref, state, style, updatePosition} = useAntiShiftFloating(virtualReference, true, {
|
const {setFloating, state, style, updatePosition} = useAntiShiftFloating(virtualReference, true, {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
offsetMainAxis: 8,
|
offsetMainAxis: 8,
|
||||||
shouldAutoUpdate: true,
|
shouldAutoUpdate: true,
|
||||||
@@ -381,7 +381,7 @@ function SelectionToolbarSurface({
|
|||||||
>
|
>
|
||||||
<flx-lexical-selection-formatting-toolbar
|
<flx-lexical-selection-formatting-toolbar
|
||||||
ref={(element) => {
|
ref={(element) => {
|
||||||
(ref as React.MutableRefObject<HTMLElement | null>).current = element;
|
setFloating(element);
|
||||||
toolbarRef.current = element;
|
toolbarRef.current = element;
|
||||||
}}
|
}}
|
||||||
className={flxElementClassName(styles.toolbar)}
|
className={flxElementClassName(styles.toolbar)}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
|
|||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
ref: popoutRef,
|
ref: popoutRef,
|
||||||
|
setFloating,
|
||||||
state,
|
state,
|
||||||
style,
|
style,
|
||||||
beginManualPositioning,
|
beginManualPositioning,
|
||||||
@@ -206,7 +207,7 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
focusRefs.setReference(target);
|
focusRefs.setReference(target);
|
||||||
}, [focusRefs, target]);
|
}, [focusRefs, target]);
|
||||||
const mergedPopoutRef = useMergeRefs([popoutRef, focusRefs.setFloating]);
|
const mergedPopoutRef = useMergeRefs([setFloating, focusRefs.setFloating]);
|
||||||
const prefersReducedMotion = Accessibility.useReducedMotion;
|
const prefersReducedMotion = Accessibility.useReducedMotion;
|
||||||
const [isVisible, setIsVisible] = useState(true);
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
const [targetInDOM, setTargetInDOM] = useState(() => ownerDocument.contains(target));
|
const [targetInDOM, setTargetInDOM] = useState(() => ownerDocument.contains(target));
|
||||||
|
|||||||
Reference in new issue
Block a user