From 2116d9972f5af98a6bb1911700263d3312d06bcf Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 7 Jan 2026 21:59:19 -0500 Subject: [PATCH] fix: Event propagation from input in SidebarLink (#11105) * fix: Event propagation from input in SidebarLink closes #11093 * Include keyboard --- app/components/EditableTitle.tsx | 6 +- app/components/Sidebar/components/NavLink.tsx | 2 +- package.json | 2 +- shared/components/EventBoundary.tsx | 62 ++++++++++++++----- 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/app/components/EditableTitle.tsx b/app/components/EditableTitle.tsx index b6f39c8e22..38f0433e3b 100644 --- a/app/components/EditableTitle.tsx +++ b/app/components/EditableTitle.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { toast } from "sonner"; import styled from "styled-components"; import { s, ellipsis } from "@shared/styles"; +import EventBoundary from "@shared/components/EventBoundary"; type Props = Omit, "onSubmit"> & { /** A callback when the title is submitted. */ @@ -141,11 +142,12 @@ function EditableTitle( return ( <> {isEditing ? ( -
+ - +
) : ( ) => { onClick?.(event); - if (isActive) { + if (isActive && !event.defaultPrevented) { onActiveClick?.(event); } diff --git a/package.json b/package.json index 6ab6ba6657..08ea851fe0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "yarn clean && yarn vite:build && yarn build:i18n && yarn build:server", "start": "node ./build/server/index.js", "dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=cron,collaboration,websockets,admin,web,worker\"", - "dev:backend": "NODE_ENV=development nodemon --quiet --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --watch server --watch shared --watch plugins --watch .env --watch .env.local --watch .env.development --ignore \"plugins/client/**/*.ts\" --ignore \"**/*.test.ts\" --ignore data/ --ignore build/ --ignore app/ --ignore shared/editor --ignore server/migrations", + "dev:backend": "NODE_ENV=development nodemon --quiet --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --watch server --watch shared --watch plugins --watch .env --watch .env.local --watch .env.development --ignore \"shared/components/**/*.tsx?\" --ignore \"plugins/client/**/*.tsx?\" --ignore \"**/*.test.ts\" --ignore data/ --ignore build/ --ignore app/ --ignore shared/editor --ignore server/migrations", "dev:watch": "NODE_ENV=development yarn concurrently -n backend,frontend \"yarn dev:backend\" \"yarn vite:dev\"", "lint": "oxlint --type-aware app server shared plugins", "lint:changed": "git diff --name-only --diff-filter=ACMRTUXB | grep -E '\\.(js|jsx|ts|tsx)$' | xargs -r oxlint", diff --git a/shared/components/EventBoundary.tsx b/shared/components/EventBoundary.tsx index a66acab539..5aeb2cd178 100644 --- a/shared/components/EventBoundary.tsx +++ b/shared/components/EventBoundary.tsx @@ -1,47 +1,75 @@ import * as React from "react"; -type Props = { +/** + * Props for the EventBoundary component. + */ +export interface Props { + /** The element or component to render as. */ + as?: T; + /** The children to be rendered within the boundary. */ children?: React.ReactNode; + /** Optional CSS class name. */ className?: string; /** * Capture all events, pointer events, or click events. * @default "all" */ - captureEvents?: "all" | "pointer" | "click"; -}; + captureEvents?: "all" | "pointer" | "click" | "mouse" | "keyboard"; +} /** * EventBoundary is a component that prevents events from propagating to parent elements. * This is useful for preventing clicks or other interactions from bubbling up the DOM tree. + * + * @param props - the properties of the component. + * @return a React component that captures events. */ -const EventBoundary: React.FC = ({ +export const EventBoundary = ({ + as, children, className, captureEvents = "all", -}: Props) => { + ...rest +}: Props & Omit, keyof Props>) => { + const Component = as || "span"; + const stopEvent = React.useCallback((event: React.SyntheticEvent) => { - event.preventDefault(); event.stopPropagation(); }, []); - let props = {}; + const eventHandlers: { + onPointerDown?: React.PointerEventHandler; + onPointerUp?: React.PointerEventHandler; + onClick?: React.MouseEventHandler; + onMouseDown?: React.MouseEventHandler; + onMouseUp?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyUp?: React.KeyboardEventHandler; + } = {}; + + if (captureEvents === "all" || captureEvents === "keyboard") { + eventHandlers.onKeyDown = stopEvent; + eventHandlers.onKeyUp = stopEvent; + } + + if (captureEvents === "all" || captureEvents === "mouse") { + eventHandlers.onMouseDown = stopEvent; + eventHandlers.onMouseUp = stopEvent; + } if (captureEvents === "all" || captureEvents === "pointer") { - props = { - onPointerDown: stopEvent, - onPointerUp: stopEvent, - }; + eventHandlers.onPointerDown = stopEvent; + eventHandlers.onPointerUp = stopEvent; } + if (captureEvents === "all" || captureEvents === "click") { - props = { - ...props, - onClick: stopEvent, - }; + eventHandlers.onClick = stopEvent; } + return ( - + {children} - + ); };