diff --git a/integrations/tests/tests/pointer-interactions.spec.tsx b/integrations/tests/tests/pointer-interactions.spec.tsx
index 73bc55e4a..5c904de50 100644
--- a/integrations/tests/tests/pointer-interactions.spec.tsx
+++ b/integrations/tests/tests/pointer-interactions.spec.tsx
@@ -405,6 +405,46 @@ test.describe("pointer interactions", () => {
});
test.describe("focus", () => {
+ for (const returnToStart of [false, true]) {
+ test(`should blur after dragging${returnToStart ? " back to the starting point" : ""}`, async ({
+ page: mainPage
+ }) => {
+ const page = await goToUrl(
+ mainPage,
+
+
+
+
+
+ );
+ const separator = page.getByRole("separator");
+ const { x, y } = getCenterCoordinates((await separator.boundingBox())!);
+
+ await page.mouse.move(x, y);
+ await page.mouse.down();
+ await expect(separator).toBeFocused();
+ await page.mouse.move(x + 25, y);
+ if (returnToStart) {
+ await page.mouse.move(x, y);
+ }
+ await page.mouse.up();
+ await expect(separator).not.toBeFocused();
+ const valueAfterDrag = await separator.getAttribute("aria-valuenow");
+ await page.keyboard.press("ArrowRight");
+ await expect(separator).toHaveAttribute(
+ "aria-valuenow",
+ valueAfterDrag!
+ );
+
+ // A subsequent click should still retain focus and support keyboard resizing.
+ await separator.click();
+ await expect(separator).toBeFocused();
+ const value = await separator.getAttribute("aria-valuenow");
+ await page.keyboard.press("ArrowRight");
+ await expect(separator).not.toHaveAttribute("aria-valuenow", value!);
+ });
+ }
+
test("should update focus to the nearest separator", async ({
page: mainPage
}) => {
@@ -563,7 +603,8 @@ test.describe("pointer interactions", () => {
await expect(separator).toHaveAttribute("data-separator", "active");
await page.mouse.up();
- await expect(separator).toHaveAttribute("data-separator", "focus");
+ await expect(separator).not.toBeFocused();
+ await expect(separator).not.toHaveAttribute("data-separator", "active");
await page.mouse.move(0, 0);
await page.mouse.down();
diff --git a/lib/components/group/ResizePreview.test.tsx b/lib/components/group/ResizePreview.test.tsx
index cd5daa3b6..3928ec2fa 100644
--- a/lib/components/group/ResizePreview.test.tsx
+++ b/lib/components/group/ResizePreview.test.tsx
@@ -205,6 +205,7 @@ describe("separator previews", () => {
let interaction: InteractionActive = {
state: "active",
cursorFlags: 0,
+ didPointerMove: false,
hitRegions: [],
initialLayoutMap: new Map(),
previewLayoutMap: new Map(),
diff --git a/lib/global/event-handlers/onDocumentPointerDown.ts b/lib/global/event-handlers/onDocumentPointerDown.ts
index b25344baa..c68f73e98 100644
--- a/lib/global/event-handlers/onDocumentPointerDown.ts
+++ b/lib/global/event-handlers/onDocumentPointerDown.ts
@@ -53,6 +53,7 @@ export function onDocumentPointerDown(event: PointerEvent) {
updateInteractionState({
cursorFlags: 0,
+ didPointerMove: false,
hitRegions,
initialLayoutMap,
pointerDownAtPoint: { x: event.clientX, y: event.clientY },
diff --git a/lib/global/mutable-state/interactions.test.ts b/lib/global/mutable-state/interactions.test.ts
index b39191d9a..27dff8d64 100644
--- a/lib/global/mutable-state/interactions.test.ts
+++ b/lib/global/mutable-state/interactions.test.ts
@@ -35,6 +35,7 @@ test("removing a group preserves the other group in a shared drag", () => {
updateInteractionState({
cursorFlags: 0,
+ didPointerMove: false,
hitRegions,
initialLayoutMap,
pointerDownAtPoint: { x: 100, y: 100 },
diff --git a/lib/global/mutable-state/interactions.ts b/lib/global/mutable-state/interactions.ts
index 66c013252..cb3826166 100644
--- a/lib/global/mutable-state/interactions.ts
+++ b/lib/global/mutable-state/interactions.ts
@@ -34,13 +34,15 @@ export function subscribeToInteractionState(
export function updateCursorFlags(
cursorFlags: number,
previews: ResizePreview[] = [],
- previewLayoutMap?: InteractionActive["previewLayoutMap"]
+ previewLayoutMap?: InteractionActive["previewLayoutMap"],
+ didPointerMove = false
) {
const prev = state;
const next = { ...state };
next.cursorFlags = cursorFlags;
if (next.state === "active") {
+ next.didPointerMove ||= didPointerMove;
next.previews = previews;
if (previewLayoutMap) {
next.previewLayoutMap = previewLayoutMap;
@@ -60,6 +62,23 @@ export function updateInteractionState(next: InteractionState) {
state = next;
+ // Keep click-to-focus behavior, but release keyboard focus after a drag.
+ // This also handles interactions completed by the missed-pointerup fallback.
+ if (
+ prev.state === "active" &&
+ next.state !== "active" &&
+ prev.didPointerMove
+ ) {
+ prev.hitRegions.forEach(({ separator }) => {
+ if (
+ separator &&
+ separator.element.ownerDocument.activeElement === separator.element
+ ) {
+ separator.element.blur();
+ }
+ });
+ }
+
eventEmitter.emit("change", {
prev,
next
diff --git a/lib/global/mutable-state/types.ts b/lib/global/mutable-state/types.ts
index d695f7f15..545097822 100644
--- a/lib/global/mutable-state/types.ts
+++ b/lib/global/mutable-state/types.ts
@@ -27,6 +27,7 @@ export type ResizePreview = {
export type InteractionActive = {
cursorFlags: number;
+ didPointerMove: boolean;
hitRegions: HitRegion[];
initialLayoutMap: Map;
pointerDownAtPoint: Point;
diff --git a/lib/global/utils/updateActiveHitRegion.test.ts b/lib/global/utils/updateActiveHitRegion.test.ts
index 1ea6aafa7..f99f6405d 100644
--- a/lib/global/utils/updateActiveHitRegion.test.ts
+++ b/lib/global/utils/updateActiveHitRegion.test.ts
@@ -48,6 +48,7 @@ describe("updateActiveHitRegions preview bounds", () => {
updateInteractionState({
cursorFlags: 0,
+ didPointerMove: false,
hitRegions,
initialLayoutMap,
pointerDownAtPoint,
@@ -121,6 +122,7 @@ describe("updateActiveHitRegions preview bounds", () => {
updateInteractionState({
state: "active",
cursorFlags: 0,
+ didPointerMove: false,
hitRegions,
initialLayoutMap,
pointerDownAtPoint,
diff --git a/lib/global/utils/updateActiveHitRegion.ts b/lib/global/utils/updateActiveHitRegion.ts
index 69df15d87..8dd01ac4a 100644
--- a/lib/global/utils/updateActiveHitRegion.ts
+++ b/lib/global/utils/updateActiveHitRegion.ts
@@ -182,6 +182,11 @@ export function updateActiveHitRegions({
cursorFlags |= nextCursorFlags & CURSOR_FLAGS_VERTICAL;
}
- updateCursorFlags(cursorFlags, previews, previewLayoutMap);
+ const didPointerMove =
+ interaction.state === "active" &&
+ (event.clientX !== interaction.pointerDownAtPoint.x ||
+ event.clientY !== interaction.pointerDownAtPoint.y);
+
+ updateCursorFlags(cursorFlags, previews, previewLayoutMap, didPointerMove);
updateCursorStyle(document);
}