From e821a4e812ef2cb9add8e7bda4fe3dbbd42848f3 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Tue, 27 Dec 2022 10:48:10 +0100 Subject: [PATCH 1/8] feat: add Pagination component --- COMPONENTS.md | 2 +- package.json | 1 + src/Pagination.tsx | 205 +++++++++++++++++++++++++++++++++ stories/Pagination.stories.tsx | 51 ++++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 src/Pagination.tsx create mode 100644 stories/Pagination.stories.tsx diff --git a/COMPONENTS.md b/COMPONENTS.md index ce489798f..43e1ef292 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -30,7 +30,7 @@ - [ ] Modal - [x] Main navigation - [x] Tabs -- [ ] Pagination +- [x] Pagination - [x] Display - [ ] Share - [x] Footer diff --git a/package.json b/package.json index fca9dc81f..716f5f3d4 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "./Stepper": "./dist/Stepper.js", "./SkipLinks": "./dist/SkipLinks.js", "./Quote": "./dist/Quote.js", + "./Pagination": "./dist/Pagination.js", "./Notice": "./dist/Notice.js", "./Highlight": "./dist/Highlight.js", "./Header": "./dist/Header/index.js", diff --git a/src/Pagination.tsx b/src/Pagination.tsx new file mode 100644 index 000000000..9d8851298 --- /dev/null +++ b/src/Pagination.tsx @@ -0,0 +1,205 @@ +import React, { memo, forwardRef } from "react"; +import { symToStr } from "tsafe/symToStr"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; +import { fr } from "./lib"; +import { cx } from "./lib/tools/cx"; +import { createComponentI18nApi } from "./lib/i18n"; +import "./dsfr/component/stepper/stepper.css"; +import { getLink } from "./lib/routing"; + +export type PaginationProps = { + className?: string; + count: number; + defaultPage?: number; + classes?: Partial>; + showFirstLast?: boolean; + getPageHref: typeof getPageHref; // todo: quid link props ? +}; + +const getPageHref = (pageNumber: number) => `/page/${pageNumber}`; + +// naive page slicing +const getPaginationParts = ({ count, defaultPage }: { count: number; defaultPage: number }) => { + const maxVisiblePages = 10; + const slicesSize = 4; + // first n pages + if (count <= maxVisiblePages) { + return Array.from({ length: count }, (_, v) => ({ + number: v + 1, + active: defaultPage === v + 1 + })); + } + // last n pages + if (defaultPage > count - maxVisiblePages) { + return Array.from({ length: maxVisiblePages }, (_, v) => { + const pageNumber = count - (maxVisiblePages - v) + 1; + return { + number: pageNumber, + active: defaultPage === pageNumber + }; + }); + } + // slices + return [ + ...Array.from({ length: slicesSize }, (_, v) => { + if (defaultPage > slicesSize) { + const pageNumber = v + defaultPage; + return { number: pageNumber, active: defaultPage === pageNumber }; + } + return { number: v + 1, active: defaultPage === v + 1 }; + }), + { number: null, active: false }, + ...Array.from({ length: slicesSize }, (_, v) => { + const pageNumber = count - (slicesSize - v) + 1; + return { + number: pageNumber, + active: defaultPage === pageNumber + }; + }) + ]; +}; + +/** @see */ +export const Pagination = memo( + forwardRef((props, ref) => { + const { + className, + count, + defaultPage = 1, + showFirstLast = true, + getPageHref, + classes = {}, + ...rest + } = props; + + assert>(); + + const { t } = useTranslation(); + + const { Link } = getLink(); + + const parts = getPaginationParts({ count, defaultPage }); + + return ( + + ); + }) +); + +Pagination.displayName = symToStr({ Pagination }); + +const { useTranslation, addPaginationTranslations } = createComponentI18nApi({ + "componentName": symToStr({ Pagination }), + "frMessages": { + "first page": "Première page", + "next page": "Page suivante", + "last page": "Dernière page", + "aria-label": "Pagination", + "previous page": "Page précédente" + } +}); + +addPaginationTranslations({ + "lang": "en", + "messages": { + "first page": "First page", + "next page": "Next page", + "last page": "Last page", + "aria-label": "Pagination", + "previous page": "Previous page" + } +}); + +export { addPaginationTranslations }; + +export default Pagination; diff --git a/stories/Pagination.stories.tsx b/stories/Pagination.stories.tsx new file mode 100644 index 000000000..2682dae31 --- /dev/null +++ b/stories/Pagination.stories.tsx @@ -0,0 +1,51 @@ +import { Pagination } from "../dist/Pagination"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + "wrappedComponent": { Pagination }, + "description": ` +- [See DSFR documentation](//www.systeme-de-design.gouv.fr/elements-d-interface/composants/pagination) +- [See DSFR demos](//main--ds-gouv.netlify.app/example/component/pagination/) +- [See source code](//github.com/codegouvfr/react-dsfr/blob/main/src/Pagination.tsx)`, + "disabledProps": ["lang"] +}); + +export default meta; + +export const Default = getStory({ + count: 100, + defaultPage: 2, + showFirstLast: true, + getPageHref: pageNumber => `/page/${pageNumber}` +}); + +export const SummaryWithNoPage = getStory({ + count: 0, + getPageHref: pageNumber => `/page/${pageNumber}` +}); + +export const SummaryWithSinglePage = getStory({ + count: 1, + getPageHref: pageNumber => `/page/${pageNumber}` +}); + +export const SummaryWith32Pages = getStory({ + count: 132, + defaultPage: 42, + getPageHref: pageNumber => `/page/${pageNumber}` +}); + +export const SummaryWithoutShowFirstLast = getStory({ + count: 45, + defaultPage: 42, + showFirstLast: false, + getPageHref: pageNumber => `/page/${pageNumber}` +}); + +export const SummaryWithLastPage = getStory({ + count: 24, + defaultPage: 24, + getPageHref: pageNumber => `/page/${pageNumber}` +}); From 37950da7feb34cc31db50bac81830a46997eb477 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Tue, 27 Dec 2022 16:34:20 +0100 Subject: [PATCH 2/8] Update Pagination.tsx Signed-off-by: Julien Bouquillon --- src/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 9d8851298..d81f83c6e 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -60,7 +60,7 @@ const getPaginationParts = ({ count, defaultPage }: { count: number; defaultPage ]; }; -/** @see */ +/** @see */ export const Pagination = memo( forwardRef((props, ref) => { const { From a3862ba85c90a6d48c8a54a294349a9b6615a0eb Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 10:43:13 +0100 Subject: [PATCH 3/8] Update src/Pagination.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- src/Pagination.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index d81f83c6e..6af547e2e 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -7,6 +7,7 @@ import { cx } from "./lib/tools/cx"; import { createComponentI18nApi } from "./lib/i18n"; import "./dsfr/component/stepper/stepper.css"; import { getLink } from "./lib/routing"; +import type { RegisteredLinkProps } from "./lib/routing"; export type PaginationProps = { className?: string; From 4058b2311b24596fc02604595d3f2d0ad2901378 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 10:43:19 +0100 Subject: [PATCH 4/8] Update src/Pagination.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- src/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 6af547e2e..c6aa58dc0 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -15,7 +15,7 @@ export type PaginationProps = { defaultPage?: number; classes?: Partial>; showFirstLast?: boolean; - getPageHref: typeof getPageHref; // todo: quid link props ? + getPageLinkProps: (pageNumber: number)=> RegisteredLinkProps; }; const getPageHref = (pageNumber: number) => `/page/${pageNumber}`; From 57f4c236aa9c18321f795b9080a68c79009f362a Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 10:44:10 +0100 Subject: [PATCH 5/8] Update src/Pagination.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- src/Pagination.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index c6aa58dc0..45ff8af61 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -93,12 +93,13 @@ export const Pagination = memo( {showFirstLast && (
  • 0 && defaultPage > 1 & getPageLinkProp(1))} className={cx( fr.cx("fr-pagination__link", "fr-pagination__link--first"), - classes.link + classes.link, + getPageLinkProp(1).className )} aria-disabled={count > 0 ? true : undefined} - href={count > 0 && defaultPage > 1 ? getPageHref(1) : undefined} role="link" > {t("first page")} From 86ba24ddd6fba83302cbdc5c0691f4f12f512b91 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 10:49:36 +0100 Subject: [PATCH 6/8] fix --- src/Pagination.tsx | 36 ++++++++++++++++------------------ stories/Pagination.stories.tsx | 14 ++++++------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 45ff8af61..dc255f966 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -2,12 +2,12 @@ import React, { memo, forwardRef } from "react"; import { symToStr } from "tsafe/symToStr"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; + import { fr } from "./lib"; import { cx } from "./lib/tools/cx"; import { createComponentI18nApi } from "./lib/i18n"; -import "./dsfr/component/stepper/stepper.css"; import { getLink } from "./lib/routing"; -import type { RegisteredLinkProps } from "./lib/routing"; +import type { RegisteredLinkProps } from "./lib/routing"; export type PaginationProps = { className?: string; @@ -15,11 +15,9 @@ export type PaginationProps = { defaultPage?: number; classes?: Partial>; showFirstLast?: boolean; - getPageLinkProps: (pageNumber: number)=> RegisteredLinkProps; + getPageLinkProps: (pageNumber: number) => RegisteredLinkProps; }; -const getPageHref = (pageNumber: number) => `/page/${pageNumber}`; - // naive page slicing const getPaginationParts = ({ count, defaultPage }: { count: number; defaultPage: number }) => { const maxVisiblePages = 10; @@ -69,7 +67,7 @@ export const Pagination = memo( count, defaultPage = 1, showFirstLast = true, - getPageHref, + getPageLinkProps, classes = {}, ...rest } = props; @@ -93,13 +91,13 @@ export const Pagination = memo( {showFirstLast && (
  • 0 && defaultPage > 1 & getPageLinkProp(1))} + {...(count > 0 && defaultPage > 1 && getPageLinkProps(1))} className={cx( fr.cx("fr-pagination__link", "fr-pagination__link--first"), classes.link, - getPageLinkProp(1).className + getPageLinkProps(1).className )} - aria-disabled={count > 0 ? true : undefined} + aria-disabled={count > 0 && defaultPage > 1 ? true : undefined} role="link" > {t("first page")} @@ -107,7 +105,7 @@ export const Pagination = memo(
  • )}
  • - 1 && getPageLinkProps(defaultPage - 1))} aria-disabled={defaultPage <= 1 ? true : undefined} - href={defaultPage > 1 ? getPageHref(defaultPage - 1) : undefined} role="link" > {t("previous page")} - +
  • {parts.map(part => (
  • @@ -134,7 +132,7 @@ export const Pagination = memo( className={cx(fr.cx("fr-pagination__link"), classes.link)} aria-current={part.active ? true : undefined} title={`Page ${part.number}`} - href={getPageHref(part.number)} + {...getPageLinkProps(part.number)} > {part.number} @@ -151,8 +149,8 @@ export const Pagination = memo( ), classes.link )} + {...(defaultPage < count && getPageLinkProps(defaultPage + 1))} aria-disabled={defaultPage < count ? true : undefined} - href={defaultPage < count ? getPageHref(defaultPage + 1) : undefined} role="link" > {t("next page")} @@ -165,8 +163,8 @@ export const Pagination = memo( fr.cx("fr-pagination__link", "fr-pagination__link--last"), classes.link )} + {...(defaultPage < count && getPageLinkProps(count))} aria-disabled={defaultPage < count ? true : undefined} - href={defaultPage < count ? getPageHref(count) : undefined} > {t("last page")} @@ -184,10 +182,10 @@ const { useTranslation, addPaginationTranslations } = createComponentI18nApi({ "componentName": symToStr({ Pagination }), "frMessages": { "first page": "Première page", + "previous page": "Page précédente", "next page": "Page suivante", "last page": "Dernière page", - "aria-label": "Pagination", - "previous page": "Page précédente" + "aria-label": "Pagination" } }); @@ -195,10 +193,10 @@ addPaginationTranslations({ "lang": "en", "messages": { "first page": "First page", + "previous page": "Previous page", "next page": "Next page", "last page": "Last page", - "aria-label": "Pagination", - "previous page": "Previous page" + "aria-label": "Pagination" } }); diff --git a/stories/Pagination.stories.tsx b/stories/Pagination.stories.tsx index 2682dae31..45ef6ac9e 100644 --- a/stories/Pagination.stories.tsx +++ b/stories/Pagination.stories.tsx @@ -18,34 +18,34 @@ export const Default = getStory({ count: 100, defaultPage: 2, showFirstLast: true, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); export const SummaryWithNoPage = getStory({ count: 0, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); export const SummaryWithSinglePage = getStory({ count: 1, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); -export const SummaryWith32Pages = getStory({ +export const SummaryWith132Pages = getStory({ count: 132, defaultPage: 42, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); export const SummaryWithoutShowFirstLast = getStory({ count: 45, defaultPage: 42, showFirstLast: false, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); export const SummaryWithLastPage = getStory({ count: 24, defaultPage: 24, - getPageHref: pageNumber => `/page/${pageNumber}` + getPageLinkProps: pageNumber => ({ href: `/page/${pageNumber}` }) }); From d027ae942316f8a03deceed3562e7c677d2f5cf0 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 10:52:40 +0100 Subject: [PATCH 7/8] link --- src/Pagination.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index dc255f966..ae5b77971 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -140,7 +140,7 @@ export const Pagination = memo(
  • ))}
  • - {t("next page")} - +
  • {showFirstLast && (
  • - {t("last page")} - +
  • )} From 8e038181d576458b9453161ff0694adaa187c7eb Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 28 Dec 2022 11:09:04 +0100 Subject: [PATCH 8/8] fix-imports --- src/Pagination.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index ae5b77971..d766a26d9 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -3,11 +3,10 @@ import { symToStr } from "tsafe/symToStr"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; -import { fr } from "./lib"; -import { cx } from "./lib/tools/cx"; -import { createComponentI18nApi } from "./lib/i18n"; -import { getLink } from "./lib/routing"; -import type { RegisteredLinkProps } from "./lib/routing"; +import { fr } from "./fr"; +import { cx } from "./tools/cx"; +import { createComponentI18nApi } from "./i18n/i18n"; +import { RegisteredLinkProps, getLink } from "./link"; export type PaginationProps = { className?: string;