Repository: Automattic/wp-calypso
Pull request #103005: [DataViews]: Pin the actions column on the table view when the width is insufficient
Author: login: arthur791004
Labels: name: [Feature] Multi-site Dashboard
Created: 2025-04-30T11:37:51Z | Merged: 2025-05-08T11:14:41Z

<!--
Link a related issue to this PR. If the PR does not immediately resolve the issue,
for example, it requires a separate deployment to production, avoid
using the "fixes" keyword and instead attach the [Status] Fix Inbound label to
the linked issue.
-->

Related to DOTCOM-10696

## Proposed Changes

* Pin the action column
* Display the border on the left side of the action column to indicate it's scrollable, and then remove it when the user scrolls to the end

### V1

https://github.com/user-attachments/assets/2886f323-844b-4c8f-a26c-58fbc728e3ec

### V2

https://github.com/user-attachments/assets/831485e3-c802-4483-98c2-6f2f51c7ff43

## Why are these changes being made?
<!--
It's easy to see what a PR does but much harder to find out why it was made,
particularly when researching old changes in history. Record an explanation of
the motivation behind this change and how it will help.
-->

*

## Testing Instructions

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

* Go to /sites on small screen
* Ensure you can see the action column and the border on the left side
* Scroll the table to the end
* Ensure the border is gone

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [ ] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [ ] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [ ] Have you checked for TypeScript, React or other console errors?
- [ ] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [ ] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [ ] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [ ] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?



Diff:
diff --git a/client/sites/components/sites-dashboard.tsx b/client/sites/components/sites-dashboard.tsx
index 0dd5becd2c92..ab2527e13230 100644
--- a/client/sites/components/sites-dashboard.tsx
+++ b/client/sites/components/sites-dashboard.tsx
@@ -51,7 +51,7 @@ import SitesDashboardBannersManager from './sites-dashboard-banners-manager';
 import SitesDashboardHeader from './sites-dashboard-header';
 import DotcomSitesDataViews, { useSiteStatusGroups } from './sites-dataviews';
 import { getSitesPagination } from './sites-dataviews/utils';
-import type { View } from '@wordpress/dataviews';
+import type { View } from '@automattic/dataviews';
 
 // todo: we are using A4A styles until we extract them as common styles in the ItemsDashboard component
 import './style.scss';
diff --git a/client/sites/components/sites-dataviews/dataview-style.scss b/client/sites/components/sites-dataviews/dataview-style.scss
index eab010a1335a..a542622cad40 100644
--- a/client/sites/components/sites-dataviews/dataview-style.scss
+++ b/client/sites/components/sites-dataviews/dataview-style.scss
@@ -1,6 +1,6 @@
+@import "@automattic/dataviews/build-style/style.css";
 @import "@wordpress/base-styles/breakpoints";
 @import "@wordpress/base-styles/mixins";
-@import "@wordpress/dataviews/build-style/style.css";
 
 .sites-dataviews {
 	table.dataviews-view-table .dataviews-view-table__row {
diff --git a/client/sites/components/sites-dataviews/index.tsx b/client/sites/components/sites-dataviews/index.tsx
index 0fa4e0ddbcbd..c9aef2752724 100644
--- a/client/sites/components/sites-dataviews/index.tsx
+++ b/client/sites/components/sites-dataviews/index.tsx
@@ -1,6 +1,6 @@
 import { TimeSince } from '@automattic/components';
+import { DataViews, Field } from '@automattic/dataviews';
 import { SiteExcerptData } from '@automattic/sites';
-import { DataViews, Field } from '@wordpress/dataviews';
 import { useI18n } from '@wordpress/react-i18n';
 import { useCallback, useMemo } from 'react';
 import { useQueryReaderTeams } from 'calypso/components/data/query-reader-teams';
diff --git a/packages/dataviews/CHANGELOG.automattic.md b/packages/dataviews/CHANGELOG.automattic.md
index e9dea34241e3..ae1097052b0c 100644
--- a/packages/dataviews/CHANGELOG.automattic.md
+++ b/packages/dataviews/CHANGELOG.automattic.md
@@ -3,6 +3,7 @@
 ## Next
 
 - Add new `boolean` field type definition and edit control. Field type definitions are able to define a default render function that will be used if the field doesn't define one.
+- Pin the actions column on the table view when the width is insufficient.
 
 ## 0.1.1
 
diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts
index ba0ec9da097c..1adb4c025d5b 100644
--- a/packages/dataviews/src/components/dataviews-context/index.ts
+++ b/packages/dataviews/src/components/dataviews-context/index.ts
@@ -1,7 +1,7 @@
 /**
  * WordPress dependencies
  */
-import { createContext } from '@wordpress/element';
+import { createContext, createRef } from '@wordpress/element';
 
 /**
  * Internal dependencies
@@ -36,6 +36,7 @@ type DataViewsContextType< Item > = {
 	onClickItem?: ( item: Item ) => void;
 	isItemClickable: ( item: Item ) => boolean;
 	containerWidth: number;
+	containerRef: React.MutableRefObject< HTMLDivElement | null >;
 	defaultLayouts: SupportedLayouts;
 	filters: NormalizedFilter[];
 	isShowingFilter: boolean;
@@ -58,6 +59,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( {
 	getItemId: ( item ) => item.id,
 	isItemClickable: () => true,
 	containerWidth: 0,
+	containerRef: createRef(),
 	defaultLayouts: { list: {}, grid: {}, table: {} },
 	filters: [],
 	isShowingFilter: false,
diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx
index cc3d53d12b99..dcb0ed15dbe5 100644
--- a/packages/dataviews/src/components/dataviews/index.tsx
+++ b/packages/dataviews/src/components/dataviews/index.tsx
@@ -7,8 +7,8 @@ import type { ReactNode } from 'react';
  * WordPress dependencies
  */
 import { __experimentalHStack as HStack } from '@wordpress/components';
-import { useContext, useMemo, useState } from '@wordpress/element';
-import { useResizeObserver } from '@wordpress/compose';
+import { useContext, useMemo, useRef, useState } from '@wordpress/element';
+import { useMergeRefs, useResizeObserver } from '@wordpress/compose';
 
 /**
  * Internal dependencies
@@ -127,8 +127,9 @@ function DataViews< Item >( {
 	header,
 	children,
 }: DataViewsProps< Item > ) {
+	const containerRef = useRef< HTMLDivElement | null >( null );
 	const [ containerWidth, setContainerWidth ] = useState( 0 );
-	const containerRef = useResizeObserver(
+	const resizeObserverRef = useResizeObserver(
 		( resizeObserverEntries: any ) => {
 			setContainerWidth(
 				resizeObserverEntries[ 0 ].borderBoxSize[ 0 ].inlineSize
@@ -182,13 +183,14 @@ function DataViews< Item >( {
 				isItemClickable,
 				onClickItem,
 				containerWidth,
+				containerRef,
 				defaultLayouts,
 				filters,
 				isShowingFilter,
 				setIsShowingFilter,
 			} }
 		>
-			<div className="dataviews-wrapper" ref={ containerRef }>
+			<div className="dataviews-wrapper" ref={ useMergeRefs( [ containerRef, resizeObserverRef ] ) }>
 				{ children ?? (
 					<DefaultUI
 						header={ header }
diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx
index 8681789cb5c4..ddacec7b43d3 100644
--- a/packages/dataviews/src/dataviews-layouts/table/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx
@@ -8,11 +8,18 @@ import clsx from 'clsx';
  */
 import { __ } from '@wordpress/i18n';
 import { Spinner } from '@wordpress/components';
-import { useEffect, useId, useRef, useState } from '@wordpress/element';
+import {
+	useContext,
+	useEffect,
+	useId,
+	useRef,
+	useState,
+} from '@wordpress/element';
 
 /**
  * Internal dependencies
  */
+import DataViewsContext from '../../components/dataviews-context';
 import DataViewsSelectionCheckbox from '../../components/dataviews-selection-checkbox';
 import ItemActions from '../../components/dataviews-item-actions';
 import { sortValues } from '../../constants';
@@ -30,6 +37,7 @@ import type {
 import type { SetSelection } from '../../private-types';
 import ColumnHeaderMenu from './column-header-menu';
 import ColumnPrimary from './column-primary';
+import { useIsHorizontalScrollEnd } from './use-is-horizontal-scroll-end';
 
 interface TableColumnFieldProps< Item > {
 	fields: NormalizedField< Item >[];
@@ -53,6 +61,7 @@ interface TableRowProps< Item > {
 	onChangeSelection: SetSelection;
 	isItemClickable: ( item: Item ) => boolean;
 	onClickItem?: ( item: Item ) => void;
+	isActionsColumnSticky?: boolean;
 }
 
 function TableColumnField< Item >( {
@@ -89,6 +98,7 @@ function TableRow< Item >( {
 	isItemClickable,
 	onClickItem,
 	onChangeSelection,
+	isActionsColumnSticky,
 }: TableRowProps< Item > ) {
 	const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item );
 	const isSelected = hasPossibleBulkAction && selection.includes( id );
@@ -192,7 +202,11 @@ function TableRow< Item >( {
 
 				/* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
 				<td
-					className="dataviews-view-table__actions-column"
+					className={ clsx( 'dataviews-view-table__actions-column', {
+						'dataviews-view-table__actions-column--sticky': true,
+						'dataviews-view-table__actions-column--stuck':
+							isActionsColumnSticky,
+					} ) }
 					onClick={ ( e ) => e.stopPropagation() }
 				>
 					<ItemActions item={ item } actions={ actions } />
@@ -218,6 +232,7 @@ function ViewTable< Item >( {
 	isItemClickable,
 	view,
 }: ViewTableProps< Item > ) {
+	const { containerRef } = useContext( DataViewsContext );
 	const headerMenuRefs = useRef<
 		Map< string, { node: HTMLButtonElement; fallback: string } >
 	>( new Map() );
@@ -278,6 +293,11 @@ function ViewTable< Item >( {
 			}
 		};
 
+	const isHorizontalScrollEnd = useIsHorizontalScrollEnd( {
+		scrollContainerRef: containerRef,
+		enabled: !! actions?.length,
+	} );
+
 	return (
 		<>
 			<table
@@ -355,7 +375,17 @@ function ViewTable< Item >( {
 							);
 						} ) }
 						{ !! actions?.length && (
-							<th className="dataviews-view-table__actions-column">
+							<th
+								className={ clsx(
+									'dataviews-view-table__actions-column',
+									{
+										'dataviews-view-table__actions-column--sticky':
+											true,
+										'dataviews-view-table__actions-column--stuck':
+											! isHorizontalScrollEnd,
+									}
+								) }
+							>
 								<span className="dataviews-view-table-header">
 									{ __( 'Actions' ) }
 								</span>
@@ -388,6 +418,9 @@ function ViewTable< Item >( {
 								onChangeSelection={ onChangeSelection }
 								onClickItem={ onClickItem }
 								isItemClickable={ isItemClickable }
+								isActionsColumnSticky={
+									! isHorizontalScrollEnd
+								}
 							/>
 						) ) }
 				</tbody>
diff --git a/packages/dataviews/src/dataviews-layouts/table/style.scss b/packages/dataviews/src/dataviews-layouts/table/style.scss
index 02ccbe4fed7b..da633e581dc6 100644
--- a/packages/dataviews/src/dataviews-layouts/table/style.scss
+++ b/packages/dataviews/src/dataviews-layouts/table/style.scss
@@ -22,6 +22,25 @@
 			text-align: right;
 		}
 
+		&.dataviews-view-table__actions-column--sticky {
+			position: sticky;
+			right: 0;
+			background-color: $white;
+		}
+
+		&.dataviews-view-table__actions-column--stuck {
+			&::after {
+				display: block;
+				content: '';
+				position: absolute;
+				top: 0;
+				bottom: 0;
+				left: 0;
+				width: 1px;
+				background-color: $gray-100;
+			}
+		}
+
 		&.dataviews-view-table__checkbox-column {
 			padding-right: 0;
 			width: 1%;
@@ -52,7 +71,8 @@
 			border-bottom: 0;
 		}
 
-		&.is-hovered {
+		&.is-hovered,
+		&.is-hovered .dataviews-view-table__actions-column--sticky {
 			background-color: #f8f8f8;
 		}
 
diff --git a/packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts b/packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
new file mode 100644
index 000000000000..a79e86a4db72
--- /dev/null
+++ b/packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
@@ -0,0 +1,71 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ *
+ * The current way receives "refs" as arguments, but it lacks a mechanism to detect when a ref has changed.
+ * As a result, when the "ref" is updated and attached to a new div, the computation should trigger again.
+ * However, this isn't possible in the current setup because the hook is unaware that the ref has changed.
+ * See https://github.com/Automattic/wp-calypso/pull/103005#discussion_r2077567912.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
+	enabled = false,
+}: {
+	scrollContainerRef: React.MutableRefObject< HTMLDivElement | null >;
+	enabled?: boolean;
+} ): boolean {
+	const [ isHorizontalScrollEnd, setIsHorizontalScrollEnd ] =
+		useState( false );
+
+	const handleIsHorizontalScrollEnd = useDebounce(
+		useCallback( () => {
+			const scrollContainer = scrollContainerRef.current;
+			if ( scrollContainer ) {
+				setIsHorizontalScrollEnd( isScrolledToEnd( scrollContainer ) );
+			}
+		}, [ scrollContainerRef, setIsHorizontalScrollEnd ] ),
+		200
+	);
+
+	useEffect( () => {
+		if (
+			typeof window === 'undefined' ||
+			! enabled ||
+			! scrollContainerRef.current
+		) {
+			return () => {};
+		}
+
+		handleIsHorizontalScrollEnd();
+		scrollContainerRef.current.addEventListener(
+			'scroll',
+			handleIsHorizontalScrollEnd
+		);
+		window.addEventListener( 'resize', handleIsHorizontalScrollEnd );
+
+		return () => {
+			scrollContainerRef.current?.removeEventListener(
+				'scroll',
+				handleIsHorizontalScrollEnd
+			);
+			window.removeEventListener( 'resize', handleIsHorizontalScrollEnd );
+		};
+	}, [ scrollContainerRef, enabled ] );
+
+	return isHorizontalScrollEnd;
+}


Reviews:
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:33:53Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T06:35:00Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:07:39Z
user: login: matt-west
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T12:59:20Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T00:30:02Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T00:30:38Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T00:32:54Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T02:16:13Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T02:26:02Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T05:10:23Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T05:21:22Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:40:40Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:41:11Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:41:26Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:52:01Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T08:59:01Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:08:02Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:17:02Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T07:37:17Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T14:57:53Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:02:37Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T15:24:52Z
user: login: oandregal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:57:53Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:19:36Z
user: login: jameskoster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T11:28:02Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:52:58Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T02:54:57Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T08:24:48Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T08:25:09Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T11:54:25Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:40:09Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:41:16Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:48:52Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T12:55:40Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:28:54Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:32:38Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:46:06Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:50:31Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:50:00Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:10:30Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:13:01Z
user: login: youknowriad
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T08:13:41Z

Review comments:
diff_hunk: @@ -101,6 +101,35 @@
 	.stats-sparkline {
 		padding-top: 0;
 	}
+
+	/**
+	 * Pin the actions column.
+	 */
+	.dataviews-view-table__actions-column {
path: client/sites/components/sites-dataviews/style.scss
user: login: oandregal
body: Hey, we shouldn't target CSS classes of libraries (including DataViews), otherwise updates are harder for us to perform.

The way forward here is to use `@automattic/dataviews` and implement these changes there. In case it's useful as a starting point to port here, there was an initial PR for that at https://github.com/WordPress/gutenberg/pull/65086.
created_at: 2025-04-30T13:33:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +101,35 @@
 	.stats-sparkline {
 		padding-top: 0;
 	}
+
+	/**
+	 * Pin the actions column.
+	 */
+	.dataviews-view-table__actions-column {
path: client/sites/components/sites-dataviews/style.scss
user: login: arthur791004
body: I've moved changes to `@automattic/dataviews`. Thank you!
created_at: 2025-05-01T06:35:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +101,35 @@
 	.stats-sparkline {
 		padding-top: 0;
 	}
+
+	/**
+	 * Pin the actions column.
+	 */
+	.dataviews-view-table__actions-column {
path: client/sites/components/sites-dataviews/style.scss
user: login: arthur791004
body: Do you think I should split this PR and add support for pinning action columns in dataviews separately?
created_at: 2025-05-01T09:07:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: StevenDufresne
body: It's possible to hold a stale reference, or this may be `null` depending on when the DOM element is committed. It would be best to pass a ref.
created_at: 2025-05-02T00:30:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
+
+		const handleIsScrolledEnd = () => {
+			if ( scrollContainer ) {
+				setIsScrolledEnd( isScrolledToEnd( scrollContainer ) );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: StevenDufresne
body: Should this be throttled?
created_at: 2025-05-02T00:30:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,25 @@
 
 		&.dataviews-view-table__actions-column {
 			text-align: right;
+
+			&.dataviews-view-table__actions-column--sticky {
path: packages/dataviews/src/dataviews-layouts/table/style.scss
user: login: StevenDufresne
body: Do these need to be nested?
created_at: 2025-05-02T00:32:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: arthur791004
body: Yeah, it’s not a perfect way for getting the scroll container, but accessing the ref directly isn’t straightforward. Here are a couple of alternatives I considered:

* Expose the `containerRef` via context so it’s easily accessible wherever it’s needed. However, the `containerRef` might only be needed in this specific place, so adding it to context could be unnecessary overhead.
* Pass the table’s ref and use [getScrollContainer](https://github.com/WordPress/gutenberg/blob/72c922b679cbc0d83220514165ea66837de131ca/packages/dom/src/dom/get-scroll-container.js#L15) to locate the scroll container. That said, I’m not sure it’s worth introducing the @wordpress/dom package just for this.

Any thoughts?
created_at: 2025-05-02T02:16:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,25 @@
 
 		&.dataviews-view-table__actions-column {
 			text-align: right;
+
+			&.dataviews-view-table__actions-column--sticky {
path: packages/dataviews/src/dataviews-layouts/table/style.scss
user: login: arthur791004
body: I don’t have a strong opinion on this. I placed them inside `.dataviews-view-table__actions-column` because they act as modifiers for it.
created_at: 2025-05-02T02:26:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,25 @@
 
 		&.dataviews-view-table__actions-column {
 			text-align: right;
+
+			&.dataviews-view-table__actions-column--sticky {
path: packages/dataviews/src/dataviews-layouts/table/style.scss
user: login: StevenDufresne
body: I would lean towards avoiding nests if possible.
created_at: 2025-05-02T05:10:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: StevenDufresne
body: > Expose the containerRef via context so it’s easily accessible wherever it’s needed. However, the containerRef might only be needed in this specific place, so adding it to context could be unnecessary overhead.

It should be very lightweight, seeing that it'll only save a reference to an object already in memory. I don't have any reservations about adding that reference to the context. 

The trade-off seems worth it to me. Passing the selector as a string seems like a less desirable outcome.
created_at: 2025-05-02T05:21:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: arthur791004
body: OK. I've added the `containerRef` to the context
created_at: 2025-05-02T07:40:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+/**
+ * WordPress dependencies
+ */
+import { useEffect, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
+	selector,
+	enabled = false,
+}: {
+	selector: string;
+	enabled?: boolean;
+} ): boolean {
+	const [ isScrolledEnd, setIsScrolledEnd ] = useState( false );
+
+	useEffect( () => {
+		if ( typeof window === 'undefined' || ! enabled ) {
+			return () => {};
+		}
+
+		const scrollContainer = document.querySelector( selector );
+
+		const handleIsScrolledEnd = () => {
+			if ( scrollContainer ) {
+				setIsScrolledEnd( isScrolledToEnd( scrollContainer ) );
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: arthur791004
body: Wrapped the `handleIsScrolledEnd` function with the `useDebounce`.
created_at: 2025-05-02T07:41:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,25 @@
 
 		&.dataviews-view-table__actions-column {
 			text-align: right;
+
+			&.dataviews-view-table__actions-column--sticky {
path: packages/dataviews/src/dataviews-layouts/table/style.scss
user: login: arthur791004
body: Updated!
created_at: 2025-05-02T07:41:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: oandregal
body: Can this behavior be automatic? cc @jameskoster @matt-west 
created_at: 2025-05-02T08:52:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: oandregal
body: 🤔 I don't see a reason why it shouldn't.
created_at: 2025-05-02T08:59:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -101,6 +101,35 @@
 	.stats-sparkline {
 		padding-top: 0;
 	}
+
+	/**
+	 * Pin the actions column.
+	 */
+	.dataviews-view-table__actions-column {
path: client/sites/components/sites-dataviews/style.scss
user: login: oandregal
body: > Do you think I should split this PR and add support for pinning action columns in dataviews separately?

Perhaps a good idea.

`@automattic/dataviews` is a 1:1 replacement for `@wordpress/dataviews` 4.18.0, so it should be fine. However, we're starting to do stuff, and changing the package merits some deeper testing given other code bits that we have in dotcom. Worst-case scenario, a revert would be easier if it's just a package switch (without affecting the actions feature).
created_at: 2025-05-02T09:08:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: arthur791004
body: I’d prefer if it could be user-controllable, but I’m okay with either.
created_at: 2025-05-02T09:17:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: youknowriad
body: Regardless of whether we keep the flag or not, I think ultimately, we should move away from the "layout" config property and instead just have top level flags/configs. 
created_at: 2025-05-05T07:37:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: oandregal
body: I'd prefer if we don't change how `layout` works for now. That's a major refactor that doesn't bring much value and complicates making syncs from upstream.
created_at: 2025-05-05T14:57:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: oandregal
body: Would you be open to making it the default behavior and only introduce the flag if there are consumers that actively ask for it?

If we have to do it, my preference would be something that can work for multiple items, rather than individual props for every modifiable behavior. For example, in the past, consumers have shared that they'd want to hide the controls for moving columns around or the config cog, etc.
created_at: 2025-05-05T15:02:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: youknowriad
body: How is this a major refactor? 

I think right now, the layout is already a superficial key, the "semantic" aspect of it has been removed essentially when we introduced title, description and media fields. 
created_at: 2025-05-05T15:24:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: oandregal
body: We still have layout-bound properties in there, like `badgeFields` and `styles`. I am hearing you're proposing to remove them from there? This adds complexity for us (sync from upstream) and I don't see much value. In a world where we could ignore upstream, I'd be more open to do whatever we want.
created_at: 2025-05-06T07:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: youknowriad
body: Yeah, I get your point, I'm not asking to change this now, but I'm thinking we should avoid adding new properties there
created_at: 2025-05-06T08:19:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: jameskoster
body: I'm struggling a bit to see scenarios where users or consumers would want to disable this behavior. It feels like a detail to explore separately to me.
created_at: 2025-05-06T11:28:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,6 +148,9 @@ const DEFAULT_LAYOUTS = {
 		fields: [ 'subscribers_count', 'status', 'backups', 'protect' ],
 		titleField: 'name',
 		descriptionField: 'URL',
+		layout: {
path: client/dashboard/sites/index.tsx
user: login: arthur791004
body: Yep, the suggestion here was to add the new option outside of the layout instead of removing it.
created_at: 2025-05-07T02:52:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,6 +218,7 @@ Properties:
 | ----------------------------------------------------------------------------------- | ----- | ---- | ---- |
 | `badgeFields`: a list of field's `id` to render without label and styled as badges. |       | ✓    |      |
 | `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column.  | ✓     |      |      |
+| `shouldPinActions`: whether to pin actions column when the width is insufficient .  | ✓     |      |      |
path: packages/dataviews/README.md
user: login: arthur791004
body: Ok, let's try to make it the default behavior
created_at: 2025-05-07T02:54:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -278,6 +293,11 @@ function ViewTable< Item >( {
 			}
 		};
 
+	const isScrolledEnd = useIsScrolledEnd( {
+		scrollContainerRef: containerRef,
path: packages/dataviews/src/dataviews-layouts/table/index.tsx
user: login: youknowriad
body: The assumption here is that the scrollable container is the dataviews-wrapper div, is that always the case? Another possibility here could be to retrieve the parent scrollable of the "table". 
created_at: 2025-05-07T08:24:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: youknowriad
body: I would love to have a doc block to understand what this hook does.
created_at: 2025-05-07T08:25:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -278,6 +293,11 @@ function ViewTable< Item >( {
 			}
 		};
 
+	const isScrolledEnd = useIsScrolledEnd( {
+		scrollContainerRef: containerRef,
path: packages/dataviews/src/dataviews-layouts/table/index.tsx
user: login: arthur791004
body: We discussed in https://github.com/Automattic/wp-calypso/pull/103005#discussion_r2071011447. This appears to be safe enough. Or do you prefer to pass the table’s ref and use [getScrollContainer](https://github.com/WordPress/gutenberg/blob/72c922b679cbc0d83220514165ea66837de131ca/packages/dom/src/dom/get-scroll-container.js#L15) to locate the scroll container? I’m not sure it’s worth introducing the @wordpress/dom package just for this.
created_at: 2025-05-07T11:54:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -278,6 +293,11 @@ function ViewTable< Item >( {
 			}
 		};
 
+	const isScrolledEnd = useIsScrolledEnd( {
+		scrollContainerRef: containerRef,
path: packages/dataviews/src/dataviews-layouts/table/index.tsx
user: login: youknowriad
body: This works for me. Just a possibility I wanted to mention just in case.
created_at: 2025-05-07T12:40:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: youknowriad
body: Should it be named `useIsHorizontalScrollEnd` instead
created_at: 2025-05-07T12:41:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+export function useIsScrolledEnd( {
path: packages/dataviews/src/dataviews-layouts/table/use-is-scrolled-end.ts
user: login: arthur791004
body: Oh yes, it would be better. Renamed by https://github.com/Automattic/wp-calypso/pull/103005/commits/e2c331fb0c7f15a4b809c9a664e95914deac59f8.
created_at: 2025-05-07T12:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: youknowriad
body: Just want to note that hooks receiving "refs" as argument is not a very good pattern. The reason is because you're not informed when the "ref" changes. Imagine the "ref" is updated and attached to a new div, the computation should trigger again.

The ideal approach in these situations is to build a hook that returns a ref instead:

```
const ref = useHorizontalScrollObserver( ( isScrolledToTheEnd ) => setIsScrolledToTheEnd( isScrolledToTheEnd ) );
```

And internally `useHorizontalScrollObserver` should be relying on `useRefEffect`.

---
Maybe it's ok in this PR, but it's something to keep in mind and avoid if possible.
created_at: 2025-05-07T12:55:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: arthur791004
body: I initially considered returning an array from the hook with both the `ref` and `isScrolledToEnd`, but I ran into an issue — there doesn't seem to be a way to assign the ref to the scroll container in this context. So I ended up dropping that approach 😞
created_at: 2025-05-07T13:28:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: youknowriad
body: You'd have needed to put `isScrolledToEnd` in the context instead.
created_at: 2025-05-07T13:32:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: arthur791004
body: Hmmm... I don't think it's a good way to put `isScrolledToEnd` to the context as it changes frequently. Instead, it seems to be better to use `useRefEffect` to store the container in the state and pass it to the context. What do you think?
created_at: 2025-05-07T13:46:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: youknowriad
body: >  don't think it's a good way to put isScrolledToEnd to the context as it changes frequently

If this context keeps growing, we might want to implement selecting context re-rendering but let's not overengineer for now.

I don't like passing DOM elements around to be honest. I think for the moment, we should just add a comment to explain the drawbacks and limits of the current hook/approach.
created_at: 2025-05-07T13:50:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: arthur791004
body: Added by https://github.com/Automattic/wp-calypso/pull/103005/commits/d4192695a660af1425c92b3d8a859b84cd941a7d.
created_at: 2025-05-08T06:50:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: StevenDufresne
body: > Just want to note that hooks receiving "refs" as argument is not a very good pattern. The reason is because you're not informed when the "ref" changes. Imagine the "ref" is updated and attached to a new div, the computation should trigger again.

I agree that the context in which it's passed is important (the parent component should re-render), I'm not convinced that passing a ref to a hook is a bad pattern. Many battle-tested open-source hooks require ref passing.
created_at: 2025-05-08T07:10:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,65 @@
+/**
+ * WordPress dependencies
+ */
+import { useDebounce } from '@wordpress/compose';
+import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
+import { isRTL } from '@wordpress/i18n';
+
+const isScrolledToEnd = ( element: Element ) => {
+	if ( isRTL() ) {
+		const scrollLeft = Math.abs( element.scrollLeft );
+		return scrollLeft <= 1;
+	}
+
+	return element.scrollLeft + element.clientWidth >= element.scrollWidth - 1;
+};
+
+/**
+ * A hook to check if a given scroll container has reached the horizontal scroll end.
+ */
+export function useIsHorizontalScrollEnd( {
+	scrollContainerRef,
path: packages/dataviews/src/dataviews-layouts/table/use-is-horizontal-scroll-end.ts
user: login: youknowriad
body: What I can say, is that it used to be a common pattern to pass refs to hooks in Gutenberg, and this led to several bugs. The main issue is that `conditionals` for components are a common practice and if you're hook is deep in a component tree, there's a huge chance that the "ref" updates but the hook doesn't. Sometimes the impact is not very noticeable (the callback might be triggered for some other reason) but fundamentally it's still a hidden bug.
created_at: 2025-05-08T08:13:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-30T11:38:02Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-143359%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359">https://calypso.live?image=registry.a8c.com/calypso/app:build-143359</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-143359%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-143359%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-143359&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-04-30T11:46:47Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**App Entrypoints** (~47 bytes removed 📉 [gzipped])
<details>

```
name                    parsed_size           gzip_size
entry-main                  +2420 B  (+0.1%)      +26 B  (+0.0%)
entry-dashboard-dotcom       -319 B  (-0.0%)     -241 B  (-0.1%)
entry-dashboard-a4a          -319 B  (-0.0%)     -241 B  (-0.1%)
entry-subscriptions          -224 B  (-0.0%)     -300 B  (-0.1%)
entry-stepper                -220 B  (-0.0%)     -212 B  (-0.1%)
entry-reauth-required        -188 B  (-0.0%)     -157 B  (-0.0%)
entry-login                  -188 B  (-0.0%)     -157 B  (-0.0%)
entry-domains-landing        -139 B  (-0.0%)     -147 B  (-0.1%)
entry-browsehappy            -139 B  (-0.1%)     -147 B  (-0.2%)
```

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.
</details>

**Sections** (~109143 bytes removed 📉 [gzipped])
<details>

```
name                                parsed_size           gzip_size
overview                               +60502 B  (+2.4%)   +17503 B  (+2.6%)
domains                                +60317 B  (+2.3%)   +17099 B  (+2.5%)
site-logs                              +59470 B  (+5.2%)   +16498 B  (+5.2%)
purchases                               +4460 B  (+0.2%)     +456 B  (+0.1%)
site-purchases                          +4158 B  (+0.2%)     -448 B  (-0.1%)
sites-dashboard                         +3773 B  (+0.4%)    +1932 B  (+0.7%)
site-monitoring                         +3721 B  (+0.3%)    +1941 B  (+0.6%)
checkout                                +3406 B  (+0.2%)      -38 B  (-0.0%)
site-performance                        +3203 B  (+0.2%)    +1586 B  (+0.4%)
a8c-for-agencies-client                 +3058 B  (+0.1%)     -201 B  (-0.0%)
plugins                                 -1768 B  (-0.1%)    +1880 B  (+0.2%)
jetpack-cloud-plugin-management         -1755 B  (-0.1%)    +1887 B  (+0.4%)
a8c-for-agencies-plugins                -1755 B  (-0.1%)    +1887 B  (+0.4%)
site-settings                           +1451 B  (+0.1%)    +2595 B  (+0.7%)
hosting                                 +1446 B  (+0.1%)    +1892 B  (+0.4%)
staging-site                            +1255 B  (+0.1%)     +637 B  (+0.2%)
github-deployments                      +1245 B  (+0.1%)     +839 B  (+0.3%)
plans                                    +510 B  (+0.0%)    +2246 B  (+0.4%)
email                                    +463 B  (+0.0%)    -1803 B  (-0.6%)
a8c-for-agencies-sites                   -356 B  (-0.0%)     +311 B  (+0.0%)
people                                   -287 B  (-0.0%)     -851 B  (-0.3%)
theme                                    -282 B  (-0.0%)      -69 B  (-0.0%)
marketing                                -282 B  (-0.0%)    -1134 B  (-0.5%)
settings                                 -259 B  (-0.0%)      -15 B  (-0.0%)
a8c-for-agencies-marketplace             -245 B  (-0.0%)     -135 B  (-0.1%)
a8c-for-agencies-woopayments             -218 B  (-0.0%)     -459 B  (-0.2%)
a8c-for-agencies-team                    -218 B  (-0.0%)     -456 B  (-0.2%)
a8c-for-agencies-referrals               -218 B  (-0.0%)     -495 B  (-0.2%)
a8c-for-agencies-migrations              -218 B  (-0.0%)     -455 B  (-0.2%)
jetpack-cloud-agency-signup              +196 B  (+0.2%)     +558 B  (+2.1%)
a8c-for-agencies-overview                -179 B  (-0.0%)     -452 B  (-0.2%)
jetpack-cloud                            -158 B  (-0.0%)     -283 B  (-0.2%)
themes                                   -152 B  (-0.0%)      +62 B  (+0.0%)
jetpack-cloud-overview                   +141 B  (+0.0%)     +458 B  (+0.3%)
jetpack-cloud-agency-dashboard           -126 B  (-0.0%)     +269 B  (+0.1%)
subscribers                              -122 B  (-0.0%)     +900 B  (+0.3%)
async-step-unified-plans                 +104 B  (+0.0%)     +198 B  (+0.1%)
woocommerce-installation                  -91 B  (-0.0%)      -26 B  (-0.0%)
woocommerce                               -91 B  (-0.0%)      -26 B  (-0.0%)
settings-jetpack                          -91 B  (-0.0%)      -26 B  (-0.0%)
scan                                      -91 B  (-0.0%)      -26 B  (-0.0%)
promote-post-i2                           -91 B  (-0.0%)     +160 B  (+0.1%)
pages                                     -91 B  (-0.0%)      -26 B  (-0.0%)
migrate                                   -91 B  (-0.0%)      -26 B  (-0.0%)
media                                     -91 B  (-0.0%)      -26 B  (-0.0%)
marketplace                               -91 B  (-0.0%)      -26 B  (-0.0%)
jetpack-search                            -91 B  (-0.0%)      -26 B  (-0.0%)
jetpack-cloud-features-comparison         -91 B  (-0.0%)      -16 B  (-0.0%)
gutenberg-editor                          -91 B  (-0.0%)      -26 B  (-0.0%)
google-my-business                        -91 B  (-0.0%)     -199 B  (-0.1%)
export                                    -91 B  (-0.0%)      -26 B  (-0.0%)
customize                                 -91 B  (-0.0%)      -26 B  (-0.0%)
concierge                                 -91 B  (-0.0%)      -26 B  (-0.0%)
backup                                    -91 B  (-0.0%)      -26 B  (-0.0%)
add-ons                                   -91 B  (-0.0%)      +47 B  (+0.0%)
a8c-for-agencies-partner-directory        +82 B  (+0.0%)     +191 B  (+0.1%)
settings-writing                          -78 B  (-0.0%)      +69 B  (+0.0%)
settings-reading                          -78 B  (-0.0%)      +19 B  (+0.0%)
settings-newsletter                       -78 B  (-0.0%)      +53 B  (+0.0%)
settings-discussion                       -78 B  (-0.0%)      +23 B  (+0.0%)
jetpack-connect                           -77 B  (-0.0%)     -149 B  (-0.0%)
jetpack-app                               +65 B  (+0.0%)      +86 B  (+0.1%)
stats                                     -64 B  (-0.0%)     -114 B  (-0.0%)
reader                                    +60 B  (+0.0%)     +920 B  (+0.1%)
a8c-for-agencies-purchases                +42 B  (+0.0%)      -53 B  (-0.0%)
security                                  +39 B  (+0.0%)      -85 B  (-0.0%)
comments                                  +34 B  (+0.0%)       +1 B  (+0.0%)
settings-security                         +30 B  (+0.0%)      +26 B  (+0.0%)
settings-podcast                          +30 B  (+0.0%)      +57 B  (+0.0%)
settings-performance                      +30 B  (+0.0%)      +42 B  (+0.0%)
home                                      +25 B  (+0.0%)      -37 B  (-0.0%)
a8c-for-agencies-settings                 -23 B  (-0.0%)     -254 B  (-0.3%)
a8c-for-agencies-feedback                 -23 B  (-0.0%)     -280 B  (-0.4%)
a8c-for-agencies-agency-tier              -23 B  (-0.0%)     -251 B  (-0.3%)
patterns                                  -22 B  (-0.0%)      -24 B  (-0.0%)
posts-custom                              +17 B  (+0.0%)      -32 B  (-0.0%)
posts                                     +17 B  (+0.0%)      -32 B  (-0.0%)
jetpack-cloud-partner-portal              +17 B  (+0.0%)     +114 B  (+0.0%)
earn                                      +17 B  (+0.0%)      +64 B  (+0.0%)
activity                                  +17 B  (+0.0%)      -28 B  (-0.0%)
a8c-for-agencies-landing                  +17 B  (+0.0%)     +114 B  (+0.2%)
async-step-unified-domains                -13 B  (-0.0%)     +103 B  (+0.0%)
jetpack-social                            +12 B  (+0.0%)      -25 B  (-0.0%)
jetpack-cloud-settings                    +12 B  (+0.0%)      -13 B  (-0.0%)
```

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.
</details>

**Async-loaded Components** (~10292 bytes removed 📉 [gzipped])
<details>

```
name                                                                              parsed_size           gzip_size
async-load-my-sites-checkout-purchase-modal-is-eligible-for-one-click-checkou...      -1221 B  (-0.3%)     +255 B  (+0.2%)
async-load-automattic-help-center                                                      -244 B  (-0.0%)     +437 B  (+0.3%)
async-load-automattic-help-center-stepper                                              -195 B  (-0.0%)     +396 B  (+0.2%)
async-load-signup-steps-page-picker                                                    -143 B  (-0.0%)     -150 B  (-0.1%)
async-load-comment-block-editor                                                        -125 B  (-0.0%)     +311 B  (+0.0%)
async-load-purchase-modal-wrapper                                                      -116 B  (-0.0%)      -56 B  (-0.0%)
async-load-design-blocks                                                               +108 B  (+0.0%)       -6 B  (-0.0%)
async-load-calypso-reader-sidebar                                                      +108 B  (+0.1%)       -6 B  (-0.0%)
async-load-calypso-my-sites-current-site-notice                                        +108 B  (+0.2%)       -5 B  (-0.0%)
async-load-calypso-components-web-preview-component                                    +108 B  (+0.0%)       -6 B  (-0.0%)
async-load-calypso-blocks-jitm-templates-sidebar-banner                                +108 B  (+0.2%)       -2 B  (-0.0%)
async-load-calypso-blocks-jitm-templates-notice                                        +108 B  (+0.2%)       -3 B  (-0.0%)
async-load-calypso-blocks-jitm-templates-default                                       +108 B  (+0.2%)       -3 B  (-0.0%)
async-load-signup-steps-plans-theme-preselected                                        +104 B  (+0.0%)     +199 B  (+0.1%)
async-load-signup-steps-plans                                                          +104 B  (+0.0%)     +199 B  (+0.1%)
async-load-signup-steps-woocommerce-install-step-store-address                         +103 B  (+0.1%)      +10 B  (+0.0%)
async-load-calypso-lib-account-settings-helper                                         -103 B  (-0.1%)      -15 B  (-0.0%)
async-load-design-wordpress-components-gallery                                          -39 B  (-0.0%)     +573 B  (+0.3%)
async-load-automattic-global-styles-src-components-global-styles-variations             +17 B  (+0.0%)      +35 B  (+0.0%)
async-load-signup-steps-domains                                                         -13 B  (-0.0%)     +105 B  (+0.0%)
async-load-design-playground                                                            +13 B  (+0.0%)      -58 B  (-0.0%)
async-load-design                                                                       +13 B  (+0.0%)      -58 B  (-0.0%)
```

React components that are loaded lazily, when a certain part of UI is displayed for the first time.
</details>

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**Parsed Size:** Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
**Gzip Size:** Compressed size of the JS and CSS files. This much data needs to be downloaded over network.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: arthur791004
created_at: 2025-05-01T09:02:44Z
body: @matt-west You can try it out via Calypso Live link [here](https://github.com/Automattic/wp-calypso/pull/103005#issuecomment-2841700375) 🙂
author_association: CONTRIBUTOR
user: login: matt-west
created_at: 2025-05-01T12:59:11Z
body: Thanks @arthur791004. The behaviour looks good to me.

The pinned action column takes up quite a lot of space, especially on mobile.

<img width="345" alt="Screenshot 2025-05-01 at 13 57 43" src="https://github.com/user-attachments/assets/3c92bbb0-03c7-46d5-be0e-18721b8360d5" />

That's caused by all the action icons we have though, not specifically related to this PR. We’re addressing that as part of the dashboard redesign. We’ll be removing the icons to open the overview and visit site.

This is good to go from a design perspective. I’ll delegate to @oandregal and @youknowriad on the implementation specifics.
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-05-02T00:42:47Z
body: I find the large empty actions column confusing. Should we display the icons if it's pinned? Alternatively, what about adding a box shadow left of the pinned column to help users identify the layers? 

### Screenshots
| Before `1100px` | After `1100px` |
|--------|--------|
| <img width="807" alt="Screenshot 2025-05-02 at 9 33 47 AM" src="https://github.com/user-attachments/assets/ddeda78b-9907-4080-86b2-97d7c2010887" /> | <img width="782" alt="Screenshot 2025-05-02 at 9 41 58 AM" src="https://github.com/user-attachments/assets/6afb11a9-4f2e-4300-9e8b-753740903342" /> | 




author_association: CONTRIBUTOR
user: login: arthur791004
created_at: 2025-05-02T02:23:07Z
body: > I find the large empty actions column confusing. Should we display the icons if it's pinned?

Yes, it's mentioned https://github.com/Automattic/wp-calypso/pull/103005#issuecomment-2844804616 as well. Maybe we could consider removing those icons, similar to what we did in v2. cc @matt-west 

> Alternatively, what about adding a box shadow left of the pinned column to help users identify the layers?

We’d prefer to use a border to help users identify the layers. See https://linear.app/a8c/issue/DOTCOM-10696/pin-actions-column-to-the-right-of-the-sites-table#comment-826b0e22
author_association: CONTRIBUTOR
user: login: matt-west
created_at: 2025-05-02T08:45:07Z
body: > Maybe we could consider removing those icons, similar to what we did in v2.

Perhaps the icon buttons move into the actions menu when the column is pinned to reduce the width of the column.

@jameskoster are there any guidelines on how actions buttons should behave on smaller screen sizes?
author_association: CONTRIBUTOR
user: login: oandregal
created_at: 2025-05-02T08:53:27Z
body: These changes are going to be done to `automattic/dataviews` so it will affect all products that use it (dotcom + woo analytics for now, but increasingly more with time), so I'd like to double-check design to make sure we're aligned.

@matt-west @jameskoster This is how the list view works (video from [this comment from Jay in Gutenberg](https://github.com/WordPress/gutenberg/pull/65086#issuecomment-2331696425)):

- The actions aren't displayed until the user hovers, so they don't overlap the content.
- It doesn't have any border.

https://github.com/user-attachments/assets/6692953d-375b-417d-b5ba-9b24f4ada428

I don't have a specific opinion on how this should work design-wise, but it feels to me that it should be the same across view types (list, table).


author_association: CONTRIBUTOR
user: login: oandregal
created_at: 2025-05-02T08:57:05Z
body: Thanks for the quick pivot, @arthur791004 

The `packages/dataviews` is very new and I want to be sure that everyone understands the requirements to make changes to it:

- Every change needs to be documented in `CHANGELOG.automattic.md`.
- We aim to synchronize changes from upstream Gutenberg (`@wordpress/dataviews`) every two weeks, so we should have a mindset of doing the minimal changes that work (e.g., no cosmetic changes, refactors, etc.). This means, I'd probably be a bit picky at the beginning about not making unnecessary changes until we understand better this process. :)

There's more info in the package's README.md (see contributing section) and the FieldGuide.
author_association: CONTRIBUTOR
user: login: jameskoster
created_at: 2025-05-02T15:09:54Z
body: <!-- linear:replyTo:2841718882 -->

@matt-west no guidelines. It's basically a trade-off decision for the consumer based on how many primary actions are supplied, as you mentioned. 
author_association: CONTRIBUTOR
user: login: oandregal
created_at: 2025-05-05T15:07:19Z
body: @arthur791004 I wanted to test this, but I couldn't. Judging by [this](https://github.com/Automattic/wp-calypso/pull/103005#issuecomment-2846686939) and [this](https://github.com/Automattic/wp-calypso/pull/103005#issuecomment-2846701631), I think it'd be good to explore a similar approach to the list view (hiding the icons and only display them on hover).
author_association: CONTRIBUTOR
user: login: matt-west
created_at: 2025-05-06T08:21:23Z
body: I'm okay with trying the same behaviour we have in the list layout here. I have some reservations as the table content will scroll horizontally beneath the overlayed icons here. I don’t believe we have that in list layout. We can give it a go and see how it feels though.
author_association: CONTRIBUTOR
user: login: arthur791004
created_at: 2025-05-07T02:51:59Z
body: Yeah, I agreed with Matt that this table is different, since the content will scroll horizontally underneath the overlaid icons.
author_association: CONTRIBUTOR
user: login: jameskoster
created_at: 2025-05-07T12:03:18Z
body: <!-- linear:replyTo:2841718882 -->

It should still be possible to scroll. Additional to the consistency point I think the list layout technique is mostly a (potential) workaround for the [confusing empty column ](https://uploads.linear.app/f629fcde-6f9b-48e8-8af3-60eed8823bff/958413d3-d42d-4588-aa62-174c6dae677e/f900ba5f-b161-4ac3-8e10-faee3ce120e8?signature=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoiL2Y2MjlmY2RlLTZmOWItNDhlOC04YWYzLTYwZWVkODgyM2JmZi85NTg0MTNkMy1kNDJkLTQ1ODgtYWE2Mi0xNzRjNmRhZTY3N2UvZjkwMGJhNWYtYjE2MS00YWMzLThlMTAtZmFlZTNjZTEyMGU4IiwiaWF0IjoxNzQ2NjE5Mzk4LCJleHAiOjMzMzE3MTc5Mzk4fQ.UaFz5L1xJrxotfxDiXIkiwuI1PpXolgY2lbDkNWWTdM)problem.
author_association: CONTRIBUTOR