Skip to content

Commit

Permalink
Protect: Add FreeList and PaidList pagination (#39058)
Browse files Browse the repository at this point in the history
* Add threats list pagination

* changelog

* Add items per page constant, and handling

* Fix mobile view

* Simplify and consolidate

* Fix versions

* Use custom media query to improve responsiveness

* Revert versions updates

* Update and improve implementation

* Compact

* Set iconSize on parent component level

* Adjustments to pagination design and accessibility (#39249)

---------

Co-authored-by: Nate Weller <[email protected]>
  • Loading branch information
dkmyta and nateweller authored Sep 5, 2024
1 parent 90fc4d9 commit b0faef5
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Adds threats list pagination
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, { useCallback } from 'react';
import { JETPACK_SCAN_SLUG } from '../../constants';
import useAnalyticsTracks from '../../hooks/use-analytics-tracks';
import FreeAccordion, { FreeAccordionItem } from '../free-accordion';
import Pagination from './pagination';
import styles from './styles.module.scss';

const ThreatAccordionItem = ( {
Expand Down Expand Up @@ -85,38 +86,42 @@ const ThreatAccordionItem = ( {

const FreeList = ( { list } ) => {
return (
<FreeAccordion>
{ list.map(
( {
description,
fixedIn,
icon,
id,
label,
name,
source,
table,
title,
type,
version,
} ) => (
<ThreatAccordionItem
description={ description }
fixedIn={ fixedIn }
icon={ icon }
id={ id }
label={ label }
key={ id }
name={ name }
source={ source }
table={ table }
title={ title }
type={ type }
version={ version }
/>
)
<Pagination list={ list }>
{ ( { currentItems } ) => (
<FreeAccordion>
{ currentItems.map(
( {
description,
fixedIn,
icon,
id,
label,
name,
source,
table,
title,
type,
version,
} ) => (
<ThreatAccordionItem
description={ description }
fixedIn={ fixedIn }
icon={ icon }
id={ id }
label={ label }
key={ id }
name={ name }
source={ source }
table={ table }
title={ title }
type={ type }
version={ version }
/>
)
) }
</FreeAccordion>
) }
</FreeAccordion>
</Pagination>
);
};

Expand Down
142 changes: 142 additions & 0 deletions projects/plugins/protect/src/js/components/threats-list/pagination.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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 = ( { pageNumber, currentPage, onPageChange } ) => {
const isCurrentPage = useMemo( () => currentPage === pageNumber, [ currentPage, pageNumber ] );

const handleClick = useCallback( () => {
onPageChange( pageNumber );
}, [ onPageChange, pageNumber ] );

return (
<Button
size={ 'medium' }
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 }
</Button>
);
};

const Pagination = ( { list, itemPerPage = 10, children } ) => {
const [ isSm ] = useBreakpointMatch( 'sm' );

const [ currentPage, setCurrentPage ] = useState( 1 );

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 ] );

const currentItems = useMemo( () => {
const indexOfLastItem = currentPage * itemPerPage;
const indexOfFirstItem = indexOfLastItem - itemPerPage;
return list.slice( indexOfFirstItem, indexOfLastItem );
}, [ currentPage, list, itemPerPage ] );

const pageNumbers = useMemo( () => {
if ( isSm ) {
return [ currentPage ];
}

const result = [ 1 ];
if ( currentPage > 3 && totalPages > 4 ) {
result.push( '…' );
}

if ( currentPage === 1 ) {
// 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 ( result[ result.length - 1 ] < totalPages - 1 ) {
result.push( '…' );
result.push( totalPages );
} else if ( result[ result.length - 1 ] < totalPages ) {
result.push( totalPages );
}

return result.filter( pageNumber => pageNumber <= totalPages || isNaN( pageNumber ) );
}, [ currentPage, isSm, totalPages ] );

return (
<>
{ children( { currentItems } ) }
{ totalPages > 1 && (
<nav
role="navigation"
aria-label={ __( 'Threat list pages', 'jetpack-protect' ) }
className={ styles[ 'pagination-container' ] }
>
<Button
onClick={ handlePreviousPageClick }
disabled={ currentPage === 1 }
variant={ 'link' }
icon={ chevronLeft }
iconSize={ 24 }
aria-label={ __( 'Previous page', 'jetpack-protect' ) }
/>
{ pageNumbers.map( ( pageNumber, index ) =>
typeof pageNumber === 'number' ? (
<PaginationButton
key={ pageNumber }
pageNumber={ pageNumber }
currentPage={ currentPage }
onPageChange={ setCurrentPage }
/>
) : (
<span key={ `ellipses_${ index }` }>{ pageNumber }</span>
)
) }
<Button
onClick={ handleNextPageClick }
disabled={ currentPage === totalPages }
variant={ 'link' }
icon={ chevronRight }
iconSize={ 24 }
aria-label={ __( 'Next page', 'jetpack-protect' ) }
/>
</nav>
) }
</>
);
};

export default Pagination;
103 changes: 54 additions & 49 deletions projects/plugins/protect/src/js/components/threats-list/paid-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { STORE_ID } from '../../state/store';
import DiffViewer from '../diff-viewer';
import MarkedLines from '../marked-lines';
import PaidAccordion, { PaidAccordionItem } from '../paid-accordion';
import Pagination from './pagination';
import styles from './styles.module.scss';

const ThreatAccordionItem = ( {
Expand Down Expand Up @@ -177,62 +178,66 @@ const PaidList = ( { list, hideAutoFixColumn = false } ) => {
return (
<>
{ ! isSmall && (
<div className={ styles[ 'accordion-heading' ] }>
<div className={ styles[ 'accordion-header' ] }>
<span>{ __( 'Details', 'jetpack-protect' ) }</span>
<span>{ __( 'Severity', 'jetpack-protect' ) }</span>
{ ! hideAutoFixColumn && <span>{ __( 'Auto-fix', 'jetpack-protect' ) }</span> }
<span></span>
</div>
) }
<PaidAccordion>
{ list.map(
( {
context,
description,
diff,
filename,
firstDetected,
fixedIn,
fixedOn,
icon,
fixable,
id,
label,
name,
severity,
source,
table,
title,
type,
version,
status,
} ) => (
<ThreatAccordionItem
context={ context }
description={ description }
diff={ diff }
filename={ filename }
firstDetected={ firstDetected }
fixedIn={ fixedIn }
fixedOn={ fixedOn }
icon={ icon }
fixable={ fixable }
id={ id }
key={ id }
label={ label }
name={ name }
severity={ severity }
source={ source }
table={ table }
title={ title }
type={ type }
version={ version }
status={ status }
hideAutoFixColumn={ hideAutoFixColumn }
/>
)
<Pagination list={ list }>
{ ( { currentItems } ) => (
<PaidAccordion>
{ currentItems.map(
( {
context,
description,
diff,
filename,
firstDetected,
fixedIn,
fixedOn,
icon,
fixable,
id,
label,
name,
severity,
source,
table,
title,
type,
version,
status,
} ) => (
<ThreatAccordionItem
context={ context }
description={ description }
diff={ diff }
filename={ filename }
firstDetected={ firstDetected }
fixedIn={ fixedIn }
fixedOn={ fixedOn }
icon={ icon }
fixable={ fixable }
id={ id }
key={ id }
label={ label }
name={ name }
severity={ severity }
source={ source }
table={ table }
title={ title }
type={ type }
version={ version }
status={ status }
hideAutoFixColumn={ hideAutoFixColumn }
/>
)
) }
</PaidAccordion>
) }
</PaidAccordion>
</Pagination>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
}
}

.accordion-heading {
.accordion-header {
display: grid;
grid-template-columns: repeat( 9, 1fr );
background-color: white;
Expand Down Expand Up @@ -99,3 +99,31 @@
}
}
}

.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
margin-top: calc( var( --spacing-base ) * 4 ); // 24px
margin-bottom: calc(var(--spacing-base) * 2); // 16px

button {
font-size: var( --font-body );
width: auto;
height: auto;
padding: 0 var( --spacing-base ); // 0 | 8px
line-height: 32px;
min-width: 32px;

&.unfocused {
color: var( --jp-black );
background: none;

&:hover:not(:disabled) {
color: var( --jp-black );
background: none;
}
}
}
}

0 comments on commit b0faef5

Please sign in to comment.