From 34ea4eced6ae1a97c6a8f6c691b97e20fdbe69f9 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Wed, 4 Sep 2024 19:44:16 -0600 Subject: [PATCH] Adjustments to pagination design and accessibility (#39249) --- .../js/components/threats-list/pagination.jsx | 168 ++++++++---------- .../threats-list/styles.module.scss | 31 +--- 2 files changed, 84 insertions(+), 115 deletions(-) diff --git a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx index 4f0e57a565820..3e17bed0eeac4 100644 --- a/projects/plugins/protect/src/js/components/threats-list/pagination.jsx +++ b/projects/plugins/protect/src/js/components/threats-list/pagination.jsx @@ -1,9 +1,10 @@ -import { Button } from '@automattic/jetpack-components'; -import { Icon, chevronLeft, chevronRight } from '@wordpress/icons'; -import React, { useCallback, useEffect, useState, useMemo, memo } from 'react'; +import { Button, useBreakpointMatch } from '@automattic/jetpack-components'; +import { __, sprintf } from '@wordpress/i18n'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; +import React, { useCallback, useState, useMemo } from 'react'; import styles from './styles.module.scss'; -const PaginationButton = memo( ( { pageNumber, currentPage, onPageChange } ) => { +const PaginationButton = ( { pageNumber, currentPage, onPageChange } ) => { const isCurrentPage = useMemo( () => currentPage === pageNumber, [ currentPage, pageNumber ] ); const handleClick = useCallback( () => { @@ -16,42 +17,30 @@ const PaginationButton = memo( ( { pageNumber, currentPage, onPageChange } ) => className={ ! isCurrentPage ? styles.unfocused : null } onClick={ handleClick } aria-current={ isCurrentPage ? 'page' : undefined } + aria-label={ sprintf( + /* translators: placeholder is a page number, i.e. "Page 123" */ + __( 'Page %d', 'jetpack-protect' ), + pageNumber + ) } > { pageNumber } ); -} ); - -const IconButton = ( { onClick, disabled, direction, iconSize } ) => { - const isLeft = direction === 'left'; - return ( - - ); }; const Pagination = ( { list, itemPerPage = 10, children } ) => { - const iconSize = 24; + const [ isSm ] = useBreakpointMatch( 'sm' ); + const [ currentPage, setCurrentPage ] = useState( 1 ); - const [ isSmall, setIsSmall ] = useState( window.matchMedia( '(max-width: 1220px)' ).matches ); + + const handlePreviousPageClick = useCallback( + () => setCurrentPage( currentPage - 1 ), + [ currentPage, setCurrentPage ] + ); + const handleNextPageClick = useCallback( + () => setCurrentPage( currentPage + 1 ), + [ currentPage, setCurrentPage ] + ); const totalPages = useMemo( () => Math.ceil( list.length / itemPerPage ), [ list, itemPerPage ] ); @@ -61,97 +50,90 @@ const Pagination = ( { list, itemPerPage = 10, children } ) => { return list.slice( indexOfFirstItem, indexOfLastItem ); }, [ currentPage, list, itemPerPage ] ); - const onPageChange = useCallback( pageNumber => { - setCurrentPage( pageNumber ); - }, [] ); - - useEffect( () => { - const mediaQuery = window.matchMedia( '(max-width: 1220px)' ); - const handleMediaChange = event => { - setIsSmall( event.matches ); - }; - mediaQuery.addEventListener( 'change', handleMediaChange ); - return () => { - mediaQuery.removeEventListener( 'change', handleMediaChange ); - }; - }, [] ); - - const handleFirstPageClick = useCallback( () => onPageChange( 1 ), [ onPageChange ] ); - const handlePreviousPageClick = useCallback( - () => onPageChange( currentPage - 1 ), - [ currentPage, onPageChange ] - ); - const handleNextPageClick = useCallback( - () => onPageChange( currentPage + 1 ), - [ currentPage, onPageChange ] - ); - const handleLastPageClick = useCallback( - () => onPageChange( totalPages ), - [ onPageChange, totalPages ] - ); - - const getPageNumbers = useCallback( () => { - if ( isSmall ) { + const pageNumbers = useMemo( () => { + if ( isSm ) { return [ currentPage ]; } + const result = [ 1 ]; + if ( currentPage > 3 && totalPages > 4 ) { + result.push( '…' ); + } + if ( currentPage === 1 ) { - return [ 1, 2, 3 ].filter( page => page <= totalPages ); + // Current page is the first page. + // i.e. [ 1 ] 2 3 4 ... 10 + result.push( currentPage + 1, currentPage + 2, currentPage + 3 ); + } else if ( currentPage === 2 ) { + // Current page is the second to first page. + // i.e. 1 [ 2 ] 3 4 ... 10 + result.push( currentPage, currentPage + 1, currentPage + 2 ); + } else if ( currentPage < totalPages - 1 ) { + // Current page is positioned in the middle of the pagination. + // i.e. 1 ... 3 [ 4 ] 5 ... 10 + result.push( currentPage - 1, currentPage, currentPage + 1 ); + } else if ( currentPage === totalPages - 1 ) { + // Current page is the second to last page. + // i.e. 1 ... 7 8 [ 9 ] 10 + currentPage > 3 && result.push( currentPage - 2 ); + currentPage > 2 && result.push( currentPage - 1 ); + result.push( currentPage ); + } else if ( currentPage === totalPages ) { + // Current page is the last page. + // i.e. 1 ... 7 8 9 [ 10 ] + currentPage >= 5 && result.push( currentPage - 3 ); + currentPage >= 4 && result.push( currentPage - 2 ); + result.push( currentPage - 1 ); } - if ( currentPage === totalPages ) { - return [ totalPages - 2, totalPages - 1, totalPages ].filter( page => page >= 1 ); + + if ( result[ result.length - 1 ] < totalPages - 1 ) { + result.push( '…' ); + result.push( totalPages ); + } else if ( result[ result.length - 1 ] < totalPages ) { + result.push( totalPages ); } - return [ currentPage - 1, currentPage, currentPage + 1 ]; - }, [ currentPage, totalPages, isSmall ] ); + + return result.filter( pageNumber => pageNumber <= totalPages || isNaN( pageNumber ) ); + }, [ currentPage, isSm, totalPages ] ); return ( <> { children( { currentItems } ) } { totalPages > 1 && ( -
- +
+ ) } ); diff --git a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss index b066baa7dab0e..4a50d87b2562b 100644 --- a/projects/plugins/protect/src/js/components/threats-list/styles.module.scss +++ b/projects/plugins/protect/src/js/components/threats-list/styles.module.scss @@ -104,39 +104,26 @@ display: flex; justify-content: center; align-items: center; - margin-top: calc( var( --spacing-base ) * 2 ); // 16px + gap: 4px; + margin-top: calc( var( --spacing-base ) * 4 ); // 24px + margin-bottom: calc(var(--spacing-base) * 2); // 16px button { font-size: var( --font-body ); - padding: var( --spacing-base ) calc( var( --spacing-base ) * 2 ); // 8px | 16px + width: auto; + height: auto; + padding: 0 var( --spacing-base ); // 0 | 8px + line-height: 32px; + min-width: 32px; &.unfocused { color: var( --jp-black ); background: none; - box-shadow: none; - &:hover:not(:disabled), &:focus:not(:disabled) { + &:hover:not(:disabled) { color: var( --jp-black ); background: none; - box-shadow: none; } } - - &.icon-button:focus:not(:disabled) { - box-shadow: none; - } - } - - .first-icon, - .last-icon { - display: flex; - - .inside { - margin-left: var( --spacing-base ); - } - - .outside { - position: absolute; - } } }