Skip to content
Merged
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
4 changes: 4 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Selection checkboxes stay visually 16px on narrow screens while retaining a 24px click target ([#83612](https://github.com/WordPress/gutenberg/pull/83612)).

### Bug Fixes

- Grid layout: measure the container as the grid attaches rather than waiting for the resize observer's first report, so the grid no longer renders a frame with every item in a full-width column of its own before snapping to its real column count ([#83752](https://github.com/WordPress/gutenberg/pull/83752)).

### Documentation

- Document that a custom `sort` receives the field values returned by `getValue`, not the items ([#83483](https://github.com/WordPress/gutenberg/pull/83483)).
Expand Down
12 changes: 2 additions & 10 deletions packages/dataviews/src/dataviews-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
useRef,
useState,
} from '@wordpress/element';
import { useResizeObserver } from '@wordpress/compose';
import { Stack } from '@wordpress/ui';
import DataViewsContext from '../components/dataviews-context';
import { VIEW_LAYOUTS } from '../components/dataviews-layouts';
Expand All @@ -34,6 +33,7 @@ import DataViewsViewConfig, {
ViewTypeMenu,
} from '../components/dataviews-view-config';
import normalizeFields from '../field-types';
import useContainerWidth from '../hooks/use-container-width';
import useData from '../hooks/use-data';
import { useInfiniteScroll } from '../hooks/use-infinite-scroll';
import usePageClamp from '../hooks/use-page-clamp';
Expand Down Expand Up @@ -261,15 +261,7 @@ function DataViewsPicker< Item >( {
setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >;
};
const containerRef = useRef< HTMLDivElement >( null );
const [ containerWidth, setContainerWidth ] = useState( 0 );
const resizeObserverRef = useResizeObserver(
( resizeObserverEntries: any ) => {
setContainerWidth(
resizeObserverEntries[ 0 ].borderBoxSize[ 0 ].inlineSize
);
},
{ box: 'border-box' }
);
const [ containerWidth, resizeObserverRef ] = useContainerWidth();
const [ openedFilter, setOpenedFilter ] = useState< string | null >( null );
function setSelectionWithChange( value: SelectionOrUpdater ) {
const newValue =
Expand Down
12 changes: 2 additions & 10 deletions packages/dataviews/src/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
useRef,
useState,
} from '@wordpress/element';
import { useResizeObserver } from '@wordpress/compose';
import { Stack } from '@wordpress/ui';
import DataViewsContext from '../components/dataviews-context';
import { VIEW_LAYOUTS } from '../components/dataviews-layouts';
Expand All @@ -26,6 +25,7 @@ import DataViewsViewConfig, {
ViewTypeMenu,
} from '../components/dataviews-view-config';
import normalizeFields from '../field-types';
import useContainerWidth from '../hooks/use-container-width';
import useData from '../hooks/use-data';
import { useInfiniteScroll } from '../hooks/use-infinite-scroll';
import usePageClamp from '../hooks/use-page-clamp';
Expand Down Expand Up @@ -136,15 +136,7 @@ function DataViews< Item >( {
setVisibleEntries?: React.Dispatch< React.SetStateAction< number[] > >;
};
const containerRef = useRef< HTMLDivElement >( null );
const [ containerWidth, setContainerWidth ] = useState( 0 );
const resizeObserverRef = useResizeObserver(
( resizeObserverEntries: any ) => {
setContainerWidth(
resizeObserverEntries[ 0 ].borderBoxSize[ 0 ].inlineSize
);
},
{ box: 'border-box' }
);
const [ containerWidth, resizeObserverRef ] = useContainerWidth();
const [ openedFilter, setOpenedFilter ] = useState< string | null >( null );
function setSelectionWithChange( value: SelectionOrUpdater ) {
const newValue =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { screen, waitFor } from '@testing-library/react';
import { render } from 'vitest-browser-react';
import { describe, expect, it } from 'vitest';
import { useLayoutEffect } from '@wordpress/element';
import DataViews from '../index';
import { LAYOUT_GRID } from '../../constants';
import type { View } from '../../types';

type Data = {
id: number;
title: string;
};

const view: View = {
type: LAYOUT_GRID,
search: '',
page: 1,
perPage: 10,
layout: {},
filters: [],
fields: [ 'title' ],
titleField: 'title',
};

const data: Data[] = [
{ id: 1, title: 'First item' },
{ id: 2, title: 'Second item' },
{ id: 3, title: 'Third item' },
];

// Counts the grid rows in the first animation frame after mount. Animation
// frame callbacks run before resize observer callbacks, so this is the layout
// the browser paints first.
function FirstFrameRowCount( {
onCount,
}: {
onCount: ( count: number ) => void;
} ) {
useLayoutEffect( () => {
requestAnimationFrame( () => {
onCount( screen.queryAllByRole( 'row' ).length );
} );
}, [ onCount ] );
return null;
}

describe( 'DataViews grid columns', () => {
it( 'paints the grid at its real column count on the first frame', async () => {
let firstFrameRowCount: number | undefined;

// A 600px wide container fits two columns at the default 230px preview
// size, so the three items render as two rows.
await render(
<div style={ { width: 600 } }>
<DataViews< Data >
view={ view }
onChangeView={ () => {} }
fields={ [ { id: 'title', label: 'Title', type: 'text' } ] }
data={ data }
getItemId={ ( item ) => item.id.toString() }
paginationInfo={ { totalItems: 3, totalPages: 1 } }
defaultLayouts={ { [ LAYOUT_GRID ]: true } }
/>
<FirstFrameRowCount
onCount={ ( count ) => {
if ( firstFrameRowCount === undefined ) {
firstFrameRowCount = count;
}
} }
/>
</div>
);

await waitFor( () => expect( firstFrameRowCount ).toBe( 2 ) );

// The observer reports the same width, so nothing changes.
expect( screen.getAllByRole( 'row' ) ).toHaveLength( 2 );
} );
} );
1 change: 1 addition & 0 deletions packages/dataviews/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as useContainerWidth } from './use-container-width';
export { default as useFormValidity } from './use-form-validity';
export { default as useData } from './use-data';
export { useInfiniteScroll } from './use-infinite-scroll';
Expand Down
39 changes: 39 additions & 0 deletions packages/dataviews/src/hooks/use-container-width.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useCallback, useState } from '@wordpress/element';
import { useMergeRefs, useResizeObserver } from '@wordpress/compose';

/**
* Tracks the width of the element the returned ref is attached to.
*
* The element is measured when it attaches, so a width is available for the
* first paint. The resize observer keeps the width up to date from then on.
*
* Detaching leaves the last width in place; reattaching measures again.
*
* @return The element's width, and the ref to attach to it.
*/
export default function useContainerWidth< T extends HTMLElement >(): [
number,
( element?: T | null ) => void,
] {
const [ width, setWidth ] = useState( 0 );

// `inlineSize` is only available on an observer entry, so it can't be read
// here. `offsetWidth` reports the same border box and, like
// `borderBoxSize`, is unaffected by transforms.
const measureRef = useCallback( ( element?: T | null ) => {
if ( element ) {
setWidth( element.offsetWidth );
}
}, [] );

const observerRef = useResizeObserver< T >(
( entries ) => {
setWidth(
Math.floor( entries[ 0 ].borderBoxSize[ 0 ].inlineSize )
);
},
{ box: 'border-box' }
);

return [ width, useMergeRefs( [ measureRef, observerRef ] ) ];
}
Loading