From 440eadb90fe4e17b6a07f98d82dd5ec4168900ed Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Fri, 23 Oct 2026 10:10:46 +0200 Subject: [PATCH] fix(rich-text-web): show the table toolbar with the advanced custom toolbar The advanced custom toolbar regroups buttons under generated group names, so the table operations toolbar never matched its "tableBetter" parent group. Match secondary groups by the parent group's buttons instead. Co-Authored-By: Claude Opus 5.5 --- .../rich-text-web/CHANGELOG.md | 4 ++ .../src/__tests__/RichText.runtime.spec.tsx | 54 +++++++++++++++++++ .../src/components/toolbars/Toolbar.tsx | 10 +--- .../src/components/toolbars/ToolbarConfig.ts | 15 ++++++ .../helpers/__tests__/ToolbarConfig.spec.ts | 23 +++++++- 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md index 5cf684c803..863d84278e 100644 --- a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md +++ b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md @@ -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 diff --git a/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.runtime.spec.tsx b/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.runtime.spec.tsx index de431f466a..5ab36df542 100644 --- a/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.runtime.spec.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/__tests__/RichText.runtime.spec.tsx @@ -385,4 +385,58 @@ describe("Rich Text runtime behaviour", () => { expect(container.querySelector(".tiptap-toolbar") !== null).toBe(hasToolbar); }); }); + + describe("table toolbar", () => { + const tableValue = "

Cell

"; + + function renderWithCaretInTable(props: Partial): RenderResult { + const result = render( + ().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(); + }); + }); }); diff --git a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx index e0785f741f..0f05c5cc3b 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx @@ -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"; @@ -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) { diff --git a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts index fb0de8aa2e..e0d5eed1df 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts +++ b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts @@ -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; + }); +} diff --git a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/__tests__/ToolbarConfig.spec.ts b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/__tests__/ToolbarConfig.spec.ts index d919ac7550..db117e08d9 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/__tests__/ToolbarConfig.spec.ts +++ b/packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/__tests__/ToolbarConfig.spec.ts @@ -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", () => { @@ -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([]); + }); +});