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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

VideoPress dashboard: truncate long video titles and filenames with an ellipsis in the library grid and table, and clamp long titles in the video details breadcrumb.
47 changes: 46 additions & 1 deletion projects/packages/videopress/routes/library/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@
// than an <a> because navigation is handled by the in-app router, not an
// href; styled to read as a link and inherit the cell's typography.
&__title-link {
display: inline;
margin: 0;
padding: 0;
border: 0;
Expand All @@ -206,6 +205,38 @@
}
}

// Truncate long titles with an ellipsis instead of letting them spill
// across neighbouring cards (grid) or into the next column (table).
// DataViews' own .dataviews-title-field truncation only targets its
// internal anchor/button markup, so custom renderers bring their own.
// `width: fit-content` keeps the button's hit area at text size while
// `max-width` provides the bound that makes text-overflow engage;
// `min-width: 0` lets the element shrink when it shares a flex row
// with a status pill. The full text stays reachable via the elements'
// `title` attribute (set in fields.tsx).
&__title-link,
&__title-text {
display: block;
width: fit-content;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

// Title + status pill row: cap the row at the cell width so the title
// (min-width: 0 above) shrinks and ellipsizes, and keep the pill from
// being crushed by a long title.
&__title-cell {
min-width: 0;
max-width: 100%;

> :last-child {
flex-shrink: 0;
}
}

// Table view: DataViews gives a 32x32 cell slot but doesn't force the img,
// and its cell wrapper uses `align-items: center` — so an img with a non-
// square intrinsic ratio collapses the wrapper to its content height (e.g.
Expand All @@ -229,6 +260,20 @@
&__failed {
display: none;
}

// Cap the two free-text columns so a pathological title + filename
// can't bloat their columns (DataViews caps cells at 80ch, which
// lets long text shove every other column out of the viewport).
// 36ch keeps the default column set within a ~1440px admin screen;
// narrower viewports fall back to the table's horizontal scroll.
&__title-link,
&__title-text {
max-width: 36ch;
}

&__filename {
max-width: 30ch;
}
}

// Status info (Local / Uploading 47% / Upload failed) renders as a Badge
Expand Down
7 changes: 6 additions & 1 deletion projects/packages/videopress/routes/video/stage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ const Editor = ( {
return (
<AdminPage
breadcrumbs={
<Breadcrumbs items={ [ getParentBreadcrumbItem(), { label: video.title } ] } />
// display: contents wrapper — a pure scoping hook so the
// stylesheet can clamp long video titles in the current-item
// crumb (Breadcrumbs' own class names are CSS-module hashes).
<div className="vp-video-details__breadcrumbs">
<Breadcrumbs items={ [ getParentBreadcrumbItem(), { label: video.title } ] } />
</div>
}
actions={
<HeaderActions
Expand Down
23 changes: 23 additions & 0 deletions projects/packages/videopress/routes/video/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@
}
}

// Scoping hook only — display: contents removes the wrapper's box from
// layout, so the breadcrumb list flows in the header exactly as before.
// The current-item crumb is targeted structurally (li:last-child h1)
// because @wordpress/admin-ui's Breadcrumbs uses hashed CSS-module class
// names. Long video titles are clamped so they can't crowd the header's
// Save button; the full title remains visible in the Title field below.
// Upstream defect this works around: Breadcrumbs ships its own
// `li:last-child { flex-shrink: 1; min-width: 0 }` intending exactly this
// truncation, but it never engages — the nav and the header's inner Stack
// are flex items left at `min-width: auto`, so no width constraint ever
// reaches the list. If that's fixed upstream, this block can shrink to
// just the max-width cap (or go away entirely).
.vp-video-details__breadcrumbs {
display: contents;

li:last-child h1 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: min(60ch, 45vw);
}
}

.vp-video-details__not-found {
padding: 48px 24px;
text-align: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,27 @@ const TitleText = ( { item }: { item: LibraryItem } ) => {
const { openVideoDetails } = useUploadActions();
const { type, upload, title, id } = item;

// `title` attributes expose the full text on hover; the elements
// themselves truncate with an ellipsis (see &__title-link /
// &__title-text in style.scss).
if ( type === 'videopress' && upload.status === 'idle' ) {
return (
<button
type="button"
className="vp-library__title-link"
title={ title }
onClick={ () => openVideoDetails( id ) }
>
{ title }
</button>
);
}

return <span>{ title }</span>;
return (
<span className="vp-library__title-text" title={ title }>
{ title }
</span>
);
};

const privacyLabel = ( privacy: LibraryItem[ 'privacy' ] ): string => {
Expand Down Expand Up @@ -116,7 +124,7 @@ export const libraryFields: Field< LibraryItem >[] = [
label: __( 'Filename', 'jetpack-videopress-pkg' ),
getValue: ( { item } ) => item.filename,
render: ( { item } ) => (
<Text variant="body-sm" className="vp-library__filename">
<Text variant="body-sm" className="vp-library__filename" title={ item.filename }>
{ item.filename }
</Text>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ const makeActions = ( overrides: Partial< UploadActions > = {} ): UploadActions
const renderField = ( ui: React.ReactNode, actions: UploadActions ) =>
render( <UploadActionsProvider value={ actions }>{ ui }</UploadActionsProvider> );

// The title cell render is whatever the exported `title` field declares.
// The cell renders are whatever the exported field declarations provide.
const TitleCellRender = ( libraryFields.find( f => f.id === 'title' ) as Field< LibraryItem > )
.render as ( args: { item: LibraryItem } ) => React.ReactNode;
const FilenameRender = ( libraryFields.find( f => f.id === 'filename' ) as Field< LibraryItem > )
.render as ( args: { item: LibraryItem } ) => React.ReactNode;

describe( 'ThumbnailField — grid Details access', () => {
it( 'renders an "Edit details" button that opens details for an idle VideoPress video', async () => {
Expand Down Expand Up @@ -114,4 +116,31 @@ describe( 'TitleCell — grid Details access', () => {
expect( screen.getByText( 'Raw Footage' ) ).toBeInTheDocument();
expect( screen.queryByRole( 'button', { name: 'Raw Footage' } ) ).not.toBeInTheDocument();
} );

it( 'exposes the full title via a title attribute on the clickable variant', () => {
const longTitle = 'A very long recording title that will be truncated with an ellipsis';
renderField( <TitleCellRender item={ item( { title: longTitle } ) } />, makeActions() );
expect( screen.getByRole( 'button', { name: longTitle } ) ).toHaveAttribute(
'title',
longTitle
);
} );

it( 'exposes the full title via a title attribute on the plain-text variant', () => {
const longTitle = 'A very long recording title that will be truncated with an ellipsis';
renderField(
<TitleCellRender item={ item( { type: 'local', title: longTitle } ) } />,
makeActions()
);
expect( screen.getByText( longTitle, { selector: 'span' } ) ).toHaveAttribute(
'title',
longTitle
);
} );

it( 'exposes the full filename via a title attribute (forwarded through the Text component)', () => {
const longName = 'a-very-long-filename-that-needs-truncation-in-the-table-layout.mov';
renderField( <FilenameRender item={ item( { filename: longName } ) } />, makeActions() );
expect( screen.getByText( longName ) ).toHaveAttribute( 'title', longName );
} );
} );
Loading