Repository: dnbexperience/eufemia
Pull request #5072: feat(Pagination): add support for horizontal `paginationBarLayout`
Author: login: tujoworker
Labels: name: released
Created: 2025-05-07T12:38:58Z | Merged: 2025-05-07T14:19:47Z

Motivation: https://dnb-it.slack.com/archives/CMXABCHEY/p1746527457334889


Diff:
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/Examples.tsx
index 579118932cd..a389eeb2b90 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/Examples.tsx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/Examples.tsx
@@ -44,6 +44,21 @@ export const PaginationExampleDefault = () => (
   </ComponentBox>
 )
 
+export const PaginationExampleWithHorizontalLayout = () => (
+  <ComponentBox data-visual-test="pagination-horizontal">
+    <Pagination
+      page_count={888}
+      current_page={4}
+      on_change={({ pageNumber }) => {
+        console.log('on_change:', pageNumber)
+      }}
+      paginationBarLayout="horizontal"
+    >
+      <P>Current Page Content</P>
+    </Pagination>
+  </ComponentBox>
+)
+
 export const PaginationExampleWithCallback = () => (
   <ComponentBox hideCode>
     <Pagination
diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/demos.mdx b/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/demos.mdx
index 6122d5e329d..6dc05d14a3c 100644
--- a/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/demos.mdx
+++ b/packages/dnb-design-system-portal/src/docs/uilib/components/pagination/demos.mdx
@@ -4,9 +4,10 @@ showTabs: true
 
 import {
   PaginationExampleDefault,
+  PaginationExampleWithHorizontalLayout,
   PaginationExampleWithCallback,
   PaginationExampleCentered,
-} from 'Docs/uilib/components/pagination/Examples'
+} from './Examples'
 
 ## Demos
 
@@ -18,6 +19,10 @@ If you need to access methods provided by the render property arguments.
 
 <PaginationExampleWithCallback />
 
+### Horizontal pagination
+
+<PaginationExampleWithHorizontalLayout />
+
 ### Centered Pagination with random delay
 
 Note that we keep the height of the previous page. All pages can for sure have their own height.
diff --git a/packages/dnb-eufemia/src/components/pagination/Pagination.d.ts b/packages/dnb-eufemia/src/components/pagination/Pagination.d.ts
index 591d65acdf8..8df351bc538 100644
--- a/packages/dnb-eufemia/src/components/pagination/Pagination.d.ts
+++ b/packages/dnb-eufemia/src/components/pagination/Pagination.d.ts
@@ -10,6 +10,7 @@ type PaginationStartupCount = string | number;
 type PaginationParallelLoadCount = string | number;
 type PaginationMinWaitTime = string | number;
 type PaginationMode = 'pagination' | 'infinity';
+type PaginationLayout = 'vertical' | 'horizontal';
 type PaginationItems = string | any[];
 type PaginationSetContentHandler = string | ((...args: any[]) => any);
 type PaginationResetContentHandler = string | ((...args: any[]) => any);
@@ -95,6 +96,10 @@ export interface PaginationProps
    * If set to `infinity`, then the pagination bar will be now shown and but infinity scrolling will do the content presentation. For more information, check out the <a href="https://eufemia.dnb.no/uilib/components/pagination/infinity-scroller">Infinity Scroller</a>. Defaults to `pagination`.
    */
   mode?: PaginationMode;
+  /**
+   * The layout of the pagination bar. Defaults to `vertical`.
+   */
+  paginationBarLayout?: PaginationLayout;
   /**
    * If set to `true` it will disable the automated infinity scrolling, but shows a load more button at the of the content instead.
    */
diff --git a/packages/dnb-eufemia/src/components/pagination/Pagination.js b/packages/dnb-eufemia/src/components/pagination/Pagination.js
index 050d9d067d1..ffcd57cdf6a 100644
--- a/packages/dnb-eufemia/src/components/pagination/Pagination.js
+++ b/packages/dnb-eufemia/src/components/pagination/Pagination.js
@@ -44,6 +44,7 @@ const paginationPropTypes = {
   disabled: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
   skeleton: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
   mode: PropTypes.oneOf(['pagination', 'infinity']),
+  paginationBarLayout: PropTypes.oneOf(['vertical', 'horizontal']),
   use_load_button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
   items: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
   hide_progress_indicator: PropTypes.oneOfType([
@@ -115,6 +116,7 @@ const paginationDefaultProps = {
   current_page: null,
   page_count: null,
   mode: 'pagination',
+  paginationBarLayout: 'vertical',
   use_load_button: false,
   items: null,
   hide_progress_indicator: false,
@@ -191,6 +193,7 @@ class PaginationInstance extends React.PureComponent {
       children,
       className,
       barSpace,
+      paginationBarLayout, // eslint-disable-line
 
       disabled: _disabled, // eslint-disable-line
       skeleton: _skeleton, // eslint-disable-line
@@ -216,6 +219,8 @@ class PaginationInstance extends React.PureComponent {
         className: classnames(
           'dnb-pagination',
           align && `dnb-pagination--${align}`,
+          paginationBarLayout &&
+            `dnb-pagination--layout-${paginationBarLayout}`,
           createSpacingClasses(props),
           className
         ),
diff --git a/packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts b/packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts
index 5dc9a0ee528..a8f72682931 100644
--- a/packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts
+++ b/packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts
@@ -6,6 +6,11 @@ export const PaginationProperties: PropertiesTableProps = {
     type: ['pagination', 'infinity'],
     status: 'optional',
   },
+  paginationBarLayout: {
+    doc: 'The layout of the pagination bar. Defaults to `vertical`.',
+    type: ['vertical', 'horizontal'],
+    status: 'optional',
+  },
   children: {
     doc: 'the given content can be either a function or a React node, depending on your needs. A function contains several helper functions. More details down below and have a look at the examples in the demos section. ',
     type: ['React.ReactNode', 'function'],
diff --git a/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.screenshot.test.ts b/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.screenshot.test.ts
index 6ffc1fc7213..f514c045a56 100644
--- a/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.screenshot.test.ts
+++ b/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.screenshot.test.ts
@@ -24,6 +24,16 @@ describe.each(['ui', 'sbanken'])('Pagination for %s', (themeName) => {
     expect(screenshot).toMatchImageSnapshot()
   })
 
+  it('have to match the pagination with horizontal layout', async () => {
+    const screenshot = await makeScreenshot({
+      selector: '[data-visual-test="pagination-horizontal"]',
+      style: {
+        width: '60rem',
+      },
+    })
+    expect(screenshot).toMatchImageSnapshot()
+  })
+
   it('have to match the current button hover', async () => {
     const screenshot = await makeScreenshot({
       selector: '[data-visual-test="pagination-default"]',
diff --git a/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.test.tsx b/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.test.tsx
index fa03978fce1..e3ca02be652 100644
--- a/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.test.tsx
+++ b/packages/dnb-eufemia/src/components/pagination/__tests__/Pagination.test.tsx
@@ -171,6 +171,27 @@ describe('Pagination bar', () => {
     ).toBe('3')
   })
 
+  it('should set correct class when paginationBarLayout is set to "horizontal"', () => {
+    render(
+      <Pagination
+        page_count={3}
+        startup_page={2}
+        paginationBarLayout="horizontal"
+      >
+        {({ pageNumber, setContent }) => {
+          setContent(pageNumber, <div>{pageNumber}</div>)
+        }}
+      </Pagination>
+    )
+    expect(
+      document.querySelector('.dnb-pagination__content').textContent
+    ).toBe('2')
+
+    expect(document.querySelector('.dnb-pagination')).toHaveClass(
+      'dnb-pagination--layout-horizontal'
+    )
+  })
+
   it('rerenders properly', () => {
     const Rerender = () => {
       const [count, incrementBy] = React.useReducer((state, count) => {
diff --git a/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-sbanken-have-to-match-the-pagination-with-horizontal-layout.snap.png b/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-sbanken-have-to-match-the-pagination-with-horizontal-layout.snap.png
new file mode 100644
index 00000000000..2311bb57813
Binary files /dev/null and b/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-sbanken-have-to-match-the-pagination-with-horizontal-layout.snap.png differ
diff --git a/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-ui-have-to-match-the-pagination-with-horizontal-layout.snap.png b/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-ui-have-to-match-the-pagination-with-horizontal-layout.snap.png
new file mode 100644
index 00000000000..7d809102ebc
Binary files /dev/null and b/packages/dnb-eufemia/src/components/pagination/__tests__/__image_snapshots__/pagination-for-ui-have-to-match-the-pagination-with-horizontal-layout.snap.png differ
diff --git a/packages/dnb-eufemia/src/components/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap b/packages/dnb-eufemia/src/components/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap
index 6110a9bf22d..d91ce700371 100644
--- a/packages/dnb-eufemia/src/components/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap
+++ b/packages/dnb-eufemia/src/components/pagination/__tests__/__snapshots__/Pagination.test.tsx.snap
@@ -875,6 +875,14 @@ html[data-visual-test] .dnb-progress-indicator__bar-transition {
 .dnb-pagination__bar__wrapper {
   display: flex;
   flex-flow: column-reverse wrap;
+  row-gap: 0.5rem;
+}
+@media screen and (min-width: 72.00625em) {
+  .dnb-pagination--layout-horizontal .dnb-pagination__bar__wrapper {
+    flex-flow: row-reverse wrap;
+    justify-content: space-between;
+    flex: 1;
+  }
 }
 .dnb-pagination--center .dnb-pagination__bar__wrapper {
   align-items: center;
@@ -929,10 +937,8 @@ html[data-visual-test] .dnb-progress-indicator__bar-transition {
   opacity: 0;
 }
 .dnb-pagination__bar__skip {
-  margin-top: 0.5rem;
-}
-.dnb-pagination__bar__skip .dnb-button:first-of-type {
-  margin-right: 1.5rem;
+  display: flex;
+  column-gap: 1.5rem;
 }
 
 @keyframes show-page {
diff --git a/packages/dnb-eufemia/src/components/pagination/style/dnb-pagination.scss b/packages/dnb-eufemia/src/components/pagination/style/dnb-pagination.scss
index 57c10f22160..d683e18120f 100644
--- a/packages/dnb-eufemia/src/components/pagination/style/dnb-pagination.scss
+++ b/packages/dnb-eufemia/src/components/pagination/style/dnb-pagination.scss
@@ -34,7 +34,7 @@
   &--center &__bar {
     justify-content: center;
 
-    @media screen and (max-width: 40em) {
+    @include allBelow(small) {
       justify-content: flex-start;
     }
   }
@@ -46,6 +46,15 @@
   &__bar__wrapper {
     display: flex;
     flex-flow: column-reverse wrap; // because of screen readers
+    row-gap: 0.5rem;
+  }
+
+  &--layout-horizontal &__bar__wrapper {
+    @include allAbove(large) {
+      flex-flow: row-reverse wrap;
+      justify-content: space-between;
+      flex: 1;
+    }
   }
 
   &--center &__bar__wrapper {
@@ -117,11 +126,8 @@
   }
 
   &__bar__skip {
-    margin-top: 0.5rem;
-  }
-
-  &__bar__skip .dnb-button:first-of-type {
-    margin-right: 1.5rem;
+    display: flex;
+    column-gap: 1.5rem;
   }
 }
 


Reviews:
user: login: langz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:15:21Z
user: login: tujoworker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T13:29:12Z

Review comments:
diff_hunk: @@ -191,6 +193,7 @@ class PaginationInstance extends React.PureComponent {
       children,
       className,
       barSpace,
+      paginationLayout, // eslint-disable-line
path: packages/dnb-eufemia/src/components/pagination/Pagination.js
user: login: langz
body: ```suggestion
      paginationLayout,
```
created_at: 2025-05-07T13:13:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,6 +6,11 @@ export const PaginationProperties: PropertiesTableProps = {
     type: ['pagination', 'infinity'],
     status: 'optional',
   },
+  paginationLayout: {
path: packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts
user: login: langz
body: ```suggestion
  paginationBarLayout: {
```

Should we name it `paginationLayout` or `paginationBarLayout`?
I see that it only changes the layout of the pagination bar, so perhaps we should rename `paginationLayout` to `paginationBarLayout`?

Worth a consideration 📿 
created_at: 2025-05-07T13:15:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,6 +6,11 @@ export const PaginationProperties: PropertiesTableProps = {
     type: ['pagination', 'infinity'],
     status: 'optional',
   },
+  paginationLayout: {
path: packages/dnb-eufemia/src/components/pagination/PaginationDocs.ts
user: login: tujoworker
body: Yes, that is better – I think too.
created_at: 2025-05-07T13:29:12Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-05-07T12:39:02Z
body: [vc]: #LjWxBIbBnTGnnzgSMwV3KyuOJQtOa/JJH3IeWYu/+gw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJldWZlbWlhIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2V1ZmVtaWEvZXVmZW1pYS82TjdwVU1WNW1KbVJCVTE4RzIzaW1XamYzMkJpIiwicHJldmlld1VybCI6ImV1ZmVtaWEtZ2l0LWZlYXQtcGFnaW5hdGlvbi1sYXlvdXQtZXVmZW1pYS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImV1ZmVtaWEtZ2l0LWZlYXQtcGFnaW5hdGlvbi1sYXlvdXQtZXVmZW1pYS52ZXJjZWwuYXBwIn0sInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **eufemia** | ✅ Ready ([Inspect](https://vercel.com/eufemia/eufemia/6N7pUMV5mJmRBU18G23imWjf32Bi)) | [Visit Preview](https://eufemia-git-feat-pagination-layout-eufemia.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/eufemia-git-feat-pagination-layout-eufemia.vercel.app?via=pr-comment-feedback-link) | May 7, 2025 1:47pm |


author_association: NONE
user: login: codesandbox-ci[bot]
created_at: 2025-05-07T12:47:36Z
body: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dnbexperience/eufemia/pr/5072/builds/614474) or the icon next to each commit SHA.
author_association: NONE
user: login: tujoworker
created_at: 2025-05-08T09:01:31Z
body: :tada: This PR is included in version 10.73.0 :tada:

The release is available on:
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@dnb/eufemia/v/10.73.0)
- [GitHub release](https://github.com/dnbexperience/eufemia/releases/tag/v10.73.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: MEMBER