diff --git a/packages/ariakit/src/input/Form.tsx b/packages/ariakit/src/input/Form.tsx
index bf964aee66..83bf0af250 100644
--- a/packages/ariakit/src/input/Form.tsx
+++ b/packages/ariakit/src/input/Form.tsx
@@ -4,9 +4,23 @@ import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
export const Form = (props: ComponentProps["Generic"]["Form"]["Root"]) => {
- const { children, ...rest } = props;
+ const { children, onSubmit, submitButton, ...rest } = props;
assertEmpty(rest);
- return {children};
+ return (
+
+
+
+ );
};
diff --git a/packages/ariakit/src/input/TextInput.tsx b/packages/ariakit/src/input/TextInput.tsx
index 555961faf0..bf4e859670 100644
--- a/packages/ariakit/src/input/TextInput.tsx
+++ b/packages/ariakit/src/input/TextInput.tsx
@@ -4,7 +4,7 @@ import {
} from "@ariakit/react";
import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
-import { ComponentProps } from "@blocknote/react";
+import { ComponentProps, useMergeRefs, useAutoFocus } from "@blocknote/react";
import { forwardRef } from "react";
export const TextInput = forwardRef<
@@ -23,7 +23,6 @@ export const TextInput = forwardRef<
disabled,
onKeyDown,
onChange,
- onSubmit,
autoComplete,
"aria-activedescendant": ariaActivedescendant,
rightSection,
@@ -32,6 +31,11 @@ export const TextInput = forwardRef<
assertEmpty(rest);
+ // Rationale (and the trap contract `data-autofocus` serves) in the hook.
+
+ const inputRef = useAutoFocus(autoFocus);
+ const setRefs = useMergeRefs([inputRef, ref]);
+
return (
<>
{props.label && {label}}
@@ -43,15 +47,13 @@ export const TextInput = forwardRef<
className || "",
variant === "large" ? "bn-ak-input-large" : "",
)}
- ref={ref}
+ ref={setRefs}
name={name}
value={value}
- autoFocus={autoFocus}
placeholder={placeholder}
disabled={disabled}
onKeyDown={onKeyDown}
onChange={onChange}
- onSubmit={onSubmit}
autoComplete={autoComplete}
aria-activedescendant={ariaActivedescendant}
/>
diff --git a/packages/ariakit/src/panel/PanelButton.tsx b/packages/ariakit/src/panel/PanelButton.tsx
index b793112416..7e067279bd 100644
--- a/packages/ariakit/src/panel/PanelButton.tsx
+++ b/packages/ariakit/src/panel/PanelButton.tsx
@@ -8,12 +8,13 @@ export const PanelButton = forwardRef<
HTMLButtonElement,
ComponentProps["FilePanel"]["Button"]
>((props, ref) => {
- const { className, children, onClick, label, ...rest } = props;
+ const { className, children, type, onClick, label, ...rest } = props;
assertEmpty(rest);
return (
diff --git a/packages/core/src/editor/managers/StyleManager.ts b/packages/core/src/editor/managers/StyleManager.ts
index e412160e4a..a3ddf0d52b 100644
--- a/packages/core/src/editor/managers/StyleManager.ts
+++ b/packages/core/src/editor/managers/StyleManager.ts
@@ -183,7 +183,13 @@ export class StyleManager<
*/
public getSelectedLinkUrl() {
return this.editor.transact((tr) => {
- return this.getLinkMarkAtPos(tr.selection.from)?.href;
+ // `from + 1` for the same boundary reason as `editLink` below: at the
+ // left edge of a link (e.g. when the whole link is selected), the mark
+ // lookup at `from` itself misses the mark and the link's URL would
+ // incorrectly read as absent.
+ return this.getLinkMarkAtPos(
+ Math.min(tr.selection.from + 1, tr.doc.content.size),
+ )?.href;
});
}
diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts
index 094671d920..1c19b810dd 100644
--- a/packages/core/src/i18n/locales/ar.ts
+++ b/packages/core/src/i18n/locales/ar.ts
@@ -406,5 +406,6 @@ export const ar: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "موافق",
},
};
diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts
index bf77a36a01..45ff9341d8 100644
--- a/packages/core/src/i18n/locales/de.ts
+++ b/packages/core/src/i18n/locales/de.ts
@@ -440,5 +440,6 @@ export const de: Dictionary = {
},
generic: {
ctrl_shortcut: "Strg",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts
index e5386f3020..307ba90c22 100644
--- a/packages/core/src/i18n/locales/en.ts
+++ b/packages/core/src/i18n/locales/en.ts
@@ -421,5 +421,6 @@ export const en = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts
index 743a1be05c..b2c05ca6b2 100644
--- a/packages/core/src/i18n/locales/es.ts
+++ b/packages/core/src/i18n/locales/es.ts
@@ -419,5 +419,6 @@ export const es: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "Aceptar",
},
};
diff --git a/packages/core/src/i18n/locales/fa.ts b/packages/core/src/i18n/locales/fa.ts
index 6b2783ab68..405cf87ddf 100644
--- a/packages/core/src/i18n/locales/fa.ts
+++ b/packages/core/src/i18n/locales/fa.ts
@@ -390,5 +390,6 @@ export const fa = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "تأیید",
},
};
diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts
index ad605db24a..4807927655 100644
--- a/packages/core/src/i18n/locales/fr.ts
+++ b/packages/core/src/i18n/locales/fr.ts
@@ -467,5 +467,6 @@ export const fr: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts
index 4662a94202..1b9338b77b 100644
--- a/packages/core/src/i18n/locales/he.ts
+++ b/packages/core/src/i18n/locales/he.ts
@@ -421,5 +421,6 @@ export const he: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "אישור",
},
};
diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts
index 03eb016eed..998a245f20 100644
--- a/packages/core/src/i18n/locales/hr.ts
+++ b/packages/core/src/i18n/locales/hr.ts
@@ -435,5 +435,6 @@ export const hr: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "U redu",
},
};
diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts
index 913b2324b0..e7effe3827 100644
--- a/packages/core/src/i18n/locales/is.ts
+++ b/packages/core/src/i18n/locales/is.ts
@@ -435,5 +435,6 @@ export const is: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "Í lagi",
},
};
diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts
index 44be22c1bd..782a3c7fc4 100644
--- a/packages/core/src/i18n/locales/it.ts
+++ b/packages/core/src/i18n/locales/it.ts
@@ -443,5 +443,6 @@ export const it: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts
index ead1f2fb30..8bac14021d 100644
--- a/packages/core/src/i18n/locales/ja.ts
+++ b/packages/core/src/i18n/locales/ja.ts
@@ -461,5 +461,6 @@ export const ja: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts
index 2981ff1c36..de94329b19 100644
--- a/packages/core/src/i18n/locales/ko.ts
+++ b/packages/core/src/i18n/locales/ko.ts
@@ -434,5 +434,6 @@ export const ko: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "확인",
},
};
diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts
index da599e017c..a90210b572 100644
--- a/packages/core/src/i18n/locales/nl.ts
+++ b/packages/core/src/i18n/locales/nl.ts
@@ -422,5 +422,6 @@ export const nl: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts
index 72efc096ed..9ed6388dc7 100644
--- a/packages/core/src/i18n/locales/no.ts
+++ b/packages/core/src/i18n/locales/no.ts
@@ -439,5 +439,6 @@ export const no: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts
index d00039633c..95751640b9 100644
--- a/packages/core/src/i18n/locales/pl.ts
+++ b/packages/core/src/i18n/locales/pl.ts
@@ -412,5 +412,6 @@ export const pl: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts
index fe719ce023..6914de9d2c 100644
--- a/packages/core/src/i18n/locales/pt.ts
+++ b/packages/core/src/i18n/locales/pt.ts
@@ -414,5 +414,6 @@ export const pt: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts
index a4a7987dfc..db116a3c4c 100644
--- a/packages/core/src/i18n/locales/ru.ts
+++ b/packages/core/src/i18n/locales/ru.ts
@@ -465,5 +465,6 @@ export const ru: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "ОК",
},
};
diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts
index 4e73dc7eca..f53c4c39d1 100644
--- a/packages/core/src/i18n/locales/sk.ts
+++ b/packages/core/src/i18n/locales/sk.ts
@@ -419,5 +419,6 @@ export const sk = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts
index e9d379ac0b..e6101c8f69 100644
--- a/packages/core/src/i18n/locales/uk.ts
+++ b/packages/core/src/i18n/locales/uk.ts
@@ -445,5 +445,6 @@ export const uk: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "ОК",
},
};
diff --git a/packages/core/src/i18n/locales/uz.ts b/packages/core/src/i18n/locales/uz.ts
index 13aee55a73..23b0f4f1a7 100644
--- a/packages/core/src/i18n/locales/uz.ts
+++ b/packages/core/src/i18n/locales/uz.ts
@@ -455,5 +455,6 @@ export const uz: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts
index 8733fbf0ba..d52db4d48d 100644
--- a/packages/core/src/i18n/locales/vi.ts
+++ b/packages/core/src/i18n/locales/vi.ts
@@ -420,5 +420,6 @@ export const vi: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "OK",
},
};
diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts
index 5ac37a80c7..0aba71ead4 100644
--- a/packages/core/src/i18n/locales/zh-tw.ts
+++ b/packages/core/src/i18n/locales/zh-tw.ts
@@ -462,5 +462,6 @@ export const zhTW: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "確定",
},
};
diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts
index 3f4c90bb56..0017c86672 100644
--- a/packages/core/src/i18n/locales/zh.ts
+++ b/packages/core/src/i18n/locales/zh.ts
@@ -462,5 +462,6 @@ export const zh: Dictionary = {
},
generic: {
ctrl_shortcut: "Ctrl",
+ form_submit: "确定",
},
};
diff --git a/packages/mantine/src/blocknoteStyles.css b/packages/mantine/src/blocknoteStyles.css
index beb3c8182f..eeccd51725 100644
--- a/packages/mantine/src/blocknoteStyles.css
+++ b/packages/mantine/src/blocknoteStyles.css
@@ -257,6 +257,19 @@ on touch devices (e.g. the mobile formatting toolbar). */
font-size: 12px;
}
+/* On touch devices, enlarge the form-popover inputs (e.g. the link popover's
+ URL field). The 16px font-size is load-bearing: iOS Safari auto-zooms the
+ page when focusing an input with a smaller computed font-size, and that zoom
+ perturbs the visual viewport the mobile toolbar positions itself from. The
+ taller min-height also gives a comfortable tap target. (From #2982.) */
+@media (pointer: coarse) {
+ .bn-form-popover .mantine-TextInput-input,
+ .bn-form-popover .mantine-FileInput-input {
+ font-size: 16px;
+ min-height: 40px;
+ }
+}
+
.bn-form-popover .mantine-FileInput-input:hover {
background-color: var(--bn-colors-hovered-background);
}
diff --git a/packages/mantine/src/components.tsx b/packages/mantine/src/components.tsx
index 6c85286e7b..f39ec593fa 100644
--- a/packages/mantine/src/components.tsx
+++ b/packages/mantine/src/components.tsx
@@ -3,6 +3,7 @@ import { Badge, BadgeGroup } from "./badge/Badge.js";
import { Card, CardSection, ExpandSectionsPrompt } from "./comments/Card.js";
import { Comment } from "./comments/Comment.js";
import { Editor } from "./comments/Editor.js";
+import { Form } from "./form/Form.js";
import { TextInput } from "./form/TextInput.js";
import {
Menu,
@@ -89,7 +90,7 @@ export const components: Components = {
Group: BadgeGroup,
},
Form: {
- Root: (props) => {props.children}
,
+ Root: Form,
TextInput: TextInput,
},
Menu: {
diff --git a/packages/mantine/src/form/Form.tsx b/packages/mantine/src/form/Form.tsx
new file mode 100644
index 0000000000..d1b23d72b5
--- /dev/null
+++ b/packages/mantine/src/form/Form.tsx
@@ -0,0 +1,22 @@
+import { assertEmpty } from "@blocknote/core";
+import { ComponentProps } from "@blocknote/react";
+
+export const Form = (props: ComponentProps["Generic"]["Form"]["Root"]) => {
+ const { children, onSubmit, submitButton, ...rest } = props;
+
+ assertEmpty(rest);
+
+ return (
+
+ );
+};
diff --git a/packages/mantine/src/form/TextInput.tsx b/packages/mantine/src/form/TextInput.tsx
index c1630fa17f..9b05e66bbb 100644
--- a/packages/mantine/src/form/TextInput.tsx
+++ b/packages/mantine/src/form/TextInput.tsx
@@ -1,7 +1,7 @@
import { TextInput as MantineTextInput } from "@mantine/core";
import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
-import { ComponentProps } from "@blocknote/react";
+import { ComponentProps, useMergeRefs, useAutoFocus } from "@blocknote/react";
import { forwardRef } from "react";
export const TextInput = forwardRef<
@@ -20,7 +20,6 @@ export const TextInput = forwardRef<
disabled,
onKeyDown,
onChange,
- onSubmit,
autoComplete,
"aria-activedescendant": ariaActivedescendant,
rightSection,
@@ -29,6 +28,11 @@ export const TextInput = forwardRef<
assertEmpty(rest);
+ // Rationale (and the trap contract `data-autofocus` serves) in the hook.
+
+ const inputRef = useAutoFocus(autoFocus);
+ const setRefs = useMergeRefs([inputRef, ref]);
+
return (
diff --git a/packages/mantine/src/panel/PanelButton.tsx b/packages/mantine/src/panel/PanelButton.tsx
index 73336a5375..95fc0152a7 100644
--- a/packages/mantine/src/panel/PanelButton.tsx
+++ b/packages/mantine/src/panel/PanelButton.tsx
@@ -8,12 +8,13 @@ export const PanelButton = forwardRef<
HTMLButtonElement,
ComponentProps["FilePanel"]["Button"]
>((props, ref) => {
- const { className, children, onClick, label, ...rest } = props;
+ const { className, children, type, onClick, label, ...rest } = props;
assertEmpty(rest);
return (
{
const { open, onOpenChange, position, portalRoot, children, ...rest } = props;
+ // A `portalRoot` is only passed by the mobile toolbar, which renders its
+ // popovers into its own container — so it doubles as "this popover belongs
+ // to the mobile toolbar", which is what the two behaviours below actually
+ // depend on. Named here so the reason isn't hidden behind an unrelated prop.
+ // TODO: clean this up once we've settled on a proper portalling solution
+ // (pending discussion) — inferring mobile-ness from `portalRoot` should
+ // become an explicit signal.
+ const isMobileToolbarPopover = !!portalRoot;
+
assertEmpty(rest);
return (
@@ -20,9 +29,16 @@ export const Popover = (
middlewares={{ size: { padding: 20 } }}
withinPortal={!!portalRoot}
portalProps={portalRoot ? { target: portalRoot } : undefined}
- // Do not move focus to the dropdown on mobile, as it blurs the editor's
- // contentEditable and dismisses the on-screen keyboard.
- trapFocus={portalRoot ? false : undefined}
+ // Pins Mantine's default: a trap would move focus into the dropdown,
+ // which on mobile blurs the contentEditable and dismisses the
+ // keyboard. BlockNote owns focus in its popovers (useAutoFocus).
+ trapFocus={false}
+ // Keep the dropdown visible through virtual-keyboard viewport resizes on
+ // mobile: hideDetached (default true) reacts to the resize by setting
+ // display:none on the dropdown, which blurs its focused input and
+ // dismisses the on-screen keyboard (the input then unmounts with the
+ // toolbar, so the whole UI collapses).
+ hideDetached={isMobileToolbarPopover ? false : undefined}
opened={open}
onChange={onOpenChange}
position={position}
diff --git a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
index 9c824ba8bf..52ef673e3b 100644
--- a/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
+++ b/packages/react/src/components/FilePanel/DefaultTabs/EmbedTab.tsx
@@ -7,7 +7,7 @@ import {
StyleSchema,
filenameFromURL,
} from "@blocknote/core";
-import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react";
+import { ChangeEvent, useCallback, useState } from "react";
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
@@ -37,25 +37,7 @@ export const EmbedTab = <
[],
);
- const handleURLEnter = useCallback(
- (event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.nativeEvent.isComposing) {
- event.preventDefault();
- if (!editor.getBlock(props.blockId)) {
- return;
- }
- editor.updateBlock(props.blockId, {
- props: {
- name: filenameFromURL(currentURL),
- url: currentURL,
- } as any,
- });
- }
- },
- [editor, props.blockId, currentURL],
- );
-
- const handleURLClick = useCallback(() => {
+ const handleSubmit = useCallback(() => {
if (!editor.getBlock(props.blockId)) {
return;
}
@@ -73,22 +55,33 @@ export const EmbedTab = <
return (
-
-
+ {dict.file_panel.embed.embed_button[block.type] ||
+ dict.file_panel.embed.embed_button["file"]}
+
+ }
>
- {dict.file_panel.embed.embed_button[block.type] ||
- dict.file_panel.embed.embed_button["file"]}
-
+
+
);
};
diff --git a/packages/react/src/components/Form/ScreenReaderOnlySubmit.tsx b/packages/react/src/components/Form/ScreenReaderOnlySubmit.tsx
new file mode 100644
index 0000000000..d80abc5a48
--- /dev/null
+++ b/packages/react/src/components/Form/ScreenReaderOnlySubmit.tsx
@@ -0,0 +1,22 @@
+import { useDictionary } from "../../i18n/dictionary.js";
+
+/**
+ * The default submit control for `Components.Generic.Form.Root`: visually
+ * hidden (clipped, not `display: none`, so it stays in the accessibility
+ * tree as a labelled control), out of the tab order so sighted keyboard
+ * users never land on a control they can't see. Its presence is what makes
+ * Enter submit a form with more than one field.
+ */
+export function ScreenReaderOnlySubmit() {
+ const dict = useDictionary();
+
+ return (
+
+ );
+}
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx
index 26ce7e04a5..ef2b7cbab8 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/CreateLinkButton.tsx
@@ -162,6 +162,9 @@ export const CreateLinkButton = () => {
text={state.text}
range={state.range}
showTextField={false}
+ // (No explicit popover close here: any editor-state change — like
+ // submitting the link — already closes it via the setShowPopover
+ // effect above.)
setToolbarOpen={(open) => formattingToolbar.store.setState(open)}
/>
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
index bd72ea451c..cc4b568476 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileCaptionButton.tsx
@@ -5,10 +5,11 @@ import {
InlineContentSchema,
StyleSchema,
} from "@blocknote/core";
-import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react";
+import { ChangeEvent, useCallback, useState } from "react";
import { RiInputField } from "react-icons/ri";
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
+import { ScreenReaderOnlySubmit } from "../../Form/ScreenReaderOnlySubmit.js";
import { useUIMode } from "../../../editor/UIModeContext.js";
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
import { useEditorState } from "../../../hooks/useEditorState.js";
@@ -88,16 +89,6 @@ export const FileCaptionButton = () => {
[block, editor],
);
- const handleKeyDown = useCallback(
- (event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.nativeEvent.isComposing) {
- event.preventDefault();
- setPopoverOpen(false);
- }
- },
- [setPopoverOpen],
- );
-
if (block === undefined) {
return null;
}
@@ -127,14 +118,16 @@ export const FileCaptionButton = () => {
className={"bn-popover-content bn-form-popover"}
variant={"form-popover"}
>
-
+ setPopoverOpen(false)}
+ submitButton={}
+ >
}
value={block.props.caption}
autoFocus={true}
placeholder={dict.formatting_toolbar.file_caption.input_placeholder}
- onKeyDown={handleKeyDown}
onChange={handleChange}
/>
diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
index b13bb45a88..39f080538a 100644
--- a/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
+++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/FileRenameButton.tsx
@@ -5,10 +5,11 @@ import {
InlineContentSchema,
StyleSchema,
} from "@blocknote/core";
-import { ChangeEvent, KeyboardEvent, useCallback, useState } from "react";
+import { ChangeEvent, useCallback, useState } from "react";
import { RiFontFamily } from "react-icons/ri";
import { useComponentsContext } from "../../../editor/ComponentsContext.js";
+import { ScreenReaderOnlySubmit } from "../../Form/ScreenReaderOnlySubmit.js";
import { useUIMode } from "../../../editor/UIModeContext.js";
import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js";
import { useEditorState } from "../../../hooks/useEditorState.js";
@@ -88,16 +89,6 @@ export const FileRenameButton = () => {
[block, editor],
);
- const handleKeyDown = useCallback(
- (event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.nativeEvent.isComposing) {
- event.preventDefault();
- setPopoverOpen(false);
- }
- },
- [setPopoverOpen],
- );
-
if (block === undefined) {
return null;
}
@@ -133,7 +124,10 @@ export const FileRenameButton = () => {
className={"bn-popover-content bn-form-popover"}
variant={"form-popover"}
>
-
+ setPopoverOpen(false)}
+ submitButton={}
+ >
}
@@ -144,7 +138,6 @@ export const FileRenameButton = () => {
block.type
] || dict.formatting_toolbar.file_rename.input_placeholder["file"]
}
- onKeyDown={handleKeyDown}
onChange={handleChange}
/>
diff --git a/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx b/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
index 1d82a6e7cc..0106f350d9 100644
--- a/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
+++ b/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
@@ -3,15 +3,10 @@ import {
LinkToolbarExtension,
VALID_LINK_PROTOCOLS,
} from "@blocknote/core/extensions";
-import {
- ChangeEvent,
- KeyboardEvent,
- useCallback,
- useEffect,
- useState,
-} from "react";
+import { ChangeEvent, useCallback, useEffect, useState } from "react";
import { RiLink, RiText } from "react-icons/ri";
import { useComponentsContext } from "../../editor/ComponentsContext.js";
+import { ScreenReaderOnlySubmit } from "../Form/ScreenReaderOnlySubmit.js";
import { useExtension } from "../../hooks/useExtension.js";
import { useDictionary } from "../../i18n/dictionary.js";
import { LinkToolbarProps } from "./LinkToolbarProps.js";
@@ -50,18 +45,6 @@ export const EditLinkMenuItems = (
setCurrentText(text);
}, [text, url]);
- const handleEnter = useCallback(
- (event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.nativeEvent.isComposing) {
- event.preventDefault();
- editLink(validateUrl(currentUrl), currentText, props.range.from);
- props.setToolbarOpen?.(false);
- props.setToolbarPositionFrozen?.(false);
- }
- },
- [editLink, currentUrl, currentText, props],
- );
-
const handleUrlChange = useCallback(
(event: ChangeEvent) =>
setCurrentUrl(event.currentTarget.value),
@@ -81,7 +64,10 @@ export const EditLinkMenuItems = (
}, [editLink, currentUrl, currentText, props]);
return (
-
+ }
+ >
{/* // TODO: add labels? */}
{showTextField !== false && (
}
placeholder={dict.link_toolbar.form.title_placeholder}
value={currentText}
- onKeyDown={handleEnter}
onChange={handleTextChange}
- onSubmit={handleSubmit}
/>
)}
diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx
index 5d71bc58dc..5a4063d7f4 100644
--- a/packages/react/src/editor/ComponentsContext.tsx
+++ b/packages/react/src/editor/ComponentsContext.tsx
@@ -1,4 +1,5 @@
import {
+ ReactElement,
ChangeEvent,
ComponentType,
createContext,
@@ -82,7 +83,15 @@ export type ComponentProps = {
};
Button: {
className?: string;
- onClick: () => void;
+ /**
+ * Explicit, because the skins' underlying buttons disagree on the
+ * default (Mantine's is `type="button"`, shadcn's was `"submit"`) and
+ * a submit button inside a `Form.Root` must reliably submit on every
+ * skin. `"submit"` buttons need no `onClick` - the form's `onSubmit`
+ * is the single commit path, so clicking cannot fire twice.
+ */
+ type: "button" | "submit";
+ onClick?: () => void;
} & (
| { children: ReactNode; label?: string }
| { children?: undefined; label: string }
@@ -103,7 +112,7 @@ export type ComponentProps = {
value: string;
placeholder: string;
onChange: (event: ChangeEvent) => void;
- onKeyDown: (event: KeyboardEvent) => void;
+ onKeyDown?: (event: KeyboardEvent) => void;
};
};
LinkToolbar: {
@@ -304,6 +313,20 @@ export type ComponentProps = {
Form: {
Root: {
children?: ReactNode;
+ /**
+ * Called on the form's `submit` event. Implementations must render a
+ * real `