diff --git a/.changeset/11684-highlight-share-row.md b/.changeset/11684-highlight-share-row.md new file mode 100644 index 0000000000..8b67b81c8c --- /dev/null +++ b/.changeset/11684-highlight-share-row.md @@ -0,0 +1,7 @@ +--- +'@object-ui/plugin-detail': patch +--- + +**The record header's highlight chips share the row's free width and truncate only when it runs out (objectui#11684).** A Product with SKU "QA Widget 1" read "QA Wid…" in the record drawer's highlight row, with most of the row empty. Every chip in `HeaderHighlight` was a fixed column: a 9rem basis (16rem for wide types and for a column being edited), no grow, and a 16rem / 24rem cap. A value wider than the chip was clipped, however much room the row had left. + +A chip's basis is now its floor, and the chips grow into the line's free width. Each chip is capped at its own content, so a chip that fits stops growing and the rest of the free width goes to the chips that still need it. Which chips share a line does not change, because line breaking still reads the floors. A short value keeps its 9rem column, and a sparse strip still packs left, because no chip is ever wider than what it shows. A value is cut with an ellipsis only once its line has no free width left. The whole value stays available as the hover title. A column being edited still takes the 16rem floor, and it can grow to its editor's natural width. diff --git a/packages/plugin-detail/src/HeaderHighlight.tsx b/packages/plugin-detail/src/HeaderHighlight.tsx index 86089cadcf..98d218122a 100644 --- a/packages/plugin-detail/src/HeaderHighlight.tsx +++ b/packages/plugin-detail/src/HeaderHighlight.tsx @@ -217,12 +217,32 @@ export const HeaderHighlight: React.FC = ({ // expand-on-edit) instead of cramming it into a 9rem column. const useWide = isWide || editorActive; + // How the row's width is shared (objectui#11684). The basis is the + // column's FLOOR, and line breaking still reads it, so which chips + // share a line is what it always was. `grow` then hands the line's + // free width to the columns, and `max-w-max` caps each one at its + // own content: a column that already fits stops growing and the + // rest of the free width goes on to the columns that still need + // it. A value truncates only once its line has no free width left. + // + // The old fixed caps (16rem / 24rem, no grow) cut "QA Widget 1" to + // "QA Wid…" in a 9rem chip with most of the drawer row empty. The + // content cap keeps what those caps were for: no column is ever + // wider than what it shows, so a sparse strip still packs left + // and the hover pencil stays beside its value. + // + // The floor is a min-width as well as the basis, so a short value + // keeps its 9rem column. The `min(…, 100%)` lets a column narrow to + // the row when the row itself is narrower than the floor. return (
diff --git a/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx new file mode 100644 index 0000000000..16c89f7f41 --- /dev/null +++ b/packages/plugin-detail/src/__tests__/HeaderHighlight.shareRowWidth-11684.test.tsx @@ -0,0 +1,147 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#11684: the header highlights share the row's free width and + * truncate only when it runs out. + * + * The 2026-10-06 console sweep created a Product with SKU "QA Widget 1" and + * read "QA Wid…" in the record drawer's highlights with most of the row empty. + * Every chip was a fixed column: a `basis-[9rem]` with no grow and a 16rem + * cap. A value wider than the chip's ~79px of content was clipped, however much + * of the row was left. + * + * The distribution now has three parts, and each is pinned below: + * - the basis stays the FLOOR, so line breaking (which chips share a line) + * reads what it always read; + * - `grow` hands the line's free width to the chips; + * - `max-w-max` caps each chip at its own content, so a chip that fits stops + * growing and the free width goes on to the chips that still need it. No + * chip is wider than what it shows, which is what kept a sparse strip + * packed left under the old fixed caps. + * + * happy-dom has no layout engine, so these are class-semantics pins, not pixel + * measurements (the same approach as + * `RecordHighlightsRenderer.phoneWidth-11659.test.tsx`). The Chromium reading + * they rest on is in the pull request that landed this file. It was taken in + * the console's own drawer at its default width, before and after. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import * as React from 'react'; +import type { HighlightField } from '@object-ui/types'; +import { InlineEditProvider, RecordContextProvider } from '@object-ui/react'; +import { HeaderHighlight } from '../HeaderHighlight'; +import { RecordHighlightsRenderer } from '../renderers/record-highlights'; + +/** The value the sweep measured, in both chips it was cut in. */ +const SKU = 'QA Widget 1'; + +/** The showcase Product's own field types (`showcase_product`). */ +const productSchema = { + fields: { + name: { type: 'text', label: 'Name' }, + sku: { type: 'text', label: 'SKU' }, + description: { type: 'text', label: 'Description' }, + unit_price: { type: 'currency', label: 'Unit Price' }, + }, +}; + +/** The fixed caps and the shrink floor this card retired. */ +const RETIRED = ['max-w-[16rem]', 'max-w-[24rem]', 'min-w-[7rem]']; + +const renderProductStrip = () => + render( + + + , + ); + +/** The chip column (`group flex flex-col …`) around an element. */ +const chipAround = (el: Element) => el.closest('div.group') as HTMLElement; + +describe('record:highlights shares the row instead of fixing each chip (objectui#11684)', () => { + it('gives every chip carrying the measured SKU a floor, a grow and a content cap', () => { + renderProductStrip(); + + const chips = screen.getAllByText(SKU).map(chipAround); + // SKU and Description: the sweep read "QA Wid…" in both. + expect(chips).toHaveLength(2); + for (const chip of chips) { + expect(chip).toHaveClass('grow', 'max-w-max', 'basis-[9rem]', 'min-w-[min(9rem,100%)]'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + } + }); + + it('keeps truncation for the case the line DOES run out, with the whole value as the title', () => { + renderProductStrip(); + + for (const text of screen.getAllByText(SKU)) { + const clipBox = chipAround(text).querySelector('span.truncate'); + expect(clipBox).not.toBeNull(); + expect(clipBox).toHaveAttribute('title', SKU); + } + }); + + it('applies the same three parts to a short chip, so no chip keeps a fixed width', () => { + const { container } = renderProductStrip(); + + const chips = Array.from(container.querySelectorAll('div.group')); + expect(chips).toHaveLength(3); + for (const chip of chips) { + expect(chip).toHaveClass('grow', 'max-w-max'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + } + }); + + it('still lays the chips out on a wrapping flex row', () => { + const { container } = renderProductStrip(); + + const row = container.querySelector('div.group')?.parentElement; + expect(row).toHaveClass('flex', 'flex-wrap'); + }); +}); + +describe('the wide floor keeps its role under the shared row (objectui#11684)', () => { + const fields: HighlightField[] = [ + { name: 'owner', label: 'Owner', type: 'text' }, + { name: 'email', label: 'Email', type: 'email' }, + ]; + const data = { owner: 'Alice', email: 'ada@example.com' }; + + it('gives a wide display type the 16rem floor, the grow and the content cap', () => { + render(); + + const chip = chipAround(screen.getByText('ada@example.com')); + expect(chip).toHaveClass('grow', 'max-w-max', 'basis-[16rem]', 'min-w-[min(16rem,100%)]'); + expect(chip).not.toHaveClass('basis-[9rem]'); + for (const retired of RETIRED) expect(chip).not.toHaveClass(retired); + }); + + it('widens an editing column to the 16rem floor, which still grows (expand-on-edit)', () => { + render( + + + , + ); + const readChip = chipAround(screen.getByText('Alice')); + expect(readChip).toHaveClass('basis-[9rem]', 'min-w-[min(9rem,100%)]'); + + fireEvent.doubleClick(screen.getByText('Alice')); + + const editChip = chipAround(screen.getByDisplayValue('Alice')); + expect(editChip).toHaveClass('grow', 'max-w-max', 'basis-[16rem]', 'min-w-[min(16rem,100%)]'); + expect(editChip).not.toHaveClass('basis-[9rem]'); + }); +}); diff --git a/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx index faf380f823..4daa550550 100644 --- a/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx +++ b/packages/plugin-detail/src/__tests__/RecordHighlightsRenderer.percentClip.test.tsx @@ -8,8 +8,9 @@ /** * objectstack#5066 — a `percent` chip in `record:highlights` showed a smaller, - * plausible number. The chip clips with `truncate` (`basis-[9rem]`, shrinking - * toward `min-w-[7rem]` as more chips are added, so ~72-104px of content), and + * plausible number. The chip clips with `truncate` (then a fixed `basis-[9rem]` + * column, ~72-104px of content; since objectui#11684 a chip grows into its + * line's free width, but a full line still clips it), and * the percent display renderer put a `w-16 shrink-0` bar in front of a * shrinkable value span: bar (64) + gap (8) left the value 0-32px and `truncate` * removed the rest. Downstream a stored `33.33` was read off the screen as `3`