Skip to content

Commit 53f6b5a

Browse files
authored
Make blueprints previews adjustable to window height (#1750)
1 parent 355c730 commit 53f6b5a

File tree

2 files changed

+123
-33
lines changed

2 files changed

+123
-33
lines changed

src/modules/add-site/components/blueprints.tsx

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import StudioButton from 'src/components/button';
1717
import { cx } from 'src/lib/cx';
1818
import { getIpcApi } from 'src/lib/get-ipc-api';
1919
import { useGetBlueprints } from 'src/stores/wpcom-api';
20+
import { useOverflowItems } from '../hooks/use-overflow-items';
2021

2122
interface Blueprint {
2223
slug: string;
@@ -47,7 +48,48 @@ interface AddSiteBlueprintProps {
4748
onFileBlueprintSelect?: ( blueprint: Blueprint ) => void;
4849
}
4950

50-
const MAX_BLUEPRINTS_CATEGORIES = 3;
51+
function CategoryBadges( { categories }: { categories: string[] } ) {
52+
const { __ } = useI18n();
53+
const containerRef = useRef< HTMLDivElement >( null );
54+
const { visible, hidden, hiddenCount, itemRefs } = useOverflowItems( categories, containerRef );
55+
56+
return (
57+
<HStack ref={ containerRef } spacing={ 3 } alignment="left" className="w-full">
58+
{ categories.map( ( category, index ) => (
59+
<Text
60+
as="span"
61+
key={ category }
62+
ref={ ( el ) => {
63+
itemRefs.current[ index ] = el;
64+
} }
65+
className="px-2.5 py-1 text-xs bg-gray-100 text-gray-700 rounded-sm flex items-center flex-shrink-0 max-w-32 truncate"
66+
style={ {
67+
visibility: index < visible.length ? 'visible' : 'hidden',
68+
position: index >= visible.length ? 'absolute' : 'static',
69+
} }
70+
>
71+
{ category }
72+
</Text>
73+
) ) }
74+
{ hiddenCount > 0 && (
75+
<Tooltip
76+
text={ hidden.join( ', ' ) }
77+
delay={ 200 }
78+
placement="top-end"
79+
className="max-w-xs"
80+
>
81+
<Text
82+
as="span"
83+
className="px-2.5 py-1 text-xs bg-gray-100 text-gray-700 rounded-sm flex items-center font-medium whitespace-nowrap flex-shrink-0"
84+
>
85+
{ /* translators: %d: Number of hidden categories */ }
86+
{ sprintf( __( '+%d more' ), hiddenCount ) }
87+
</Text>
88+
</Tooltip>
89+
) }
90+
</HStack>
91+
);
92+
}
5193

5294
export function AddSiteBlueprintSelector( {
5395
blueprints,
@@ -108,6 +150,7 @@ export function AddSiteBlueprintSelector( {
108150
alt={ item.title }
109151
className={ cx(
110152
'w-full h-32 object-cover object-top cursor-pointer transition-all duration-150 rounded-lg group',
153+
'[@media(min-height:680px)]:h-48',
111154
'hover:shadow-md hover:outline hover:outline-2 hover:outline-blue-500',
112155
'transition-transform duration-150',
113156
'hover:scale-105',
@@ -154,37 +197,7 @@ export function AddSiteBlueprintSelector( {
154197
const categories = ( item.blueprint.meta?.categories || [] ).filter(
155198
( category ) => category !== 'Studio'
156199
);
157-
const visibleCategories = categories.slice( 0, MAX_BLUEPRINTS_CATEGORIES );
158-
const remainingCount = categories.length - MAX_BLUEPRINTS_CATEGORIES;
159-
160-
return (
161-
<HStack spacing={ 3 } wrap alignment="left">
162-
{ visibleCategories.map( ( category ) => (
163-
<Text
164-
as="span"
165-
key={ category }
166-
className="px-2.5 py-1 text-xs bg-gray-100 text-gray-700 rounded-sm flex items-center"
167-
>
168-
{ category }
169-
</Text>
170-
) ) }
171-
{ remainingCount > 0 && (
172-
<Tooltip
173-
text={ categories.slice( MAX_BLUEPRINTS_CATEGORIES ).join( ', ' ) }
174-
delay={ 200 }
175-
position="top right"
176-
className="max-w-xs"
177-
>
178-
<Text
179-
as="span"
180-
className="px-2.5 py-1 text-xs bg-gray-100 text-gray-700 rounded-sm flex items-center font-medium"
181-
>
182-
+{ remainingCount } more
183-
</Text>
184-
</Tooltip>
185-
) }
186-
</HStack>
187-
);
200+
return <CategoryBadges categories={ categories } />;
188201
},
189202
},
190203
{
@@ -357,7 +370,7 @@ export function AddSiteBlueprintSelector( {
357370
) }
358371
</HStack>
359372

360-
<div className="w-full px-3 [&_.dataviews-view-grid]:!grid [&_.dataviews-view-grid]:!grid-cols-3 [&_.dataviews-view-grid]:!gap-4 [&_.dataviews-view-grid]:!items-start [&_.components-badge]:!bg-transparent [&_.components-badge]:!p-0">
373+
<div className="w-full px-3 [&_.dataviews-view-grid]:!grid [&_.dataviews-view-grid]:!grid-cols-3 [&_.dataviews-view-grid]:!gap-4 [&_.dataviews-view-grid]:!items-start [&_.components-badge]:!bg-transparent [&_.components-badge]:!p-0 [&_.components-badge]:!w-full [&_.components-badge_.components-badge__content]:!w-full [&_.components-badge>*]:!w-full">
361374
{ isFetchingBlueprints && (
362375
<Text className="text-[14px] block text-center py-[100px]">
363376
{ __( 'Loading blueprints...' ) }
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useLayoutEffect, useState, useRef, useCallback } from 'react';
2+
3+
// Constants for layout calculations
4+
const ITEM_SPACING = 12; // Gap between category items in pixels
5+
const MORE_BUTTON_WIDTH = 70; // Estimated width of "+X more" button in pixels
6+
const DEBOUNCE_DELAY = 50; // Delay in milliseconds for debounced recalculation
7+
const TOLERANCE = 10; // Extra pixels to account for measurement precision
8+
9+
export function useOverflowItems( items: string[], containerRef: React.RefObject< HTMLElement > ) {
10+
const [ visibleCount, setVisibleCount ] = useState( items.length );
11+
const itemRefs = useRef< ( HTMLElement | null )[] >( [] );
12+
const lastWidth = useRef( 0 );
13+
const debounceTimeout = useRef< NodeJS.Timeout | null >( null );
14+
15+
const calculate = useCallback( () => {
16+
if ( ! containerRef.current || items.length === 0 ) {
17+
return;
18+
}
19+
20+
const container = containerRef.current;
21+
const containerWidth = container.offsetWidth;
22+
23+
if ( containerWidth === 0 || containerWidth === lastWidth.current ) {
24+
return;
25+
}
26+
27+
lastWidth.current = containerWidth;
28+
29+
let totalWidth = 0;
30+
let count = 0;
31+
32+
for ( let i = 0; i < itemRefs.current.length; i++ ) {
33+
const item = itemRefs.current[ i ];
34+
if ( ! item ) continue;
35+
36+
const itemWidth = item.offsetWidth;
37+
const spacing = i > 0 ? ITEM_SPACING : 0;
38+
const moreButtonSpace = i < items.length - 1 ? MORE_BUTTON_WIDTH : 0;
39+
40+
if ( totalWidth + spacing + itemWidth + moreButtonSpace <= containerWidth ) {
41+
totalWidth += spacing + itemWidth;
42+
count = i + 1;
43+
} else {
44+
break;
45+
}
46+
}
47+
48+
if ( count === items.length - 1 && itemRefs.current[ items.length - 1 ] ) {
49+
const lastItem = itemRefs.current[ items.length - 1 ];
50+
const lastItemWidth = lastItem?.offsetWidth || 0;
51+
if ( totalWidth + ITEM_SPACING + lastItemWidth <= containerWidth + TOLERANCE ) {
52+
count = items.length;
53+
}
54+
}
55+
56+
setVisibleCount( Math.max( 1, count ) );
57+
}, [ items, containerRef ] );
58+
59+
useLayoutEffect( () => {
60+
if ( debounceTimeout.current ) {
61+
clearTimeout( debounceTimeout.current );
62+
}
63+
debounceTimeout.current = setTimeout( calculate, DEBOUNCE_DELAY );
64+
return () => {
65+
if ( debounceTimeout.current ) {
66+
clearTimeout( debounceTimeout.current );
67+
}
68+
};
69+
}, [ calculate ] );
70+
71+
return {
72+
visible: items.slice( 0, visibleCount ),
73+
hidden: items.slice( visibleCount ),
74+
hiddenCount: items.length - visibleCount,
75+
itemRefs,
76+
};
77+
}

0 commit comments

Comments
 (0)