Skip to content

Commit 029935d

Browse files
authored
Merge branch 'trunk' into fix/wp-env-lazy-descriptive-cache-dir
2 parents 59c7203 + f1c758b commit 029935d

9 files changed

Lines changed: 240 additions & 40 deletions

File tree

packages/base-styles/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44

55
### Breaking Changes
66

7-
- Remove the following entries from the `z-index()` helper ([#77619](https://github.com/WordPress/gutenberg/pull/77619), [#77620](https://github.com/WordPress/gutenberg/pull/77620), [#77621](https://github.com/WordPress/gutenberg/pull/77621), [#77714](https://github.com/WordPress/gutenberg/pull/77714), [#77715](https://github.com/WordPress/gutenberg/pull/77715), [#77717](https://github.com/WordPress/gutenberg/pull/77717)):
7+
- Remove the following entries from the `z-index()` helper ([#77619](https://github.com/WordPress/gutenberg/pull/77619), [#77620](https://github.com/WordPress/gutenberg/pull/77620), [#77621](https://github.com/WordPress/gutenberg/pull/77621), [#77714](https://github.com/WordPress/gutenberg/pull/77714), [#77715](https://github.com/WordPress/gutenberg/pull/77715), [#77717](https://github.com/WordPress/gutenberg/pull/77717), [#77774](https://github.com/WordPress/gutenberg/pull/77774)):
88
- `.block-editor-block-contextual-toolbar`
99
- `.block-editor-block-list__block {core/image aligned wide or fullwide}`
1010
- `.block-editor-block-list__block::before`
1111
- `.block-editor-block-list__block.has-block-overlay`
12+
- `.block-editor-block-list__block .reusable-block-edit-panel *`
1213
- `.block-editor-block-list__insertion-point`
1314
- `.block-editor-block-switcher__arrow`
1415
- `.block-editor-url-input__suggestions`

packages/base-styles/_z-index.scss

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ $z-layers: (
3838
// The draggable element should show up above the entire UI
3939
".components-draggable__clone": 1000000000,
4040

41-
".block-editor-block-list__block .reusable-block-edit-panel *": 1,
42-
4341
// Show drop zone above most standard content, but below any overlays
4442
".components-drop-zone": 40,
4543
".components-drop-zone__content": 50,

packages/block-editor/src/components/block-list/content.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,6 @@ _::-webkit-full-page-media, _:future, :root [data-has-multi-selection="true"] .b
153153
}
154154
}
155155

156-
.reusable-block-edit-panel * {
157-
z-index: z-index(".block-editor-block-list__block .reusable-block-edit-panel *");
158-
}
159-
160156
/**
161157
* Notices
162158
*/

packages/grid/src/resolve-fill-widths.ts

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import type { DashboardGridLayoutItem } from './types';
55

66
/**
77
* Resolves items with `width: 'fill'` by computing how many columns they
8-
* should span. Simulates CSS Grid row packing to determine remaining space
9-
* in each row, then assigns that space to fill items.
8+
* should span. Simulates CSS Grid's row-sparse auto-flow placement so the
9+
* resolved span matches the free run that CSS Grid will actually use.
1010
*
11-
* Complexity: O(n). The inner look-ahead breaks at the next fill or full
12-
* item, so the fixed items between two fills are scanned exactly once —
13-
* each fixed item is visited by at most one fill's look-ahead.
11+
* Two paths:
12+
* - Fast path (no `height > 1` items): single-row column tracker, O(n).
13+
* Each fixed item between two fills is visited at most once by a fill's
14+
* look-ahead.
15+
* - Multi-row path (any item with `height > 1`): per-column skyline that
16+
* tracks shadow occupation of tall tiles. Placement scans rows in
17+
* row-major order, so worst-case cost depends on both columns and rows
18+
* scanned (rows bounded by the sum of item heights), not only on
19+
* `maxColumns`.
1420
*
1521
* @param sortedKeys - Item keys in display order.
1622
* @param layoutMap - Map of key to DashboardGridLayoutItem.
@@ -25,11 +31,13 @@ export function resolveFillWidths(
2531
const resolved = new Map< string, number >();
2632
const n = sortedKeys.length;
2733

28-
// Pre-extract items into a flat array and pre-compute clamped widths.
29-
// This avoids repeated Map.get() and Math.min() calls in the hot loops.
34+
// Pre-extract items, clamp widths and heights, detect which path to take.
3035
const items = new Array< DashboardGridLayoutItem | undefined >( n );
3136
const widths = new Array< number >( n );
37+
const heights = new Array< number >( n );
3238
let hasFill = false;
39+
let hasMultiRow = false;
40+
let totalRows = 0;
3341

3442
for ( let i = 0; i < n; i++ ) {
3543
const item = layoutMap.get( sortedKeys[ i ] );
@@ -38,31 +46,127 @@ export function resolveFillWidths(
3846
item && typeof item.width === 'number'
3947
? Math.min( item.width, maxColumns )
4048
: 1;
49+
// Clamp to a positive integer so `0`, fractional, or negative
50+
// values match the `|| 1` defaulting used in GridItem styles.
51+
const h = Math.max( 1, Math.floor( item?.height ?? 1 ) );
52+
heights[ i ] = h;
4153
if ( item?.width === 'fill' ) {
4254
hasFill = true;
4355
}
56+
if ( h > 1 ) {
57+
hasMultiRow = true;
58+
}
59+
totalRows += h;
4460
}
4561

4662
if ( ! hasFill ) {
4763
return resolved;
4864
}
4965

50-
let currentCol = 0;
66+
if ( ! hasMultiRow ) {
67+
let currentCol = 0;
68+
69+
for ( let i = 0; i < n; i++ ) {
70+
const item = items[ i ];
71+
if ( ! item ) {
72+
continue;
73+
}
74+
75+
if ( item.width === 'full' ) {
76+
currentCol = 0;
77+
continue;
78+
}
79+
80+
if ( item.width === 'fill' ) {
81+
let reserved = 0;
82+
for ( let j = i + 1; j < n; j++ ) {
83+
const next = items[ j ];
84+
if (
85+
! next ||
86+
next.width === 'full' ||
87+
next.width === 'fill'
88+
) {
89+
break;
90+
}
91+
const nextW = widths[ j ];
92+
if ( currentCol + 1 + reserved + nextW <= maxColumns ) {
93+
reserved += nextW;
94+
} else {
95+
break;
96+
}
97+
}
98+
const fillCols = Math.max(
99+
1,
100+
maxColumns - currentCol - reserved
101+
);
102+
resolved.set( item.key, fillCols );
103+
currentCol += fillCols;
104+
} else {
105+
const w = widths[ i ];
106+
if ( currentCol + w > maxColumns ) {
107+
currentCol = 0;
108+
}
109+
currentCol += w;
110+
}
111+
112+
if ( currentCol >= maxColumns ) {
113+
currentCol = 0;
114+
}
115+
}
116+
117+
return resolved;
118+
}
119+
120+
// `rowOccupancy[ col ]` is the index of the next free row at that
121+
// column; rows below it are taken by previously placed items. This
122+
// captures the "shadow" of tall tiles into the rows they span.
123+
const rowOccupancy = new Array< number >( maxColumns ).fill( 0 );
124+
let cursorRow = 0;
125+
let cursorCol = 0;
51126

52127
for ( let i = 0; i < n; i++ ) {
53128
const item = items[ i ];
54129
if ( ! item ) {
55130
continue;
56131
}
57132

133+
const h = heights[ i ];
134+
58135
if ( item.width === 'full' ) {
59-
currentCol = 0;
136+
let r = cursorRow;
137+
for ( let c = 0; c < maxColumns; c++ ) {
138+
if ( rowOccupancy[ c ] > r ) {
139+
r = rowOccupancy[ c ];
140+
}
141+
}
142+
for ( let c = 0; c < maxColumns; c++ ) {
143+
rowOccupancy[ c ] = r + h;
144+
}
145+
cursorRow = r + h;
146+
cursorCol = 0;
60147
continue;
61148
}
62149

63150
if ( item.width === 'fill' ) {
64-
// Look ahead: reserve columns for subsequent
65-
// non-fill items that fit in this row.
151+
let r = cursorRow;
152+
let c = cursorCol;
153+
scan: for ( ; r <= totalRows; r++ ) {
154+
const start = r === cursorRow ? cursorCol : 0;
155+
for ( c = start; c < maxColumns; c++ ) {
156+
if ( rowOccupancy[ c ] <= r ) {
157+
break scan;
158+
}
159+
}
160+
}
161+
const fillStartRow = r;
162+
const fillStartCol = c;
163+
let runLength = 0;
164+
while (
165+
fillStartCol + runLength < maxColumns &&
166+
rowOccupancy[ fillStartCol + runLength ] <= fillStartRow
167+
) {
168+
runLength++;
169+
}
66170
let reserved = 0;
67171
for ( let j = i + 1; j < n; j++ ) {
68172
const next = items[ j ];
@@ -74,28 +178,46 @@ export function resolveFillWidths(
74178
break;
75179
}
76180
const nextW = widths[ j ];
77-
// 1 = minimum span for the fill item itself
78-
if ( currentCol + 1 + reserved + nextW <= maxColumns ) {
181+
if ( 1 + reserved + nextW <= runLength ) {
79182
reserved += nextW;
80183
} else {
81184
break;
82185
}
83186
}
84-
85-
const fillCols = Math.max( 1, maxColumns - currentCol - reserved );
187+
const fillCols = Math.max( 1, runLength - reserved );
86188
resolved.set( item.key, fillCols );
87-
currentCol += fillCols;
88-
} else {
89-
const w = widths[ i ];
90-
if ( currentCol + w > maxColumns ) {
91-
currentCol = 0;
189+
for ( let k = 0; k < fillCols; k++ ) {
190+
rowOccupancy[ fillStartCol + k ] = fillStartRow + h;
92191
}
93-
currentCol += w;
192+
cursorRow = fillStartRow;
193+
cursorCol = fillStartCol + fillCols;
194+
continue;
94195
}
95196

96-
if ( currentCol >= maxColumns ) {
97-
currentCol = 0;
197+
const w = widths[ i ];
198+
let r = cursorRow;
199+
let c = cursorCol;
200+
place: for ( ; r <= totalRows; r++ ) {
201+
c = r === cursorRow ? cursorCol : 0;
202+
while ( c + w <= maxColumns ) {
203+
let blocked = -1;
204+
for ( let k = 0; k < w; k++ ) {
205+
if ( rowOccupancy[ c + k ] > r ) {
206+
blocked = c + k;
207+
break;
208+
}
209+
}
210+
if ( blocked === -1 ) {
211+
break place;
212+
}
213+
c = blocked + 1;
214+
}
215+
}
216+
for ( let k = 0; k < w; k++ ) {
217+
rowOccupancy[ c + k ] = r + h;
98218
}
219+
cursorRow = r;
220+
cursorCol = c + w;
99221
}
100222

101223
return resolved;

packages/grid/src/test/resolve-fill-widths.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,68 @@ describe( 'resolveFillWidths', () => {
183183
const result = resolveFillWidths( [], new Map(), 6 );
184184
expect( result.size ).toBe( 0 );
185185
} );
186+
187+
describe( 'with multi-row items (height > 1)', () => {
188+
it( 'accounts for the shadow of a tall tile on the left', () => {
189+
const items: DashboardGridLayoutItem[] = [
190+
{ key: 'tall', width: 3, height: 2 },
191+
{ key: 'header', width: 9, height: 1 },
192+
{ key: 'sub', width: 3, height: 1 },
193+
{ key: 'fill', width: 'fill', height: 1 },
194+
];
195+
const result = resolveFillWidths(
196+
keys( items ),
197+
makeMap( items ),
198+
12
199+
);
200+
expect( result.get( 'fill' ) ).toBe( 6 );
201+
} );
202+
203+
it( 'accounts for the shadow of a tall tile in the middle', () => {
204+
const items: DashboardGridLayoutItem[] = [
205+
{ key: 'a', width: 3, height: 1 },
206+
{ key: 'b', width: 3, height: 2 },
207+
{ key: 'c', width: 6, height: 1 },
208+
{ key: 'd', width: 3, height: 1 },
209+
{ key: 'fill', width: 'fill', height: 1 },
210+
];
211+
const result = resolveFillWidths(
212+
keys( items ),
213+
makeMap( items ),
214+
12
215+
);
216+
expect( result.get( 'fill' ) ).toBe( 6 );
217+
} );
218+
219+
it( 'accounts for the shadow of a tall tile on the right', () => {
220+
const items: DashboardGridLayoutItem[] = [
221+
{ key: 'a', width: 3, height: 1 },
222+
{ key: 'b', width: 6, height: 1 },
223+
{ key: 'c', width: 3, height: 2 },
224+
{ key: 'd', width: 6, height: 1 },
225+
{ key: 'fill', width: 'fill', height: 1 },
226+
];
227+
const result = resolveFillWidths(
228+
keys( items ),
229+
makeMap( items ),
230+
12
231+
);
232+
expect( result.get( 'fill' ) ).toBe( 3 );
233+
} );
234+
235+
it( 'tracks shadow across multiple rows for height > 2', () => {
236+
const items: DashboardGridLayoutItem[] = [
237+
{ key: 'tall', width: 3, height: 3 },
238+
{ key: 'a', width: 9, height: 1 },
239+
{ key: 'b', width: 9, height: 1 },
240+
{ key: 'fill', width: 'fill', height: 1 },
241+
];
242+
const result = resolveFillWidths(
243+
keys( items ),
244+
makeMap( items ),
245+
12
246+
);
247+
expect( result.get( 'fill' ) ).toBe( 9 );
248+
} );
249+
} );
186250
} );

packages/ui/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- Remove the transitive peer dependency on `date-fns` / `@date-fns/tz` ([#77520](https://github.com/WordPress/gutenberg/pull/77520)), resolving [#77395](https://github.com/WordPress/gutenberg/issues/77395).
2525
- `Text`: Apply both heading and paragraph CSS defenses regardless of variant, so the correct defense kicks in based on the rendered element rather than the typographic variant ([#77461](https://github.com/WordPress/gutenberg/pull/77461)).
2626
- `CollapsibleCard`: Fix missing keyboard focus ring on the header chevron icon when rendered inside wp-admin ([#77468](https://github.com/WordPress/gutenberg/pull/77468)).
27+
- `CollapsibleCard`: Prevent the focus ring of focusable descendants from being clipped by the panel's overflow once the panel is fully expanded ([#77667](https://github.com/WordPress/gutenberg/pull/77667)).
2728
- `Tabs`: Fix missing keyboard focus ring on the panel in Windows High Contrast mode when rendered inside wp-admin ([#77469](https://github.com/WordPress/gutenberg/pull/77469)).
2829

2930
### Enhancements

packages/ui/src/collapsible-card/content.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,18 @@ export const Content = forwardRef< HTMLDivElement, ContentProps >(
1717
return (
1818
<Collapsible.Panel
1919
ref={ ref }
20-
className={ clsx( styles.content, className ) }
20+
// @ts-expect-error Base UI supports the callback-style
21+
// version of the `className` prop, but we're purposefully
22+
// not advertising it in our `@wordpress/ui` re-export.
23+
className={ ( state ) =>
24+
clsx(
25+
styles.content,
26+
state.open &&
27+
state.transitionStatus === 'idle' &&
28+
styles.overflowVisible,
29+
className
30+
)
31+
}
2132
hiddenUntilFound={ hiddenUntilFound }
2233
{ ...restProps }
2334
>

0 commit comments

Comments
 (0)