diff --git a/.changeset/connectable-item-tooltip.md b/.changeset/connectable-item-tooltip.md new file mode 100644 index 000000000..fb2e23c80 --- /dev/null +++ b/.changeset/connectable-item-tooltip.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/sdk': patch +--- + +Decision branch rows and AI tool rows show the full label in a tooltip when the text is truncated. diff --git a/.changeset/node-text-ellipsis.md b/.changeset/node-text-ellipsis.md new file mode 100644 index 000000000..a817db82e --- /dev/null +++ b/.changeset/node-text-ellipsis.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/ui': patch +--- + +`NodeDescription` keeps both the title and the subtitle on one line: long text is truncated with an ellipsis and exposed in full through the element's native tooltip, and the text no longer widens nodes whose body sizes to its content (for example the Decision node). Nodes keep the fixed design width and grow in height only. diff --git a/apps/docs/src/content/docs/ui-library/diagram-components/node-description.mdx b/apps/docs/src/content/docs/ui-library/diagram-components/node-description.mdx index 3b9698f1d..e914f4b46 100644 --- a/apps/docs/src/content/docs/ui-library/diagram-components/node-description.mdx +++ b/apps/docs/src/content/docs/ui-library/diagram-components/node-description.mdx @@ -30,6 +30,12 @@ function NodeHeader({ icon, label, description }) { } ``` +## Truncation + +Both lines are single-line. Text that does not fit the node width is cut with an ellipsis and the +full text is available through the element's native tooltip (`title`). The block never widens its +node: a node keeps the design width and grows in height only through its body content. + ## Props diff --git a/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.spec.tsx b/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.spec.tsx new file mode 100644 index 000000000..087005930 --- /dev/null +++ b/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.spec.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { ConnectableItem } from './connectable-item'; + +vi.mock('@xyflow/react', () => ({ + Handle: ({ id }: { id: string }) => , + Position: { Right: 'right', Bottom: 'bottom' }, +})); + +vi.mock('../../../../../store/store', () => ({ + useStore: (selector: (state: { layoutDirection: 'RIGHT' | 'DOWN' }) => unknown) => + selector({ layoutDirection: 'RIGHT' }), +})); + +describe('ConnectableItem', () => { + it('exposes the full label as a tooltip so a clipped row stays readable', () => { + const label = 'Small and medium business with an unusually long branch label'; + + render(); + + expect(screen.getByText(label).getAttribute('title')).toBe(label); + expect(screen.getByTestId('handle').dataset.handleId).toBe('source:inner:b2'); + }); +}); diff --git a/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.tsx b/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.tsx index fe1ec7c32..273b5cf12 100644 --- a/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.tsx +++ b/packages/sdk/src/features/diagram/nodes/components/connectable-item/connectable-item.tsx @@ -22,7 +22,9 @@ export function ConnectableItem({ handleId, label, canHaveBottomHandle = true }: [styles['connectable-item--right']]: layoutDirection === 'RIGHT', })} > -
{label}
+
+ {label} +
diff --git a/packages/ui/src/components/node/node-description/node-description.module.css b/packages/ui/src/components/node/node-description/node-description.module.css index 2ab9ca640..9981c910b 100644 --- a/packages/ui/src/components/node/node-description/node-description.module.css +++ b/packages/ui/src/components/node/node-description/node-description.module.css @@ -11,15 +11,21 @@ align-items: flex-start; overflow: hidden; width: 100%; + min-width: 0; + contain: inline-size; - .title { - color: var(--wb-public-node-title-color); + .title, + .subtitle { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; width: 100%; } + .title { + color: var(--wb-public-node-title-color); + } + .subtitle { color: var(--wb-public-node-title-subtitle); } diff --git a/packages/ui/src/components/node/node-description/node-description.spec.tsx b/packages/ui/src/components/node/node-description/node-description.spec.tsx new file mode 100644 index 000000000..30cdd515d --- /dev/null +++ b/packages/ui/src/components/node/node-description/node-description.spec.tsx @@ -0,0 +1,39 @@ +import { act } from 'react'; +import { type Root, createRoot } from 'react-dom/client'; + +import { NodeDescription } from './node-description'; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +let container: HTMLDivElement; +let root: Root; + +beforeEach(() => { + container = document.createElement('div'); + document.body.append(container); + root = createRoot(container); +}); + +afterEach(() => { + act(() => root.unmount()); + container.remove(); +}); + +describe('NodeDescription', () => { + it('exposes the full label and description as native tooltips for truncated text', () => { + act(() => + root.render(), + ); + + const [title, subtitle] = container.querySelectorAll('span'); + expect(title.getAttribute('title')).toBe('Route by Type'); + expect(subtitle.getAttribute('title')).toBe('Sends the ticket to the right responder.'); + }); + + it('renders no tooltip attribute when there is no description', () => { + act(() => root.render()); + + const [, subtitle] = container.querySelectorAll('span'); + expect(subtitle.hasAttribute('title')).toBe(false); + }); +}); diff --git a/packages/ui/src/components/node/node-description/node-description.tsx b/packages/ui/src/components/node/node-description/node-description.tsx index daab9ce01..80a8a9bd5 100644 --- a/packages/ui/src/components/node/node-description/node-description.tsx +++ b/packages/ui/src/components/node/node-description/node-description.tsx @@ -11,8 +11,12 @@ export type NodeDescriptionProps = { export function NodeDescription({ label, description, className }: NodeDescriptionProps) { return (
- {label} - {description} + + {label} + + + {description} +
); }