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
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/rich-text-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Fixed

- We fixed an issue where the table toolbar did not appear when the cursor was in a table and the custom toolbar was set to "Advanced" with a "Table" item.

## [5.1.1] - 2026-10-15

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,58 @@ describe("Rich Text runtime behaviour", () => {
expect(container.querySelector(".tiptap-toolbar") !== null).toBe(hasToolbar);
});
});

describe("table toolbar", () => {
const tableValue = "<table><tbody><tr><td><p>Cell</p></td></tr></tbody></table>";

function renderWithCaretInTable(props: Partial<RichTextContainerProps>): RenderResult {
const result = render(
<RichText
{...defaultProps}
{...props}
preset="custom"
stringAttribute={new EditableValueBuilder<string>().withValue(tableValue).build()}
/>
);
const editor = getEditor(result.container);
let cellTextPos = -1;
editor.state.doc.descendants((node, pos) => {
if (cellTextPos === -1 && node.isText) {
cellTextPos = pos;
}
});
act(() => {
editor.commands.setTextSelection(cellTextPos);
});
return result;
}

function tableConfigurationButton(container: HTMLElement): Element | null {
return container.querySelector('button[title="Table Configuration"]');
}

it("shows the table toolbar with the basic custom toolbar when the table group is enabled", () => {
const { container } = renderWithCaretInTable({ toolbarConfig: "basic", tableBetter: true });

expect(tableConfigurationButton(container)).toBeInTheDocument();
});

it("shows the table toolbar with the advanced custom toolbar when it contains the table item", () => {
const { container } = renderWithCaretInTable({
toolbarConfig: "advanced",
advancedConfig: [{ ctItemType: "bold" }, { ctItemType: "separator" }, { ctItemType: "tableBetter" }]
});

expect(tableConfigurationButton(container)).toBeInTheDocument();
});

it("hides the table toolbar with the advanced custom toolbar when it has no table item", () => {
const { container } = renderWithCaretInTable({
toolbarConfig: "advanced",
advancedConfig: [{ ctItemType: "bold" }]
});

expect(tableConfigurationButton(container)).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import { HelpButton } from "./components/HelpButton";
import { TableGridToolbarButton } from "./components/TableGrid";
import { ToolbarButton } from "./components/ToolbarButton";
import {
SECONDARY_TOOLBAR_GROUP,
ToolbarContext,
DropdownCommand,
ToolbarGroupConfig,
ToolbarButtonConfig,
getFilteredToolbarGroups,
getSecondaryToolbarGroups,
ToolbarGroupsConfig,
TOOLBAR_GROUPS
} from "./ToolbarConfig";
Expand Down Expand Up @@ -178,13 +178,7 @@ export default function Toolbar(props: ToolbarProps): ReactElement | null {
// (preset "full", or custom with every group enabled).
const showHelpButton = helpButton !== false && filteredGroups.length === TOOLBAR_GROUPS.length;

const filteredSecondaryGroups = useMemo(
() =>
SECONDARY_TOOLBAR_GROUP.filter(group =>
filteredGroups.some(filteredGroup => filteredGroup.name === group.parentName)
),
[filteredGroups]
);
const filteredSecondaryGroups = useMemo(() => getSecondaryToolbarGroups(filteredGroups), [filteredGroups]);

useEffect(() => {
if (!editor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,3 +807,18 @@ export function getFilteredToolbarGroups(

return enhancedToolbarGroups(filteredGroups, customFonts);
}

/**
* Get the secondary toolbar groups that belong to the given toolbar groups.
* A secondary group is included when any button of its parent group is present. Buttons are
* matched by name because the advanced custom toolbar regroups them under generated group names.
* @param groups - Filtered toolbar groups
* @returns Secondary toolbar groups to render
*/
export function getSecondaryToolbarGroups(groups: ToolbarGroupConfig[]): ToolbarGroupConfig[] {
const buttonNames = new Set(groups.flatMap(group => group.buttons.map(button => button.name)));
return SECONDARY_TOOLBAR_GROUP.filter(secondaryGroup => {
const parentGroup = TOOLBAR_GROUPS.find(group => group.name === secondaryGroup.parentName);
return parentGroup?.buttons.some(button => buttonNames.has(button.name)) ?? false;
});
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildAdvancedToolbar } from "../../ToolbarConfig";
import { buildAdvancedToolbar, getFilteredToolbarGroups, getSecondaryToolbarGroups } from "../../ToolbarConfig";

describe("buildAdvancedToolbar", () => {
it("maps the header item to the text format dropdown", () => {
Expand All @@ -9,3 +9,24 @@ describe("buildAdvancedToolbar", () => {
expect(groups[0].buttons[0].name).toBe("textFormat");
});
});

describe("getSecondaryToolbarGroups", () => {
it("includes the table operations for a preset that has the table group", () => {
const groups = getSecondaryToolbarGroups(getFilteredToolbarGroups("full"));

expect(groups.map(group => group.name)).toEqual(["table-operations"]);
});

it("includes the table operations for an advanced toolbar with the table item in any group", () => {
const groups = getSecondaryToolbarGroups(
buildAdvancedToolbar([{ ctItemType: "bold" }, { ctItemType: "separator" }, { ctItemType: "tableBetter" }])
);

expect(groups.map(group => group.name)).toEqual(["table-operations"]);
});

it("excludes the table operations when no group has the table button", () => {
expect(getSecondaryToolbarGroups(getFilteredToolbarGroups("basic"))).toEqual([]);
expect(getSecondaryToolbarGroups(buildAdvancedToolbar([{ ctItemType: "bold" }]))).toEqual([]);
});
});
Loading