mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +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,
|
||||
} = options;
|
||||
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 {x, y} = target ? getInitialGuess(target, placement, offsetMainAxis, offsetCrossAxis) : {x: -9999, y: -9999};
|
||||
return {x, y, isReady: false, offsetX: 0, offsetY: 0, placement};
|
||||
@@ -481,7 +486,7 @@ export function useAntiShiftFloating(
|
||||
};
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
if (!enabled || target == null || floatingRef.current == null) {
|
||||
if (!enabled || target == null || floatingElement == null) {
|
||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||
return;
|
||||
}
|
||||
@@ -489,7 +494,7 @@ export function useAntiShiftFloating(
|
||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||
return;
|
||||
}
|
||||
const floating = floatingRef.current;
|
||||
const floating = floatingElement;
|
||||
updatePosition();
|
||||
if (shouldAutoUpdate) {
|
||||
const cleanupCallbacks = [
|
||||
@@ -502,7 +507,7 @@ export function useAntiShiftFloating(
|
||||
}
|
||||
};
|
||||
} else if (shouldObserveFloatingResize) {
|
||||
cleanupRef.current = observeFloatingResize(floatingRef.current, updatePosition);
|
||||
cleanupRef.current = observeFloatingResize(floating, updatePosition);
|
||||
}
|
||||
return () => {
|
||||
positionRevisionRef.current += 1;
|
||||
@@ -519,9 +524,18 @@ export function useAntiShiftFloating(
|
||||
}
|
||||
setState((prev) => ({...prev, isReady: false, offsetX: 0, offsetY: 0}));
|
||||
};
|
||||
}, [enabled, target, shouldAutoUpdate, shouldObserveFloatingResize, updatePosition, updatePositionNow]);
|
||||
}, [
|
||||
enabled,
|
||||
target,
|
||||
floatingElement,
|
||||
shouldAutoUpdate,
|
||||
shouldObserveFloatingResize,
|
||||
updatePosition,
|
||||
updatePositionNow,
|
||||
]);
|
||||
return {
|
||||
ref: floatingRef,
|
||||
setFloating,
|
||||
state,
|
||||
style: {
|
||||
position: 'fixed' as const,
|
||||
|
||||
@@ -271,7 +271,7 @@ function SelectionToolbarSurface({
|
||||
}),
|
||||
[editor],
|
||||
);
|
||||
const {ref, state, style, updatePosition} = useAntiShiftFloating(virtualReference, true, {
|
||||
const {setFloating, state, style, updatePosition} = useAntiShiftFloating(virtualReference, true, {
|
||||
placement: 'top',
|
||||
offsetMainAxis: 8,
|
||||
shouldAutoUpdate: true,
|
||||
@@ -381,7 +381,7 @@ function SelectionToolbarSurface({
|
||||
>
|
||||
<flx-lexical-selection-formatting-toolbar
|
||||
ref={(element) => {
|
||||
(ref as React.MutableRefObject<HTMLElement | null>).current = element;
|
||||
setFloating(element);
|
||||
toolbarRef.current = element;
|
||||
}}
|
||||
className={flxElementClassName(styles.toolbar)}
|
||||
|
||||
@@ -186,6 +186,7 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
|
||||
);
|
||||
const {
|
||||
ref: popoutRef,
|
||||
setFloating,
|
||||
state,
|
||||
style,
|
||||
beginManualPositioning,
|
||||
@@ -206,7 +207,7 @@ const PopoutItem: React.FC<PopoutItemProps> = observer(
|
||||
useLayoutEffect(() => {
|
||||
focusRefs.setReference(target);
|
||||
}, [focusRefs, target]);
|
||||
const mergedPopoutRef = useMergeRefs([popoutRef, focusRefs.setFloating]);
|
||||
const mergedPopoutRef = useMergeRefs([setFloating, focusRefs.setFloating]);
|
||||
const prefersReducedMotion = Accessibility.useReducedMotion;
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [targetInDOM, setTargetInDOM] = useState(() => ownerDocument.contains(target));
|
||||
|
||||
Reference in new issue
Block a user