Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Step suggestions accept either an array of `{ id, title, ... }` or the JSON:API

When a user types in Step Title, autocomplete filters these titles; Tab/Enter/Ctrl/Cmd+Space or the ⌄ button will insert the selection.

To stop suggestions from popping up while typing, set `data-step-autocomplete="false"` on any ancestor of the editor (for example the element the editor is mounted into). The "Show suggestions" button and Ctrl/Cmd+Space still open the list. The attribute is read on every edit, so toggling it takes effect immediately; `isStepAutocompleteOnTypeEnabled(element)` exposes the same check.

## Running Tests

Vitest covers the Markdown/block converter. Run the suite with:
Expand Down
13 changes: 12 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ function App() {
"idle",
);
const [darkMode, setDarkMode] = useState(false);
const [suggestWhileTyping, setSuggestWhileTyping] = useState(true);
const [markdownInput, setMarkdownInput] = useState("");
const [inputError, setInputError] = useState<string | null>(null);

Expand Down Expand Up @@ -771,6 +772,13 @@ function App() {
>
Insert Test
</button>
<button
type="button"
className="app__action app__action--ghost"
onClick={() => setSuggestWhileTyping((v) => !v)}
>
Suggest while typing: {suggestWhileTyping ? "on" : "off"}
</button>
<button
type="button"
className="app__dark-toggle"
Expand All @@ -787,7 +795,10 @@ function App() {
</header>

<section className="app__workspace">
<div className="app__editor">
<div
className="app__editor"
data-step-autocomplete={suggestWhileTyping ? "true" : "false"}
>
<BlockNoteView
editor={editor}
theme={darkMode ? "dark" : "light"}
Expand Down
14 changes: 12 additions & 2 deletions src/editor/blocks/stepField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, use
import type { ReactNode, ChangeEvent } from "react";
import { useComponentsContext } from "@blocknote/react";
import { EditLinkMenuItems } from "@blocknote/react";
import { useStepAutocomplete, type StepSuggestion } from "../stepAutocomplete";
import { isStepAutocompleteOnTypeEnabled, useStepAutocomplete, type StepSuggestion } from "../stepAutocomplete";
import { type SnippetSuggestion } from "../snippetAutocomplete";
import { useStepImageUpload } from "../stepImageUpload";
import { escapeMarkdownText, normalizePlainText } from "./markdown";
Expand Down Expand Up @@ -884,6 +884,10 @@ export function StepField({
const [isFocused, setIsFocused] = useState(false);
const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
const [suggestWhileTyping, setSuggestWhileTyping] = useState(true);
// Suggestions opened explicitly (button / Ctrl+Space) keep filtering while
// typing even when the host disabled suggest-while-typing.
const [suggestionsRequested, setSuggestionsRequested] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null);
const [showLinkPopover, setShowLinkPopover] = useState(false);
Expand Down Expand Up @@ -959,6 +963,7 @@ export function StepField({
prevTextRef.current = nextValue;

const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
setSuggestWhileTyping(isStepAutocompleteOnTypeEnabled(editorContainerRef.current));
setPlainTextValue((prev) => {
const normalized = markdownToPlainText(markdown);
return prev === normalized ? prev : normalized;
Expand Down Expand Up @@ -1255,6 +1260,7 @@ export function StepField({
const handleBlur = () => {
setIsFocused(false);
setShowAllSuggestions(false);
setSuggestionsRequested(false);
// Re-apply formatting highlights after blur because OverType may
// re-render the preview (via debounced selectionchange) and strip them.
const instance = editorInstanceRef.current;
Expand Down Expand Up @@ -1641,7 +1647,8 @@ export function StepField({
isFocused &&
filteredSuggestions.length > 0 &&
(!hasExactMatch || showAllSuggestions) &&
(showAllSuggestions || normalizedQuery.length >= 1);
(showAllSuggestions ||
(normalizedQuery.length >= 1 && (suggestWhileTyping || suggestionsRequested)));

useEffect(() => {
setActiveSuggestionIndex(0);
Expand Down Expand Up @@ -1736,6 +1743,7 @@ export function StepField({
onSuggestionSelect?.(suggestion);
setActiveSuggestionIndex(0);
setShowAllSuggestions(false);
setSuggestionsRequested(false);
requestAnimationFrame(() => {
textareaNode?.focus();
if (textareaNode) {
Expand Down Expand Up @@ -1914,6 +1922,7 @@ export function StepField({
) {
event.preventDefault();
setShowAllSuggestions(true);
setSuggestionsRequested(true);
return;
}

Expand Down Expand Up @@ -2183,6 +2192,7 @@ export function StepField({
onMouseDown={(event) => {
event.preventDefault();
setShowAllSuggestions(true);
setSuggestionsRequested(true);
textareaNode?.focus();
}}
aria-label="Show suggestions"
Expand Down
33 changes: 32 additions & 1 deletion src/editor/stepAutocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest";
import { parseStepsFromJsonApi } from "./stepAutocomplete";
import { renderToStaticMarkup } from "react-dom/server";
import React from "react";
import { setStepsFetcher, useStepAutocomplete } from "./stepAutocomplete";
import {
STEP_AUTOCOMPLETE_ATTRIBUTE,
isStepAutocompleteOnTypeEnabled,
setStepsFetcher,
useStepAutocomplete,
} from "./stepAutocomplete";

describe("parseStepsFromJsonApi", () => {
it("converts JSON:API resources into step suggestions", () => {
Expand Down Expand Up @@ -101,3 +106,29 @@ describe("parseStepsFromJsonApi", () => {
setStepsFetcher(null);
});
});

describe("isStepAutocompleteOnTypeEnabled", () => {
const nodeWithHost = (value: string | null) =>
({
closest: (selector: string) => {
expect(selector).toBe(`[${STEP_AUTOCOMPLETE_ATTRIBUTE}]`);
return value === null ? null : { getAttribute: () => value };
},
}) as unknown as Element;

it("is enabled without a node", () => {
expect(isStepAutocompleteOnTypeEnabled(null)).toBe(true);
});

it("is enabled when no ancestor carries the attribute", () => {
expect(isStepAutocompleteOnTypeEnabled(nodeWithHost(null))).toBe(true);
});

it("is enabled when the attribute is \"true\"", () => {
expect(isStepAutocompleteOnTypeEnabled(nodeWithHost("true"))).toBe(true);
});

it("is disabled when the attribute is \"false\"", () => {
expect(isStepAutocompleteOnTypeEnabled(nodeWithHost("false"))).toBe(false);
});
});
9 changes: 9 additions & 0 deletions src/editor/stepAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export function setStepsFetcher(fetcher: StepSuggestionsFetcher | null) {
inflightPromise = null;
}

// Hosts that cannot reach `setX` setters opt out of suggest-while-typing with
// `data-step-autocomplete="false"` on any ancestor of the editor.
export const STEP_AUTOCOMPLETE_ATTRIBUTE = "data-step-autocomplete";

export function isStepAutocompleteOnTypeEnabled(node: Element | null | undefined): boolean {
const host = node?.closest(`[${STEP_AUTOCOMPLETE_ATTRIBUTE}]`);
return host?.getAttribute(STEP_AUTOCOMPLETE_ATTRIBUTE) !== "false";
}

export function useStepAutocomplete(): StepSuggestion[] {
const [suggestions, setSuggestions] = useState<StepSuggestion[]>(() => {
if (cachedSuggestions.length > 0) return cachedSuggestions;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export {
useStepAutocomplete,
parseStepsFromJsonApi,
setStepsFetcher,
isStepAutocompleteOnTypeEnabled,
STEP_AUTOCOMPLETE_ATTRIBUTE,
type StepSuggestion,
type StepJsonApiDocument,
type StepJsonApiResource,
Expand Down
Loading