Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/ariakit/src/panel/PanelFileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@ import {
} from "@ariakit/react";

import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef, type ChangeEvent } from "react";

export const PanelFileInput = forwardRef<
HTMLInputElement,
ComponentProps["FilePanel"]["FileInput"]
>((props, ref) => {
const { className, accept, value, placeholder, onChange, ...rest } = props;
const {
autoFocus,
className,
accept,
value,
placeholder,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLInputElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

return (
<AriakitFormProvider>
<AriakitFormInput
className={className}
ref={ref}
ref={mergedRef}
name={"panel-input"}
type={"file"}
accept={accept}
Expand Down
17 changes: 14 additions & 3 deletions packages/ariakit/src/panel/PanelTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,28 @@ import {
} from "@ariakit/react";

import { assertEmpty, mergeCSSClasses } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef } from "react";

export const PanelTextInput = forwardRef<
HTMLInputElement,
ComponentProps["FilePanel"]["TextInput"]
>((props, ref) => {
const { className, value, placeholder, onKeyDown, onChange, ...rest } = props;
const {
autoFocus,
className,
value,
placeholder,
onKeyDown,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLInputElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

return (
<AriakitFormProvider>
<AriakitFormInput
Expand All @@ -25,7 +36,7 @@ export const PanelTextInput = forwardRef<
onChange={onChange}
onKeyDown={onKeyDown}
data-test={"embed-input"}
ref={ref}
ref={mergedRef}
/>
</AriakitFormProvider>
);
Expand Down
17 changes: 14 additions & 3 deletions packages/mantine/src/panel/PanelFileInput.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { FileInput as MantineFileInput } from "@mantine/core";

import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef } from "react";

export const PanelFileInput = forwardRef<
HTMLButtonElement,
ComponentProps["FilePanel"]["FileInput"]
>((props, ref) => {
const { className, accept, value, placeholder, onChange, ...rest } = props;
const {
autoFocus,
className,
accept,
value,
placeholder,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLButtonElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

return (
<MantineFileInput
size={"xs"}
className={className}
ref={ref}
ref={mergedRef}
accept={accept}
value={value}
placeholder={placeholder}
Expand Down
17 changes: 14 additions & 3 deletions packages/mantine/src/panel/PanelTextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,34 @@
import { TextInput as MantineTextInput } from "@mantine/core";

import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef } from "react";

export const PanelTextInput = forwardRef<
HTMLInputElement,
ComponentProps["FilePanel"]["TextInput"]
>((props, ref) => {
const { className, value, placeholder, onKeyDown, onChange, ...rest } = props;
const {
autoFocus,
className,
value,
placeholder,
onKeyDown,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLInputElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

return (
<MantineTextInput
size={"xs"}
data-test={"embed-input"}
className={className}
ref={ref}
ref={mergedRef}
value={value}
placeholder={placeholder}
onKeyDown={onKeyDown}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const EmbedTab = <
}
>
<Components.FilePanel.TextInput
autoFocus
className={"bn-text-input"}
placeholder={dict.file_panel.embed.url_placeholder}
value={currentURL}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const UploadTab = <
return (
<Components.FilePanel.TabPanel className={"bn-tab-panel"}>
<Components.FilePanel.FileInput
autoFocus
className="bn-file-input"
data-test="upload-input"
accept={accept}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export const FilePanelController = (props: {
...props.floatingUIOptions?.useFloatingOptions,
},
focusManagerProps: {
disabled: true,
// Move keyboard focus into the panel when it opens, including when
// inserting a file block from the slash menu.
disabled: false,
// The default panel focuses its input. Custom panels retain the
// focus manager's default of focusing their first control.
initialFocus: props.filePanel ? 0 : -1,
...props.floatingUIOptions?.focusManagerProps,
},
elementProps: {
Expand All @@ -55,7 +60,7 @@ export const FilePanelController = (props: {
...props.floatingUIOptions?.elementProps,
},
}),
[blockId, editor, filePanel, props.floatingUIOptions],
[blockId, editor, filePanel, props.filePanel, props.floatingUIOptions],
);

const Component = props.filePanel || FilePanel;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from "vite-plus/test";
import { page, userEvent } from "vite-plus/test/browser";
import { openEditor, pressKey } from "@root/tests/a11y/helpers.js";

async function openImagePanel() {
await pressKey("Tab");
await expect.element(page.getByRole("textbox")).toHaveFocus();
await userEvent.keyboard("/image");
await expect
.element(page.getByRole("option", { name: /Image/ }))
.toBeVisible();
await pressKey("Enter");
const panel = page.elementLocator(
document.querySelector<HTMLElement>(".bn-panel")!,
);
await expect.element(panel).toBeVisible();
return panel;
}

test("file panel: Embed URL input receives focus on opening", async () => {
await openEditor([{ type: "paragraph" }]);
const panel = await openImagePanel();
await expect.element(panel.getByPlaceholder("Enter URL")).toHaveFocus();
});

test("file panel: Upload button receives focus on opening", async () => {
await openEditor([{ type: "paragraph" }], {
uploadFile: async () => "/image.svg",
});
const panel = await openImagePanel();
await expect
.element(panel.getByRole("button", { name: "Upload image", exact: true }))
.toHaveFocus();
});
2 changes: 2 additions & 0 deletions packages/react/src/editor/ComponentsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export type ComponentProps = {
| { children?: undefined; label: string }
);
FileInput: {
autoFocus?: boolean;
className?: string;
accept: string;
value: File | null;
Expand All @@ -126,6 +127,7 @@ export type ComponentProps = {
children?: ReactNode;
};
TextInput: {
autoFocus?: boolean;
className?: string;
value: string;
placeholder: string;
Expand Down
17 changes: 14 additions & 3 deletions packages/shadcn/src/panel/PanelFileInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef } from "react";

import { useShadCNComponentsContext } from "../ShadCNComponentsContext.js";
Expand All @@ -8,17 +8,28 @@ export const PanelFileInput = forwardRef<
HTMLInputElement,
ComponentProps["FilePanel"]["FileInput"]
>((props, ref) => {
const { className, accept, value, placeholder, onChange, ...rest } = props;
const {
autoFocus,
className,
accept,
value,
placeholder,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLInputElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

const ShadCNComponents = useShadCNComponentsContext()!;

return (
<ShadCNComponents.Input.Input
type={"file"}
className={className}
ref={ref}
ref={mergedRef}
accept={accept}
value={value ? value.name : undefined}
onChange={async (e) => onChange?.(e.target.files![0])}
Expand Down
17 changes: 14 additions & 3 deletions packages/shadcn/src/panel/PanelTextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assertEmpty } from "@blocknote/core";
import { ComponentProps } from "@blocknote/react";
import { ComponentProps, useAutoFocus, useMergeRefs } from "@blocknote/react";
import { forwardRef } from "react";

import { cn } from "../lib/utils.js";
Expand All @@ -9,17 +9,28 @@ export const PanelTextInput = forwardRef<
HTMLInputElement,
ComponentProps["FilePanel"]["TextInput"]
>((props, ref) => {
const { className, value, placeholder, onKeyDown, onChange, ...rest } = props;
const {
autoFocus,
className,
value,
placeholder,
onKeyDown,
onChange,
...rest
} = props;

assertEmpty(rest);

const inputRef = useAutoFocus<HTMLInputElement>(autoFocus);
const mergedRef = useMergeRefs([ref, inputRef]);

const ShadCNComponents = useShadCNComponentsContext()!;

return (
<ShadCNComponents.Input.Input
data-test={"embed-input"}
className={cn(className, "w-80 max-w-full")}
ref={ref}
ref={mergedRef}
value={value}
placeholder={placeholder}
onKeyDown={onKeyDown}
Expand Down
14 changes: 11 additions & 3 deletions tests/a11y/helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PartialBlock } from "@blocknote/core";
import type { BlockNoteEditor, PartialBlock } from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
Expand Down Expand Up @@ -27,12 +27,14 @@ function EditorFixture({
initialContent,
editable,
theme,
uploadFile,
}: {
initialContent: PartialBlock[];
editable: boolean;
theme: "light" | "dark";
uploadFile?: BlockNoteEditor["uploadFile"];
}) {
const editor = useCreateBlockNote({ initialContent });
const editor = useCreateBlockNote({ initialContent, uploadFile });
return (
<main style={{ maxWidth: 900, margin: "80px auto", padding: 24 }}>
<h1>BlockNote accessibility fixture</h1>
Expand All @@ -48,14 +50,20 @@ export async function openEditor(
{
editable = true,
theme = "light",
}: { editable?: boolean; theme?: "light" | "dark" } = {},
uploadFile,
}: {
editable?: boolean;
theme?: "light" | "dark";
uploadFile?: BlockNoteEditor["uploadFile"];
} = {},
) {
await page.viewport(1280, 900);
await render(
<EditorFixture
initialContent={initialContent}
editable={editable}
theme={theme}
uploadFile={uploadFile}
/>,
);
await expect.element(page.getByRole("textbox")).toBeVisible();
Expand Down
Loading