Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a3127a8
feat(Pagination): add Pagination component
AlexandraGallipoliRodrigues May 26, 2026
6d5331f
Merge branch 'master' of github.com:Telefonica/mistica-web into alex/…
AlexandraGallipoliRodrigues Jun 1, 2026
08e0990
feat(Pagination): align with Cyber skin and improve accessibility
AlexandraGallipoliRodrigues Jun 1, 2026
aa4aada
fix(Pagination): replace .closest() with Testing Library queries in test
AlexandraGallipoliRodrigues Jun 1, 2026
d30e104
minor change
AlexandraGallipoliRodrigues Jun 1, 2026
a9064f8
modified hoover to be neutralLow and modified chevron to be chevron-r…
AlexandraGallipoliRodrigues Jun 2, 2026
ad0df1c
Merge branch 'master' of github.com:Telefonica/mistica-web into alex/…
AlexandraGallipoliRodrigues Jun 2, 2026
c5c4061
changed text color with the updated cyber skins and modified translat…
AlexandraGallipoliRodrigues Jun 2, 2026
8e9de99
added screenshot tests
AlexandraGallipoliRodrigues Jun 2, 2026
f68dd1f
Merge branch 'master' of github.com:Telefonica/mistica-web into alex/…
AlexandraGallipoliRodrigues Jun 2, 2026
588bf02
renamed theme at playroom
AlexandraGallipoliRodrigues Jun 2, 2026
ba5d146
minor changes
AlexandraGallipoliRodrigues Jun 2, 2026
7a4f114
added lang or testing a11y at playroom
AlexandraGallipoliRodrigues Jun 2, 2026
857005b
a11y changes
AlexandraGallipoliRodrigues Jun 2, 2026
b932a73
Merge branch 'master' of github.com:Telefonica/mistica-web into alex/…
AlexandraGallipoliRodrigues Jun 3, 2026
c4026f3
style changes
AlexandraGallipoliRodrigues Jun 3, 2026
cedf287
Potential fix for pull request finding
AlexandraGallipoliRodrigues Jun 3, 2026
fbb08ae
Potential fix for pull request finding
AlexandraGallipoliRodrigues Jun 3, 2026
b3f213c
deleted pagination text token
AlexandraGallipoliRodrigues Jun 3, 2026
337bb8b
PR changes: boundary disabled, controls story, table tests
AlexandraGallipoliRodrigues Jun 10, 2026
7d7f4de
PR changes: boundary disabled, controls story, table tests
AlexandraGallipoliRodrigues Jun 10, 2026
604be41
Merge branch 'master' of github.com:Telefonica/mistica-web into alex/…
AlexandraGallipoliRodrigues Jun 10, 2026
cbc31aa
fix lint
AlexandraGallipoliRodrigues Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions playroom/frame-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
VIVO_SKIN,
BLAU_SKIN,
} from '../src';
import {CYBER_SKIN} from '../src/community';
import {Movistar_New as defaultThemeConfig} from './themes';

import type {ThemeConfig} from '../src';
Expand Down Expand Up @@ -62,6 +63,7 @@ const skinToLang: Record<string, string> = {
[O2_SKIN]: 'en-GB',
[O2_NEW_SKIN]: 'en-GB',
[ESIMFLAG_SKIN]: 'es-ES',
[CYBER_SKIN]: 'es-ES',

@aweell aweell Jun 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm idk it does work for me...

Grabacion.de.pantalla.2026-06-09.a.las.16.05.01.mov

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is the PreviewTools component that is broken

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Marcosld correct, we figured it out yesterday in pair programming. the skin is missing/not supported yet. I'd open a ticket to address it in a different branch. it's a quick and easy fix :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlexandraGallipoliRodrigues we can resolve this I guess!

};

const App = ({children, skinName}: {children: React.ReactNode; skinName: string}) => {
Expand Down
21 changes: 21 additions & 0 deletions playroom/snippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4583,6 +4583,27 @@ export default [
name: 'NavigationBreadcrumbs',
code: '<NavigationBreadcrumbs title="Facturas" breadcrumbs={[{title: "Cuenta", url: "/consumptions"}]} />',
},
{
group: 'Pagination',
name: 'Pagination',
code: `
<Pagination
totalPages={10}
currentPage={getState("paginationPage", 3)}
onChange={(page) => setState("paginationPage", page)}
/>`,
},
{
group: 'Pagination',
name: 'Pagination iconOnly',
code: `
<Pagination
totalPages={10}
mode="iconOnly"
currentPage={getState("paginationIconPage", 3)}
onChange={(page) => setState("paginationIconPage", page)}
/>`,
},
...titlesSnippets,
...emptyStatesGroup,
{
Expand Down
5 changes: 5 additions & 0 deletions playroom/themes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ export const O2_New: ThemeConfig = {...themes.O2_New, ...common};
export const Telefonica: ThemeConfig = {...themes.Telefonica, ...common};
export const Blau: ThemeConfig = {...themes.Blau, ...common};
export const Esimflag: ThemeConfig = {...themes.Esimflag, ...common};
export const Community_Cyber: ThemeConfig = {...themes.Cyber, ...common};

export const Movistar_New_iOS: ThemeConfig = {...Movistar_New, platformOverrides: {platform: 'ios'}};
export const Vivo_New_iOS: ThemeConfig = {...Vivo_New, platformOverrides: {platform: 'ios'}};
export const O2_New_iOS: ThemeConfig = {...O2_New, platformOverrides: {platform: 'ios'}};
export const Telefonica_iOS: ThemeConfig = {...Telefonica, platformOverrides: {platform: 'ios'}};
export const Blau_iOS: ThemeConfig = {...Blau, platformOverrides: {platform: 'ios'}};
export const Esimflag_iOS: ThemeConfig = {...Esimflag, platformOverrides: {platform: 'ios'}};
export const Community_Cyber_iOS: ThemeConfig = {
...Community_Cyber,
platformOverrides: {platform: 'ios'},
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions src/__screenshot_tests__/pagination-screenshot-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {openStoryPage, screen} from '../test-utils';

import type {Device} from '../test-utils';

const STORIES = [
{id: 'components-pagination--default', name: 'Default'},
{id: 'components-pagination--first-page', name: 'FirstPage'},
{id: 'components-pagination--last-page', name: 'LastPage'},
{id: 'components-pagination--with-ellipsis', name: 'WithEllipsis'},
{id: 'components-pagination--nav-only-responsive', name: 'NavOnlyResponsive'},
{id: 'components-pagination--pages-only', name: 'PagesOnly'},
{id: 'components-pagination--icon-only-controls', name: 'IconOnlyControls'},
{id: 'components-pagination--next-chapter-link', name: 'NextChapterLink'},
];
const DEVICES = ['DESKTOP', 'MOBILE_IOS'] as const;

const getCases = () => {
const cases: Array<[string, string, Device]> = [];
for (const story of STORIES) {
for (const device of DEVICES) {
cases.push([story.name, story.id, device]);
}
}
return cases;
};

test.each(getCases())('Pagination %s - %s', async (_name, id, device) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a table?

await openStoryPage({id, device});

const pagination = await screen.findByTestId('Pagination');
const image = await pagination.screenshot();
expect(image).toMatchImageSnapshot();
});

// Compact view only renders below 375px viewport. We use MOBILE_IOS_SMALL (320px)
// to drive the vertical layout and the current ± 1 page reduction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a lot of comments that we probably don't need, right? We should probably update AGENTS.md so it doesn't generate that much comments

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could take advantage of a feedback loop by using a skill that "auto documents" (better said: it helps updating/creating) things that the AI does not the way we want. each time we say "no", the skill /create-doc (or something similar) can update the thing to fine-tune the thing for the next round.
I've tried it and it works quite well! it saved me time and it improved my agentic code experience. take look :)

https://github.com/Telefonica/webapp/blob/a8839f60de1ce350613289f459abb69222edb495/.agents/skills/create-doc/SKILL.md

p.d.
I've found it very effective the fact of giving a good and bad example. most of the time it does help the AI doing the thing you expect

test('Pagination CompactView - MOBILE_IOS_SMALL', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure we want this? by fiddling with the component, I see that even smaller viewports could take advantage of a single line, maybe.

http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AlGBnADhAduglgG4wAyAhgJ4QCuALgHwA6OABC0gApkDm%2BOZtfLha0ItMgBsu3DAF5gAJgDMAXxZhqAJ00wctaTHkBGNS1gAzMtQn6ehxWqgV%2BAW3xgAwjT0sA9ExwkXzQsXAJicio6ehAVIA

should we talk to design maybe?

@AlexandraGallipoliRodrigues AlexandraGallipoliRodrigues Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await openStoryPage({
id: 'components-pagination--compact-view',
device: 'MOBILE_IOS_SMALL',
});

const pagination = await screen.findByTestId('Pagination');
const image = await pagination.screenshot();
expect(image).toMatchImageSnapshot();
});
78 changes: 78 additions & 0 deletions src/__stories__/pagination-story.tsx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have added controls instead of creating a lot of examples like the rest of the stories. Perhaps this is something we should add to AGENTS.md so it is the default behaviour

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% agreed. we can improve this by exploiting controls. that way it will be improving the DX!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that some props are not exposed on storybook right? I'd cover onChange because it's important, even though it's test covered!

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as React from 'react';
import {Pagination} from '..';

export default {
title: 'Components/Pagination',
};

/**
* Mid-range page with both navigation controls. On desktop renders
* "Previous 1 2 3 4 ... 9 Next", on mobile collapses labels into chevrons.
*/
export const Default: StoryComponent = () => <Pagination totalPages={9} defaultPage={3} />;
Default.storyName = 'Default';

/**
* First page: the Previous control must be hidden (showPrevious logic).
*/
export const FirstPage: StoryComponent = () => <Pagination totalPages={10} defaultPage={1} />;
FirstPage.storyName = 'FirstPage';

/**
* Last page: the Next control must be hidden.
*/
export const LastPage: StoryComponent = () => <Pagination totalPages={10} defaultPage={10} />;
LastPage.storyName = 'LastPage';

/**
* Wide page count with active page in the middle: ellipses should appear on
* both sides of the visible range ("1 ... 9 10 11 ... 20").
*/
export const WithEllipsis: StoryComponent = () => <Pagination totalPages={20} defaultPage={10} />;
WithEllipsis.storyName = 'WithEllipsis';

/**
* No page list, only Previous/Next. On desktop the labels are visible,
* on mobile the labels are hidden and only chevrons are shown.
*/
export const NavOnlyResponsive: StoryComponent = () => (
<Pagination totalPages={10} defaultPage={5} hidePageList />
);
NavOnlyResponsive.storyName = 'NavOnlyResponsive';

/**
* Page list without navigation controls. Matches the product-list scenario
* from the Figma examples panel.
*/
export const PagesOnly: StoryComponent = () => (
<Pagination totalPages={5} defaultPage={3} hideNavigationControls />
);
PagesOnly.storyName = 'PagesOnly';

/**
* iconOnly mode forces chevron-only Previous/Next even on desktop.
* Combined with hidePageList matches the "Mapa / Listado" card scenario.
*/
export const IconOnlyControls: StoryComponent = () => (
<Pagination totalPages={10} defaultPage={5} mode="iconOnly" hidePageList />
);
IconOnlyControls.storyName = 'IconOnlyControls';

/**
* Compact view: triggered automatically when the viewport is narrower than
* 375px (high-zoom or space-limited contexts). Layout stacks vertically with
* Previous / page list / Next and only renders the current ± 1 pages.
* Only visible when the screenshot test runs this story at MOBILE_IOS_SMALL.
*/
export const CompactView: StoryComponent = () => <Pagination totalPages={50} defaultPage={24} />;
CompactView.storyName = 'CompactView';

/**
* Pagination acting as a single next-chapter link: page list is hidden, only
* the Next control is rendered with a custom navRightLabel ("Siguiente
* capítulo"). Matches the chapter-reader scenario from the Figma examples.
*/
export const NextChapterLink: StoryComponent = () => (
<Pagination totalPages={10} defaultPage={1} hidePageList navRightLabel="Siguiente capítulo" />
);
NextChapterLink.storyName = 'NextChapterLink';
99 changes: 99 additions & 0 deletions src/__tests__/pagination-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ThemeContextProvider from '../theme-context-provider';
import {makeTheme} from './test-utils';
import Pagination, {getPaginationItems} from '../pagination';

test('renders pagination navigation landmark', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={5} defaultPage={1} />
</ThemeContextProvider>
);

expect(screen.getByRole('navigation', {name: 'Paginación - Página 1 de 5'})).toBeInTheDocument();
});

test('does not render when there is a single page', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={1} />
</ThemeContextProvider>
);

expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});

test('calls onChange when a page button is clicked (uncontrolled)', async () => {
const onChange = jest.fn();
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={5} defaultPage={1} onChange={onChange} />
</ThemeContextProvider>
);

await userEvent.click(screen.getByRole('button', {name: 'Ir a la página 3'}));

expect(onChange).toHaveBeenCalledWith(3);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see we always use the same number for testing things? 5 pages, call next expected 3.

  1. I'd change the values of the test below at least, just to broaden the test cases and make them more robust
  2. a more general opinion (and to be discussed outside this PR): what about using something like https://www.npmjs.com/package/@faker-js/faker to create stable tests, but with data that could cover even more cases?

});

test('calls onChange when Next is clicked', async () => {
const onChange = jest.fn();
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={5} defaultPage={2} onChange={onChange} />
</ThemeContextProvider>
);

await userEvent.click(screen.getByRole('button', {name: 'Página siguiente'}));

expect(onChange).toHaveBeenCalledWith(3);
});

test('does not change page when disabled', async () => {
const onChange = jest.fn();
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={5} defaultPage={1} onChange={onChange} disabled />
</ThemeContextProvider>
);

await userEvent.click(screen.getByRole('button', {name: 'Ir a la página 3'}));

expect(onChange).not.toHaveBeenCalled();
});

test('honors controlled currentPage', () => {
render(
<ThemeContextProvider theme={makeTheme()}>
<Pagination totalPages={5} currentPage={3} />
</ThemeContextProvider>
);

// The current page is rendered as an aria-disabled button so VoiceOver can
// still land on it and announce "Página 3, página actual" — but the
// "Ir a la página 3" navigation button is absent (it's not navigable).
expect(screen.queryByRole('button', {name: 'Ir a la página 3'})).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Página 3, página actual'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Ir a la página 2'})).toBeInTheDocument();
});

describe('getPaginationItems', () => {
test('returns an empty array for a single page', () => {
expect(getPaginationItems({totalPages: 1, currentPage: 1})).toEqual([]);
});

test('returns all pages when total fits without ellipsis', () => {
const items = getPaginationItems({totalPages: 5, currentPage: 3});
expect(items.filter((i) => i.type === 'ellipsis')).toHaveLength(0);
expect(items).toHaveLength(5);
});

test('inserts ellipsis when middle pages are skipped', () => {
const items = getPaginationItems({totalPages: 20, currentPage: 10});
expect(items.some((i) => i.type === 'ellipsis')).toBe(true);
expect(items[0]).toMatchObject({type: 'page', page: 1});
expect(items[items.length - 1]).toMatchObject({type: 'page', page: 20});
});
});
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export {default as Inline} from './inline';
export {default as HorizontalScroll} from './horizontal-scroll';
export {default as Stepper} from './stepper';
export {ProgressBar, ProgressBarStepped} from './progress-bar';
export {default as Pagination} from './pagination';
Comment thread
AlexandraGallipoliRodrigues marked this conversation as resolved.
Comment thread
AlexandraGallipoliRodrigues marked this conversation as resolved.
export {default as Meter} from './meter';
export {Rating, InfoRating} from './rating';
export {VerticalMosaic, HorizontalMosaic} from './mosaic';
Expand Down
Loading
Loading