Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

### Enhancements

- DataViews: Add `scrollY` layout option for table and pickerTable views to keep filters and actions fixed while the table body scrolls vertically. [#76091](https://github.com/WordPress/gutenberg/pull/76091)
- Documentation: Update README.md. [#75881](https://github.com/WordPress/gutenberg/pull/75881)
- DataViews: Improve UI in `list` layout when we render only title and/or media fields. [#76042](https://github.com/WordPress/gutenberg/pull/76042)
- DataViews: Adjust column spacing in table layout when no titleField is provided. [#75410](https://github.com/WordPress/gutenberg/pull/75410)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type DataViewsContextType< Item > = {
isItemClickable: ( item: Item ) => boolean;
containerWidth: number;
containerRef: React.MutableRefObject< HTMLDivElement | null >;
scrollContainerRef: React.MutableRefObject< HTMLDivElement | null >;
resizeObserverRef:
| ( ( element?: HTMLDivElement | null ) => void )
| React.RefObject< HTMLDivElement >;
Expand Down Expand Up @@ -80,6 +81,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( {
renderItemLink: undefined,
containerWidth: 0,
containerRef: createRef(),
scrollContainerRef: createRef(),
resizeObserverRef: () => {},
defaultLayouts: { list: {}, grid: {}, table: {} },
filters: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ function ViewTable< Item >( {
className,
empty,
}: ViewTableProps< Item > ) {
const { containerRef } = useContext( DataViewsContext );
const { containerRef, scrollContainerRef } = useContext( DataViewsContext );
const isDelayedLoading = useDelayedLoading( isLoading );
const headerMenuRefs = useRef<
Map< string, { node: HTMLButtonElement; fallback: string } >
Expand All @@ -323,8 +323,13 @@ function ViewTable< Item >( {

const tableNoticeId = useId();

const isVerticalScrollInTable = view.layout?.scrollY === 'table';
const horizontalScrollContainerRef = isVerticalScrollInTable
? scrollContainerRef
: containerRef;

const isHorizontalScrollEnd = useIsHorizontalScrollEnd( {
scrollContainerRef: containerRef,
scrollContainerRef: horizontalScrollContainerRef,
enabled: !! actions?.length,
} );

Expand Down Expand Up @@ -413,7 +418,7 @@ function ViewTable< Item >( {
);
}

return (
const tableElement = (
<>
<table
className={ clsx( 'dataviews-view-table', className, {
Expand Down Expand Up @@ -674,6 +679,23 @@ function ViewTable< Item >( {
</tbody>
) }
</table>
</>
);

return (
<>
{ isVerticalScrollInTable ? (
<div className="dataviews-view-table__scroll-container">
<div
className="dataviews-view-table__vertical-scroll"
ref={ scrollContainerRef }
>
{ tableElement }
</div>
</div>
) : (
tableElement
) }
{ isInfiniteScroll && isLoading && (
<div className="dataviews-loading" id={ tableNoticeId }>
<p className="dataviews-loading-more">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,22 @@
}
}

.dataviews-view-table__scroll-container {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
background-color: inherit;
}

.dataviews-view-table__vertical-scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: auto;
background-color: inherit;
}


.dataviews-view-table-selection-checkbox {
--checkbox-input-size: 24px;
Expand Down
25 changes: 21 additions & 4 deletions packages/dataviews/src/dataviews-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ function DataViewsPicker< Item >( {
}: DataViewsPickerProps< Item > ) {
const { infiniteScrollHandler } = paginationInfo;
const containerRef = useRef< HTMLDivElement >( null );
const scrollContainerRef = useRef< HTMLDivElement >( null );
const [ containerWidth, setContainerWidth ] = useState( 0 );
const resizeObserverRef = useResizeObserver(
( resizeObserverEntries: any ) => {
Expand Down Expand Up @@ -166,6 +167,8 @@ function DataViewsPicker< Item >( {
}, [ hasPrimaryOrLockedFilters, isShowingFilter ] );

// Attach scroll event listener for infinite scroll
const viewScrollY = ( view.layout as { scrollY?: string } | undefined )
?.scrollY;
useEffect( () => {
if ( ! view.infiniteScrollEnabled || ! containerRef.current ) {
return;
Expand All @@ -184,13 +187,19 @@ function DataViewsPicker< Item >( {
}, 100 ); // Throttle to 100ms

const container = containerRef.current;
container.addEventListener( 'scroll', handleScroll );
const scrollContainer = scrollContainerRef.current ?? container;
scrollContainer.addEventListener( 'scroll', handleScroll );

return () => {
container.removeEventListener( 'scroll', handleScroll );
scrollContainer.removeEventListener( 'scroll', handleScroll );
handleScroll.cancel(); // Cancel any pending throttled calls
};
}, [ infiniteScrollHandler, view.infiniteScrollEnabled ] );
}, [
infiniteScrollHandler,
view.infiniteScrollEnabled,
view.type,
viewScrollY,
] );

// Filter out DataViewsPicker layouts.
const defaultLayouts = useMemo(
Expand All @@ -211,6 +220,13 @@ function DataViewsPicker< Item >( {
return null;
}

const shouldScrollYInTable =
view.type === 'pickerTable' && view.layout?.scrollY === 'table';

const wrapperClassName = shouldScrollYInTable
? 'dataviews-picker-wrapper dataviews-picker-wrapper--scroll-y-table'
: 'dataviews-picker-wrapper';

return (
<DataViewsContext.Provider
value={ {
Expand All @@ -229,6 +245,7 @@ function DataViewsPicker< Item >( {
getItemId,
containerWidth,
containerRef,
scrollContainerRef,
resizeObserverRef,
defaultLayouts,
filters,
Expand All @@ -241,7 +258,7 @@ function DataViewsPicker< Item >( {
hasInfiniteScrollHandler: !! infiniteScrollHandler,
} }
>
<div className="dataviews-picker-wrapper" ref={ containerRef }>
<div className={ wrapperClassName } ref={ containerRef }>
{ children ?? (
<DefaultUI search={ search } searchLabel={ searchLabel } />
) }
Expand Down
29 changes: 25 additions & 4 deletions packages/dataviews/src/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function DataViews< Item >( {
}: DataViewsProps< Item > ) {
const { infiniteScrollHandler } = paginationInfo;
const containerRef = useRef< HTMLDivElement >( null );
const scrollContainerRef = useRef< HTMLDivElement >( null );
const [ containerWidth, setContainerWidth ] = useState( 0 );
const resizeObserverRef = useResizeObserver(
( resizeObserverEntries: any ) => {
Expand Down Expand Up @@ -196,6 +197,8 @@ function DataViews< Item >( {
}, [ hasPrimaryOrLockedFilters, isShowingFilter ] );

// Attach scroll event listener for infinite scroll
const viewScrollY = ( view.layout as { scrollY?: string } | undefined )
?.scrollY;
useEffect( () => {
if ( ! view.infiniteScrollEnabled || ! containerRef.current ) {
return;
Expand All @@ -214,13 +217,19 @@ function DataViews< Item >( {
}, 100 ); // Throttle to 100ms

const container = containerRef.current;
container.addEventListener( 'scroll', handleScroll );
const scrollContainer = scrollContainerRef.current ?? container;
scrollContainer.addEventListener( 'scroll', handleScroll );

return () => {
container.removeEventListener( 'scroll', handleScroll );
scrollContainer.removeEventListener( 'scroll', handleScroll );
handleScroll.cancel(); // Cancel any pending throttled calls
};
}, [ infiniteScrollHandler, view.infiniteScrollEnabled ] );
}, [
infiniteScrollHandler,
view.infiniteScrollEnabled,
view.type,
viewScrollY,
] );

// Filter out DataViewsPicker layouts.
const defaultLayouts = useMemo(
Expand All @@ -247,6 +256,17 @@ function DataViews< Item >( {
return null;
}

const scrollYMode =
view.type === 'table' || view.type === 'pickerTable'
? view.layout?.scrollY
: undefined;

const shouldScrollYInTable = scrollYMode === 'table';

const wrapperClassName = shouldScrollYInTable
? 'dataviews-wrapper dataviews-wrapper--scroll-y-table'
: 'dataviews-wrapper';

return (
<DataViewsContext.Provider
value={ {
Expand All @@ -268,6 +288,7 @@ function DataViews< Item >( {
renderItemLink,
containerWidth,
containerRef,
scrollContainerRef,
resizeObserverRef,
defaultLayouts,
filters,
Expand All @@ -280,7 +301,7 @@ function DataViews< Item >( {
onReset,
} }
>
<div className="dataviews-wrapper" ref={ containerRef }>
<div className={ wrapperClassName } ref={ containerRef }>
{ children ?? (
<DefaultUI
header={ header }
Expand Down
7 changes: 7 additions & 0 deletions packages/dataviews/src/dataviews/stories/index.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const LayoutTable = {
hasClickableItems: true,
perPageSizes: [ 10, 25, 50, 100 ],
showMedia: true,
scrollY: 'wrapper',
},
argTypes: {
backgroundColor: {
Expand Down Expand Up @@ -75,6 +76,12 @@ export const LayoutTable = {
control: 'boolean',
description: 'Whether to display the media field',
},
scrollY: {
control: 'select',
options: [ 'wrapper', 'table' ],
description:
'Where vertical scrolling occurs. Use "table" to keep filters and actions fixed while the table body scrolls.',
},
},
};

Expand Down
15 changes: 12 additions & 3 deletions packages/dataviews/src/dataviews/stories/layout-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,24 @@ export const LayoutTableComponent = ( {
groupByLabel = true,
perPageSizes = [ 10, 25, 50, 100 ],
showMedia = true,
scrollY = 'wrapper',
}: {
backgroundColor?: string;
hasClickableItems?: boolean;
groupBy?: boolean;
groupByLabel?: boolean;
perPageSizes?: number[];
showMedia?: boolean;
scrollY?: 'wrapper' | 'table';
} ) => {
const [ view, setView ] = useState< View >( {
type: LAYOUT_TABLE,
search: '',
page: 1,
perPage: 10,
layout: {},
layout: {
scrollY,
},
filters: [],
fields: [ 'categories' ],
titleField: 'title',
Expand All @@ -45,6 +49,10 @@ export const LayoutTableComponent = ( {
setView( ( prevView ) => {
return {
...prevView,
layout: {
...prevView.layout,
scrollY,
},
groupBy: groupBy
? {
field: 'type',
Expand All @@ -53,9 +61,9 @@ export const LayoutTableComponent = ( {
}
: undefined,
showMedia,
};
} as View;
} );
}, [ groupBy, groupByLabel, showMedia ] );
}, [ groupBy, groupByLabel, showMedia, scrollY ] );

const { data: shownData, paginationInfo } = useMemo( () => {
return filterSortAndPaginate( data, view, fields );
Expand All @@ -65,6 +73,7 @@ export const LayoutTableComponent = ( {
style={
{
'--wp-dataviews-color-background': backgroundColor,
height: scrollY === 'table' ? '400px' : undefined,
} as React.CSSProperties
}
>
Expand Down
6 changes: 6 additions & 0 deletions packages/dataviews/src/dataviews/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
background-color: var(--wp-dataviews-color-background, $white);
}

.dataviews-wrapper--scroll-y-table,
.dataviews-picker-wrapper--scroll-y-table {
overflow-y: hidden;
overflow-x: auto;
}

.dataviews__view-actions,
.dataviews-filters__container {
box-sizing: border-box;
Expand Down
20 changes: 20 additions & 0 deletions packages/dataviews/src/types/dataviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,16 @@ export interface ViewTable extends ViewBase {
type: 'table';

layout?: {
/**
* Where vertical scrolling should live for the table.
*
* Use `table` to keep the view actions and filters fixed while the table
* scrolls vertically.
*
* @default 'wrapper'
*/
scrollY?: 'wrapper' | 'table';

/**
* The styles for the columns.
*/
Expand Down Expand Up @@ -308,6 +318,16 @@ export interface ViewPickerTable extends ViewBase {
type: 'pickerTable';

layout?: {
/**
* Where vertical scrolling should live for the table.
*
* Use `table` to keep the view actions and filters fixed while the table
* scrolls vertically.
*
* @default 'wrapper'
*/
scrollY?: 'wrapper' | 'table';

/**
* The styles for the columns.
*/
Expand Down
Loading