fix: Event propagation from input in SidebarLink (#11105)

* fix: Event propagation from input in SidebarLink

closes #11093

* Include keyboard
This commit is contained in:
Tom Moor
2026-01-07 21:59:19 -05:00
committed by GitHub
parent 974c5f9f70
commit 2116d9972f
4 changed files with 51 additions and 21 deletions
+4 -2
View File
@@ -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<React.HTMLAttributes<HTMLInputElement>, "onSubmit"> & {
/** A callback when the title is submitted. */
@@ -141,11 +142,12 @@ function EditableTitle(
return (
<>
{isEditing ? (
<form onSubmit={handleSave}>
<EventBoundary as="form" onSubmit={handleSave}>
<Input
dir="auto"
type="text"
lang=""
name="title"
value={value}
onClick={stopPropagation}
onKeyDown={handleKeyDown}
@@ -155,7 +157,7 @@ function EditableTitle(
autoFocus
{...rest}
/>
</form>
</EventBoundary>
) : (
<Text
onDoubleClick={canUpdate ? handleDoubleClick : undefined}
@@ -139,7 +139,7 @@ const NavLink = ({
(event: React.MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (isActive) {
if (isActive && !event.defaultPrevented) {
onActiveClick?.(event);
}
+1 -1
View File
@@ -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",
+45 -17
View File
@@ -1,47 +1,75 @@
import * as React from "react";
type Props = {
/**
* Props for the EventBoundary component.
*/
export interface Props<T extends React.ElementType> {
/** 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<Props> = ({
export const EventBoundary = <T extends React.ElementType = "span">({
as,
children,
className,
captureEvents = "all",
}: Props) => {
...rest
}: Props<T> & Omit<React.ComponentPropsWithoutRef<T>, keyof Props<T>>) => {
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 (
<span {...props} className={className}>
<Component {...rest} {...eventHandlers} className={className}>
{children}
</span>
</Component>
);
};