diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 0498418d90bc43..03dae9f85c1df7 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -15,6 +15,7 @@ ### Enhancements +- DataViews table layout: remove row click-to-select behavior and hover styles. Selection is now only possible via checkboxes, or by ctrl/cmd clicking. [#73873](https://github.com/WordPress/gutenberg/pull/73873) - Better labels for operators and deprecate the `isNotAll` operator. [#73671](https://github.com/WordPress/gutenberg/pull/73671) - Field API: move validation to the field type. [#73642](https://github.com/WordPress/gutenberg/pull/73642) - DataForm: add support for `min`/`max` and `minLength`/`maxLength` validation for relevant controls. [#73465](https://github.com/WordPress/gutenberg/pull/73465) diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index bc97a8b975f42d..1d98e2b3c95851 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -122,20 +122,12 @@ function TableRow< Item >( { const { paginationInfo } = useContext( DataViewsContext ); const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); const isSelected = hasPossibleBulkAction && selection.includes( id ); - const [ isHovered, setIsHovered ] = useState( false ); const { showTitle = true, showMedia = true, showDescription = true, infiniteScrollEnabled, } = view; - const handleMouseEnter = () => { - setIsHovered( true ); - }; - const handleMouseLeave = () => { - setIsHovered( false ); - }; - // Will be set to true if `onTouchStart` fires. This happens before // `onClick` and can be used to exclude touchscreen devices from certain // behaviours. @@ -150,11 +142,8 @@ function TableRow< Item >( { { isTouchDeviceRef.current = true; } } @@ -183,29 +172,22 @@ function TableRow< Item >( { return; } + // Only handle Ctrl/Cmd+Click for multi-selection + const isModifierKeyPressed = isAppleOS() + ? event.metaKey + : event.ctrlKey; + if ( + isModifierKeyPressed && ! isTouchDeviceRef.current && document.getSelection()?.type !== 'Range' ) { - if ( isAppleOS() ? event.metaKey : event.ctrlKey ) { - // Handle non-consecutive selection. - onChangeSelection( - selection.includes( id ) - ? selection.filter( - ( itemId ) => id !== itemId - ) - : [ ...selection, id ] - ); - } else { - // Handle single selection - onChangeSelection( - selection.includes( id ) - ? selection.filter( - ( itemId ) => id !== itemId - ) - : [ id ] - ); - } + // Handle non-consecutive selection with Ctrl/Cmd+Click + onChangeSelection( + selection.includes( id ) + ? selection.filter( ( itemId ) => id !== itemId ) + : [ ...selection, id ] + ); } } } > diff --git a/packages/dataviews/src/dataviews-layouts/table/style.scss b/packages/dataviews/src/dataviews-layouts/table/style.scss index 00e7ddda560fb9..a69848aa1adf2b 100644 --- a/packages/dataviews/src/dataviews-layouts/table/style.scss +++ b/packages/dataviews/src/dataviews-layouts/table/style.scss @@ -30,7 +30,7 @@ &.dataviews-view-table__actions-column--sticky { position: sticky; right: 0; - background-color: inherit; + background-color: var(--wp-dataviews-color-background, $white); } &.dataviews-view-table__actions-column--stuck { @@ -73,11 +73,6 @@ border-bottom: 0; } - &.is-hovered, - &.is-hovered .dataviews-view-table__actions-column--sticky { - background-color: #f8f8f8; - } - .dataviews-item-actions .components-button:not(.dataviews-all-actions-button) { opacity: 0; } @@ -98,38 +93,31 @@ border-top: 1px solid rgba(var(--wp-admin-theme-color--rgb), 0.12); } - &:hover { - background-color: color-mix(in srgb, rgb(var(--wp-admin-theme-color--rgb)) 8%, $white); - } - .dataviews-view-table__actions-column--sticky { background-color: color-mix(in srgb, rgb(var(--wp-admin-theme-color--rgb)) 4%, $white); } - - &:hover .dataviews-view-table__actions-column--sticky { - background-color: color-mix(in srgb, rgb(var(--wp-admin-theme-color--rgb)) 8%, $white); - } } } - // Hover styles only apply when bulk actions are available + // Show interactive elements on focus &.has-bulk-actions { tr { - // Only apply hover background to non-selected rows - &:not(.is-selected) { - &.is-hovered, - &.is-hovered .dataviews-view-table__actions-column--sticky { - background-color: #f8f8f8; - } - } - &:focus-within, - &.is-hovered, &:hover { - .components-checkbox-control__input, .dataviews-item-actions .components-button:not(.dataviews-all-actions-button) { opacity: 1; } + + .dataviews-view-table__actions-column--sticky { + background-color: var(--wp-dataviews-color-background, $white); + } + } + + &.is-selected, + &.is-selected:hover { + .dataviews-view-table__actions-column--sticky { + background-color: color-mix(in srgb, rgb(var(--wp-admin-theme-color--rgb)) 4%, $white); + } } } } diff --git a/packages/dataviews/src/test/dataviews.tsx b/packages/dataviews/src/test/dataviews.tsx index aef345483b8d2a..67cd7eb1bffe6d 100644 --- a/packages/dataviews/src/test/dataviews.tsx +++ b/packages/dataviews/src/test/dataviews.tsx @@ -302,35 +302,6 @@ describe( 'DataViews component', () => { expect( onClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] ); } ); - it( 'accepts click for single selection', async () => { - render( - - ); - const firstItemElement = screen.getByText( data[ 0 ].title ); - const thirdItemElement = screen.getByText( data[ 2 ].title ); - const user = userEvent.setup(); - await user.click( firstItemElement ); - - // First item should be selected. - expect( - screen.getByRole( 'checkbox', { name: data[ 0 ].title } ) - ).toBeChecked(); - await user.click( thirdItemElement ); - - // Third item should be selected. First item was deselected. - expect( - screen.getByRole( 'checkbox', { name: data[ 2 ].title } ) - ).toBeChecked(); - } ); - it( 'accepts ctrl/cmd key and click for non-consecutive multi-selection', async () => { render( { const firstItemElement = screen.getByText( data[ 0 ].title ); const thirdItemElement = screen.getByText( data[ 2 ].title ); const user = userEvent.setup(); + await user.keyboard( '{Control>}' ); await user.click( firstItemElement ); // First item should be selected. expect( screen.getByRole( 'checkbox', { name: data[ 0 ].title } ) ).toBeChecked(); - await user.keyboard( '{Control>}' ); await user.click( thirdItemElement ); // Both items should be selected. @@ -445,66 +416,65 @@ describe( 'DataViews component', () => { expect( mediaClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] ); } ); - it( 'accepts click for single selection', async () => { + it( 'accepts checkbox click for selection', async () => { render( ); - const firstItemElement = screen.getByText( data[ 0 ].title ); - const thirdItemElement = screen.getByText( data[ 2 ].title ); + const firstCheckbox = screen.getByRole( 'checkbox', { + name: data[ 0 ].title, + } ); + const thirdCheckbox = screen.getByRole( 'checkbox', { + name: data[ 2 ].title, + } ); const user = userEvent.setup(); - await user.click( firstItemElement ); + await user.click( firstCheckbox ); // First item should be selected. - expect( - screen.getByRole( 'checkbox', { name: data[ 0 ].title } ) - ).toBeChecked(); - await user.click( thirdItemElement ); + expect( firstCheckbox ).toBeChecked(); + await user.click( thirdCheckbox ); - // Third item should be selected. First item was deselected. - expect( - screen.getByRole( 'checkbox', { name: data[ 2 ].title } ) - ).toBeChecked(); + // Both items should be selected (checkboxes toggle independently). + expect( firstCheckbox ).toBeChecked(); + expect( thirdCheckbox ).toBeChecked(); } ); - it( 'accepts ctrl/cmd key and click for non-consecutive multi-selection', async () => { + it( 'accepts ctrl/cmd key and click for multi-selection', async () => { render( false } /> ); - const firstItemElement = screen.getByText( data[ 0 ].title ); - const thirdItemElement = screen.getByText( data[ 2 ].title ); + // Click on the gridcell directly (not wrapped in ItemClickWrapper) + const firstItemCard = screen.getByRole( 'gridcell', { + name: /Hello World/, + } ); const user = userEvent.setup(); - await user.click( firstItemElement ); - - // First item should be selected. - expect( - screen.getByRole( 'checkbox', { name: data[ 0 ].title } ) - ).toBeChecked(); await user.keyboard( '{Control>}' ); - await user.click( thirdItemElement ); + await user.click( firstItemCard ); - // Both items should be selected. + // First item should be selected. expect( screen.getByRole( 'checkbox', { name: data[ 0 ].title } ) ).toBeChecked(); - expect( - screen.getByRole( 'checkbox', { name: data[ 2 ].title } ) - ).toBeChecked(); await user.keyboard( '{/Control}' ); } );