Skip to content

Commit

Permalink
feat: pagination hrefs (#434)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrednic-1A authored Feb 14, 2024
1 parent 35a0b70 commit 484b45f
Show file tree
Hide file tree
Showing 16 changed files with 708 additions and 146 deletions.
31 changes: 31 additions & 0 deletions angular/demo/src/app/samples/pagination/hash.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {AgnosUIAngularModule, toAngularSignal} from '@agnos-ui/angular';
import {hash$} from '@agnos-ui/common/utils';
import {Component, computed} from '@angular/core';
@Component({
standalone: true,
imports: [AgnosUIAngularModule],
template: `
<p>A pagination with hrefs provided for each pagination element:</p>
<p>
Page hash: <small>{{ '#' + hash() }}</small>
</p>
<nav
auPagination
auCollectionSize="60"
auBoundaryLinks="true"
[auPage]="pageNumber()"
[auPageLink]="pageLink"
(auPageChange)="pageChange($event)"
auAriaLabel="Page navigation with customized hrefs"
></nav>
`,
})
export default class HashPaginationComponent {
hash = toAngularSignal(hash$);

pageNumber = computed(() => +(this.hash().split('#')[1] ?? 4));

pageLink = (currentPage: number) => `#${this.hash().split('#')[0]}#${currentPage}`;

pageChange = (currentPage: number) => (location.hash = `#${this.hash().split('#')[0]}#${currentPage}`);
}
28 changes: 18 additions & 10 deletions angular/lib/src/components/pagination/pagination.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ export class PaginationPagesDirective {
<a
[attr.aria-label]="state.pagesLabel[i]"
class="page-link au-page"
href="#"
(click)="widget.actions.select(page); $event.preventDefault()"
[attr.href]="state.pagesHrefs[i]"
(click)="widget.actions.select(page, $event)"
[attr.tabindex]="state.disabled ? '-1' : null"
[attr.aria-disabled]="state.disabled ? 'true' : null"
>
Expand Down Expand Up @@ -165,8 +165,8 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaFirstLabel"
class="page-link au-first"
href="#"
(click)="widget.actions.first(); $event.preventDefault()"
[attr.href]="state.pagesHrefs[0]"
(click)="widget.actions.first($event)"
[attr.tabindex]="state.previousDisabled ? '-1' : null"
[attr.aria-disabled]="state.previousDisabled ? 'true' : null"
>
Expand All @@ -181,8 +181,8 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaPreviousLabel"
class="page-link au-previous"
href="#"
(click)="widget.actions.previous(); $event.preventDefault()"
[attr.href]="state.directionsHrefs.previous"
(click)="widget.actions.previous($event)"
[attr.tabindex]="state.previousDisabled ? '-1' : null"
[attr.aria-disabled]="state.previousDisabled ? 'true' : null"
>
Expand All @@ -198,8 +198,8 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaNextLabel"
class="page-link au-next"
href="#"
(click)="widget.actions.next(); $event.preventDefault()"
[attr.href]="state.directionsHrefs.next"
(click)="widget.actions.next($event)"
[attr.tabindex]="state.nextDisabled ? '-1' : null"
[attr.aria-disabled]="state.nextDisabled ? 'true' : null"
>
Expand All @@ -214,8 +214,8 @@ const defaultConfig: Partial<PaginationProps> = {
<a
[attr.aria-label]="state.ariaLastLabel"
class="page-link au-last"
href="#"
(click)="widget.actions.last(); $event.preventDefault()"
[attr.href]="state.pagesHrefs.at(-1)"
(click)="widget.actions.last($event)"
[attr.tabindex]="state.nextDisabled ? '-1' : null"
[attr.aria-disabled]="state.nextDisabled ? 'true' : null"
>
Expand Down Expand Up @@ -290,6 +290,14 @@ export class PaginationComponent extends BaseWidgetDirective<PaginationWidget> i
*/
@Input('auAriaLastLabel') ariaLastLabel: string | undefined;

/**
* Factory function providing the href for a "Page" page anchor,
* based on the current page number
* @param pageNumber - The index to use in the link
*
*/
@Input('auPageLink') pageLink: ((pageNumber: number) => string) | undefined;

readonly _widget = callWidgetFactory({
factory: createPagination,
widgetName: 'pagination',
Expand Down
104 changes: 95 additions & 9 deletions core/src/components/pagination/pagination.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {beforeEach, describe, expect, test, vi} from 'vitest';
import type {PaginationState, PaginationWidget} from './pagination';
import {createPagination, getPaginationDefaultConfig} from './pagination';
import {ngBootstrapPagination} from './bootstrap';
import {assign} from '../../../../common/utils';

describe(`Pagination`, () => {
let pagination: PaginationWidget;
let state: PaginationState;
let expectedState: PaginationState;

let consoleErrorSpy: MockInstance<Parameters<typeof console.error>, ReturnType<typeof console.error>>;

Expand All @@ -20,6 +22,7 @@ describe(`Pagination`, () => {
const unsubscribe = pagination.state$.subscribe((newState) => {
state = newState;
});
expectedState = state;
return () => {
unsubscribe();
expect(consoleErrorSpy).not.toHaveBeenCalled();
Expand All @@ -34,22 +37,36 @@ describe(`Pagination`, () => {
};

test(`should have sensible state`, () => {
// TODO we don't test ariaPageLabel here...
expect(state).toMatchObject({
expect(state).toStrictEqual({
pageCount: 1, // total number of page
page: 1, // current page
pages: [1], // list of the visible pages
previousDisabled: true,
ariaLabel: 'Page navigation',
className: '',
nextDisabled: true,
disabled: false,
directionLinks: true,
boundaryLinks: false,
slotEllipsis: '…',
slotFirst: '«',
slotLast: '»',
slotNext: '›',
slotNumberLabel: state.slotNumberLabel,
slotPages: undefined,
slotPrevious: '‹',
size: null,
activeLabel: '(current)',
ariaFirstLabel: 'Action link for first page',
ariaLastLabel: 'Action link for last page',
ariaNextLabel: 'Action link for next page',
ariaPreviousLabel: 'Action link for previous page',
directionsHrefs: {
next: '#',
previous: '#',
},
pagesHrefs: ['#'],
pagesLabel: ['Page 1 of 1'],
});
});

Expand All @@ -64,24 +81,93 @@ describe(`Pagination`, () => {

test('should warn using invalid size value', () => {
pagination.patch({size: 'invalidSize' as 'sm'});
expect(state.size).toStrictEqual(null);
expect(state).toStrictEqual(assign(expectedState, {size: null}));
expectLogInvalidValue();
pagination.patch({size: 'sm'});
expect(state.size).toStrictEqual('sm');
expect(state).toStrictEqual(assign(expectedState, {size: 'sm'}));
});

test('actions should update the state', () => {
pagination.patch({collectionSize: 200});
const pagesLabel = Array.from({length: 20}, (_, index) => `Page ${index + 1} of 20`);
const pages = Array.from({length: 20}, (_, index) => index + 1);
const pagesHrefs = Array.from({length: 20}, (_, __) => `#`);

pagination.actions.first();
expect(state).toMatchObject({page: 1, pageCount: 20});
expect(state).toStrictEqual(assign(expectedState, {page: 1, pageCount: 20, pagesLabel, nextDisabled: false, pages, pagesHrefs}));

pagination.actions.next();
expect(state).toMatchObject({page: 2, pageCount: 20});
expect(state).toStrictEqual(assign(expectedState, {page: 2, previousDisabled: false}));

pagination.actions.select(5);
expect(state).toMatchObject({page: 5, pageCount: 20});
expect(state).toStrictEqual(assign(expectedState, {page: 5}));

pagination.actions.last();
expect(state).toMatchObject({page: 20, pageCount: 20});
expect(state).toStrictEqual(assign(expectedState, {page: 20, nextDisabled: true}));

pagination.actions.previous();
expect(state).toMatchObject({page: 19, pageCount: 20});
expect(state).toStrictEqual(assign(expectedState, {page: 19, nextDisabled: false}));
});

test('should prepare pages hrefs', () => {
pagination.patch({page: 3, collectionSize: 50, pageSize: 10, pageLink: (p) => `${p}/5`});
const pagesLabel = Array.from({length: 5}, (_, index) => `Page ${index + 1} of 5`);
const pages = Array.from({length: 5}, (_, index) => index + 1);
const pagesHrefs = Array.from({length: 5}, (_, index) => `${index + 1}/5`);
expectedState = {
...expectedState,
page: 3,
pageCount: 5,
pagesLabel,
nextDisabled: false,
pages,
pagesHrefs,
previousDisabled: false,
directionsHrefs: {
next: '4/5',
previous: '2/5',
},
};
expect(state).toStrictEqual(expectedState);

pagination.actions.next();
expectedState = assign(expectedState, {
page: 4,
directionsHrefs: {
next: '5/5',
previous: '3/5',
},
});
expect(state).toStrictEqual(expectedState);

pagination.actions.next();
expectedState = assign(expectedState, {
page: 5,
nextDisabled: true,
directionsHrefs: {
next: '5/5',
previous: '4/5',
},
});
expect(state).toStrictEqual(expectedState);

pagination.actions.first();
expectedState = assign(expectedState, {
page: 1,
nextDisabled: false,
previousDisabled: true,
directionsHrefs: {
next: '2/5',
previous: '1/5',
},
});
expect(state).toStrictEqual(expectedState);
});

test('should prepare pages hrefs when 1 page', () => {
pagination.patch({page: 1, collectionSize: 20, pageSize: 20, pageLink: (p) => `${p}/1`});
const pagesHrefs = ['1/1'];
expect(state).toStrictEqual(assign(expectedState, {pagesHrefs, directionsHrefs: {previous: '1/1', next: '1/1'}}));
});

test('should return api isEllipisis', () => {
Expand Down
Loading

0 comments on commit 484b45f

Please sign in to comment.